声明式事务原理

请看下面这一段代码:

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     public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
19         Object result;
20         if (method.isAnnotationPresent(Transaction.class)) {
21             try {
22                 // 开启事务
23                 DBHelper.beginTransaction();
24  
25                 // 执行操作
26                 method.setAccessible(true);
27                 result = proxy.invokeSuper(obj, args);
28  
29                 // 提交事务
30                 DBHelper.commitTransaction();
31             catch (Exception e) {
32                 // 回滚事务
33                 DBHelper.rollbackTransaction();
34  
35                 e.printStackTrace();
36                 throw new RuntimeException();
37             }
38         else {
39             result = proxy.invokeSuper(obj, args);
40         }
41         return result;
42     }
43 }

我选用的是 CGLib 类库实现的动态代理,因为我认为它比 JDK 提供的动态代理更为强大一些,它可以代理没有接口的类,而 JDK 的动态代理是有限制的,目标类必须实现接口才能被代理。

在这个 TransactionProxy 类中还用到了“Singleton 模式”,作用是提高一些性能,同时也简化了 API 调用方式。

下面是最重要的地方了,如何才能将这些具有事务的 Service 类加入 IoC 容器呢?这样在 Action 中注入的 Service 就不再是普通的实现类了,而是通过 CGLib 动态生成的实现类(可以在 IDE 中打个断点看看就知道)。

好了,看看负责 IoC 容器的 BeanHelper吧,我又是如何修改的呢?

01 public class BeanHelper {
02  
03     // Bean 类 => Bean 实例
04     private static final Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
05  
06     static {
07         System.out.println("Init BeanHelper...");
08  
09         try {
10             // 获取并遍历所有的 Bean(带有 @Bean 注解的类)
11             List<Class<?>> beanClassList = ClassHelper.getClassListByAnnotation(Bean.class);
12             for (Class<?> beanClass : beanClassList) {
13                 // 创建 Bean 实例
14                 Object beanInstance;
15                 if (BaseService.class.isAssignableFrom(beanClass)) {
16                     // 若为 Service 类,则获取动态代理实例(可以使用 CGLib 动态代理,不能使用 JDK 动态代理,因为初始化 Bean 字段时会报错)
17                     beanInstance = TransactionProxy.getInstance().getProxy(beanClass);
18                 else {
19                     // 否则通过反射创建实例
20                     beanInstance = beanClass.newInstance();
21                 }
22                 // 将 Bean 实例放入 Bean Map 中(键为 Bean 类,值为 Bean 实例)
23                 beanMap.put(beanClass, beanInstance);
24             }
25  
26             // 遍历 Bean Map
27             for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
28                 ...
29             }
30         catch (Exception e) {
31             e.printStackTrace();
32         }
33     }
34  
35     ...
36 }

在遍历 beanClassList 时,判断当前的 beanClass 是否继承于 BaseService?如果是,那么就创建动态代理实例给 beanInstance;否则,就像以前一样,通过反射来创建 beanInstance。

改动量还不算太大,动态代理就会初始化到相应的 Bean 对象上了。

到此为止,事务管理实现原理已全部结束。当然问题还有很多,比如:我没有考虑事务隔离级别、事务传播行为、事务超时、只读事务等问题,甚至还有更复杂的 JTA 事务。

但我个人认为,事务管理功能实用就行了,标注了 @Transaction 注解的方法就有事务,没有标注就没有事务,很简单。没必要真的做得和 Spring 事务管理器那样完备,比如:支持 7 种事务传播行为。那有人就会提到,为什么不提供“嵌套事务”和“JTA 事务”呢?我想说的是,追求是无止境的,即便是 Spring 也有它的不足之处。关键是对框架的定位要看准,该框架仅用于开发中、小规模的 Java Web 应用系统,那么这类复杂的事务处理情况又会有多少呢?所以我暂时就此打住了,我的直觉告诉我,深入下去将一定是一个无底洞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值