这篇博文已经“难产”好几天了,压力还是有些大的,因为 Transaction(事务管理)的问题,争论一直就没有停止过。由于个人能力真的非常有限,花了好多功夫去学习,总算基本上解决了问题,所以这才第一时间就拿出来与网友们共享,也听听大家的想法。
提示:对 Transaction 不太理解的朋友们,可阅读这篇博文《Transaction 那点事儿》。
现在就开始吧!
请看下面这一段代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Beanpublic class ProductServiceImpl extends BaseService implements ProductService {
...
@Override
public boolean createProduct(Map<String, Object> productFieldMap) {
String sql = SQLHelper.getSQL("insert.product");
Object[] params = {
productFieldMap.get("productTypeId"),
productFieldMap.get("productName"),
productFieldMap.get("productCode"),
productFieldMap.get("price"),
productFieldMap.get("description")
};
int rows = DBHelper.update(sql, params);
return rows == 1;
}
} |
我们先不去考虑 createProduct() 方法中那段不够优雅的代码,总之这一坨 shi 就是为了完成一个 insert 语句的,后续我会将其简化。
除此以外,大家可能已经看出一些问题。没有事务管理!
如果执行过程中抛出了一个异常,事务无法回滚。这个案例仅仅是一条 SQL 语句,如果是多条呢?前面的执行成功了,就最后一条执行失败,那应该是整个事务都要回滚,前面做的都不算数才对。
为了实现这个目标,我山寨了 Spring 的做法,它有一个 @Transactional 注解,可以标注在方法上,那么被标注的方法就是具备事务特性了,还可以设置事务传播方式与隔离级别等功能,确实够强大的,完全取代了以前的 XML 配置方式。
于是我也做了一个 @Transaction 注解(注意:我这里是事务的名词,Spring 用的是形容词),代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Beanpublic class ProductServiceImpl extends BaseService implements ProductService {
...
@Override
@Transaction
public boolean createProduct(Map<String, Object> productFieldMap) {
String sql = SQLHelper.getSQL("insert.product");
Object[] params = {
productFieldMap.get("productTypeId"),
productFieldMap.get("productName"),
productFieldMap.get("productCode"),
productFieldMap.get("price"),
productFieldMap.get("description")
};
int rows = DBHelper.update(sql, params);
if (true) {
throw new RuntimeException("Insert log failure!"); // 故意抛出异常,让事务回滚
}
return rows == 1;
}
} |
在执行 DBHelper.update() 方法以后,我故意抛出了一个 RuntimeException,我想看看事务能否回滚,也就是那条 insert 语句没有生效。
做了一个单元测试,测了一把,果然报错了,product 表里也没有插入任何数据。
看来事务管理功能的确生效了,那么,我是如何实现 @Transaction 这个注解所具有的功能?请接着往下看,下面的才是精华所在。
一开始我修改了 DBHelper 的代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
public class DBHelper {
private static final BasicDataSource ds = new BasicDataSource();
private static final QueryRunner runner = new QueryRunner(ds);
// 定义一个局部线程变量(使每个线程都拥有自己的连接)
private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();
static {
System.out.println("Init DBHelper...");
// 初始化数据源
ds.setDriverClassName(ConfigHelper.getStringProperty("jdbc.driver"));
ds.setUrl(ConfigHelper.getStringProperty("jdbc.url"));
ds.setUsername(ConfigHelper.getStringProperty("jdbc.username"));
ds.setPassword(ConfigHelper.getStringProperty("jdbc.password"));
ds.setMaxActive(ConfigHelper.getNumberProperty("jdbc.max.active"));
ds.setMaxIdle(ConfigHelper.getNumberProperty("jdbc.max.idle"));
}
// 获取数据源
public static DataSource getDataSource() {
return ds;
}
// 开启事务
public static void beginTransaction() {
Connection conn = connContainer.get();
if (conn == null) {
try {
conn = ds.getConnection();
conn.setAutoCommit(false);
} catch (Exception e) {
e.printStackTrace();
} finally {
connContainer.set(conn);
}
}
}
// 提交事务
public static void commitTransaction() {
Connection conn = connContainer.get();
if (conn != null) {
try {
conn.commit();
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
connContainer.remove();
}
}
}
// 回滚事务
public static void rollbackTransaction() {
Connection conn = connContainer.get();
if (conn != null) {
try {
conn.rollback();
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
connContainer.remove();
}
}
}
...
// 执行更新(包括 UPDATE、INSERT、DELETE)
public static int update(String sql, Object... params) {
// 若当前线程中存在连接,则传入(用于事务处理),否则将从数据源中获取连接
Connection conn = connContainer.get();
return DBUtil.update(runner, conn, sql, params);
}
} |
首先,我将 Connection 放到 ThreadLocal 容器中了,这样每个线程之间对 Connection 的访问就是隔离的了(不会共享),保证了线程安全。
然后,我增加了几个关于事务的方法,例如:beginTransaction()、commitTransaction()、rollbackTransaction(),这三个方法中的代码非常重要,一定要细看!我就不解释了。
最后,我修改了 update() 方法,先从 ThreadLocal 中拿出 Connection,然后传入到 DBUtil.update() 方法中。注意:有可能从 ThreadLocal 中根本拿不到 Connection,因为此时的 Connection 是从 DataSource 中获取的(这是非事务的情况),只要执行了 beginTransaction() 方法,就会从 DataSource 中获取一个 Connection,然后将事务自动提交功能关闭,最后往 ThreadLocal 中放入一个 Connection。
提示:对 ThreadLocal 不太理解的朋友们,可阅读这篇博文《ThreadLocal 那点事儿》。
那问题来了,DBUtil 又是如何处理事务的呢?我对 DBUtil 是这样修改的:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class DBUtil {
...
// 更新(包括 UPDATE、INSERT、DELETE,返回受影响的行数)
public static int update(QueryRunner runner, Connection conn, String sql, Object... params) {
int result = 0;
try {
if (conn != null) {
result = runner.update(conn, sql, params);
} else {
result = runner.update(sql, params);
}
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
} |
这里,我首先对传入进来的 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() 方法呢?又是在哪里调用的呢?
最简单又最直接的方式莫过于此:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@Beanpublic class ProductServiceImpl extends BaseService implements ProductService {
...
public boolean createProduct(Map<String, Object> productFieldMap) {
int rows = 0;
try {
// 开启事务
DBHelper.beginTransaction();
String sql = SQLHelper.getSQL("insert.product");
Object[] params = {
productFieldMap.get("productTypeId"),
productFieldMap.get("productName"),
productFieldMap.get("productCode"),
productFieldMap.get("price"),
productFieldMap.get("description")
};
rows = DBHelper.update(sql, params);
} catch (Exception e) {
// 回滚事务
DBHelper.rollbackTransaction();
e.printStackTrace();
throw new RuntimeException();
} finally {
// 提交事务
DBHelper.commitTransaction();
}
return rows == 1;
}
} |
但这样写,总感觉太累赘,以后凡是需要考虑事务问题的,都要用一个 try...catch...finally 语句来处理,还要手工调用那些 DBHelper.xxxTransaction() 方法。对于开发人员而言,简直这就像噩梦!
这里就要用到一点设计模式了,我选择了“Proxy 模式”,就是“代理模式”,说准确一点应该是“动态代理模式”。
提示:对 Proxy 不太理解的朋友,可阅读这篇博文《Proxy 那点事儿》。
我想把一头一尾的代码都放在 Proxy 中,这里仅保留最核心的逻辑。代理类会自动拦截到 Service 类中所有的方法,先判断该方法是否带有 @Transaction 注解,如果有的话,就开启事务,然后调用方法,最后提交事务,遇到异常还要回滚事务。若没有 @Transaction 注解呢?什么都不做,直接调用目标方法即可。
这就是我的思路,下面看看这个动态代理类是如何实现的吧:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public class TransactionProxy implements MethodInterceptor {
private static TransactionProxy instance = new TransactionProxy();
private TransactionProxy() {
}
public static TransactionProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Object result;
if (method.isAnnotationPresent(Transaction.class)) {
try {
// 开启事务
DBHelper.beginTransaction();
// 执行操作
method.setAccessible(true);
result = proxy.invokeSuper(obj, args);
// 提交事务
DBHelper.commitTransaction();
} catch (Exception e) {
// 回滚事务
DBHelper.rollbackTransaction();
e.printStackTrace();
throw new RuntimeException();
}
} else {
result = proxy.invokeSuper(obj, args);
}
return result;
}
} |
我选用的是 CGLib 类库实现的动态代理,因为我认为它比 JDK 提供的动态代理更为强大一些,它可以代理没有接口的类,而 JDK 的动态代理是有限制的,目标类必须实现接口才能被代理。
在这个 TransactionProxy 类中还用到了“Singleton 模式”,作用是提高一些性能,同时也简化了 API 调用方式。
下面是最重要的地方了,如何才能将这些具有事务的 Service 类加入 IoC 容器呢?这样在 Action 中注入的 Service 就不再是普通的实现类了,而是通过 CGLib 动态生成的实现类(可以在 IDE 中打个断点看看就知道)。
好了,看看负责 IoC 容器的 BeanHelper吧,我又是如何修改的呢?
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class BeanHelper {
// Bean 类 => Bean 实例
private static final Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
static {
System.out.println("Init BeanHelper...");
try {
// 获取并遍历所有的 Bean(带有 @Bean 注解的类)
List<Class<?>> beanClassList = ClassHelper.getClassListByAnnotation(Bean.class);
for (Class<?> beanClass : beanClassList) {
// 创建 Bean 实例
Object beanInstance;
if (BaseService.class.isAssignableFrom(beanClass)) {
// 若为 Service 类,则获取动态代理实例(可以使用 CGLib 动态代理,不能使用 JDK 动态代理,因为初始化 Bean 字段时会报错)
beanInstance = TransactionProxy.getInstance().getProxy(beanClass);
} else {
// 否则通过反射创建实例
beanInstance = beanClass.newInstance();
}
// 将 Bean 实例放入 Bean Map 中(键为 Bean 类,值为 Bean 实例)
beanMap.put(beanClass, beanInstance);
}
// 遍历 Bean Map
for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
...
}
} catch (Exception e) {
e.printStackTrace();
}
}
...
} |
在遍历 beanClassList 时,判断当前的 beanClass 是否继承于 BaseService?如果是,那么就创建动态代理实例给 beanInstance;否则,就像以前一样,通过反射来创建 beanInstance。
改动量还不算太大,动态代理就会初始化到相应的 Bean 对象上了。
到此为止,事务管理实现原理已全部结束。当然问题还有很多,比如:我没有考虑事务隔离级别、事务传播行为、事务超时、只读事务等问题,甚至还有更复杂的 JTA 事务。
但我个人认为,事务管理功能实用就行了,标注了 @Transaction 注解的方法就有事务,没有标注就没有事务,很简单。没必要真的做得和 Spring 事务管理器那样完备,比如:支持 7 种事务传播行为。那有人就会提到,为什么不提供“嵌套事务”和“JTA 事务”呢?我想说的是,追求是无止境的,即便是 Spring 也有它的不足之处。关键是对框架的定位要看准,该框架仅用于开发中、小规模的 Java Web 应用系统,那么这类复杂的事务处理情况又会有多少呢?所以我暂时就此打住了,我的直觉告诉我,深入下去将一定是一个无底洞。
我想有必要先听听大家的想法,避免走弯路的最佳方式就是及时沟通。