這篇博文已經「難產」好幾天了,壓力仍是有些大的,由於 Transaction(事務管理)的問題,爭論一直就沒有中止過。因爲我的能力真的很是有限,花了好多功夫去學習,總算基本上解決了問題,因此這才第一時間就拿出來與網友們共享,也聽聽你們的想法。java
提示:對 Transaction 不太理解的朋友們,可閱讀這篇博文《Transaction 那點事兒》。sql
如今就開始吧!設計模式
請看下面這一段代碼:安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Bean
public
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 語句的,後續我會將其簡化。框架
除此之外,你們可能已經看出一些問題。沒有事務管理!ide
若是執行過程當中拋出了一個異常,事務沒法回滾。這個案例僅僅是一條 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
|
@Bean
public
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
|
@Bean
public
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 應用系統,那麼這類複雜的事務處理狀況又會有多少呢?因此我暫時就此打住了,個人直覺告訴我,深刻下去將必定是一個無底洞。
我想有必要先聽聽你們的想法,避免走彎路的最佳方式就是及時溝通。