Junit集成


前面多次用到@RunWith与@ContextConfiguration,在测试类添加这两个注解,程序就会自动加载spring配置并初始化Spring容器,方便Junit与Spring集成测试.使用这个功能需要在pom.xml中添加如下依赖:


pom.xml

<dependency>

    <groupId>org.springframework</groupId>

    <artifactId>spring-test</artifactId>

    <version>4.2.0.RELEASE</version>

</dependency>


以@RunWith和@ContextConfiguration加载Spring容器

/**

 * Spring 整合 Junit

 * Created by jifang on 15/12/9.

 */

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")

public class BeanTest {


    @Autowired

    private Bean bean;


    @Test

    public void testConstruct() {

        Car car = bean.getCar();

        System.out.println(car);

    }

}


Web集成


我们可以利用ServletContext容器保存数据的唯一性, 以及ServletContextListener会在容器初始化时只被调用一次的特性. 在web.xml中配置spring-web包下的ContextLoaderListener来加载Spring配置文件/初始化Spring容器:


pom.xml/spring-web

<dependency>

    <groupId>org.springframework</groupId>

    <artifactId>spring-web</artifactId>

    <version>4.2.0.RELEASE</version>

</dependency>


配置监听器(web.xml)

<listener>

    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>


加载Spring配置文件

<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>classpath:spring/applicationContext.xml</param-value>

</context-param>


附: 完整web.xml文件git地址.

测试Servlet

@WebServlet(urlPatterns = "/servlet")

public class Servlet extends HttpServlet {


    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        this.doGet(request, response);

    }


    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());

        Bean bean = context.getBean("bean", Bean.class);

        Car car = bean.getCar();

        System.out.println(car);

    }

}


在应用中,普通的JavaBean由Spring管理,可以使用@Autowired自动注入.但Filter与Servlet例外,他们都是由Servlet容器管理,因此其属性不能用Spring注入,所以在实际项目中,一般都不会直接使用Servlet,而是用SpringMVC/WebX/Struts2之类的MVC框架以简化开发,后面会有专门的博客介绍这类框架,在此就不做深入介绍了.

注: 运行Servlet不要忘记添加servlet-api依赖:

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>3.1.0</version>

</dependency>


文件加载


1. 引入properties


可以将需要经常修改的属性参数值放到properties文件, 并在Spring文件中引入.

db.properties

## Data Source

mysql.driver.class=com.mysql.jdbc.Driver

mysql.url=jdbc:mysql://host:port/db?useUnicode=true&characterEncoding=UTF8

mysql.user=user

mysql.password=password


注意: value后不能有空格.

1.1 property-placeholde引入


在Spring配置文件中使用<context:property-placeholder/>标签引入properties文件,XML文件可通过${key}引用, Java可通过@Value("${key}")引用:


XML

<context:property-placeholder location="classpath:common.properties"/>


<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">

    <property name="driverClassName" value="${mysql.driver.class}"/>

    <property name="jdbcUrl" value="${mysql.url}"/>

    <property name="username" value="${mysql.user}"/>

    <property name="password" value="${mysql.password}"/>

</bean>


Java

@Component

public class AccessLog {


    @Value("${mysql.url}")

    private String value;


    // ...

}


1.2 PropertiesFactoryBean引入


Spring提供了org.springframework.beans.factory.config.PropertiesFactoryBean,以加载properties文件, 方便在JavaBean中注入properties属性值.


XML

<bean id="commonProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">

    <property name="locations">

        <list>

            <value>classpath*:common.properties</value>

        </list>

    </property>

</bean>


Java

@Controller

public class Bean {


    @Value("#{commonProperties['bean.properties.name']}")

    private String name;


    // ...

}


2. import其他Spring配置


如果Spring的配置项过多,可以按模块将配置划分多个配置文件(-datasource.xml/-dubbo-provider.xml/-bean.xml), 并由主配置applicationContext.xml文件引用他们,此时可用<import/>标签引入:


<import resource="applicationContext-bean.xml"/>

<import resource="applicationContext-dubbo-provider.xml"/>

<import resource="applicationContext-dubbo-consumer.xml"/>


事务管理


Spring事务管理高层抽象主要由PlatformTransactionManager/TransactionDefinition/TransactionStatus三个接口提供支持:

PlatformTransactionManager(事务管理器)


PlatformTransactionManager的主要功能是事务管理,Spring为不同的持久层框架提供了不同的PlatformTransactionManager实现:


事务

描述

​DataSourceTransactionManager​

JDBCTemplate/MyBatis/iBatis持久化使用

​HibernateTransactionManager​

Hibernate持久化使用

​JpaTransactionManager​

JPA持久化使用

​JdoTransactionManager​

JDO持久化使用

​JtaTransactionManager​

JTA实现管理事务,一个事务跨越多个资源时使用

因此使用Spring管理事务,需要为不同持久层配置不同事务管理器实现.


TransactionDefinition(事务定义信息)


TransactionDefinition提供了对事务的相关配置, 如事务隔离级别/传播行为/只读/超时等:


隔离级别(isolation) 

为解决事务并发引起的问题(脏读/幻读/不可重复读),引入四个隔离级别:

隔离级别

描述

​DEFAULT​

使用数据库默认的隔离级别

​READ_UNCOMMITED​

读未提交

​READ_COMMITTED​

读已提交(Oracle默认)

​REPEATABLE_READ​

可重复读(MySQL默认)

​SERIALIZABLE​

串行化

关于事务隔离级别的讨论, 可参考我的博客JDBC基础-事务隔离级别部分.

传播行为(propagation) 

传播行为不是数据库的特性, 而是为了在业务层解决两个事务相互调用的问题:

传播类型

描述

​REQUIRED​

支持当前事务,如果不存在就新建一个(默认)

​SUPPORTS​

支持当前事务,如果不存在就不使用事务

​MANDATORY​

支持当前事务,如果不存在则抛出异常

REQUIRES_NEW

如果有事务存在,则挂起当前事务新建一个

​NOT_SUPPORTED​

以非事务方式运行,如果有事务存在则挂起当前事务

​NEVER​

以非事务方式运行,如果有事务存在则抛出异常

NESTED

如果当前事务存在,则嵌套事务执行(只对​​DataSourceTransactionManager​​有效)

超时时间(timeout)

只读(read-only) 

只读事务, 不能执行INSERT/UPDATE/DELETE操作.

TransactionStatus(事务状态信息)

Spring-拾遗_mysql

获得事务执行过程中某一个时间点状态.

声明式事务管理


Spring声明式事务管理:无需要修改原来代码,只需要为Spring添加配置(XML/Annotation),就可以为目标代码添加事务管理功能.


需求: 账案例(使用MyBatis).

AccountDAO

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.fq.dao.AccountDAO">


    <update id="transferIn">

        UPDATE account

        SET money = money + #{0}

        WHERE name = #{1};

    </update>


    <update id="transferOut">

        UPDATE account

        SET money = money - #{0}

        WHERE name = #{1};

    </update>

</mapper>


/**

 * @author jifang

 * @since 16/3/3 上午11:16.

 */

public interface AccountDAO {


    void transferIn(Double inMoney, String name);


    void transferOut(Double outMoney, String name);

}


Service

public interface AccountService {


    void transfer(String from, String to, Double money);

}


@Service("service")

public class AccountServiceImpl implements AccountService {


    @Autowired

    private AccountDAO dao;


    @Override

    public void transfer(String from, String to, Double money) {

        dao.transferOut(money, from);


        // 此处抛出异常, 没有事务将导致数据不一致

        int a = 1 / 0;


        dao.transferIn(money, to);

    }

}


mybatis-configuration.xml/applicationContext-datasource.xml

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <!-- 加载mapper映射文件 -->

    <mappers>

        <mapper resource="mybatis/mapper/AccountDAO.xml"/>

    </mappers>

</configuration>


<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

       http://www.springframework.org/schema/beans/spring-beans.xsd

       http://www.springframework.org/schema/context

       http://www.springframework.org/schema/context/spring-context.xsd">


    <context:property-placeholder location="classpath:db.properties"/>


    <!-- 配置数据源 -->

    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">

        <property name="driverClassName" value="${mysql.driver.class}"/>

        <property name="jdbcUrl" value="${mysql.url}"/>

        <property name="username" value="${mysql.user}"/>

        <property name="password" value="${mysql.password}"/>

        <property name="maximumPoolSize" value="5"/>

        <property name="maxLifetime" value="700000"/>

        <property name="idleTimeout" value="600000"/>

        <property name="connectionTimeout" value="10000"/>

        <property name="dataSourceProperties">

            <props>

                <prop key="dataSourceClassName">com.mysql.jdbc.jdbc2.optional.MysqlDataSource</prop>

                <prop key="cachePrepStmts">true</prop>

                <prop key="prepStmtCacheSize">250</prop>

                <prop key="prepStmtCacheSqlLimit">2048</prop>

            </props>

        </property>

    </bean>


    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">

        <constructor-arg ref="hikariConfig"/>

    </bean>


    <!-- 配置SqlSessionFactory -->

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

        <property name="dataSource" ref="dataSource"/>

        <property name="configLocation" value="classpath:mybatis/mybatis-configuration.xml"/>

    </bean>


    <!-- 基于包扫描的mapper配置 -->

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

        <property name="basePackage" value="com.fq.dao"/>

        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

    </bean>


</beans>


applicationContext.xml(没有事务)

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">


    <context:component-scan base-package="com.fq.service"/>


    <import resource="applicationContext-datasource.xml"/>

</beans>


Client

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")

public class SpringClient {


    @Autowired

    private AccountService service;


    @Test

    public void client() {

        service.transfer("from", "to", 10D);

    }

}


执行以上代码, 将会导致数据前后不一致.


XML配置


Spring事务管理依赖AOP,而AOP需要定义切面(Advice+PointCut),在Spring内部提供了事务管理的默认Adviceorg.springframework.transaction.interceptor.TransactionInterceptor,并且Spring为了简化事务配置,引入tx标签:


引入tx的命名空间,配置Advice:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="dataSource"/>

</bean>


<tx:advice id="txAdvice" transaction-manager="transactionManager">

    <!-- 事务配置属性, 对什么方法应用怎样的配置, 成为TransactionDefinition对象 -->

    <tx:attributes>

        <!--

            name: 方法名, 支持通配符

            isolation: 隔离级别

            propagation: 传播行为

            timeout: 超时时间

            read-only: 是否只读

            rollback-for: 配置异常类型, 发生这些异常回滚事务

            no-rollback-for: 配置异常类型, 发生这些异常不回滚事务

        -->

        <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false"/>

    </tx:attributes>

</tx:advice>


配置切面 

Spring事务管理Advice基于SpringAOP,因此使用<aop:advisor/>配置:

<aop:config>

    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.fq.service.impl.AccountServiceImpl.*(..))"/>

</aop:config>


注解配置


使用注解配置事务, 可以省略切点的定义(因为注解放置位置就已经确定了PointCut的置), 只需配置Advice即可:


激活注解事务管理功能

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="dataSource"/>

</bean>


<tx:annotation-driven transaction-manager="transactionManager"/>


在需要管理事务的业务类/业务方法上添加@Transactional注解

@Override

@Transactional(transactionManager = "transactionManger", readOnly = true)

public void transfer(String from, String to, Double money) {

    // ...

}


可以在注解@Transactional中配置与XML相同的事务属性(isolation/propagation等).

实践


推荐使用XML方式来配置事务,实际开发时一般将事务集中配置管理. 另外, 事务的isolation/propagation一般默认的策略就已经足够, 反而我们需要配置是否只读(比如MySQL主从备份时,主库一般提供读写操作,而从库只提供读操作), 因此其配置可以如下:

<!-- 配置声明式事务 -->

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="dataSource"/>

</bean>


<tx:advice id="txAdvice" transaction-manager="transactionManager">

    <!-- 定义方法的过滤规则 -->

    <tx:attributes>

        <!-- 定义所有get开头的方法都是只读的 -->

        <tx:method name="get*" read-only="true"/>

        <tx:method name="find*" read-only="true"/>

        <tx:method name="select*" read-only="true"/>

        <tx:method name="*"/>

    </tx:attributes>

</tx:advice>


<!-- 配置事务AOP -->

<aop:config>

    <!-- 定义切点 -->

    <aop:pointcut id="dao" expression="execution (* com.fq.core.dao.*.*(..))"/>

    <!-- 为切点定义通知 -->

    <aop:advisor advice-ref="txAdvice" pointcut-ref="dao"/>

</aop:config>


主从

<tx:advice id="txAdvice_slave" transaction-manager="transactionManager_slave">

    <!-- 定义方法的过滤规则 -->

    <tx:attributes>

        <tx:method name="*" read-only="true"/>

    </tx:attributes>

</tx:advice>


<tx:advice id="txAdvice_master" transaction-manager="transactionManager_slave">

    <!-- 定义方法的过滤规则 -->

    <tx:attributes>

        <!-- 定义所有get开头的方法都是只读的 -->

        <tx:method name="get*" read-only="true"/>

        <tx:method name="find*" read-only="true"/>

        <tx:method name="select*" read-only="true"/>

        <tx:method name="*"/>

    </tx:attributes>

</tx:advice>