概述
Spring 最成功,最吸引人的地方莫過於輕量級的聲明式事務管理,僅此一點,它就宣告了 重量級 EJB 容器的覆滅。Spring 聲明式事務管理將開發者從繁復的事務管理代碼中解脫出來 ,專注於業務邏輯的開發上,這是一件可以被拿來頂禮膜拜的事情。但是,世界並未從此消停 ,開發人員需要面對的是層出不窮的應用場景,這些場景往往逾越了普通 Spring 技術書籍的 理想界定。因此,隨著應用開發的深入,在使用經過 Spring 層層封裝的聲明式事務時,開發 人員越來越覺得自己墜入了迷霧,陷入了沼澤,體會不到外界所宣稱的那種暢快淋漓。本系列 文章的目標旨在整理並剖析實際應用中種種讓我們迷茫的場景,讓陽光照進雲遮霧障的山頭。
DAO 和事務管理的牽絆
很少有使用 Spring 但不使用 Spring 事務管理器的應用,因此常常有人會問:是否用了 Spring,就一定要用 Spring 事務管理器,否則就無法進行數據的持久化操作呢?事務管理器 和 DAO 是什麼關系呢?
也許是 DAO 和事務管理如影隨行的緣故吧,這個看似簡單的問題實實在在地存在著,從初 學者心中湧出,萦繞在開發老手的腦際。答案當然是否定的!我們都知道:事務管理是保證數 據操作的事務性(即原子性、一致性、隔離性、持久性,也即所謂的 ACID),脫離了事務性, DAO 照樣可以順利地進行數據的操作。
下面,我們來看一段使用 Spring JDBC 進行數據訪問的代碼:
清單 1. UserJdbcWithoutTransManagerService.java
package user.withouttm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.apache.commons.dbcp.BasicDataSource;
@Service("service1")
public class UserJdbcWithoutTransManagerService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void addScore(String userName,int toAdd){
String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
jdbcTemplate.update(sql,toAdd,userName);
}
public static void main(String[] args) {
ApplicationContext ctx =
new ClassPathXmlApplicationContext ("user/withouttm/jdbcWithoutTransManager.xml");
UserJdbcWithoutTransManagerService service =
(UserJdbcWithoutTransManagerService)ctx.getBean("service1");
JdbcTemplate jdbcTemplate = (JdbcTemplate)ctx.getBean ("jdbcTemplate");
BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();
//①.檢查數據源autoCommit的設置
System.out.println("autoCommit:"+ basicDataSource.getDefaultAutoCommit());
//②.插入一條記錄,初始分數為10
jdbcTemplate.execute(
"INSERT INTO t_user(user_name,password,score) VALUES ('tom','123456',10)");
//③.調用工作在無事務環境下的服務類方法,將分數添加20分
service.addScore("tom",20);
//④.查看此時用戶的分數
int score = jdbcTemplate.queryForInt(
"SELECT score FROM t_user WHERE user_name ='tom'");
System.out.println("score:"+score);
jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
}
}
jdbcWithoutTransManager.xml 的配置文件如下所示:
清單 2. jdbcWithoutTransManager.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context- 3.0.xsd">
<context:component-scan base-package="user.withouttm"/>
<!-- 數據源默認將autoCommit設置為true -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="oracle.jdbc.driver.OracleDriver"
p:url="jdbc:oracle:thin:@localhost:1521:orcl"
p:username="test"
p:password="test"/>
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource"/>
</beans>
運行 UserJdbcWithoutTransManagerService,在控制台上打出如下的結果:
defaultAutoCommit:true
score:30
在 jdbcWithoutTransManager.xml 中,沒有配置任何事務管理器,但是數據已經成功持久 化到數據庫中。在默認情況下,dataSource 數據源的 autoCommit 被設置為 true ―― 這也 意謂著所有通過 JdbcTemplate 執行的語句馬上提交,沒有事務。如果將 dataSource 的 defaultAutoCommit 設置為 false,再次運行 UserJdbcWithoutTransManagerService,將拋出 錯誤,原因是新增及更改數據的操作都沒有提交到數據庫,所以 ④ 處的語句因無法從數據庫 中查詢到匹配的記錄而引發異常。
對於強調讀速度的應用,數據庫本身可能就不支持事務,如使用 MyISAM 引擎的 MySQL 數 據庫。這時,無須在 Spring 應用中配置事務管理器,因為即使配置了,也是沒有實際用處的 。
不過,對於 Hibernate 來說,情況就有點復雜了。因為 Hibernate 的事務管理擁有其自身 的意義,它和 Hibernate 一級緩存有密切的關系:當我們調用 Session 的 save、update 等 方法時,Hibernate 並不直接向數據庫發送 SQL 語句,而是在提交事務(commit)或 flush 一級緩存時才真正向數據庫發送 SQL。所以,即使底層數據庫不支持事務,Hibernate 的事務 管理也是有一定好處的,不會對數據操作的效率造成負面影響。所以,如果是使用 Hibernate 數據訪問技術,沒有理由不配置 HibernateTransactionManager 事務管理器。
但是,不使用 Hibernate 事務管理器,在 Spring 中,Hibernate 照樣也可以工作,來看 下面的例子:
清單 3.UserHibernateWithoutTransManagerService.java
package user.withouttm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.apache.commons.dbcp.BasicDataSource;
import user.User;
@Service("service2")
public class UserHibernateWithoutTransManagerService {
@Autowired
private HibernateTemplate hibernateTemplate;
public void addScore(String userName,int toAdd){
User user = (User)hibernateTemplate.get(User.class,userName);
user.setScore(user.getScore()+toAdd);
hibernateTemplate.update(user);
}
public static void main(String[] args) {
//參考UserJdbcWithoutTransManagerService相應代碼
…
}
}
此時,采用 hiberWithoutTransManager.xml 的配置文件,其配置內容如下:
清單 4.hiberWithoutTransManager.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context- 3.0.xsd">
<!--省略掉包掃描,數據源,JdbcTemplate配置部分,參見jdbcWithoutTransManager.xml -->
…
<bean id="sessionFactory"
class=
"org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
p:dataSource-ref="dataSource">
<property name="annotatedClasses">
<list>
<value>user.User</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.Oracle10gDialect
</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<bean id="hibernateTemplate"
class="org.springframework.orm.hibernate3.HibernateTemplate"
p:sessionFactory-ref="sessionFactory"/>
</beans>
運行 UserHibernateWithoutTransManagerService,程序正確執行,並得到類似於 UserJdbcWithoutTransManagerService 的執行結果,這說明 Hibernate 在 Spring 中,在沒 有事務管理器的情況下,依然可以正常地進行數據的訪問。
應用分層的迷惑
Web、 Service 及 DAO 三層劃分就像西方國家的立法、行政、司法三權分立一樣被奉為金 科玉律,甚至有開發人員認為如果要使用 Spring 的事務管理就一定先要進行三層的劃分。這 個看似荒唐的論調在開發人員中頗有市場。更有甚者,認為每層必須先定義一個接口,然後再 定義一個實現類。其結果是:一個很簡單的功能,也至少需要 3 個接口,3 個類,再加上視圖 層的 JSP 和 JS 等,打牌都可以轉上兩桌了,這種誤解贻害不淺。
對將“面向接口編程”奉為圭臬,認為放之四海而皆准的論調,筆者深不以為然。是的,“ 面向接口編程”是 Martin Fowler,Rod Johnson 這些大師提倡的行事原則。如果拿這條原則 去開發架構,開發產品,怎麼強調都不為過。但是,對於我們一般的開發人員來說,做的最多 的是普通工程項目,往往最多的只是一些對數據庫增、刪、查、改的功能。此時,“面向接口 編程”除了帶來更多的類文件外,看不到更多其它的好處。
Spring 框架提供的所有附加的好處(AOP、注解增強、注解 MVC 等)唯一的前提就是讓 POJO 的類變成一個受 Spring 容器管理的 Bean,除此以外沒有其它任何的要求。下面的實例 用一個 POJO 完成所有的功能,既是 Controller,又是 Service,還是 DAO:
清單 5. MixLayerUserService.java
package user.mixlayer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
//①.將POJO類通過注解變成Spring MVC的Controller
@Controller
public class MixLayerUserService {
//②.自動注入JdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
//③.通過Spring MVC注解映URL請求
@RequestMapping("/logon.do")
public String logon(String userName,String password){
if(isRightUser(userName,password)){
String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
jdbcTemplate.update(sql,20,userName);
return "success";
}else{
return "fail";
}
}
private boolean isRightUser(String userName,String password){
//do sth...
return true;
}
}
通過 @Controller 注解將 MixLayerUserService 變成 Web 層的 Controller,同時也是 Service 層的服務類。此外,由於直接使用 JdbcTemplate 訪問數據,所以 MixLayerUserService 還是一個 DAO。來看一下對應的 Spring 配置文件:
清單 6.applicationContext.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!--掃描Web類包,通過注釋生成Bean-->
<context:component-scan base-package="user.mixlayer"/>
<!--①.啟動Spring MVC的注解功能,完成請求和注解POJO的映射-->
<bean class="org.springframework.web.servlet.mvc.annotation
.AnnotationMethodHandlerAdapter"/>
<!--模型視圖名稱的解析,即在模型視圖名稱添加前後綴 -->
<bean class="org.springframework.web.servlet.view
.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>
<!--普通數據源 -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="oracle.jdbc.driver.OracleDriver"
p:url="jdbc:oracle:thin:@localhost:1521:orcl"
p:username="test"
p:password="test"/>
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource"/>
<!--事務管理器 -->
<bean id="jdbcManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!--②使用aop和tx命名空間語法為MixLayerUserService所有公用方法添加事務增強 -- >
<aop:config proxy-target-class="true">
<aop:pointcut id="serviceJdbcMethod"
expression="execution(public * user.mixlayer.MixLayerUserService.*(..))"/>
<aop:advisor pointcut-ref="serviceJdbcMethod"
advice-ref="jdbcAdvice" order="0"/>
</aop:config>
<tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
</beans>
在 ① 處,我們定義配置了 AnnotationMethodHandlerAdapter,以便啟用 Spring MVC 的 注解驅動功能。而②和③處通過 Spring 的 aop 及 tx 命名空間,以及 Aspject 的切點表達 式語法進行事務增強的定義,對 MixLayerUserService 的所有公有方法進行事務增強。要使程 序能夠運行起來還必須進行 web.xml 的相關配置:
清單 7.web.xml
<?xml version="1.0" encoding="GB2312"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:user/mixlayer/applicationContext.xml</param- value>
</context-param>
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/classes/log4j.properties</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.util.Log4jConfigListener
</listener-class>
</listener>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>user</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<!--①通過contextConfigLocation參數指定Spring配置文件的位置 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:user/mixlayer/applicationContext.xml</param- value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>user</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
這個配置文件很簡單,唯一需要注意的是 DispatcherServlet 的配置。默認情況下 Spring MVC 根據 Servlet 的名字查找 WEB-INF 下的 <servletName>-servlet.xml 作為 Spring MVC 的配置文件,在此,我們通過 contextConfigLocation 參數顯式指定 Spring MVC 配置文件的確切位置。
將 org.springframework.jdbc 及 org.springframework.transaction 的日志級別設置為 DEBUG,啟動項目,並訪問 http://localhost:8088/logon.do?userName=tom 應用, MixLayerUserService#logon 方法將作出響應,查看後台輸出日志:
清單 8 執行日志
13:24:22,625 DEBUG (AbstractPlatformTransactionManager.java:365) -
Creating new transaction with name
[user.mixlayer.MixLayerUserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
13:24:22,906 DEBUG (DataSourceTransactionManager.java:205) -
Acquired Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf]
for JDBC transaction
13:24:22,921 DEBUG (DataSourceTransactionManager.java:222) -
Switching JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@6e1cbf] to manual commit
13:24:22,921 DEBUG (JdbcTemplate.java:785) -
Executing prepared SQL update
13:24:22,921 DEBUG (JdbcTemplate.java:569) -
Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
13:24:23,140 DEBUG (JdbcTemplate.java:794) -
SQL update affected 0 rows
13:24:23,140 DEBUG (AbstractPlatformTransactionManager.java:752) -
Initiating transaction commit
13:24:23,140 DEBUG (DataSourceTransactionManager.java:265) -
Committing JDBC transaction on Connection
[org.apache.commons.dbcp.PoolableConnection@6e1cbf]
13:24:23,140 DEBUG (DataSourceTransactionManager.java:323) -
Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf]
after transaction
13:24:23,156 DEBUG (DataSourceUtils.java:312) -
Returning JDBC Connection to DataSource
日志中粗體部分說明了 MixLayerUserService#logon 方法已經正確運行在事務上下文中。
Spring 框架本身不應該是復雜化代碼的理由,使用 Spring 的開發者應該是無拘無束的: 從實際應用出發,去除掉那些所謂原則性的接口,去除掉強制分層的束縛,簡單才是硬道理。
事務方法嵌套調用的迷茫
Spring 事務一個被訛傳很廣說法是:一個事務方法不應該調用另一個事務方法,否則將產 生兩個事務。結果造成開發人員在設計事務方法時束手束腳,生怕一不小心就踩到地雷。
其實這種是不認識 Spring 事務傳播機制而造成的誤解,Spring 對事務控制的支持統一在 TransactionDefinition 類中描述,該類有以下幾個重要的接口方法:
int getPropagationBehavior():事務的傳播行為
int getIsolationLevel():事務的隔離級別
int getTimeout():事務的過期時間
boolean isReadOnly():事務的讀寫特性。
很明顯,除了事務的傳播行為外,事務的其它特性 Spring 是借助底層資源的功能來完成的 ,Spring 無非只充當個代理的角色。但是事務的傳播行為卻是 Spring 憑借自身的框架提供的 功能,是 Spring 提供給開發者最珍貴的禮物,訛傳的說法玷污了 Spring 事務框架最美麗的 光環。
所謂事務傳播行為就是多個事務方法相互調用時,事務如何在這些方法間傳播。Spring 支 持 7 種事務傳播行為:
PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中, 加入到這個事務中。這是最常見的選擇。
PROPAGATION_SUPPORTS 支持當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 使用當前的事務,如果當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛 起。
PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則拋出異常。
PROPAGATION_NESTED 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執 行與 PROPAGATION_REQUIRED 類似的操作。
Spring 默認的事務傳播行為是 PROPAGATION_REQUIRED,它適合於絕大多數的情況。假設 ServiveX#methodX() 都工作在事務環境下(即都被 Spring 事務增強了),假設程序中存在如 下的調用鏈:Service1#method1()->Service2#method2()->Service3#method3(),那麼 這 3 個服務類的 3 個方法通過 Spring 的事務傳播機制都工作在同一個事務中。
下面,我們來看一下實例,UserService#logon() 方法內部調用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法,這兩個類都繼承於 BaseService。它們之間的類結構說明如下:
圖 1. UserService 和 ScoreService
具體的代碼如下所示:
清單 9 UserService.java
@Service("userService")
public class UserService extends BaseService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ScoreService scoreService;
public void logon(String userName) {
updateLastLogonTime(userName);
scoreService.addScore(userName, 20);
}
public void updateLastLogonTime(String userName) {
String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
}
}
UserService 中注入了 ScoreService 的 Bean,ScoreService 的代碼如下所示:
清單 10 ScoreService.java
@Service("scoreUserService")
public class ScoreService extends BaseService{
@Autowired
private JdbcTemplate jdbcTemplate;
public void addScore(String userName, int toAdd) {
String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
jdbcTemplate.update(sql, toAdd, userName);
}
}
通過 Spring 的事務配置為 ScoreService 及 UserService 中所有公有方法都添加事務增 強,讓這些方法都工作於事務環境下。下面是關鍵的配置代碼:
清單 11 事務增強配置
<!-- 添加Spring事務增強 -->
<aop:config proxy-target-class="true">
<aop:pointcut id="serviceJdbcMethod"
<!-- 所有繼承於BaseService類的子孫類的public方法都進行事務增強-->
expression="within(user.nestcall.BaseService+)"/>
<aop:advisor pointcut-ref="serviceJdbcMethod"
advice-ref="jdbcAdvice" order="0"/>
</aop:config>
<tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
將日志級別設置為 DEBUG,啟動 Spring 容器並執行 UserService#logon() 的方法,仔細 觀察如下的輸出日志:
清單 12 執行日志
16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) -
Creating new transaction with name [user.nestcall.UserService.logon]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①為UserService#logon方法啟動一個事務
16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) -
Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]
for JDBC transaction
logon method...
updateLastLogonTime... ②直接執行updateLastLogonTime方法
16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update
16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]
16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows
16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) - Participating
in existing transaction ③ScoreService#addScore方法加入到 UserService#logon的事務中
addScore...
16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update
16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows
16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) -
Initiating transaction commit
④提交事務
16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]
16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@32bd65] after transaction
16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
從上面的輸入日志中,可以清楚地看到 Spring 為 UserService#logon() 方法啟動了一個 新的事務,而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的類中 ,沒有觀察到有事務傳播行為的發生,其代碼塊好像“直接合並”到 UserService#logon() 中 。接著,當執行到 ScoreService#addScore() 方法時,我們就觀察到了發生了事務傳播的行為 :Participating in existing transaction,這說明 ScoreService#addScore() 添加到 UserService#logon() 的事務上下文中,兩者共享同一個事務。所以最終的結果是 UserService 的 logon(), updateLastLogonTime() 以及 ScoreService 的 addScore 都工作 於同一事務中。
多線程的困惑
由於 Spring 的事務管理器是通過線程相關的 ThreadLocal 來保存數據訪問基礎設施,再 結合 IOC 和 AOP 實現高級聲明式事務的功能,所以 Spring 的事務天然地和線程有著千絲萬 縷的聯系。
我們知道 Web 容器本身就是多線程的,Web 容器為一個 Http 請求創建一個獨立的線程, 所以由此請求所牽涉到的 Spring 容器中的 Bean 也是運行於多線程的環境下。在絕大多數情 況下,Spring 的 Bean 都是單實例的(singleton),單實例 Bean 的最大的好處是線程無關 性,不存在多線程並發訪問的問題,也即是線程安全的。
一個類能夠以單實例的方式運行的前提是“無狀態”:即一個類不能擁有狀態化的成員變量 。我們知道,在傳統的編程中,DAO 必須執有一個 Connection,而 Connection 即是狀態化的 對象。所以傳統的 DAO 不能做成單實例的,每次要用時都必須 new 一個新的實例。傳統的 Service 由於將有狀態的 DAO 作為成員變量,所以傳統的 Service 本身也是有狀態的。
但是在 Spring 中,DAO 和 Service 都以單實例的方式存在。Spring 是通過 ThreadLocal 將有狀態的變量(如 Connection 等)本地線程化,達到另一個層面上的“線程無關”,從而 實現線程安全。Spring 不遺余力地將狀態化的對象無狀態化,就是要達到單實例化 Bean 的目 的。
由於 Spring 已經通過 ThreadLocal 的設施將 Bean 無狀態化,所以 Spring 中單實例 Bean 對線程安全問題擁有了一種天生的免疫能力。不但單實例的 Service 可以成功運行於多 線程環境中,Service 本身還可以自由地啟動獨立線程以執行其它的 Service。下面,通過一 個實例對此進行描述:
清單 13 UserService.java 在事務方法中啟動獨立線程運行另一個事務方法
@Service("userService")
public class UserService extends BaseService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ScoreService scoreService;
//① 在logon方法體中啟動一個獨立的線程,在該獨立的線程中執行 ScoreService#addScore()方法
public void logon(String userName) {
System.out.println("logon method...");
updateLastLogonTime(userName);
Thread myThread = new MyThread(this.scoreService,userName,20);
myThread.start();
}
public void updateLastLogonTime(String userName) {
System.out.println("updateLastLogonTime...");
String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
}
//② 封裝ScoreService#addScore()的線程
private class MyThread extends Thread{
private ScoreService scoreService;
private String userName;
private int toAdd;
private MyThread(ScoreService scoreService,String userName,int toAdd) {
this.scoreService = scoreService;
this.userName = userName;
this.toAdd = toAdd;
}
public void run() {
scoreService.addScore(userName,toAdd);
}
}
}
將日志級別設置為 DEBUG,執行 UserService#logon() 方法,觀察以下輸出的日志:
清單 14 執行日志
[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name
[user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①
[main] (DataSourceTransactionManager.java:205) - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transaction
logon method...
updateLastLogonTime...
[main] (JdbcTemplate.java:785) - Executing prepared SQL update
[main] (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]
[main] (JdbcTemplate.java:794) - SQL update affected 0 rows
[main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit
[Thread-2](AbstractPlatformTransactionManager.java:365) -
Creating new transaction with name [user.multithread.ScoreService.addScore]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②
[main] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③
[main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@1353249] after transaction
[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transaction
addScore...
[main] (JdbcTemplate.java:416) - Executing SQL statement
[DELETE FROM t_user WHERE user_name='tom']
[main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource
[Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update
[Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
[Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows
[Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit
[Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④
[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction
在 ① 處,在主線程(main)執行的 UserService#logon() 方法的事務啟動,在 ③ 處, 其對應的事務提交,而在子線程(Thread-2)執行的 ScoreService#addScore() 方法的事務在 ② 處啟動,在 ④ 處對應的事務提交。
所以,我們可以得出這樣的結論:在 相同線程中進行相互嵌套調用的事務方法工作於相同 的事務中。如果這些相互嵌套調用的方法工作在不同的線程中,不同線程下的事務方法工作在 獨立的事務中。
小結
Spring 聲明式事務是 Spring 最核心,最常用的功能。由於 Spring 通過 IOC 和 AOP 的 功能非常透明地實現了聲明式事務的功能,一般的開發者基本上無須了解 Spring 聲明式事務 的內部細節,僅需要懂得如何配置就可以了。
但是在實際應用開發過程中,Spring 的這種透明的高階封裝在帶來便利的同時,也給我們 帶來了迷惑。就像通過流言傳播的消息,最終聽眾已經不清楚事情的真相了,而這對於應用開 發來說是很危險的。本系列文章通過剖析實際應用中給開發者造成迷惑的各種難點,通過分析 Spring 事務管理的內部運作機制將真相還原出來。
在本文中,我們通過剖析了解到以下的真相:
在沒有事務管理的情況下,DAO 照樣可以順利進行數據操作;
將應用分成 Web,Service 及 DAO 層只是一種參考的開發模式,並非是事務管理工作的前 提條件;
Spring 通過事務傳播機制可以很好地應對事務方法嵌套調用的情況,開發者無須為了事務 管理而刻意改變服務方法的設計;
由於單實例的對象不存在線程安全問題,所以進行事務管理增強的 Bean 可以很好地工作在 多線程環境下。
在下一篇文章中,筆者將繼續分析 Spring 事務管理的以下難點:
混合使用多種數據訪問技術(如 Spring JDBC+Hibernate)的事務管理問題;
在通過 Bean 的方法通過 Spring AOP 增強存在哪些特殊的情況。