目录
一、Spring JDBC模板类(JdbcTemplate)
2.事务定义(TransactionDefinition)的属性
一、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为事务管理提供的接口主要有TransactionDefinition和PlatformTransactionManager。
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);
}
}
上述流程主要分为如下几个步骤:
- 定义事务管理器PlatformTransactionManager:它有多个实现类,如下:
- DataSourceTransactionManager:用指定数据源地方式操作数据库。由于上面我们使用JdbcTemplate操作数据库,需要指定数据源,所以使用这个事务管理器。MyBatis框架的事务处理也使用它。
- JpaTransactionManager:如果用jpa来操作数据库,那么就需要该事务管理器帮我们控制事务。
- HibernateTransactionManager:使用Hibernate操作数据库时使用。
- JtaTransactionManager:使用JTA来操作数据库时使用。
- 定义事务属性TransactionDefinition:设置事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等。
- 开启事务:调用事务管理器地getTransaction方法就可以开启一个事务。该方法返回一个TransactionStatus表示事务状态的一个对象,通过TransactionStatus提供的一些方法可以用来控制事务的一些状态。
- 执行业务操作。
2.事务定义(TransactionDefinition)的属性
TransactionDefinition是事务定义的接口,用于定义事务的属性,包括是否可读、事务隔离级别和事务传播行为等。其中最常用的两个属性就是事务传播行为和事务隔离级别。
2.1 事务传播行为
事务传播行为是Spring提出来的概念,是指多个事务方法在嵌套调用时的事务处理行为,是使用传播过来的原有事务,还是新开事务或者不使用事务。事务传播行为增强了代码中事务开发的功能。TransactionDefinition接口定义了7种类型的事务传播行为,具体如下:
传播行为属性 | 值 | 描述 |
PROPAGATION_REQUIRED | 0 | 使用当前事务,如果当前没有事务就新建一个。这是Spring默认的传播行为 |
PROPAGATION_SUPPORTS | 1 | 支持当前事务,如果当前没有事务,就以非事务方式执行 |
PROPAGATION_MANDATORY | 2 | 使用当前事务,如果当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 3 | 新建事务,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 4 | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 5 | 以非事务方式执行,如果当前存在事务,就抛出异常 |
PROPAGATION_NESTED | 6 | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与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_DEFAULT | TRANSACTION_NONE | -1或0 | 默认值。前者值为-1,后者值为0 |
ISOLATION_READ_UNCOMMITTED | TRANSACTION_READ_UNCOMMITTED | 1 | 读未提交 |
ISOLATION_READ_COMMITTED | TRANSACTION_READ_COMMITTED | 2 | 读已提交 |
ISOLATION_REPEATABLE_READ | TRANSACTION_REPEATABLE_READ | 4 | 可重复读 |
ISOLATION_SERIALIZABLE | TRANSACTION_SERIALIZABLE | 8 | 串行化。解决所有读问题(脏读、幻读、不可重复度) |
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注解可以设定的属性如下:
属性 | 类型 | 默认值 | 描述 |
value | String | 指定使用的事务管理器,是transactionManager属性的别名 | |
propagation | Propagation的枚举类型 | Propagation.REQUIRED | 设置事务传播行为 |
isolation | Isolation的枚举类型 | Isolation.DEFAULT(-1) | 设置事务隔离级别 |
readOnly | boolean | false | 读写或只读事务 |
timeout | int | TransactionDefinition.TIMEOUT_DEFAULT(-1) | 设置事务超时时间,默认永不超时 |
rollbackFor | Class对象数组 | 空数组 | 需要回滚的异常类数组 |
rollbackForClassName | 类名数组 | 空数组 | 需要回滚的异常类名字数组 |
noRollbackFor | Class对象数组 | 空数组 | 不需要回滚的异常类数组 |
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_SUPPORTED和NEVER),Spring事务管理器默认会捕捉任何未处理(Unchecked)的异常,在事务执行上下文抛出Unchecked异常时回滚事务。但抛出Checked类型的异常时不会及进行事务回滚,而是提交。如果不想使用默认的异常事务行为,使用@Transaction的rollbackFor和notRollbackFor属性可以指定回滚和不回滚的异常类型(Unchecked和Checked异常配置都可以配置)。特别的,如果在@Transaction注解的方法中使用try-catch捕获异常并处理运行时异常,则事务也会提交。
2.事务方法导致
- 因为Spring使用AOP控制事务,其底层是代理机制,也就是说当前事务方法的调用是被当前类的代理类来调用的,只有这样事务才能生效,所以要确保该事务方法所在的类被代理了。
- 事务方法必须是public修饰的。private、static、final修饰的方法事务不生效。
- 非事务方法调用本类中的另外一个事务方法也会导致事务不生效。这种可以通过如下两个方式解决:
- 该类内部注入一个自己的Bean(比如UserService类中注入userService对应的Bean),通过该成员显示调用这个事务方法;
- 利用AOP上下文来获取代理对象,通过该代理对象调用该事务方法。(比如:(UserService)AopContext.currentProxy().事务方法();)需要注意的是Aop上下文Spring是默认关闭的,需要手动开启。
- 确保业务和事务方法入口在同一个线程中,否则事务也不会生效。
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>进行过滤,那么该过滤也不会生效,所以使用这两个标签时一定要关闭默认的过滤器。