请看下面这一段代码:
01 |
@Bean |
02 |
public class ProductServiceImpl extends BaseService implements ProductService { |
03 |
04 |
... |
05 |
06 |
@Override |
07 |
public boolean createProduct(Map<String, Object> productFieldMap) { |
08 |
String sql = SQLHelper.getSQL( "insert.product" ); |
09 |
Object[] params = { |
10 |
productFieldMap.get( "productTypeId" ), |
11 |
productFieldMap.get( "productName" ), |
12 |
productFieldMap.get( "productCode" ), |
13 |
productFieldMap.get( "price" ), |
14 |
productFieldMap.get( "description" ) |
15 |
}; |
16 |
int rows = DBHelper.update(sql, params); |
17 |
return rows == 1 ; |
18 |
} |
19 |
} |
我们先不去考虑 createProduct() 方法中那段不够优雅的代码,总之这一坨 shi 就是为了完成一个 insert 语句的,后续我会将其简化。
除此以外,大家可能已经看出一些问题。没有事务管理!
如果执行过程中抛出了一个异常,事务无法回滚。这个案例仅仅是一条 SQL 语句,如果是多条呢?前面的执行成功了,就最后一条执行失败,那应该是整个事务都要回滚,前面做的都不算数才对。
为了实现这个目标,我山寨了 Spring 的做法,它有一个 @Transactional 注解,可以标注在方法上,那么被标注的方法就是具备事务特性了,还可以设置事务传播方式与隔离级别等功能,确实够强大的,完全取代了以前的 XML 配置方式。
于是我也做了一个 @Transaction 注解(注意:我这里是事务的名词,Spring 用的是形容词),代码如下:
01 |
@Bean |
02 |
public class ProductServiceImpl extends BaseService implements ProductService { |
03 |
04 |
... |
05 |
06 |
@Override |
07 |
@Transaction |
08 |
public boolean createProduct(Map<String, Object> productFieldMap) { |
09 |
String sql = SQLHelper.getSQL( "insert.product" ); |
10 |
Object[] params = { |
11 |
productFieldMap.get( "productTypeId" ), |
12 |
productFieldMap.get( "productName" ), |
13 |
productFieldMap.get( "productCode" ), |
14 |
productFieldMap.get( "price" ), |
15 |
productFieldMap.get( "description" ) |
16 |
}; |
17 |
int rows = DBHelper.update(sql, params); |
18 |
if ( true ) { |
19 |
throw new RuntimeException( "Insert log failure!" ); // 故意抛出异常,让事务回滚 |
20 |
} |
21 |
return rows == 1 ; |
22 |
} |
23 |
} |
在执行 DBHelper.update() 方法以后,我故意抛出了一个 RuntimeException,我想看看事务能否回滚,也就是那条 insert 语句没有生效。
做了一个单元测试,测了一把,果然报错了,product 表里也没有插入任何数据。
看来事务管理功能的确生效了,那么,我是如何实现 @Transaction 这个注解所具有的功能?请接着往下看,下面的才是精华所在。
一开始我修改了 DBHelper 的代码:
01 |
public class DBHelper { |
02 |
03 |
private static final BasicDataSource ds = new BasicDataSource(); |
04 |
private static final QueryRunner runner = new QueryRunner(ds); |
05 |
06 |
// 定义一个局部线程变量(使每个线程都拥有自己的连接) |
07 |
private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>(); |
08 |
09 |
static { |
10 |
System.out.println( "Init DBHelper..." ); |
11 |
12 |
// 初始化数据源 |
13 |
ds.setDriverClassName(ConfigHelper.getStringProperty( "jdbc.driver" )); |
14 |
ds.setUrl(ConfigHelper.getStringProperty( "jdbc.url" )); |
15 |
ds.setUsername(ConfigHelper.getStringProperty( "jdbc.username" )); |
16 |
ds.setPassword(ConfigHelper.getStringProperty( "jdbc.password" )); |
17 |
ds.setMaxActive(ConfigHelper.getNumberProperty( "jdbc.max.active" )); |
18 |
ds.setMaxIdle(ConfigHelper.getNumberProperty( "jdbc.max.idle" )); |
19 |
} |
20 |
21 |
// 获取数据源 |
22 |
public static DataSource getDataSource() { |
23 |
return ds; |
24 |
} |
25 |
26 |
// 开启事务 |
27 |
public static void beginTransaction() { |
28 |
Connection conn = connContainer.get(); |
29 |
if (conn == null ) { |
30 |
try { |
31 |
conn = ds.getConnection(); |
32 |
conn.setAutoCommit( false ); |
33 |
} catch (Exception e) { |
34 |
e.printStackTrace(); |
35 |
} finally { |
36 |
connContainer.set(conn); |
37 |
} |
38 |
} |
39 |
} |
40 |
41 |
// 提交事务 |
42 |
public static void commitTransaction() { |
43 |
Connection conn = connContainer.get(); |
44 |
if (conn != null ) { |
45 |
try { |
46 |
conn.commit(); |
47 |
conn.close(); |
48 |
} catch (Exception e) { |
49 |
e.printStackTrace(); |
50 |
} finally { |
51 |
connContainer.remove(); |
52 |
} |
53 |
} |
54 |
} |
55 |
56 |
// 回滚事务 |
57 |
public static void rollbackTransaction() { |
58 |
Connection conn = connContainer.get(); |
59 |
if (conn != null ) { |
60 |
try { |
61 |
conn.rollback(); |
62 |
conn.close(); |
63 |
} catch (Exception e) { |
64 |
e.printStackTrace(); |
65 |
} finally { |
66 |
connContainer.remove(); |
67 |
} |
68 |
} |
69 |
} |
70 |
71 |
... |
72 |
73 |
// 执行更新(包括 UPDATE、INSERT、DELETE) |
74 |
public static int update(String sql, Object... params) { |
75 |
// 若当前线程中存在连接,则传入(用于事务处理),否则将从数据源中获取连接 |
76 |
Connection conn = connContainer.get(); |
77 |
return DBUtil.update(runner, conn, sql, params); |
78 |
} |
79 |
} |
首先,我将 Connection 放到 ThreadLocal 容器中了,这样每个线程之间对 Connection 的访问就是隔离的了(不会共享),保证了线程安全。
然后,我增加了几个关于事务的方法,例如:beginTransaction()、commitTransaction()、rollbackTransaction(),这三个方法中的代码非常重要,一定要细看!我就不解释了。
最后,我修改了 update() 方法,先从 ThreadLocal 中拿出 Connection,然后传入到 DBUtil.update() 方法中。注意:有可能从 ThreadLocal 中根本拿不到 Connection,因为此时的 Connection 是从 DataSource 中获取的(这是非事务的情况),只要执行了 beginTransaction() 方法,就会从 DataSource 中获取一个 Connection,然后将事务自动提交功能关闭,最后往 ThreadLocal 中放入一个 Connection。
提示:对 ThreadLocal 不太理解的朋友们,可阅读这篇博文《ThreadLocal 那点事儿》。
那问题来了,DBUtil 又是如何处理事务的呢?我对 DBUtil 是这样修改的:
01 |
public class DBUtil { |
02 |
03 |
... |
04 |
05 |
// 更新(包括 UPDATE、INSERT、DELETE,返回受影响的行数) |
06 |
public static int update(QueryRunner runner, Connection conn, String sql, Object... params) { |
07 |
int result = 0 ; |
08 |
try { |
09 |
if (conn != null ) { |
10 |
result = runner.update(conn, sql, params); |
11 |
} else { |
12 |
result = runner.update(sql, params); |
13 |
} |
14 |
} catch (SQLException e) { |
15 |
e.printStackTrace(); |
16 |
} |
17 |
return result; |
18 |
} |
19 |
} |
这里,我首先对传入进来的 Connection 对象进行判断:
若不为空(事务情况),调用 runner.update(conn, sql, params) 方法,将 conn 传递到 QueryRunner 中,也就是说,完全交给 Apache Commons DbUtils 来处理事务了,因为此时的 conn 是动过手脚的(在 beginTransaction() 方法中,做了 conn.setAutoCommit(false) 操作)。
若为空(非事务情况),调用 runner.update(sql, params) 方法,此时没有将 conn 传递到 QueryRunner 中,也就是说,Connection 由 Apache Commons DbUtils 从 DataSource 中获取,无需考虑事务问题,或者说,事务是自动提交的。
我想到这里,我已经解释清楚了。但还有必要再做一下总结:
获取 Connection 分两种情况,若自动从 DataSource 中获取,则为非事务情况;反之,从关闭 Connection 自动提交功能后,强制传入 Connection 时,则为事务情况。因为传递过去的是同一个 Connection,那么 Apache Commons DbUtils 是不会自动从 DataSource 中获取 Connection 了。
好了,地基终于建设完毕,剩下的就是什么时候调用那些 xxxTransaction() 方法呢?又是在哪里调用的呢?
最简单又最直接的方式莫过于此:
01 |
@Bean |
02 |
public class ProductServiceImpl extends BaseService implements ProductService { |
03 |
04 |
... |
05 |
06 |
public boolean createProduct(Map<String, Object> productFieldMap) { |
07 |
int rows = 0 ; |
08 |
try { |
09 |
// 开启事务 |
10 |
DBHelper.beginTransaction(); |
11 |
12 |
String sql = SQLHelper.getSQL( "insert.product" ); |
13 |
Object[] params = { |
14 |
productFieldMap.get( "productTypeId" ), |
15 |
productFieldMap.get( "productName" ), |
16 |
productFieldMap.get( "productCode" ), |
17 |
productFieldMap.get( "price" ), |
18 |
productFieldMap.get( "description" ) |
19 |
}; |
20 |
rows = DBHelper.update(sql, params); |
21 |
} catch (Exception e) { |
22 |
// 回滚事务 |
23 |
DBHelper.rollbackTransaction(); |
24 |
25 |
e.printStackTrace(); |
26 |
throw new RuntimeException(); |
27 |
} finally { |
28 |
// 提交事务 |
29 |
DBHelper.commitTransaction(); |
30 |
} |
31 |
return rows == 1 ; |
32 |
} |
33 |
} |
但这样写,总感觉太累赘,以后凡是需要考虑事务问题的,都要用一个 try...catch...finally 语句来处理,还要手工调用那些 DBHelper.xxxTransaction() 方法。对于开发人员而言,简直这就像噩梦!
这里就要用到一点设计模式了,我选择了“Proxy 模式”,就是“代理模式”,说准确一点应该是“动态代理模式”。
提示:对 Proxy 不太理解的朋友,可阅读这篇博文《Proxy 那点事儿》。
我想把一头一尾的代码都放在 Proxy 中,这里仅保留最核心的逻辑。代理类会自动拦截到 Service 类中所有的方法,先判断该方法是否带有 @Transaction 注解,如果有的话,就开启事务,然后调用方法,最后提交事务,遇到异常还要回滚事务。若没有 @Transaction 注解呢?什么都不做,直接调用目标方法即可。
这就是我的思路,下面看看这个动态代理类是如何实现的吧:
01 |
public class TransactionProxy implements MethodInterceptor { |
02 |
03 |
private static TransactionProxy instance = new TransactionProxy(); |
04 |
05 |
private TransactionProxy() { |
06 |
} |
07 |
08 |
public static TransactionProxy getInstance() { |
09 |
return instance; |
10 |
} |
11 |
12 |
@SuppressWarnings ( "unchecked" ) |
13 |
public <T> T getProxy(Class<T> cls) { |
14 |
return (T) Enhancer.create(cls, this ); |
15 |
} |
16 |
17 |
@Override |
18 |