Bootstrap

Spring【六】数据访问与事务处理

目录

一、Spring JDBC模板类(JdbcTemplate)

1 独立使用

2 结合容器使用

3.关于JdbcTemplate的事务行为

二、Spring DAO支持类(DaoSupport)

三、Java事务处理

四、Spring事务管理

1.Spring编程式事务示例

2.事务定义(TransactionDefinition)的属性

2.1 事务传播行为

2.2 事务隔离级别

2.3 超时设置

五、基于数据源事务管理器的编程式事务

六、基于数据源事务管理器的声明式事务

1.单个Bean的事务代理

2.配置事务拦截器与自动代理方式

3.使用标签配置拦截器

4.全注解(@Transactional)

七、@Transactional注解事务的失效场景整理

1.异常事务的配置导致

2.事务方法导致

3.数据库本身导致

4.SSM项目中事务控制层的Bean被重复扫描


一、Spring JDBC模板类(JdbcTemplate)

JDBC的Connection接口查询和操作数据的步骤包括:打开连接进行SQL预编译执行SQL语句获取结果集解析结果集释放资源和关闭连接。Spring使用JDBC模板类(JdbcTemplate)简化了数据操作,应用程序只需要提供SQL语句和对结果进行解析就可以了,其他模板化的代码(例如获取关闭连接、资源释放以及异常处理)由模板类自动处理。

JDBC除了定义Connection接口,还定义了数据源接口DataSource。数据源比数据连接的功能更多,除了包括数据连接之外,还支持数据库连接池和分布式事务的实现。Spring对JDBC的封装就是通过定义DataSource接口的实现类,DriverManagerDataSource驱动管理数据源实现的,这个类位于spring-jdbc模块中。Spring在DriverManagerDataSource基础上对JDBC操作进行了封装,提供了数据操作的模板类JdbcTemplate,该模板类简化了数据访问方法。此外Spring还提供了RowMapper接口,可以很容易地将返回地数据结果映射成Java类型对象。JdbcTemplate和DriverManagerDataSource可以独立于容器使用,但常见的使用方式是配置成Bean交由容器管理。

1 独立使用

public static void main(String[] args) throws SQLException {
		String url = "jdbc:mysql://localhost:3306/mystudy";
		String username = "root";
		String password = "root";
		DriverManagerDataSource dataSource = new DriverManagerDataSource(url, username, password);

		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		String sql = "select * from t_user";
		List<User> userList = jdbcTemplate.query(sql, new RowMapper<User>() {

			@Override
			public User mapRow(ResultSet rs, int rowNum) throws SQLException {
				int id = rs.getInt("id");
				String name = rs.getString("name");
				String sex = rs.getString("sex");

				return new User(id, name, sex);
			}

		});
}

上面示例中调用JdbcTemplate对象的query(String, RowMapper)方法来查询数据。其中第一个参数是SQL语句,第二个参数是实现了RowMapper接口的匿名内部类,该内部类通过实现mapRow方法将每一行数据库数据映射成User类型的对象。

JdbcTemplate提供了数据库查询、更新和删除等方法,有的方法对应多个不同参数类型的重载方法,具体如下:

  • query():查询数据,有多个重载方法,返回类型包括对象类型(使用结果解析器ResultSetExtractory解析)和列表类型(使用行映射器RowMapper对每行结果进行映射)的结果
  • queryForMap():返回Map<String,Object>类型的结果。Map的键是查询得到列名的小写,值是该列的值。
  • queryForObject():返回Java对象类型的结果
  • queryForList():返回列表类型的结果。这里还可以返回List<Map<String,Object>>类型的列表,用于查询多行多列数据,List中存储每一行的数据,Map中存储改行中每一列的数据(键是列名小写,值为列的值)。
  • queryForRowSet():返回SqlRowSet类型的结果,SqlRowSet是Spring提供的结果类型接口,实现类是ResultSetWrappingSqlRowSet,该类型除了包括标准的结果集类型ResultSet成员属性外,还包括数据表及栏位的一些属性,比如字段的类型,长度和表名等
  • update():执行更新语句,返回更新的行数
  • batchUpdate():批量更新
  • execute():数据操作的通用方法
  • call():执行可调用类型语句(CallableStatement),比如调用存储过程

其中查询方法比较多,根据查询得到不同的数据,可以将其存储到不同的类型的容器上返回。查询方法比较多样,而增删改操作都是由update及其重载方法完成的,下面两个三个方法中,第一个用于执行不存在“?”占位符的sql语句,后两个用于处理sql语句中存在占位符的情况,通过方法中的args传递参数。如下:

public int update(final String sql) throws DataAccessException {//……}

public int update(String sql, @Nullable Object... args) throws DataAccessException {
    return update(sql, newArgPreparedStatementSetter(args));
}

public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
    return update(sql, newArgTypePreparedStatementSetter(args, argTypes));
}

其中的参数设置都是通过实现Spring提供的PreparedStatementSetter接口完成的,该接口定义如下,我们可以自定义该接口的实现类,然后实现其中的setValues方法,我们可以通过该方法提供的PreparedStatement对象将参数设置进去:

public interface PreparedStatementSetter {
    void setValues(PreparedStatement ps) throws SQLException;
}

除此之外Spring还将创建PreparedStatement对象的逻辑也抽取到了另一个接口PreparedStatementCreator中去,该接口方法为我们提供了Connection对象实例:

public interface PreparedStatementCreator {
    PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}

除此之外其中一个更新方法中还存在一个KeyHolder接口类型的参数,我们可以通过它来获取自增的主键值(update方法返回的是更新成功的行数),如下:

public static int update(JdbcTemplate jdbcTemplate) {
    KeyHolder keyHolder = new GeneratedKeyHolder();
    String sql = "INSERT INTO t_user(name, sex) VALUES(?,?)";
    int res = jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
            PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            ps.setString(1, "洪七公");
            ps.setString(2, "男");
            return ps;
        }
    }, keyHolder);
    System.out.println("新记录:" + keyHolder.getKey().intValue());
    return res;
}

2 结合容器使用

DriverManagerDataSource和JdbcTemplate更常见的用法是配置在容器中,由容器进行管理。若采用XML方式可以进行如下配置:

<bean id = "dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://localhost:3306/mystudy"></property>
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
</bean>

然后可以在自定义的Dao类中注入JdbcTemplate对象完成操作。(需要注意的是JdbcTemplate这个类中没有dataSource这个成员,这里通过<property>元素注入,Spring会找到setDataSource方法将其注入。而该方法继承自父类JdbcAccessor)

DriverManagerDataSource虽然实现了DataSource接口,但是没有实现连接池的功能,每次获取连接时都是简单地创建一个连接。一般我们会使用其他厂商提供的数据源,比如DBCP的BasicDataSource或C3P0的数据源等等,此时我们只需要切换JdbcTemplate依赖的数据源Bean即可。此外,Spring还提供了JndiObjectFactoryBean用于JNDI查找并使用容器数据源(比如WebLogic管理的数据源),只需要配置该类中jndiName属性即可。示例如下:

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="……"></property>
</bean>

3.关于JdbcTemplate的事务行为

JDBC规范规定,连接对象建立时应该处于自动提交模式,也就是使用JDBC接口的数据操作执行完成默认会自动提交事务,同样使用JdbcTemplate也会自动提交事务。Connection提供了setAutoCommit()方法关闭事务自动提交,但JdbcTemplate和驱动管理数据源(DriverManagerDataSource)都没有直接提供关闭事务自动提交的方法,所以要关闭自动提交就要到Connection层级操作。如下:

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);    //DriverManagerDataSource
Connection connection = jdbcTemplate.getDataSource().getConnection();
connection.setAutoCommit(false); // 关闭自动提交事务
String sql = "SELECT name, sex FROM t_user WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "1");
pstmt.execute();
pstmt.close();
connection.close();

Spring的DriverManagerDataSource类型数据源没有提供关闭自动提交的配置,但DBCP2的连接池数据源提供了关闭自动提交的配置,使用defaultAutoCommit属性直接配置即可。如下:

<bean id="dbcpdataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    <!-- 省略数据库驱动、url、账号密码的配置 ,如下为关闭自动提交事务的配置-->
    <property name="defaultAutoCommit" value="false"></property>
</bean>

二、Spring DAO支持类(DaoSupport)

DaoSupport是Spring定义的DAO的通用基类,该类中定义了DAO初始化的模板方法。该抽象类实现了InitializingBean接口,会在Bean初始化时进行DAO相关配置(比如数据库连接属性)的检查。该类还定义了日志的成员属性,继承该类的DAO实现类就可以直接使用logger成员属性记录日志,而不需要在每个DAO中单独定义。

public abstract class DaoSupport implements InitializingBean {
    protected final Log logger = LogFactory.getLog(getClass());
    
    @Override
    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        checkDaoConfig();

        try {
	    initDao();
	}catch (Exception ex) {
	    throw new BeanInitializationException("Initialization of DAO failed", ex);
	}    
    }
    
    protected abstract void checkDaoConfig() throws IllegalArgumentException;
    protected void initDao() throws Exception {}
}

Spring默认提供了JDBC和Hibernate的DAO支持子类:JdbcDaoSupport和HibernateDaoSupport。JdbcDaoSupport包含依据数据源创建JdbcTemplate和获取JdbcTemplate对象的方法。继承JdbcDaoSupport类的DAO实现类不需要再依赖JdbcTemplate类型对象,而直接依赖DataSource即可,JdbcTemplate可以通过getJdbcTemplate()方法获取(JdbcDaoSupport也支持直接传入JdbcTemplate依赖)。如下创建UserDaoSupportImpl示例:

public class UserDaoSupportImpl extends JdbcDaoSupport implements UserDao {

	@Override
	public User getUser(int id) {
		User user = null;
		JdbcTemplate jdbcTemplate = getJdbcTemplate();
		// 使用jdbcTemplate查询数据
		return user;
	}
}

此时可以省去对jdbcTemplate的配置,直接配置DAO类即可,在DAO实现类中注入数据源,如下:

<bean id="userDaoSupport" class="com.mec.spring.db.UserDaoSupportImpl">
    <property name="dataSource" ref="dataSource"></property>
</bean>

使用注解的配置方式,如下:

@Repository("userDaoSupport")
public class UserDaoSupportImpl extends JdbcDaoSupport implements UserDao {
	@Autowired
	private DataSource dataSource;

	@Override
	public User getUser(int id) {
		// ……
	}

	// 注解的方法会在Bean构造函数之后执行,可用这个注解配置刚实例化完毕后的初始化方法,它在Autowired注入完毕后执行
	@PostConstruct
	public void initialize() {
		setDataSource(dataSource);
	}

}

由于DataSource并不是JdbcDaoSupport这个抽象类的属性,它是通过setDataSource(dataSource)方法将其设置进去的 ,且该方法是final修饰的,子类不可重写。所以此处利用@PostConstruct注解配置初始化方法。

通过DaoSupport减少了业务代码的依赖层次和配置量,此外,DaoSupport还提供了检查、异常转换和日志等功能,简化了DAO实现类的方法。但继承特定的框架类,增加了系统的耦合性,这是Spring框架不提倡的。

三、Java事务处理

从事务管理范围的角度,事务划分为本地事务和分布式事务,两者最大的差别就是数据库的数量。本地事务是传统的也是最常用的事务管理,只有单一的数据库,但本地事务无法解决分布式场景的事务问题。因为涉及多个数据库,数据库类型还可能不一样,仅靠数据库本身是不够的,需要一个专门的事务管理器来协调不同数据库之间的事务。为协调多个数据库的事务同步,X/Open组织提出了分布式事务处理参考模型(DTP模型),该模型定义了三个组件和两个协议。

三个组件分别是:应用程序(AP, Application Program)、数据库资源管理器(RM, Resource Manager)和事务管理器(TM, Transaction Manager)。两个协议是:XA协议(应用事务管理之间的通信接口)、TX协议(全局事务管理器与资源管理器之间的通信接口)。其中XA协议中定义了两段式提交的方式,即第一阶段看各个资源管理器是否都准备好了提交方式;第二阶段确认都准备好了后就提交,如果有一个不同意就回滚。全局事务是标准的分布式事务解决方案。

对应本地事务和分布式事务,Java分别提供了JDBC事务JTA事务的实现方式。JDBC事务使用java.sql.Connection的commit()和rollback()等方法实现提交或回滚事务。JDBC事务限定在单一的数据库连接上,不能跨多个数据库,无法处理多数据源和分布式事务。

JTA事务(Java Transaction API)是Java官方定义的事务处理接口,支持分布式事务处理。JTA只是规范接口,具体实现组件由各应用服务提供。主要接口和类如下:

  • UserTransaction:开发者使用的接口;
  • TransactionManager:事务管理接口,由各厂商提供实现组件;
  • Transaction:事务接口,各厂商提供实现组件;
  • XAResource:XA协议资源接口,各厂商提供实现组件。

四、Spring事务管理

Spring本身并没有实现事务管理,但提供了对JDBC事务和JTA事务两种事务类型实现技术的统一封装接口。Spring事务封装接口支持的具体事务处理技术包括JTA、JDBC、Hibernate、JPA和JDO(Java Data Objects)等。Spring为事务管理提供的接口主要有TransactionDefinitionPlatformTransactionManager

TransactionDefinition(事务属性)接口定义了Spring处理事务的属性,再该接口中主要定义了事务传播行为的类型(PROPAGATION)、事务隔离级别的类型(ISOLATION)、事务超时设置(TIMEOUT_DEFAULT)及获取是否只读(isReadOnly())

PlatformTransactionManager(事务管理器,相当于一个管理员,帮我们控制事务)则提供了获取TransactionStatus对象,以及使用TransactionStatus类型对象进行提交和回滚的方法。TransactionStatus是事务运行状态的接口,用来标识事务对象是否为新事务、是否有保存点(Savepoint)、是否为rollback-only事务或者事务是否已经执行完成。事务管理器通过TransactionStatus获取事务的状态信息,进行事务的控制。

使用以上接口及实现类,调用相关方法可以在应用程序中进行编程式事务。此外,Spring处理事务更强大的地方是支持声明式事务处理,特别是使用@Transactional注解可以灵活地在类和方法层级中控制事务,简单、易用。

1.Spring编程式事务示例

public static void main(String[] args) {
    String url = "jdbc:mysql://localhost:3306/mystudy";
    String username = "root";
    String password = "root";
    DriverManagerDataSource dataSource = new DriverManagerDataSource(url, username, password);
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    // 定义事务管理器
    PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
    // 定义事务属性
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    // 设置事务传播行为
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    // 开启事务
    TransactionStatus status = transactionManager.getTransaction(def);
    try {
        // 执行业务操作
        jdbcTemplate.update("sql语句", "value");
        jdbcTemplate.update("sql语句", "value");
        // 提交事务
        transactionManager.commit(status);
    } catch (DataAccessException e) {
	// 回滚事务
	transactionManager.rollback(status);
    }
}

上述流程主要分为如下几个步骤:

  1. 定义事务管理器PlatformTransactionManager:它有多个实现类,如下:
    1. DataSourceTransactionManager:用指定数据源地方式操作数据库。由于上面我们使用JdbcTemplate操作数据库,需要指定数据源,所以使用这个事务管理器。MyBatis框架的事务处理也使用它。
    2. JpaTransactionManager:如果用jpa来操作数据库,那么就需要该事务管理器帮我们控制事务。
    3. HibernateTransactionManager:使用Hibernate操作数据库时使用。
    4. JtaTransactionManager:使用JTA来操作数据库时使用。
  2. 定义事务属性TransactionDefinition:设置事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等。
  3. 开启事务:调用事务管理器地getTransaction方法就可以开启一个事务。该方法返回一个TransactionStatus表示事务状态的一个对象,通过TransactionStatus提供的一些方法可以用来控制事务的一些状态。
  4. 执行业务操作。

2.事务定义(TransactionDefinition)的属性

TransactionDefinition是事务定义的接口,用于定义事务的属性,包括是否可读、事务隔离级别和事务传播行为等。其中最常用的两个属性就是事务传播行为和事务隔离级别。

2.1 事务传播行为

事务传播行为是Spring提出来的概念,是指多个事务方法在嵌套调用时的事务处理行为,是使用传播过来的原有事务,还是新开事务或者不使用事务。事务传播行为增强了代码中事务开发的功能。TransactionDefinition接口定义了7种类型的事务传播行为,具体如下:

传播行为属性描述
PROPAGATION_REQUIRED0使用当前事务,如果当前没有事务就新建一个。这是Spring默认的传播行为
PROPAGATION_SUPPORTS1支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY2使用当前事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW3新建事务,如果当前存在事务,就把当前事务挂起
PROPAGATION_NOT_SUPPORTED4以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER5以非事务方式执行,如果当前存在事务,就抛出异常
PROPAGATION_NESTED6如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作

表中新建事务(PROPAGATION_REQUIRES_NEW)和嵌套事务(PROPAGATION_NESTED)都会新建子方法的事务,两者的差别如下:

  • PROPAGATION_REQUIRES_NEW会启动一个不依赖于当前环境的事务,这个事务拥有自己的隔离范围和锁等。如果外部没有事务,则新建事务;如果存在外部事务,当内部事务开始执行时,外部事务将被挂起,内部事务执行结束,外部事务将继续执行。它的使用场景之一就是日志记录。
  • PROPAGATION_NESTED是严格意义上的父子事务。如果当前没有事务,其行为和PROPAGATION_REQUIRES_NEW一样,如果当前存在事务,则会以嵌套父子事务方式执行。子事务嵌套在父事务中执行,属于父事务的一部分。Spring在进入子事务之前,父事务会建立一个保存点(Save Point),然后执行子事务,执行逻辑如下:
    • 如果子事务提交,需要等外部事务提交,则子事务才会被提交;如果外部事务回滚,无论子事务是否被成功提交,都会回滚。
    • 如果子事务回滚,父事务会回滚到进入子事务前建立的保存点,如果父事务中的方法捕获异常并进行了处理,则会继续进行其他业务逻辑父事务,之前的操作不会受到影响。

2.2 事务隔离级别

Spring在TransactionDefinition接口中定义了5种隔离级别属性,这5个属性与java.sql.Connection事务隔离级别属性基本是对应的。如下:

事务隔离属性JDBC Connection属性描述
ISOLATION_DEFAULTTRANSACTION_NONE-1或0默认值。前者值为-1,后者值为0
ISOLATION_READ_UNCOMMITTEDTRANSACTION_READ_UNCOMMITTED1读未提交
ISOLATION_READ_COMMITTEDTRANSACTION_READ_COMMITTED2读已提交
ISOLATION_REPEATABLE_READTRANSACTION_REPEATABLE_READ4可重复读
ISOLATION_SERIALIZABLETRANSACTION_SERIALIZABLE8串行化。解决所有读问题(脏读、幻读、不可重复度)

2.3 超时设置

在TransactionDefinition种还可以设置事务的超时等属性。超时设置是指单个事务所被允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动执行回滚事务。在TransactionDefinition中以int类型的值来设置超时时间,默认是-1,表示没有限制。

五、基于数据源事务管理器的编程式事务

DataSourceTransactionManager事务管理器用于处理本地事务,可以使用在JDBC Connection、Spring JdbcTemplate和MyBatis中。在Spring开发中,事务管理器一般交由容器管理。通过属性注入的方式注入数据源。配置如下:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

DataSourceTransactionManager可以使用在JdbcTemplate层级,也可以使用在底层的java.sql.Connection层级,如下示例是使用在Connection层级。

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    context.registerShutdownHook();

    // 获取事务管理器
    PlatformTransactionManager transactionManager = (DataSourceTransactionManager) context
				.getBean("transactionManager");
    // 定义事务对象
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    // 设置传播行为
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    // 事务状态对象
    TransactionStatus status = transactionManager.getTransaction(def);
    // 获取数据源
    DataSource dataSource = (DataSource) context.getBean("dataSource");
    // 获取连接
    Connection conn = DataSourceUtils.getConnection(dataSource);

    // ……定义sql语句、进行预编译、设置参数、执行sql

    // 提交事务
    transactionManager.commit(status);
    DataSourceUtils.releaseConnection(conn, dataSource); // 释放连接
}

上述代码通过Connection对象操作数据库,然而在提交事务的时候却没有使用该Connection对象。这是因为框架内部自己可以找到对应的Connection对象,框架内部使用TransactionSynchronizationManager维护线程和Connection对象的对应。连接获取的主要细节如下:

使用transactionManager的getTransaction(def)获取TransactionStatus时会创建DataSourceTransactionObject的事务类,该类是DataSourceTransactionManager的静态内部类,继承自JdbcTransactionObjectSupport抽象类,该类中维护了ConnectionHolder用来维护Connection对象及创建保存点。

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException {
    TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
    Object transaction = doGetTransaction();

    //……省略一些代码
    else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED     ||
    def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
    def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {

	SuspendedResourcesHolder suspendedResources = suspend(null);
	if (debugEnabled) {
	    logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
	}
	try {
	    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            //该DefaultTransactionStatus对象中存储了transaction对象
	    DefaultTransactionStatus status = newTransactionStatus(def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
	    doBegin(transaction, def);
	    prepareSynchronization(status, def);
	    return status;
	}catch (RuntimeException | Error ex) {
	    resume(null, suspendedResources);
	    throw ex;
	}
    }
    //……省略一些代码
}

上面时getTransaction(def)方法,该方法中首先调用doGetTransaction()获取到DataSourceTransactionObject对象,其中逻辑如下:

protected Object doGetTransaction() {
    DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    txObject.setSavepointAllowed(isNestedTransactionAllowed());
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());    //这里内部是先从ThreadLocal对象中获取到一个Map<Object,Object>(假设就叫map对象),接着从该map对象中获取ConnectionHolder对象
    txObject.setConnectionHolder(conHolder, false);
    return txObject;
}

其中obtainDataSource()方法获取的就是我们注入的数据源,该数据源维护在DataSourceTransactionManager类中,紧接着将ConnectionHolder对象保存在DataSourceTransactionObject对象中。注意此时并没有创建Connection对象。接下来在doBegin(transaction,def)方法中获取连接Connection对象并保存至transaction(DataSourceTransactionObject)对象中,而且关闭了connection自动提交事务。

接着在示例代码中,我们从容器中获取到数据源对象,然后利用DataSourceUtils创建了一个连接Connection对象。DataSourceUtils是Spring提供的一个工具类,通过它获取连接时,其大致流程为:根据dataSource对象获取当前线程中(ThreadLocal中获取)的ConnectionHolder对象,这个跟上面doGetTransaction方法中获取的方式一致(获取到的ConnectionHolder对象也是一致的)。如果该ConnectionHolder对象存在且保存了Connection对象则返回该Connection对象,否则创建连接并保存至ConnectionHolder对象中。

接着我们拿到Connection对象之后我们进行了一些数据库的操作,然后利用transactionManager对象提交了事务。提交事务的核心方法如下:

protected void doCommit(DefaultTransactionStatus status) {    
    //在实例化DefaultTransactionStatus对象时保存了DataSourceTransactionObject对象
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    if (status.isDebug()) {
        logger.debug("Committing JDBC transaction on Connection [" + con + "]");
    }
    try {
	con.commit();
    }catch (SQLException ex) {
        throw new TransactionSystemException("Could not commit JDBC transaction", ex);
    }
}

Spring提供了事务处理的模板类TransactionTemplate,与JdbcTemplate简化JDBC的操作类似,该模板类简化了事务的操作。需要提交的执行语句使用TransactionCallback回调接口或TransactionCallbackWithoutResult回调接口实现。

  • TransactionCallback:实现该接口 T doInTransaction(TransactionStatus status)方法定义需要返回的事务管理的操作代码。
  • TransactionCallbackWithoutResult:继承自TransactionCallback接口,提供 void doInTransactionWithoutResult(TransactionStatus status)接口用于不需要返回值的事务操作代码。

事务模板通过new TransactionTemplate(txManager)创建,构造器参数类型是PlatformTransactionManager。模板类提供了设置事务隔离级别、传播行为等事务属性的方法,其本身设置了这些属性的默认值,比如传播行为的默认值是PROPAGATION_REQUIRED。示例如下:

// 获取事务管理器
PlatformTransactionManager transactionManager = (DataSourceTransactionManager) context.getBean("transactionManager");
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
// 设置传播行为
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 设置隔离级别
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    @Override
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        // 执行业务操作,如jdbcTemplate.query等
    }
});

除过execute方法外,transactionTemplate还提供了一个executeWithoutResult(Consumer<TransactionStatus> action)方法来执行业务操作, 该方法没有返回值,需要传递一个Consumer对象,在accept方法中做业务操作。通过这两个方法,事务管理器会自动提交事务或者让事务回滚。

  • 事务回滚:在execute方法或者executeWithoutResult方法内部执行transactionStatus.setRollbackOnly()将事务状态标注为回滚状态,Spring会自动让事务回滚;或者在这两个方法内部抛出任意异常也可以回滚。
  • 事务提交:在execute方法或者executeWithoutResult方法内部没有抛出异常并且没有显示调用transactionStatus.setRollbackOnly()方法。

当然我们还可以将事务模板类配置成Bean交由容器管理,如下:

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager"></property>
    <property name="isolationLevelName" value="ISOLATION_READ_COMMITTED"></property>
    <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property>
    <property name="timeout" value="30"></property> <!-- 超时设置 -->
</bean>

六、基于数据源事务管理器的声明式事务

Spring使用代理机制实现事务处理,在不影响源代码的基础上进行事务管理。Spring通过AOP完成了非侵入式编码的事务实现,通过对方法执行前后进行拦截,在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或回滚事务,从而达到不需要编写事务代码的声明式事务。

声明式事务不需要在业务逻辑代码中掺杂事务管理代码,只需要在配置文件中做相关的事务规则声明(或在类和方法中使用@Transactional注解的方式),可以将事务规则应用到业务逻辑中。声明式事务在开发上更加简洁、方便,一个普通的方法只需要加上注解就可以得到安全的事务支持。声明事务的唯一不足是事务管理的细度最多只能到方法级别,而无法细到码段。@Transactional注解是使用最频繁也是最简洁的声明式事务的方式。

1.单个Bean的事务代理

代理是对目标对象构造一个代理对象,使用Java反射机制调用目标方法,在目标方法执行前后执行额外的操作。Spring使用TransactionProxyFactoryBean作为事务处理的代理类,该类的属性包括:transactionManager(事务管理器)、target(目标对象)和transactionAttributes(事务属性,比如方法匹配与传播行为的对应)。以实现UserDao接口的UserDaoImpl类型对象的事务代理Bean为例,配置如下:

<!-- UserDao的事务代理Bean配置 -->
<bean id="userDaoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <!-- 事务管理器 -->
    <property name="transactionManager" ref="transactionManager"></property>
    <!-- 代理的目标对象 -->
    <property name="target" ref="userDaoImpl"></property>
    <!-- 代理类需要实现的接口 -->
    <property name="proxyInterfaces" value="com.mec.spring.db.UserDao"></property>
    <!-- 配置事务属性 -->
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly,-OneException,+TwoException</prop> <!-- 事务传播属性 -->				
        </props>
    </property>
</bean>

上述配置中UserDao是用户类型的DAO接口,userDaoImpl是该接口的实现类实例,实现类实现接口的getUser方法。proxyInterfaces设置接口的全路径名(也可以省略)。Spring默认支持接口的代理,整合CGLIB后也可以对具体类生成代理,类代理需要注意设置proxyTargetClass属性为true。对于上述事务属性的配置,<prop>元素可以有多个。上述配置表示,

  • 当执行代理对象中以get为前缀的方法时会新建一个事务(PROPAGATION_REQUIRED行为);
  • readOnly表示获取时只读,一般用于查询的方法;
  • -OneException表示有OneException异常抛出时就回滚(-表示回滚),不是说只抛出OneException 异常时,事务才回滚,如果程序抛出RuntimeException和Error时,事务一样会回滚,即使这里没有配置,因为Spring中默认对所有的RuntimeException和Error都会回滚事务
  • +TwoException表示即使有TwoException异常抛出时依然提交事务(+表示提交事务)。

在实际开发中,如果每个需要事务处理的DAO都配置代理Bean会很麻烦,可以利用Spring容器抽象父Bean的特性,定义一个抽象的TransactionProxyFactoryBean类型的父Bean,注入transactionManager和transactionAttributes属性。其他需要事务代理的DAO Bean继承该父Bean,在各自Bean配置中注入目标DAO实现类对象。事务代理父Bean可以设置abstract属性为true。配置示例如下:

<bean id="transactionBase" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" lazy-init="true" abstract="true">
    <property name="transactionManager" ref="transactionManager"></property>
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>			
        </props>
    </property>
</bean>
<!-- 继承父Bean -->
<bean id="userDaoProxy" parent="transactionBase">
    <!-- 代理目标Bean -->
    <property name="target" ref="userDaoImpl"></property>
</bean>

配置完成之后如果要进行UserDao操作,那么从容器中获取userDaoProxy即可,执行其中方法会开启事务。

2.配置事务拦截器与自动代理方式

以上事务代理方式虽然不会修改原有的逻辑代码,但需要配置代理Bean或者与代理Bean的继承关系,业务代码的耦合性也增强了。为此,Spring使用AOP方式将负责事务操作的增强处理植入目标Bean的业务方法中,通过自动代理创建器(AutoProxyCreator)对满足条件的对象自动创建代理对象,并使用事务拦截器(TransactionInterceptor)实现事务处理

事务拦截器的Bean配置和TransactionProxyFactoryBean的Bean配置类似,需要指定transactionManager和transactionAttributes,但不需要目标对象,因为代理对象通过AutoProxyCreator自动创建,BeanNameAutoProxyCreator继承自BeanPostProcessor,Bean实例化后,使用回调AbstractAutoProxyCreator#postProcessAfterInitialization完成代理的创建。

AbstractAutoProxyCreator是自动代理构建器的超类,InfrastructureAdvisorAutoProxyCreator(基础设施)是Spring内部使用的构建器,Spring对外提供的自动创建代理的方式有如下3种:

  • BeanNameAutoProxyCreator:匹配Bean名称自动创建匹配到的Bean代理。
  • AnnotationAwareAspectJAutoProxyCreator:根据Bean种的AspectJ注解自动创建代理。
  • DefaultAdvisorAutoProxyCreator:根据Advisor的匹配机制自动创建代理,会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中。

如下以BeanNameAutoProxyCreator为例进行事务拦截器和代理创建器的配置:

<!-- 配置事务拦截器 -->
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
    <!-- 事务管理器 -->
    <property name="transactionManager" ref="transactionManager"></property>
    <!-- 事务属性 -->
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>
	</props>
    </property>
</bean>
	
<bean id="bnapc" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">	<!-- Bean的名字匹配规则 -->
        <list>
	    <value>*DaoImpl</value>	<!-- 匹配以DaoImpl结尾的类 -->
	</list>
    </property>
    <!-- 拦截器依赖 -->
    <property name="interceptorNames">  <!--以interceptorNames指定拦截器的名字-->
        <list>
            <!-- 上面定义的事务拦截器 ,此处也可以有多个事务拦截器-->
	    <value>transactionInterceptor</value>
	</list>
    </property>
</bean>

通过上述配置由于Spring自动帮我们构建代理,所以省去了之前userDaoProxy的配置,此时只需要配置目标Dao类即可,如下:

<bean id="userDaoImpl" class="com.mec.spring.db.UserDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>

这样我们通过容器获取这个userDaoImpl,执行其中方法时框架会自动创建代理执行并进行事务操作。

3.使用标签配置拦截器

除了以<bean>方式配置拦截器和代理创建外,Spring还提供了AOP的相关标签:<aop:config><aop:advice>。这两个标签分别用于配置切面和增强。在<aop:config>标签中使用<aop:pointcut>子标签添加切点,切点支持切点表达式定义;<aop:davisor>用于在切点植入事务的增强。配置示例如下:

<!-- 事务增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- 所有get前缀的方法开启事务 -->
        <tx:method name="get*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- 切面定义 -->
<aop:config>
    <aop:pointcut id="interceptorPointCuts" expression="execution(* com.mec.spring.db.UserDaoImpl.getUser(int))"/>
    <!-- 植入增强 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="interceptorPointCuts"/>
</aop:config>

<tx:method>标签用于配置前缀匹配方法的事务行为,propagation属性是事务传播行为。除此之外,可以设置的属性还有isolation(事务隔离级别)、rollback-for(回滚异常)、read-only(事务是否只读)及timeout(超时设置)。

注意:使用aop和tx标签需要在配置文件根节点<beans>中添加aop和tx的命名空间及对应的约束,两种命名空间定义如下:

xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"

命名空间与文档结构定义地址的对应如下:

http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd

使用aspectj需要添加依赖,如下:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${aspectj.version}</version>    <!--这里的版本为1.8.8-->
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
</dependency>

4.全注解(@Transactional)

除了上述方式,Spring提供了更便捷的全注解方式,只需要在配置文件中开启事务注解驱动,就可以在类和方法中使用@Transactional注解控制事务。事务注解驱动开启的配置如下:

<!-- transactionManager属性指定的是事务管理器Bean的id。如果该Bean的id是transactionManager则可以省略 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" />

proxy-target-class属性值决定是基于接口还是类的代理被创建。基于类的代理需要导入CGLIB包。proxy-target-class属性设置为false或者不设置该属性时,默认使用JDK内置代理,但如果运行类没有继承接口,Spring也会自动使用CGLIB基于类的代理。proxy-target-class属性设置为true时则强制 使用基于类的代理。一般该属性也不需要设定,Spring会自动处理。完成上述配置之后,就可以在类和方法中使用@Transactional注解实现事务控制

@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
@Override
public User getUser(int id) {
    //……
}

以上注解中,propagation设置事务传播级别;readOnly限定connection级别的读写特性,默认为false,表示支持读写,如果为true,则执行插入或者更新语句会抛出异常。除了这两个属性外,@Transactional注解可以设定的属性如下:

属性类型默认值描述
valueString 指定使用的事务管理器,是transactionManager属性的别名
propagationPropagation的枚举类型Propagation.REQUIRED设置事务传播行为
isolationIsolation的枚举类型Isolation.DEFAULT(-1)设置事务隔离级别
readOnlybooleanfalse读写或只读事务
timeoutintTransactionDefinition.TIMEOUT_DEFAULT(-1)设置事务超时时间,默认永不超时
rollbackForClass对象数组空数组需要回滚的异常类数组
rollbackForClassName类名数组空数组需要回滚的异常类名字数组
noRollbackForClass对象数组空数组不需要回滚的异常类数组
noRollbackForClassName类名数组空数组不需要回滚的类名字数组

@Transactional注解虽然可以使用在接口、接口方法、类及类方法中,但需要注意的是:

  • 尽量使用在类和类方法中,不建议使用在接口或者接口方法中。如果使用在接口中只有基于接口的代理时才会生效。(使用在类上时该类以及其所有无下限子类中所有public方法将被Spring自动加上事务)
  • 使用在public方法中。这是由Spring AOP的特性决定的,外部方法调用才会被AOP代理捕获,内部方法之间的调用不会被处理。如果使用在protected和private等方法中,事务将会被Spring忽略,而且不会抛出任何异常。
  • 方法级别会覆盖父级别的设定。

Spring基于反射机制拦截@Transactional注解。进行事务处理,如果是在方法中使用@Transactional注解,则该方法的类会被代理。Spring基于接口的代理类型是

org.springframework.aop.framework.JdkDynamicAopProxy,所以,以被注解类为参数,调用容器的getBean方法是无法获取代理的Bean的。

七、@Transactional注解事务的失效场景整理

1.异常事务的配置导致

在项目中Dao层负责数据库操作,但事务通常配置在service层以保证业务逻辑数据的正确性。在开启事务的状况下,Spring默认会在Unchecked异常时回滚事务其他类型异常时提交事务,也可以自定义异常状况的事务处理。JDBC默认自动提交是开启的,也就是在方法执行完成后会自动提交。

使用@Transaction注解,propagation属性不设置或者设置以事务方式运行时(值不为NOT_SUPPORTEDNEVER),Spring事务管理器默认会捕捉任何未处理(Unchecked)的异常,在事务执行上下文抛出Unchecked异常时回滚事务。但抛出Checked类型的异常时不会及进行事务回滚,而是提交。如果不想使用默认的异常事务行为,使用@Transaction的rollbackFornotRollbackFor属性可以指定回滚不回滚的异常类型(Unchecked和Checked异常配置都可以配置)。特别的,如果在@Transaction注解的方法中使用try-catch捕获异常并处理运行时异常,则事务也会提交

2.事务方法导致

  1. 因为Spring使用AOP控制事务,其底层是代理机制,也就是说当前事务方法的调用是被当前类的代理类来调用的,只有这样事务才能生效,所以要确保该事务方法所在的类被代理了。
  2. 事务方法必须是public修饰的。private、static、final修饰的方法事务不生效。
  3. 非事务方法调用本类中的另外一个事务方法也会导致事务不生效。这种可以通过如下两个方式解决:
    1. 该类内部注入一个自己的Bean(比如UserService类中注入userService对应的Bean),通过该成员显示调用这个事务方法;
    2. 利用AOP上下文来获取代理对象,通过该代理对象调用该事务方法。(比如:(UserService)AopContext.currentProxy().事务方法();)需要注意的是Aop上下文Spring是默认关闭的,需要手动开启。
  4. 确保业务和事务方法入口在同一个线程中,否则事务也不会生效。

3.数据库本身导致

事务功能需要数据库引擎的支持,比如MySQL数据库,表的定义要使用支持事务的InnoDB引擎,如果是MyISAM,事务是不起作用的。

4.SSM项目中事务控制层的Bean被重复扫描

SSM项目中我们通常会存在两个核心配置(一个spring和一个mvc的配置)。通常在Spring的配置文件中我们会扫描注册非Controller的类,而在Spring MVC的配置中会只扫描Controller层的Bean。如下:

springmvc.xml:

<context:component-scan base-package="pers.xy.bs" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

applicationContext.xml

<context:component-scan base-package="pers.xy.bs" use-default-filters="false">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

上述两个配置中分别使用<context:include-filter><context:exclude-filter>控制包扫描。前者是告诉容器只需要扫描指定包及其子包下被Controller注解的类,这一点能够实现的前提是use-default-filters属性需要设置为false,默认为true。如果为true,那么即使利用<context:include-filter>进行过滤,那么该过滤也不会生效,所以使用这两个标签时一定要关闭默认的过滤器。

;