Spring 最成功,最吸引人的地方莫過(guò)于輕量級(jí)的聲明式事務(wù)管理,僅此一點(diǎn),它就宣告了重量級(jí) EJB 容器的覆滅。Spring 聲明式事務(wù)管理將開(kāi)發(fā)者從繁復(fù)的事務(wù)管理代碼中解脫出來(lái),專(zhuān)注于業(yè)務(wù)邏輯的開(kāi)發(fā)上,這是一件可以被拿來(lái)頂禮膜拜的事情。但是,世界并未從此消停,開(kāi)發(fā)人員需要面對(duì)的是層出不窮的應(yīng)用場(chǎng)景,這些場(chǎng)景往往逾越了普通 Spring 技術(shù)書(shū)籍的理想界定。因此,隨著應(yīng)用開(kāi)發(fā)的深入,在使用經(jīng)過(guò) Spring 層層封裝的聲明式事務(wù)時(shí),開(kāi)發(fā)人員越來(lái)越覺(jué)得自己墜入了迷霧,陷入了沼澤,體會(huì)不到外界所宣稱(chēng)的那種暢快淋漓。本系列文章的目標(biāo)旨在整理并剖析實(shí)際應(yīng)用中種種讓我們迷茫的場(chǎng)景,讓陽(yáng)光照進(jìn)云遮霧障的山頭。
很少有使用 Spring 但不使用 Spring 事務(wù)管理器的應(yīng)用,因此常常有人會(huì)問(wèn):是否用了 Spring,就一定要用 Spring 事務(wù)管理器,否則就無(wú)法進(jìn)行數(shù)據(jù)的持久化操作呢?事務(wù)管理器和 DAO 是什么關(guān)系呢?
也許是 DAO 和事務(wù)管理如影隨行的緣故吧,這個(gè)看似簡(jiǎn)單的問(wèn)題實(shí)實(shí)在在地存在著,從初學(xué)者心中涌出,縈繞在開(kāi)發(fā)老手的腦際。答案當(dāng)然是否定的!我們都知道:事務(wù)管理是保證數(shù)據(jù)操作的事務(wù)性(即原子性、一致性、隔離性、持久性,也即所謂的 ACID),脫離了事務(wù)性,DAO 照樣可以順利地進(jìn)行數(shù)據(jù)的操作。
下面,我們來(lái)看一段使用 Spring JDBC 進(jìn)行數(shù)據(jù)訪(fǎng)問(wèn)的代碼:
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(); //①.檢查數(shù)據(jù)源autoCommit的設(shè)置 System.out.println("autoCommit:"+ basicDataSource.getDefaultAutoCommit()); //②.插入一條記錄,初始分?jǐn)?shù)為10 jdbcTemplate.execute( "INSERT INTO t_user(user_name,password,score) VALUES('tom','123456',10)"); //③.調(diào)用工作在無(wú)事務(wù)環(huán)境下的服務(wù)類(lèi)方法,將分?jǐn)?shù)添加20分 service.addScore("tom",20); //④.查看此時(shí)用戶(hù)的分?jǐn)?shù) 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 的配置文件如下所示:
<?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"/> <!-- 數(shù)據(jù)源默認(rèn)將autoCommit設(shè)置為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> |
運(yùn)行 UserJdbcWithoutTransManagerService,在控制臺(tái)上打出如下的結(jié)果:
defaultAutoCommit:true score:30 |
在 jdbcWithoutTransManager.xml 中,沒(méi)有配置任何事務(wù)管理器,但是數(shù)據(jù)已經(jīng)成功持久化到數(shù)據(jù)庫(kù)中。在默認(rèn)情況下,dataSource 數(shù)據(jù)源的 autoCommit 被設(shè)置為 true ―― 這也意謂著所有通過(guò) JdbcTemplate 執(zhí)行的語(yǔ)句馬上提交,沒(méi)有事務(wù)。如果將 dataSource 的 defaultAutoCommit 設(shè)置為 false,再次運(yùn)行 UserJdbcWithoutTransManagerService,將拋出錯(cuò)誤,原因是新增及更改數(shù)據(jù)的操作都沒(méi)有提交到數(shù)據(jù)庫(kù),所以 ④ 處的語(yǔ)句因無(wú)法從數(shù)據(jù)庫(kù)中查詢(xún)到匹配的記錄而引發(fā)異常。
對(duì)于強(qiáng)調(diào)讀速度的應(yīng)用,數(shù)據(jù)庫(kù)本身可能就不支持事務(wù),如使用 MyISAM 引擎的 MySQL 數(shù)據(jù)庫(kù)。這時(shí),無(wú)須在 Spring 應(yīng)用中配置事務(wù)管理器,因?yàn)榧词古渲昧?,也是沒(méi)有實(shí)際用處的。
不過(guò),對(duì)于 Hibernate 來(lái)說(shuō),情況就有點(diǎn)復(fù)雜了。因?yàn)?Hibernate 的事務(wù)管理?yè)碛衅渥陨淼囊饬x,它和 Hibernate 一級(jí)緩存有密切的關(guān)系:當(dāng)我們調(diào)用 Session 的 save、update 等方法時(shí),Hibernate 并不直接向數(shù)據(jù)庫(kù)發(fā)送 SQL 語(yǔ)句,而是在提交事務(wù)(commit)或 flush 一級(jí)緩存時(shí)才真正向數(shù)據(jù)庫(kù)發(fā)送 SQL。所以,即使底層數(shù)據(jù)庫(kù)不支持事務(wù),Hibernate 的事務(wù)管理也是有一定好處的,不會(huì)對(duì)數(shù)據(jù)操作的效率造成負(fù)面影響。所以,如果是使用 Hibernate 數(shù)據(jù)訪(fǎng)問(wèn)技術(shù),沒(méi)有理由不配置 HibernateTransactionManager 事務(wù)管理器。
但是,不使用 Hibernate 事務(wù)管理器,在 Spring 中,Hibernate 照樣也可以工作,來(lái)看下面的例子:
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相應(yīng)代碼 … } } |
此時(shí),采用 hiberWithoutTransManager.xml 的配置文件,其配置內(nèi)容如下:
<?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"> <!--省略掉包掃描,數(shù)據(jù)源,JdbcTemplate配置部分,參見(jiàn)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> |
運(yùn)行 UserHibernateWithoutTransManagerService,程序正確執(zhí)行,并得到類(lèi)似于 UserJdbcWithoutTransManagerService 的執(zhí)行結(jié)果,這說(shuō)明 Hibernate 在 Spring 中,在沒(méi)有事務(wù)管理器的情況下,依然可以正常地進(jìn)行數(shù)據(jù)的訪(fǎng)問(wèn)。
Web、Service 及 DAO 三層劃分就像西方國(guó)家的立法、行政、司法三權(quán)分立一樣被奉為金科玉律,甚至有開(kāi)發(fā)人員認(rèn)為如果要使用 Spring 的事務(wù)管理就一定先要進(jìn)行三層的劃分。這個(gè)看似荒唐的論調(diào)在開(kāi)發(fā)人員中頗有市場(chǎng)。更有甚者,認(rèn)為每層必須先定義一個(gè)接口,然后再定義一個(gè)實(shí)現(xiàn)類(lèi)。其結(jié)果是:一個(gè)很簡(jiǎn)單的功能,也至少需要 3 個(gè)接口,3 個(gè)類(lèi),再加上視圖層的 JSP 和 JS 等,打牌都可以轉(zhuǎn)上兩桌了,這種誤解貽害不淺。
對(duì)將“面向接口編程”奉為圭臬,認(rèn)為放之四海而皆準(zhǔn)的論調(diào),筆者深不以為然。是的,“面向接口編程”是 Martin Fowler,Rod Johnson 這些大師提倡的行事原則。如果拿這條原則去開(kāi)發(fā)架構(gòu),開(kāi)發(fā)產(chǎn)品,怎么強(qiáng)調(diào)都不為過(guò)。但是,對(duì)于我們一般的開(kāi)發(fā)人員來(lái)說(shuō),做的最多的是普通工程項(xiàng)目,往往最多的只是一些對(duì)數(shù)據(jù)庫(kù)增、刪、查、改的功能。此時(shí),“面向接口編程”除了帶來(lái)更多的類(lèi)文件外,看不到更多其它的好處。
Spring 框架提供的所有附加的好處(AOP、注解增強(qiáng)、注解 MVC 等)唯一的前提就是讓 POJO 的類(lèi)變成一個(gè)受 Spring 容器管理的 Bean,除此以外沒(méi)有其它任何的要求。下面的實(shí)例用一個(gè) POJO 完成所有的功能,既是 Controller,又是 Service,還是 DAO:
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類(lèi)通過(guò)注解變成Spring MVC的Controller @Controller public class MixLayerUserService { //②.自動(dòng)注入JdbcTemplate @Autowired private JdbcTemplate jdbcTemplate; //③.通過(guò)Spring MVC注解映URL請(qǐng)求 @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; } } |
通過(guò) @Controller 注解將 MixLayerUserService 變成 Web 層的 Controller,同時(shí)也是 Service 層的服務(wù)類(lèi)。此外,由于直接使用 JdbcTemplate 訪(fǎng)問(wèn)數(shù)據(jù),所以 MixLayerUserService 還是一個(gè) DAO。來(lái)看一下對(duì)應(yīng)的 Spring 配置文件:
<?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類(lèi)包,通過(guò)注釋生成Bean--> <context:component-scan base-package="user.mixlayer"/> <!--①.啟動(dòng)Spring MVC的注解功能,完成請(qǐng)求和注解POJO的映射--> <bean class="org.springframework.web.servlet.mvc.annotation .AnnotationMethodHandlerAdapter"/> <!--模型視圖名稱(chēng)的解析,即在模型視圖名稱(chēng)添加前后綴 --> <bean class="org.springframework.web.servlet.view .InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/> <!--普通數(shù)據(jù)源 --> <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"/> <!--事務(wù)管理器 --> <bean id="jdbcManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <!--②使用aop和tx命名空間語(yǔ)法為MixLayerUserService所有公用方法添加事務(wù)增強(qiáng) --> <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 的注解驅(qū)動(dòng)功能。而②和③處通過(guò) Spring 的 aop 及 tx 命名空間,以及 Aspject 的切點(diǎn)表達(dá)式語(yǔ)法進(jìn)行事務(wù)增強(qiáng)的定義,對(duì) MixLayerUserService 的所有公有方法進(jìn)行事務(wù)增強(qiáng)。要使程序能夠運(yùn)行起來(lái)還必須進(jìn)行 web.xml 的相關(guān)配置:
<?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> <!--①通過(guò)contextConfigLocation參數(shù)指定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> |
這個(gè)配置文件很簡(jiǎn)單,唯一需要注意的是 DispatcherServlet 的配置。默認(rèn)情況下 Spring MVC 根據(jù) Servlet 的名字查找 WEB-INF 下的 <servletName>-servlet.xml 作為 Spring MVC 的配置文件,在此,我們通過(guò) contextConfigLocation 參數(shù)顯式指定 Spring MVC 配置文件的確切位置。
將 org.springframework.jdbc 及 org.springframework.transaction 的日志級(jí)別設(shè)置為 DEBUG,啟動(dòng)項(xiàng)目,并訪(fǎng)問(wèn) http://localhost:8088/logon.do?userName=tom 應(yīng)用,MixLayerUserService#logon 方法將作出響應(yīng),查看后臺(tái)輸出日志:
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 |
日志中粗體部分說(shuō)明了 MixLayerUserService#logon 方法已經(jīng)正確運(yùn)行在事務(wù)上下文中。
Spring 框架本身不應(yīng)該是復(fù)雜化代碼的理由,使用 Spring 的開(kāi)發(fā)者應(yīng)該是無(wú)拘無(wú)束的:從實(shí)際應(yīng)用出發(fā),去除掉那些所謂原則性的接口,去除掉強(qiáng)制分層的束縛,簡(jiǎn)單才是硬道理。
Spring 事務(wù)一個(gè)被訛傳很廣說(shuō)法是:一個(gè)事務(wù)方法不應(yīng)該調(diào)用另一個(gè)事務(wù)方法,否則將產(chǎn)生兩個(gè)事務(wù)。結(jié)果造成開(kāi)發(fā)人員在設(shè)計(jì)事務(wù)方法時(shí)束手束腳,生怕一不小心就踩到地雷。
其實(shí)這種是不認(rèn)識(shí) Spring 事務(wù)傳播機(jī)制而造成的誤解,Spring 對(duì)事務(wù)控制的支持統(tǒng)一在 TransactionDefinition 類(lèi)中描述,該類(lèi)有以下幾個(gè)重要的接口方法:
很明顯,除了事務(wù)的傳播行為外,事務(wù)的其它特性 Spring 是借助底層資源的功能來(lái)完成的,Spring 無(wú)非只充當(dāng)個(gè)代理的角色。但是事務(wù)的傳播行為卻是 Spring 憑借自身的框架提供的功能,是 Spring 提供給開(kāi)發(fā)者最珍貴的禮物,訛傳的說(shuō)法玷污了 Spring 事務(wù)框架最美麗的光環(huán)。
所謂事務(wù)傳播行為就是多個(gè)事務(wù)方法相互調(diào)用時(shí),事務(wù)如何在這些方法間傳播。Spring 支持 7 種事務(wù)傳播行為:
Spring 默認(rèn)的事務(wù)傳播行為是 PROPAGATION_REQUIRED,它適合于絕大多數(shù)的情況。假設(shè) ServiveX#methodX() 都工作在事務(wù)環(huán)境下(即都被 Spring 事務(wù)增強(qiáng)了),假設(shè)程序中存在如下的調(diào)用鏈:Service1#method1()->Service2#method2()->Service3#method3(),那么這 3 個(gè)服務(wù)類(lèi)的 3 個(gè)方法通過(guò) Spring 的事務(wù)傳播機(jī)制都工作在同一個(gè)事務(wù)中。
下面,我們來(lái)看一下實(shí)例,UserService#logon() 方法內(nèi)部調(diào)用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法,這兩個(gè)類(lèi)都繼承于 BaseService。它們之間的類(lèi)結(jié)構(gòu)說(shuō)明如下:
具體的代碼如下所示:
@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 的代碼如下所示:
@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); } } |
通過(guò) Spring 的事務(wù)配置為 ScoreService 及 UserService 中所有公有方法都添加事務(wù)增強(qiáng),讓這些方法都工作于事務(wù)環(huán)境下。下面是關(guān)鍵的配置代碼:
<!-- 添加Spring事務(wù)增強(qiáng) --> <aop:config proxy-target-class="true"> <aop:pointcut id="serviceJdbcMethod" <!-- 所有繼承于BaseService類(lèi)的子孫類(lèi)的public方法都進(jìn)行事務(wù)增強(qiáng)--> 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> |
將日志級(jí)別設(shè)置為 DEBUG,啟動(dòng) Spring 容器并執(zhí)行 UserService#logon() 的方法,仔細(xì)觀察如下的輸出日志:
16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.nestcall.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①為UserService#logon方法啟動(dòng)一個(gè)事務(wù) 16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65] for JDBC transaction logon method... updateLastLogonTime... ②直接執(zhí)行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的事務(wù)中 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 ④提交事務(wù) 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() 方法啟動(dòng)了一個(gè)新的事務(wù),而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的類(lèi)中,沒(méi)有觀察到有事務(wù)傳播行為的發(fā)生,其代碼塊好像“直接合并”到 UserService#logon() 中。接著,當(dāng)執(zhí)行到 ScoreService#addScore() 方法時(shí),我們就觀察到了發(fā)生了事務(wù)傳播的行為:Participating in existing transaction,這說(shuō)明 ScoreService#addScore() 添加到 UserService#logon() 的事務(wù)上下文中,兩者共享同一個(gè)事務(wù)。所以最終的結(jié)果是 UserService 的 logon(), updateLastLogonTime() 以及 ScoreService 的 addScore 都工作于同一事務(wù)中。
由于 Spring 的事務(wù)管理器是通過(guò)線(xiàn)程相關(guān)的 ThreadLocal 來(lái)保存數(shù)據(jù)訪(fǎng)問(wèn)基礎(chǔ)設(shè)施,再結(jié)合 IOC 和 AOP 實(shí)現(xiàn)高級(jí)聲明式事務(wù)的功能,所以 Spring 的事務(wù)天然地和線(xiàn)程有著千絲萬(wàn)縷的聯(lián)系。
我們知道 Web 容器本身就是多線(xiàn)程的,Web 容器為一個(gè) Http 請(qǐng)求創(chuàng)建一個(gè)獨(dú)立的線(xiàn)程,所以由此請(qǐng)求所牽涉到的 Spring 容器中的 Bean 也是運(yùn)行于多線(xiàn)程的環(huán)境下。在絕大多數(shù)情況下,Spring 的 Bean 都是單實(shí)例的(singleton),單實(shí)例 Bean 的最大的好處是線(xiàn)程無(wú)關(guān)性,不存在多線(xiàn)程并發(fā)訪(fǎng)問(wèn)的問(wèn)題,也即是線(xiàn)程安全的。
一個(gè)類(lèi)能夠以單實(shí)例的方式運(yùn)行的前提是“無(wú)狀態(tài)”:即一個(gè)類(lèi)不能擁有狀態(tài)化的成員變量。我們知道,在傳統(tǒng)的編程中,DAO 必須執(zhí)有一個(gè) Connection,而 Connection 即是狀態(tài)化的對(duì)象。所以傳統(tǒng)的 DAO 不能做成單實(shí)例的,每次要用時(shí)都必須 new 一個(gè)新的實(shí)例。傳統(tǒng)的 Service 由于將有狀態(tài)的 DAO 作為成員變量,所以傳統(tǒng)的 Service 本身也是有狀態(tài)的。
但是在 Spring 中,DAO 和 Service 都以單實(shí)例的方式存在。Spring 是通過(guò) ThreadLocal 將有狀態(tài)的變量(如 Connection 等)本地線(xiàn)程化,達(dá)到另一個(gè)層面上的“線(xiàn)程無(wú)關(guān)”,從而實(shí)現(xiàn)線(xiàn)程安全。Spring 不遺余力地將狀態(tài)化的對(duì)象無(wú)狀態(tài)化,就是要達(dá)到單實(shí)例化 Bean 的目的。
由于 Spring 已經(jīng)通過(guò) ThreadLocal 的設(shè)施將 Bean 無(wú)狀態(tài)化,所以 Spring 中單實(shí)例 Bean 對(duì)線(xiàn)程安全問(wèn)題擁有了一種天生的免疫能力。不但單實(shí)例的 Service 可以成功運(yùn)行于多線(xiàn)程環(huán)境中,Service 本身還可以自由地啟動(dòng)獨(dú)立線(xiàn)程以執(zhí)行其它的 Service。下面,通過(guò)一個(gè)實(shí)例對(duì)此進(jìn)行描述:
@Service("userService") public class UserService extends BaseService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private ScoreService scoreService; //① 在logon方法體中啟動(dòng)一個(gè)獨(dú)立的線(xiàn)程,在該獨(dú)立的線(xiàn)程中執(zhí)行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()的線(xiàn)程 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); } } } |
將日志級(jí)別設(shè)置為 DEBUG,執(zhí)行 UserService#logon() 方法,觀察以下輸出的日志:
[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 |
在 ① 處,在主線(xiàn)程(main)執(zhí)行的 UserService#logon() 方法的事務(wù)啟動(dòng),在 ③ 處,其對(duì)應(yīng)的事務(wù)提交,而在子線(xiàn)程(Thread-2)執(zhí)行的 ScoreService#addScore() 方法的事務(wù)在 ② 處啟動(dòng),在 ④ 處對(duì)應(yīng)的事務(wù)提交。
所以,我們可以得出這樣的結(jié)論:在 相同線(xiàn)程中進(jìn)行相互嵌套調(diào)用的事務(wù)方法工作于相同的事務(wù)中。如果這些相互嵌套調(diào)用的方法工作在不同的線(xiàn)程中,不同線(xiàn)程下的事務(wù)方法工作在獨(dú)立的事務(wù)中。
Spring 聲明式事務(wù)是 Spring 最核心,最常用的功能。由于 Spring 通過(guò) IOC 和 AOP 的功能非常透明地實(shí)現(xiàn)了聲明式事務(wù)的功能,一般的開(kāi)發(fā)者基本上無(wú)須了解 Spring 聲明式事務(wù)的內(nèi)部細(xì)節(jié),僅需要懂得如何配置就可以了。
但是在實(shí)際應(yīng)用開(kāi)發(fā)過(guò)程中,Spring 的這種透明的高階封裝在帶來(lái)便利的同時(shí),也給我們帶來(lái)了迷惑。就像通過(guò)流言傳播的消息,最終聽(tīng)眾已經(jīng)不清楚事情的真相了,而這對(duì)于應(yīng)用開(kāi)發(fā)來(lái)說(shuō)是很危險(xiǎn)的。本系列文章通過(guò)剖析實(shí)際應(yīng)用中給開(kāi)發(fā)者造成迷惑的各種難點(diǎn),通過(guò)分析 Spring 事務(wù)管理的內(nèi)部運(yùn)作機(jī)制將真相還原出來(lái)。
在本文中,我們通過(guò)剖析了解到以下的真相:
在 下一篇 文章中,筆者將繼續(xù)分析 Spring 事務(wù)管理的以下難點(diǎn):
學(xué)習(xí)
討論
陳雄華,2002 年畢業(yè)于廈門(mén)大學(xué)計(jì)算機(jī)與信息工程學(xué)院,獲碩士學(xué)位。擁有 10 多年的 Java 開(kāi)發(fā)、設(shè)計(jì)、架構(gòu)的經(jīng)驗(yàn)。技術(shù)研發(fā)之余,常將經(jīng)驗(yàn)所得行諸于文字,作者是國(guó)內(nèi)多個(gè)著名技術(shù)網(wǎng)站的專(zhuān)欄作者,在各大技術(shù)網(wǎng)站、報(bào)刊雜志發(fā)表過(guò)數(shù)十篇技術(shù)文章,廣受讀者好評(píng)。于 2005 年出版《精通 JBuilder 2005》,于 2007 年出版《精通 Spring 2.x -- 企業(yè)應(yīng)用開(kāi)發(fā)詳解》。
聯(lián)系客服