日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 前端技术 > javascript >内容正文

javascript

Spring 事务管理高级应用难点剖析--转

發(fā)布時(shí)間:2025/4/5 javascript 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Spring 事务管理高级应用难点剖析--转 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

第 1 部分

http://www.ibm.com/search/csass/search/?q=%E4%BA%8B%E5%8A%A1&sn=dw&lang=zh&cc=CN&en=utf&hpp=20&dws=cndw&lo=zh

概述

Spring 最成功,最吸引人的地方莫過于輕量級(jí)的聲明式事務(wù)管理,僅此一點(diǎn),它就宣告了重量級(jí) EJB 容器的覆滅。Spring 聲明式事務(wù)管理將開發(fā)者從繁復(fù)的事務(wù)管理代碼中解脫出來(lái),專注于業(yè)務(wù)邏輯的開發(fā)上,這是一件可以被拿來(lái)頂禮膜拜的事情。但是,世界并未從此消停,開發(fā)人員需要面對(duì)的是層出不窮的應(yīng)用場(chǎng)景,這些場(chǎng)景往往逾越了普通 Spring 技術(shù)書籍的理想界定。因此,隨著應(yīng)用開發(fā)的深入,在使用經(jīng)過 Spring 層層封裝的聲明式事務(wù)時(shí),開發(fā)人員越來(lái)越覺得自己墜入了迷霧,陷入了沼澤,體會(huì)不到外界所宣稱的那種暢快淋漓。本系列文章的目標(biāo)旨在整理并剖析實(shí)際應(yīng)用中種種讓我們迷茫的場(chǎng)景,讓陽(yáng)光照進(jìn)云遮霧障的山頭。

DAO 和事務(wù)管理的牽絆

很少有使用 Spring 但不使用 Spring 事務(wù)管理器的應(yīng)用,因此常常有人會(huì)問:是否用了 Spring,就一定要用 Spring 事務(wù)管理器,否則就無(wú)法進(jìn)行數(shù)據(jù)的持久化操作呢?事務(wù)管理器和 DAO 是什么關(guān)系呢?

也許是 DAO 和事務(wù)管理如影隨行的緣故吧,這個(gè)看似簡(jiǎn)單的問題實(shí)實(shí)在在地存在著,從初學(xué)者心中涌出,縈繞在開發(fā)老手的腦際。答案當(dāng)然是否定的!我們都知道:事務(wù)管理是保證數(shù)據(jù)操作的事務(wù)性(即原子性、一致性、隔離性、持久性,也即所謂的 ACID),脫離了事務(wù)性,DAO 照樣可以順利地進(jìn)行數(shù)據(jù)的操作。

下面,我們來(lái)看一段使用 Spring JDBC 進(jìn)行數(shù)據(jù)訪問的代碼:

清單 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 {@Autowiredprivate 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ù)為10jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score) VALUES('tom','123456',10)");//③.調(diào)用工作在無(wú)事務(wù)環(huán)境下的服務(wù)類方法,將分?jǐn)?shù)添加20分service.addScore("tom",20);//④.查看此時(shí)用戶的分?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 的配置文件如下所示:

清單 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.xsdhttp://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 中,沒有配置任何事務(wù)管理器,但是數(shù)據(jù)已經(jīng)成功持久化到數(shù)據(jù)庫(kù)中。在默認(rèn)情況下,dataSource 數(shù)據(jù)源的 autoCommit 被設(shè)置為 true ―― 這也意謂著所有通過 JdbcTemplate 執(zhí)行的語(yǔ)句馬上提交,沒有事務(wù)。如果將 dataSource 的 defaultAutoCommit 設(shè)置為 false,再次運(yùn)行 UserJdbcWithoutTransManagerService,將拋出錯(cuò)誤,原因是新增及更改數(shù)據(jù)的操作都沒有提交到數(shù)據(jù)庫(kù),所以 ④ 處的語(yǔ)句因無(wú)法從數(shù)據(jù)庫(kù)中查詢到匹配的記錄而引發(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)榧词古渲昧?#xff0c;也是沒有實(shí)際用處的。

不過,對(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ù)訪問技術(shù),沒有理由不配置 HibernateTransactionManager 事務(wù)管理器。

但是,不使用 Hibernate 事務(wù)管理器,在 Spring 中,Hibernate 照樣也可以工作,來(lái)看下面的例子:

清單 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 {@Autowiredprivate 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)容如下:

清單 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.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsd"><!--省略掉包掃描,數(shù)據(jù)源,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>

運(yùn)行 UserHibernateWithoutTransManagerService,程序正確執(zhí)行,并得到類似于 UserJdbcWithoutTransManagerService 的執(zhí)行結(jié)果,這說(shuō)明 Hibernate 在 Spring 中,在沒有事務(wù)管理器的情況下,依然可以正常地進(jìn)行數(shù)據(jù)的訪問。

應(yīng)用分層的迷惑

Web、Service 及 DAO 三層劃分就像西方國(guó)家的立法、行政、司法三權(quán)分立一樣被奉為金科玉律,甚至有開發(fā)人員認(rèn)為如果要使用 Spring 的事務(wù)管理就一定先要進(jìn)行三層的劃分。這個(gè)看似荒唐的論調(diào)在開發(fā)人員中頗有市場(chǎng)。更有甚者,認(rèn)為每層必須先定義一個(gè)接口,然后再定義一個(gè)實(shí)現(xiàn)類。其結(jié)果是:一個(gè)很簡(jiǎn)單的功能,也至少需要 3 個(gè)接口,3 個(gè)類,再加上視圖層的 JSP 和 JS 等,打牌都可以轉(zhuǎn)上兩桌了,這種誤解貽害不淺。

對(duì)將“面向接口編程”奉為圭臬,認(rèn)為放之四海而皆準(zhǔn)的論調(diào),筆者深不以為然。是的,“面向接口編程”是 Martin Fowler,Rod Johnson 這些大師提倡的行事原則。如果拿這條原則去開發(fā)架構(gòu),開發(fā)產(chǎn)品,怎么強(qiáng)調(diào)都不為過。但是,對(duì)于我們一般的開發(fā)人員來(lái)說(shuō),做的最多的是普通工程項(xiàng)目,往往最多的只是一些對(duì)數(shù)據(jù)庫(kù)增、刪、查、改的功能。此時(shí),“面向接口編程”除了帶來(lái)更多的類文件外,看不到更多其它的好處。

Spring 框架提供的所有附加的好處(AOP、注解增強(qiáng)、注解 MVC 等)唯一的前提就是讓 POJO 的類變成一個(gè)受 Spring 容器管理的 Bean,除此以外沒有其它任何的要求。下面的實(shí)例用一個(gè) 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 {//②.自動(dòng)注入JdbcTemplate@Autowiredprivate JdbcTemplate jdbcTemplate;//③.通過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;} }

通過 @Controller 注解將 MixLayerUserService 變成 Web 層的 Controller,同時(shí)也是 Service 層的服務(wù)類。此外,由于直接使用 JdbcTemplate 訪問數(shù)據(jù),所以 MixLayerUserService 還是一個(gè) DAO。來(lái)看一下對(duì)應(yīng)的 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.xsdhttp://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"/><!--①.啟動(dòng)Spring MVC的注解功能,完成請(qǐng)求和注解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"/><!--普通數(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)功能。而②和③處通過 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)配置:

清單 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/j2eehttp://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參數(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 的配置文件,在此,我們通過 contextConfigLocation 參數(shù)顯式指定 Spring MVC 配置文件的確切位置。

將 org.springframework.jdbc 及 org.springframework.transaction 的日志級(jí)別設(shè)置為 DEBUG,啟動(dòng)項(xiàng)目,并訪問 http://localhost:8088/logon.do?userName=tom 應(yīng)用,MixLayerUserService#logon 方法將作出響應(yīng),查看后臺(tái)輸出日志:

清單 8 執(zhí)行日志
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 的開發(fā)者應(yīng)該是無(wú)拘無(wú)束的:從實(shí)際應(yīng)用出發(fā),去除掉那些所謂原則性的接口,去除掉強(qiáng)制分層的束縛,簡(jiǎn)單才是硬道理。

事務(wù)方法嵌套調(diào)用的迷茫

Spring 事務(wù)一個(gè)被訛傳很廣說(shuō)法是:一個(gè)事務(wù)方法不應(yīng)該調(diào)用另一個(gè)事務(wù)方法,否則將產(chǎn)生兩個(gè)事務(wù)。結(jié)果造成開發(fā)人員在設(shè)計(jì)事務(wù)方法時(shí)束手束腳,生怕一不小心就踩到地雷。

其實(shí)這種是不認(rèn)識(shí) Spring 事務(wù)傳播機(jī)制而造成的誤解,Spring 對(duì)事務(wù)控制的支持統(tǒng)一在 TransactionDefinition 類中描述,該類有以下幾個(gè)重要的接口方法:

  • int getPropagationBehavior():事務(wù)的傳播行為
  • int getIsolationLevel():事務(wù)的隔離級(jí)別
  • int getTimeout():事務(wù)的過期時(shí)間
  • boolean isReadOnly():事務(wù)的讀寫特性。

很明顯,除了事務(wù)的傳播行為外,事務(wù)的其它特性 Spring 是借助底層資源的功能來(lái)完成的,Spring 無(wú)非只充當(dāng)個(gè)代理的角色。但是事務(wù)的傳播行為卻是 Spring 憑借自身的框架提供的功能,是 Spring 提供給開發(fā)者最珍貴的禮物,訛傳的說(shuō)法玷污了 Spring 事務(wù)框架最美麗的光環(huán)。

所謂事務(wù)傳播行為就是多個(gè)事務(wù)方法相互調(diào)用時(shí),事務(wù)如何在這些方法間傳播。Spring 支持 7 種事務(wù)傳播行為:

  • PROPAGATION_REQUIRED 如果當(dāng)前沒有事務(wù),就新建一個(gè)事務(wù),如果已經(jīng)存在一個(gè)事務(wù)中,加入到這個(gè)事務(wù)中。這是最常見的選擇。
  • PROPAGATION_SUPPORTS 支持當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),就以非事務(wù)方式執(zhí)行。
  • PROPAGATION_MANDATORY 使用當(dāng)前的事務(wù),如果當(dāng)前沒有事務(wù),就拋出異常。
  • PROPAGATION_REQUIRES_NEW 新建事務(wù),如果當(dāng)前存在事務(wù),把當(dāng)前事務(wù)掛起。
  • PROPAGATION_NOT_SUPPORTED 以非事務(wù)方式執(zhí)行操作,如果當(dāng)前存在事務(wù),就把當(dāng)前事務(wù)掛起。
  • PROPAGATION_NEVER 以非事務(wù)方式執(zhí)行,如果當(dāng)前存在事務(wù),則拋出異常。
  • PROPAGATION_NESTED 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)內(nèi)執(zhí)行。如果當(dāng)前沒有事務(wù),則執(zhí)行與 PROPAGATION_REQUIRED 類似的操作。

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ù)類的 3 個(gè)方法通過 Spring 的事務(wù)傳播機(jī)制都工作在同一個(gè)事務(wù)中。

下面,我們來(lái)看一下實(shí)例,UserService#logon() 方法內(nèi)部調(diào)用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法,這兩個(gè)類都繼承于 BaseService。它們之間的類結(jié)構(gòu)說(shuō)明如下:

圖 1. UserService 和 ScoreService

具體的代碼如下所示:

清單 9 UserService.java
@Service("userService") public class UserService extends BaseService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate 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{@Autowiredprivate 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 的事務(wù)配置為 ScoreService 及 UserService 中所有公有方法都添加事務(wù)增強(qiáng),讓這些方法都工作于事務(wù)環(huán)境下。下面是關(guān)鍵的配置代碼:

清單 11 事務(wù)增強(qiáng)配置
<!-- 添加Spring事務(wù)增強(qiáng) --> <aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"<!-- 所有繼承于BaseService類的子孫類的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ì)觀察如下的輸出日志:

清單 12 執(zhí)行日志
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 transactionlogon method...updateLastLogonTime... ②直接執(zhí)行updateLastLogonTime方法16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update16: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 rows16: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 update16: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 rows16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit④提交事務(wù)16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transactionon 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 transaction16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

從上面的輸入日志中,可以清楚地看到 Spring 為 UserService#logon() 方法啟動(dòng)了一個(gè)新的事務(wù),而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的類中,沒有觀察到有事務(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ān)的 ThreadLocal 來(lái)保存數(shù)據(jù)訪問基礎(chǔ)設(shè)施,再結(jié)合 IOC 和 AOP 實(shí)現(xiàn)高級(jí)聲明式事務(wù)的功能,所以 Spring 的事務(wù)天然地和線程有著千絲萬(wàn)縷的聯(lián)系。

我們知道 Web 容器本身就是多線程的,Web 容器為一個(gè) Http 請(qǐng)求創(chuàng)建一個(gè)獨(dú)立的線程,所以由此請(qǐng)求所牽涉到的 Spring 容器中的 Bean 也是運(yùn)行于多線程的環(huán)境下。在絕大多數(shù)情況下,Spring 的 Bean 都是單實(shí)例的(singleton),單實(shí)例 Bean 的最大的好處是線程無(wú)關(guān)性,不存在多線程并發(fā)訪問的問題,也即是線程安全的。

一個(gè)類能夠以單實(shí)例的方式運(yùn)行的前提是“無(wú)狀態(tài)”:即一個(gè)類不能擁有狀態(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 是通過 ThreadLocal 將有狀態(tài)的變量(如 Connection 等)本地線程化,達(dá)到另一個(gè)層面上的“線程無(wú)關(guān)”,從而實(shí)現(xiàn)線程安全。Spring 不遺余力地將狀態(tài)化的對(duì)象無(wú)狀態(tài)化,就是要達(dá)到單實(shí)例化 Bean 的目的。

由于 Spring 已經(jīng)通過 ThreadLocal 的設(shè)施將 Bean 無(wú)狀態(tài)化,所以 Spring 中單實(shí)例 Bean 對(duì)線程安全問題擁有了一種天生的免疫能力。不但單實(shí)例的 Service 可以成功運(yùn)行于多線程環(huán)境中,Service 本身還可以自由地啟動(dòng)獨(dú)立線程以執(zhí)行其它的 Service。下面,通過一個(gè)實(shí)例對(duì)此進(jìn)行描述:

清單 13 UserService.java 在事務(wù)方法中啟動(dòng)獨(dú)立線程運(yùn)行另一個(gè)事務(wù)方法
@Service("userService") public class UserService extends BaseService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate ScoreService scoreService;//① 在logon方法體中啟動(dòng)一個(gè)獨(dú)立的線程,在該獨(dú)立的線程中執(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()的線程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() 方法,觀察以下輸出的日志:

清單 14 執(zhí)行日志
[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 transactionlogon 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 transactionon 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 transactionaddScore...[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)執(zhí)行的 UserService#logon() 方法的事務(wù)啟動(dòng),在 ③ 處,其對(duì)應(yīng)的事務(wù)提交,而在子線程(Thread-2)執(zhí)行的 ScoreService#addScore() 方法的事務(wù)在 ② 處啟動(dòng),在 ④ 處對(duì)應(yīng)的事務(wù)提交。

所以,我們可以得出這樣的結(jié)論:在?相同線程中進(jìn)行相互嵌套調(diào)用的事務(wù)方法工作于相同的事務(wù)中。如果這些相互嵌套調(diào)用的方法工作在不同的線程中,不同線程下的事務(wù)方法工作在獨(dú)立的事務(wù)中。

小結(jié)

Spring 聲明式事務(wù)是 Spring 最核心,最常用的功能。由于 Spring 通過 IOC 和 AOP 的功能非常透明地實(shí)現(xiàn)了聲明式事務(wù)的功能,一般的開發(fā)者基本上無(wú)須了解 Spring 聲明式事務(wù)的內(nèi)部細(xì)節(jié),僅需要懂得如何配置就可以了。

但是在實(shí)際應(yīng)用開發(fā)過程中,Spring 的這種透明的高階封裝在帶來(lái)便利的同時(shí),也給我們帶來(lái)了迷惑。就像通過流言傳播的消息,最終聽眾已經(jīng)不清楚事情的真相了,而這對(duì)于應(yīng)用開發(fā)來(lái)說(shuō)是很危險(xiǎn)的。本系列文章通過剖析實(shí)際應(yīng)用中給開發(fā)者造成迷惑的各種難點(diǎn),通過分析 Spring 事務(wù)管理的內(nèi)部運(yùn)作機(jī)制將真相還原出來(lái)。

在本文中,我們通過剖析了解到以下的真相:

  • 在沒有事務(wù)管理的情況下,DAO 照樣可以順利進(jìn)行數(shù)據(jù)操作;
  • 將應(yīng)用分成 Web,Service 及 DAO 層只是一種參考的開發(fā)模式,并非是事務(wù)管理工作的前提條件;
  • Spring 通過事務(wù)傳播機(jī)制可以很好地應(yīng)對(duì)事務(wù)方法嵌套調(diào)用的情況,開發(fā)者無(wú)須為了事務(wù)管理而刻意改變服務(wù)方法的設(shè)計(jì);
  • 由于單實(shí)例的對(duì)象不存在線程安全問題,所以進(jìn)行事務(wù)管理增強(qiáng)的 Bean 可以很好地工作在多線程環(huán)境下。

在?下一篇?文章中,筆者將繼續(xù)分析 Spring 事務(wù)管理的以下難點(diǎn):

  • 混合使用多種數(shù)據(jù)訪問技術(shù)(如 Spring JDBC + Hibernate)的事務(wù)管理問題;
  • 進(jìn)行 Spring AOP 增強(qiáng)的 Bean 存在哪些特殊的情況。

第 2 部分

http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts2/

聯(lián)合軍種作戰(zhàn)的混亂

Spring 抽象的 DAO 體系兼容多種數(shù)據(jù)訪問技術(shù),它們各有特色,各有千秋。像 Hibernate 是非常優(yōu)秀的 ORM 實(shí)現(xiàn)方案,但對(duì)底層 SQL 的控制不太方便;而 iBatis 則通過模板化技術(shù)讓您方便地控制 SQL,但沒有 Hibernate 那樣高的開發(fā)效率;自由度最高的當(dāng)然是直接使用 Spring JDBC 莫屬了,但是它也是最底層的,靈活的代價(jià)是代碼的繁復(fù)。很難說(shuō)哪種數(shù)據(jù)訪問技術(shù)是最優(yōu)秀的,只有在某種特定的場(chǎng)景下,才能給出答案。所以在一個(gè)應(yīng)用中,往往采用多個(gè)數(shù)據(jù)訪問技術(shù):一般是兩種,一種采用 ORM 技術(shù)框架,而另一種采用偏 JDBC 的底層技術(shù),兩者珠聯(lián)璧合,形成聯(lián)合軍種,共同御敵。

但是,這種聯(lián)合軍種如何應(yīng)對(duì)事務(wù)管理的問題呢?我們知道 Spring 為每種數(shù)據(jù)訪問技術(shù)提供了相應(yīng)的事務(wù)管理器,難道需要分別為它們配置對(duì)應(yīng)的事務(wù)管理器嗎?它們到底是如何協(xié)作,如何工作的呢?這些層出不窮的問題往往壓制了開發(fā)人員使用聯(lián)合軍種的想法。

其實(shí),在這個(gè)問題上,我們低估了 Spring 事務(wù)管理的能力。如果您采用了一個(gè)高端 ORM 技術(shù)(Hibernate,JPA,JDO),同時(shí)采用一個(gè) JDBC 技術(shù)(Spring JDBC,iBatis),由于前者的會(huì)話(Session)是對(duì)后者連接(Connection)的封裝,Spring 會(huì)“足夠智能地”在同一個(gè)事務(wù)線程讓前者的會(huì)話封裝后者的連接。所以,我們只要直接采用前者的事務(wù)管理器就可以了。下表給出了混合數(shù)據(jù)訪問技術(shù)所對(duì)應(yīng)的事務(wù)管理器:

表 1. 混合數(shù)據(jù)訪問技術(shù)的事務(wù)管理器
混合數(shù)據(jù)訪問技術(shù)事務(wù)管理器
ORM 技術(shù)框架JDBC 技術(shù)框架
HibernateSpring JDBC 或 iBatisHibernateTransactionManager
JPASpring JDBC 或 iBatisJpaTransactionManager
JDOSpring JDBC 或 iBatisJdoTransactionManager

由于一般不會(huì)出現(xiàn)同時(shí)使用多個(gè) ORM 框架的情況(如 Hibernate + JPA),我們不擬對(duì)此命題展開論述,只重點(diǎn)研究 ORM 框架 + JDBC 框架的情況。Hibernate + Spring JDBC 可能是被使用得最多的組合,下面我們通過實(shí)例觀察事務(wù)管理的運(yùn)作情況。

清單 1.User.java:使用了注解聲明的實(shí)體類
import javax.persistence.Entity; import javax.persistence.Table; import javax.persistence.Column; import javax.persistence.Id; import java.io.Serializable; @Entity @Table(name="T_USER") public class User implements Serializable{ @Id@Column(name = "USER_NAME") private String userName; private String password; private int score; @Column(name = "LAST_LOGON_TIME")private long lastLogonTime = 0; }

再來(lái)看下 UserService 的關(guān)鍵代碼:

清單 2.UserService.java:使用 Hibernate 數(shù)據(jù)訪問技術(shù)
package user.mixdao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Service; import org.springframework.orm.hibernate3.HibernateTemplate; import org.apache.commons.dbcp.BasicDataSource; import user.User;@Service("userService") public class UserService extends BaseService {@Autowiredprivate HibernateTemplate hibernateTemplate;@Autowiredprivate ScoreService scoreService;public void logon(String userName) {System.out.println("logon method...");updateLastLogonTime(userName); //①使用Hibernate數(shù)據(jù)訪問技術(shù)scoreService.addScore(userName, 20); //②使用Spring JDBC數(shù)據(jù)訪問技術(shù)}public void updateLastLogonTime(String userName) {System.out.println("updateLastLogonTime...");User user = hibernateTemplate.get(User.class,userName);user.setLastLogonTime(System.currentTimeMillis());hibernateTemplate.flush(); //③請(qǐng)看下文的分析} }

在①處,使用 Hibernate 操作數(shù)據(jù),而在②處調(diào)用 ScoreService#addScore(),該方法內(nèi)部使用 Spring JDBC 操作數(shù)據(jù)。

在③處,我們顯式調(diào)用了 flush() 方法,將 Session 中的緩存同步到數(shù)據(jù)庫(kù)中,這個(gè)操作將即時(shí)向數(shù)據(jù)庫(kù)發(fā)送一條更新記錄的 SQL 語(yǔ)句。之所以要在此顯式執(zhí)行 flush() 方法,原因是:默認(rèn)情況下,Hibernate 要在事務(wù)提交時(shí)才將數(shù)據(jù)的更改同步到數(shù)據(jù)庫(kù)中,而事務(wù)提交發(fā)生在 logon() 方法返回前。如果所有針對(duì)數(shù)據(jù)庫(kù)的更改都使用 Hibernate,這種數(shù)據(jù)同步延遲的機(jī)制不會(huì)產(chǎn)生任何問題。但是,我們?cè)?logon() 方法中同時(shí)采用了 Hibernate 和 Spring JDBC 混合數(shù)據(jù)訪問技術(shù)。Spring JDBC 無(wú)法自動(dòng)感知 Hibernate 一級(jí)緩存,所以如果不及時(shí)調(diào)用 flush() 方法將數(shù)據(jù)更改同步到數(shù)據(jù)庫(kù),則②處通過 Spring JDBC 進(jìn)行數(shù)據(jù)更改的結(jié)果將被 Hibernate 一級(jí)緩存中的更改覆蓋掉,因?yàn)?#xff0c;一級(jí)緩存在 logon() 方法返回前才同步到數(shù)據(jù)庫(kù)!

ScoreService 使用 Spring JDBC 數(shù)據(jù)訪問技術(shù),其代碼如下:

清單 3.ScoreService.java:使用 Spring JDBC 數(shù)據(jù)訪問技術(shù)
package user.mixdao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.apache.commons.dbcp.BasicDataSource;@Service("scoreUserService") public class ScoreService extends BaseService{@Autowiredprivate JdbcTemplate jdbcTemplate;public void addScore(String userName, int toAdd) {System.out.println("addScore...");String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";jdbcTemplate.update(sql, toAdd, userName);//① 查看此處數(shù)據(jù)庫(kù)激活的連接數(shù)BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();System.out.println("激活連接數(shù)量:"+basicDataSource.getNumActive());} }

Spring 關(guān)鍵的配置文件代碼如下所示:

清單 4. applicationContext.xml 事務(wù)配置代碼部分
<!-- 使用Hibernate事務(wù)管理器 --> <bean id="hiberManager"class="org.springframework.orm.hibernate3.HibernateTransactionManager"p:sessionFactory-ref="sessionFactory"/><!-- 對(duì)所有繼承BaseService類的公用方法實(shí)施事務(wù)增強(qiáng) --> <aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"expression="within(user.mixdao.BaseService+)"/><aop:advisor pointcut-ref="serviceJdbcMethod"advice-ref="hiberAdvice"/> </aop:config><tx:advice id="hiberAdvice" transaction-manager="hiberManager"><tx:attributes><tx:method name="*"/></tx:attributes> </tx:advice>

啟動(dòng) Spring 容器,執(zhí)行 UserService#logon() 方法,可以查看到如下的執(zhí)行日志:

清單 5. 代碼運(yùn)行日志
12:38:57,062 (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT12:38:57,093 (SessionImpl.java:220) - opened session at timestamp: 1266640737012:38:57,093 (HibernateTransactionManager.java:493) - Opened new Session [org.hibernate.impl.SessionImpl@83020] for Hibernate transaction ①12:38:57,093 (HibernateTransactionManager.java:504) - Preparing JDBC Connection of Hibernate Session [org.hibernate.impl.SessionImpl@83020]12:38:57,109 (JDBCTransaction.java:54) - begin…logon method... updateLastLogonTime... …12:38:57,109 (AbstractBatcher.java:401) - select user0_.USER_NAME as USER1_0_0_, user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_, user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=?Hibernate: select user0_.USER_NAME as USER1_0_0_, user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_, user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=?…12:38:57,187 (HibernateTemplate.java:422) - Not closing pre-bound Hibernate Session after HibernateTemplate12:38:57,187 (HibernateTemplate.java:397) - Found thread-bound Sessionfor HibernateTemplateHibernate: update T_USER set LAST_LOGON_TIME=?, password=?, score=? where USER_NAME=?…2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:470) - Participating in existing transaction ② addScore...2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:785) - Executing prepared SQL update2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:569)- Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:794) - SQL update affected 1 rows激活連接數(shù)量:1 ③ 2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit 2010-02-20 12:38:57,203 DEBUG [main] (HibernateTransactionManager.java:652) - Committing Hibernate transaction on Session [org.hibernate.impl.SessionImpl@83020] ④2010-02-20 12:38:57,203 DEBUG [main] (JDBCTransaction.java:103) - commit ⑤

仔細(xì)觀察這段輸出日志,在①處 UserService#logon() 開啟一個(gè)新的事務(wù),在②處 ScoreService#addScore() 方法加入到①處開啟的事務(wù)上下文中。③處的輸出是 ScoreService#addScore() 方法內(nèi)部的輸出,匯報(bào)此時(shí)數(shù)據(jù)源激活的連接數(shù)為 1,這清楚地告訴我們 Hibernate 和 JDBC 這兩種數(shù)據(jù)訪問技術(shù)在同一事務(wù)上下文中“共用”一個(gè)連接。在④處,提交 Hibernate 事務(wù),接著在⑤處觸發(fā)調(diào)用底層的 Connection 提交事務(wù)。

從以上的運(yùn)行結(jié)果,我們可以得出這樣的結(jié)論:使用 Hibernate 事務(wù)管理器后,可以混合使用 Hibernate 和 Spring JDBC 數(shù)據(jù)訪問技術(shù),它們將工作于同一事務(wù)上下文中。但是使用 Spring JDBC 訪問數(shù)據(jù)時(shí),Hibernate 的一級(jí)或二級(jí)緩存得不到同步,此外,一級(jí)緩存延遲數(shù)據(jù)同步機(jī)制可能會(huì)覆蓋 Spring JDBC 數(shù)據(jù)更改的結(jié)果。

由于混合數(shù)據(jù)訪問技術(shù)的方案的事務(wù)同步而緩存不同步的情況,所以最好用 Hibernate 完成讀寫操作,而用 Spring JDBC 完成讀的操作。如用 Spring JDBC 進(jìn)行簡(jiǎn)要列表的查詢,而用 Hibernate 對(duì)查詢出的數(shù)據(jù)進(jìn)行維護(hù)。如果確實(shí)要同時(shí)使用 Hibernate 和 Spring JDBC 讀寫數(shù)據(jù),則必須充分考慮到 Hibernate 緩存機(jī)制引發(fā)的問題:必須充分分析數(shù)據(jù)維護(hù)邏輯,根據(jù)需要,及時(shí)調(diào)用 Hibernate 的 flush() 方法,以免覆蓋 Spring JDBC 的更改,在 Spring JDBC 更改數(shù)據(jù)庫(kù)時(shí),維護(hù) Hibernate 的緩存。

可以將以上結(jié)論推廣到其它混合數(shù)據(jù)訪問技術(shù)的方案中,如 Hibernate+iBatis,JPA+Spring JDBC,JDO+Spring JDBC 等。

特殊方法成漏網(wǎng)之魚

由于 Spring 事務(wù)管理是基于接口代理或動(dòng)態(tài)字節(jié)碼技術(shù),通過 AOP 實(shí)施事務(wù)增強(qiáng)的。雖然,Spring 還支持 AspectJ LTW 在類加載期實(shí)施增強(qiáng),但這種方法很少使用,所以我們不予關(guān)注。

對(duì)于基于接口動(dòng)態(tài)代理的 AOP 事務(wù)增強(qiáng)來(lái)說(shuō),由于接口的方法是 public 的,這就要求實(shí)現(xiàn)類的實(shí)現(xiàn)方法必須是 public 的(不能是 protected,private 等),同時(shí)不能使用 static 的修飾符。所以,可以實(shí)施接口動(dòng)態(tài)代理的方法只能是使用“public”或“public final”修飾符的方法,其它方法不可能被動(dòng)態(tài)代理,相應(yīng)的也就不能實(shí)施 AOP 增強(qiáng),也不能進(jìn)行 Spring 事務(wù)增強(qiáng)了。

基于 CGLib 字節(jié)碼動(dòng)態(tài)代理的方案是通過擴(kuò)展被增強(qiáng)類,動(dòng)態(tài)創(chuàng)建子類的方式進(jìn)行 AOP 增強(qiáng)植入的。由于使用 final、static、private 修飾符的方法都不能被子類覆蓋,相應(yīng)的,這些方法將不能被實(shí)施 AOP 增強(qiáng)。所以,必須特別注意這些修飾符的使用,以免不小心成為事務(wù)管理的漏網(wǎng)之魚。

下面通過具體的實(shí)例說(shuō)明基于 CGLib 字節(jié)碼動(dòng)態(tài)代理無(wú)法享受 Spring AOP 事務(wù)增強(qiáng)的特殊方法。

清單 6.UserService.java:4 個(gè)不同修飾符的方法
package user.special; import org.springframework.stereotype.Service;@Service("userService") public class UserService {//① private方法因訪問權(quán)限的限制,無(wú)法被子類覆蓋private void method1() {System.out.println("method1");}//② final方法無(wú)法被子類覆蓋public final void method2() {System.out.println("method2");}//③ static是類級(jí)別的方法,無(wú)法被子類覆蓋public static void method3() {System.out.println("method3");}//④ public方法可以被子類覆蓋,因此可以被動(dòng)態(tài)字節(jié)碼增強(qiáng)public void method4() {System.out.println("method4");} }

Spring 通過 CGLib 動(dòng)態(tài)代理技術(shù)對(duì) UserService Bean 實(shí)施 AOP 事務(wù)增強(qiáng)的配置如下所示:

清單 7.applicationContext.xml:對(duì) UserService 用 CGLib 實(shí)施事務(wù)增強(qiáng)
<?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.xsdhttp://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"><!-- 省略聲明數(shù)據(jù)源及DataSourceTransactionManager事務(wù)管理器-->…<aop:config proxy-target-class="true"><!-- ①顯式使用CGLib動(dòng)態(tài)代理 --><!-- ②希望對(duì)UserService所有方法實(shí)施事務(wù)增強(qiáng) --><aop:pointcut id="serviceJdbcMethod"expression="execution(* user.special.UserService.*(..))"/><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>

在 ① 處,我們通過 proxy-target-class="true"顯式使用 CGLib 動(dòng)態(tài)代理技術(shù),在 ② 處通過 AspjectJ 切點(diǎn)表達(dá)式表達(dá) UserService 所有的方法,希望對(duì) UserService 所有方法都實(shí)施 Spring AOP 事務(wù)增強(qiáng)。

在 UserService 添加一個(gè)可執(zhí)行的方法,如下所示:

清單 8.UserService.java 添加 main 方法
package user.special; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Service;@Service("userService") public class UserService {…public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("user/special/applicationContext.xml");UserService service = (UserService) ctx.getBean("userService");System.out.println("before method1");service.method1();System.out.println("after method1");System.out.println("before method2");service.method2();System.out.println("after method2");System.out.println("before method3");service.method3();System.out.println("after method3");System.out.println("before method4");service.method4();System.out.println("after method4");} }

在運(yùn)行 UserService 之前,將 Log4J 日志級(jí)別設(shè)置為 DEBUG,運(yùn)行以上代碼查看輸出日志,如下所示:

17:24:10,953 (AbstractBeanFactory.java:241) - Returning cached instance of singleton bean 'userService'before method1 method1 after method1 before method2 method2 after method2 before method3 method3 after method3 before method417:24:10,953 (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.special.UserService.method4]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT17:24:11,109 (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@165b7e] for JDBC transaction…17:24:11,109 (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@165b7e]17:24:11,125 (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@165b7e] after transaction17:24:11,125 (DataSourceUtils.java:312) - Returning JDBC Connection to DataSourceafter method4

觀察以上輸出日志,很容易發(fā)現(xiàn) method1~method3 這 3 個(gè)方法都沒有被實(shí)施 Spring 的事務(wù)增強(qiáng),只有 method4 被實(shí)施了事務(wù)增強(qiáng)。這個(gè)結(jié)果剛才驗(yàn)證了我們前面的論述。

我們通過下表描述哪些特殊方法將成為 Spring AOP 事務(wù)增強(qiáng)的漏網(wǎng)之魚:

表 2. 不能被 Spring AOP 事務(wù)增強(qiáng)的方法
動(dòng)態(tài)代理策略不能被事務(wù)增強(qiáng)的方法
基于接口的動(dòng)態(tài)代理除 public 外的其它所有的方法,此外 public static 也不能被增強(qiáng)
基于 CGLib 的動(dòng)態(tài)代理private、static、final 的方法

不過,需要特別指出的是,這些不能被 Spring 事務(wù)增強(qiáng)的特殊方法并非就不工作在事務(wù)環(huán)境下。只要它們被外層的事務(wù)方法調(diào)用了,由于 Spring 的事務(wù)管理的傳播特殊,內(nèi)部方法也可以工作在外部方法所啟動(dòng)的事務(wù)上下文中。我們說(shuō),這些方法不能被 Spring 進(jìn)行 AOP 事務(wù)增強(qiáng),是指這些方法不能啟動(dòng)事務(wù),但是外層方法的事務(wù)上下文依就可以順利地傳播到這些方法中。

這些不能被 Spring 事務(wù)增強(qiáng)的方法和可被 Spring 事務(wù)增強(qiáng)的方法唯一的區(qū)別在?“是否可以主動(dòng)啟動(dòng)一個(gè)新事務(wù)”:前者不能而后者可以。對(duì)于事務(wù)傳播行為來(lái)說(shuō),二者是完全相同的,前者也和后者一樣不會(huì)造成數(shù)據(jù)連接的泄漏問題。換句話說(shuō),如果這些“特殊方法”被無(wú)事務(wù)上下文的方法調(diào)用,則它們就工作在無(wú)事務(wù)上下文中;反之,如果被具有事務(wù)上下文的方法調(diào)用,則它們就工作在事務(wù)上下文中。

對(duì)于 private 的方法,由于最終都會(huì)被 public 方法封裝后再開放給外部調(diào)用,而 public 方法是可以被事務(wù)增強(qiáng)的,所以基本上沒有什么問題。在實(shí)際開發(fā)中,最容易造成隱患的是基于 CGLib 的動(dòng)態(tài)代理時(shí)的“public static”和“public final”這兩種特殊方法。原因是它們本身是 public 的,所以可以直接被外部類(如 Web 層的 Controller 類)調(diào)用,只要調(diào)用者沒有事務(wù)上下文,這些特殊方法也就以無(wú)事務(wù)的方式運(yùn)作。

小結(jié)

在本文中,我們通過剖析了解到以下的真相:

  • 混合使用多個(gè)數(shù)據(jù)訪問技術(shù)框架的最佳組合是一個(gè) ORM 技術(shù)框架(如 Hibernate 或 JPA 等)+ 一個(gè) JDBC 技術(shù)框架(如 Spring JDBC 或 iBatis)。直接使用 ORM 技術(shù)框架對(duì)應(yīng)的事務(wù)管理器就可以了,但必須考慮 ORM 緩存同步的問題;
  • Spring AOP 增強(qiáng)有兩個(gè)方案:其一為基于接口的動(dòng)態(tài)代理,其二為基于 CGLib 動(dòng)態(tài)生成子類的代理。由于 Java 語(yǔ)法的特性,有些特殊方法不能被 Spring AOP 代理,所以也就無(wú)法享受 AOP 織入帶來(lái)的事務(wù)增強(qiáng)。

在下一篇文章中,筆者將繼續(xù)分析 Spring 事務(wù)管理的以下難點(diǎn):

  • 直接獲取 Connection 時(shí),哪些情況會(huì)造成數(shù)據(jù)連接的泄漏,以及如何應(yīng)對(duì);
  • 除 Spring JDBC 外,其它數(shù)據(jù)訪問技術(shù)數(shù)據(jù)連接泄漏的應(yīng)對(duì)方案。

第 3 部分

http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts3/

概述

對(duì)于應(yīng)用開發(fā)者來(lái)說(shuō),數(shù)據(jù)連接泄漏無(wú)疑是一個(gè)可怕的夢(mèng)魘。如果存在數(shù)據(jù)連接泄漏問題,應(yīng)用程序?qū)⒁驍?shù)據(jù)連接資源的耗盡而崩潰,甚至還可能引起數(shù)據(jù)庫(kù)的崩潰。數(shù)據(jù)連接泄漏像黑洞一樣讓開發(fā)者避之唯恐不及。

Spring DAO 對(duì)所有支持的數(shù)據(jù)訪問技術(shù)框架都使用模板化技術(shù)進(jìn)行了薄層的封裝。只要您的程序都使用 Spring DAO 模板(如 JdbcTemplate、HibernateTemplate 等)進(jìn)行數(shù)據(jù)訪問,一定不會(huì)存在數(shù)據(jù)連接泄漏的問題 ―― 這是 Spring 給予我們鄭重的承諾!因此,我們無(wú)需關(guān)注數(shù)據(jù)連接(Connection)及其衍生品(Hibernate 的 Session 等)的獲取和釋放的操作,模板類已經(jīng)通過其內(nèi)部流程替我們完成了,且對(duì)開發(fā)者是透明的。

但是由于集成第三方產(chǎn)品,整合遺產(chǎn)代碼等原因,可能需要直接訪問數(shù)據(jù)源或直接獲取數(shù)據(jù)連接及其衍生品。這時(shí),如果使用不當(dāng),就可能在無(wú)意中創(chuàng)造出一個(gè)魔鬼般的連接泄漏問題。

我們知道:當(dāng) Spring 事務(wù)方法運(yùn)行時(shí),就產(chǎn)生一個(gè)事務(wù)上下文,該上下文在本事務(wù)執(zhí)行線程中針對(duì)同一個(gè)數(shù)據(jù)源綁定了一個(gè)唯一的數(shù)據(jù)連接(或其衍生品),所有被該事務(wù)上下文傳播的方法都共享這個(gè)數(shù)據(jù)連接。這個(gè)數(shù)據(jù)連接從數(shù)據(jù)源獲取及返回給數(shù)據(jù)源都在 Spring 掌控之中,不會(huì)發(fā)生問題。如果在需要數(shù)據(jù)連接時(shí),能夠獲取這個(gè)被 Spring 管控的數(shù)據(jù)連接,則使用者可以放心使用,無(wú)需關(guān)注連接釋放的問題。

那么,如何獲取這些被 Spring 管控的數(shù)據(jù)連接呢? Spring 提供了兩種方法:其一是使用數(shù)據(jù)資源獲取工具類,其二是對(duì)數(shù)據(jù)源(或其衍生品如 Hibernate SessionFactory)進(jìn)行代理。在具體介紹這些方法之前,讓我們先來(lái)看一下各種引發(fā)數(shù)據(jù)連接泄漏的場(chǎng)景。

Spring JDBC 數(shù)據(jù)連接泄漏

如果直接從數(shù)據(jù)源獲取連接,且在使用完成后不主動(dòng)歸還給數(shù)據(jù)源(調(diào)用 Connection#close()),則將造成數(shù)據(jù)連接泄漏的問題。

一個(gè)具體的實(shí)例

下面,來(lái)看一個(gè)具體的實(shí)例:

清單 1.JdbcUserService.java:主體代碼
package user.connleak; import org.apache.commons.dbcp.BasicDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.sql.Connection; @Service("jdbcUserService") public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; public void logon(String userName) { try { // ①直接從數(shù)據(jù)源獲取連接,后續(xù)程序沒有顯式釋放該連接Connection conn = jdbcTemplate.getDataSource().getConnection(); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000);// ②模擬程序代碼的執(zhí)行時(shí)間} catch (Exception e) { e.printStackTrace(); } } }

JdbcUserService 通過 Spring AOP 事務(wù)增強(qiáng)的配置,讓所有 public 方法都工作在事務(wù)環(huán)境中。即讓 logon() 和 updateLastLogonTime() 方法擁有事務(wù)功能。在 logon() 方法內(nèi)部,我們?cè)冖偬幫ㄟ^調(diào)用?jdbcTemplate.getDataSource().getConnection()顯式獲取一個(gè)連接,這個(gè)連接不是 logon() 方法事務(wù)上下文線程綁定的連接,所以如果開發(fā)者如果沒有手工釋放這連接(顯式調(diào)用 Connection#close() 方法),則這個(gè)連接將永久被占用(處于 active 狀態(tài)),造成連接泄漏!下面,我們編寫模擬運(yùn)行的代碼,查看方法執(zhí)行對(duì)數(shù)據(jù)連接的實(shí)際占用情況:

清單 2.JdbcUserService.java:模擬運(yùn)行代碼
… @Service("jdbcUserService") public class JdbcUserService {…//①以異步線程的方式執(zhí)行JdbcUserService#logon()方法,以模擬多線程的環(huán)境public static void asynchrLogon(JdbcUserService userService, String userName) {UserServiceRunner runner = new UserServiceRunner(userService, userName);runner.start();}private static class UserServiceRunner extends Thread {private JdbcUserService userService;private String userName;public UserServiceRunner(JdbcUserService userService, String userName) {this.userService = userService;this.userName = userName;}public void run() {userService.logon(userName);}}//② 讓主執(zhí)行線程睡眠一段指定的時(shí)間public static void sleep(long time) {try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}//③ 匯報(bào)數(shù)據(jù)源的連接占用情況public static void reportConn(BasicDataSource basicDataSource) {System.out.println("連接數(shù)[active:idle]-[" +basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]");}public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("user/connleak/applicatonContext.xml");JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");//④匯報(bào)數(shù)據(jù)源初始連接占用情況JdbcUserService.reportConn(basicDataSource);JdbcUserService.asynchrLogon(userService, "tom");JdbcUserService.sleep(500);//⑤此時(shí)線程A正在執(zhí)行JdbcUserService#logon()方法JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000);//⑥此時(shí)線程A所執(zhí)行的JdbcUserService#logon()方法已經(jīng)執(zhí)行完畢JdbcUserService.reportConn(basicDataSource);JdbcUserService.asynchrLogon(userService, "john");JdbcUserService.sleep(500);//⑦此時(shí)線程B正在執(zhí)行JdbcUserService#logon()方法JdbcUserService.reportConn(basicDataSource);JdbcUserService.sleep(2000);//⑧此時(shí)線程A和B都已完成JdbcUserService#logon()方法的執(zhí)行JdbcUserService.reportConn(basicDataSource);}

在 JdbcUserService 中添加一個(gè)可異步執(zhí)行 logon() 方法的 asynchrLogon() 方法,我們通過異步執(zhí)行 logon() 以及讓主線程睡眠的方式模擬多線程環(huán)境下的執(zhí)行場(chǎng)景。在不同的執(zhí)行點(diǎn),通過 reportConn() 方法匯報(bào)數(shù)據(jù)源連接的占用情況。

使用如下的 Spring 配置文件對(duì) JdbcUserServie 的方法進(jìn)行事務(wù)增強(qiáng):

清單 3.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.xsdhttp://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"><context:component-scan base-package="user.connleak"/><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"p:defaultAutoCommit="false"/><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"/><!-- 對(duì)JdbcUserService的所有方法實(shí)施事務(wù)增強(qiáng) --><aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"expression="within(user.connleak.JdbcUserService+)"/><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>

保證 BasicDataSource 數(shù)據(jù)源的配置默認(rèn)連接為 0,運(yùn)行以上程序代碼,在控制臺(tái)中將輸出以下的信息:

清單 4. 輸出日志
連接數(shù) [active:idle]-[0:0] 連接數(shù) [active:idle]-[2:0] 連接數(shù) [active:idle]-[1:1] 連接數(shù) [active:idle]-[3:0] 連接數(shù) [active:idle]-[2:1]

我們通過下表對(duì)數(shù)據(jù)源連接的占用和泄漏情況進(jìn)行描述:

表 1. 執(zhí)行過程數(shù)據(jù)源連接占用情況
時(shí)間執(zhí)行線程 1執(zhí)行線程 2數(shù)據(jù)源連接activeidleleak
T0未啟動(dòng)未啟動(dòng)000
T1正在執(zhí)行方法未啟動(dòng)200
T2執(zhí)行完畢未啟動(dòng)111
T3執(zhí)行完畢正式執(zhí)行方法301
T4執(zhí)行完畢執(zhí)行完畢212

可見在執(zhí)行線程 1 執(zhí)行完畢后,只釋放了一個(gè)數(shù)據(jù)連接,還有一個(gè)數(shù)據(jù)連處于 active 狀態(tài),說(shuō)明泄漏了一個(gè)連接。相似的,執(zhí)行線程 2 執(zhí)行完畢后,也泄漏了一個(gè)連接:原因是直接通過數(shù)據(jù)源獲取連接(jdbcTemplate.getDataSource().getConnection())而沒有顯式釋放造成的。

通過 DataSourceUtils 獲取數(shù)據(jù)連接

Spring 提供了一個(gè)能從當(dāng)前事務(wù)上下文中獲取綁定的數(shù)據(jù)連接的工具類,那就是 DataSourceUtils。Spring 強(qiáng)調(diào)必須使用 DataSourceUtils 工具類獲取數(shù)據(jù)連接,Spring 的 JdbcTemplate 內(nèi)部也是通過 DataSourceUtils 來(lái)獲取連接的。DataSourceUtils 提供了若干獲取和釋放數(shù)據(jù)連接的靜態(tài)方法,說(shuō)明如下:

  • static Connection doGetConnection(DataSource dataSource):首先嘗試從事務(wù)上下文中獲取連接,失敗后再?gòu)臄?shù)據(jù)源獲取連接;
  • static Connection getConnection(DataSource dataSource):和 doGetConnection 方法的功能一樣,實(shí)際上,它內(nèi)部就是調(diào)用 doGetConnection 方法獲取連接的;
  • static void doReleaseConnection(Connection con, DataSource dataSource):釋放連接,放回到連接池中;
  • static void releaseConnection(Connection con, DataSource dataSource):和 doReleaseConnection 方法的功能一樣,實(shí)際上,它內(nèi)部就是調(diào)用 doReleaseConnection 方法獲取連接的;

來(lái)看一下 DataSourceUtils 從數(shù)據(jù)源獲取連接的關(guān)鍵代碼:

清單 5. DataSourceUtils.java 獲取連接的工具類
public abstract class DataSourceUtils {…public static Connection doGetConnection(DataSource dataSource) throws SQLException {Assert.notNull(dataSource, "No DataSource specified");//①首先嘗試從事務(wù)同步管理器中獲取數(shù)據(jù)連接ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested();if (!conHolder.hasConnection()) {logger.debug("Fetching resumed JDBC Connection from DataSource");conHolder.setConnection(dataSource.getConnection());}return conHolder.getConnection();}//②如果獲取不到,則直接從數(shù)據(jù)源中獲取連接Connection con = dataSource.getConnection();//③如果擁有事務(wù)上下文,則將連接綁定到事務(wù)上下文中if (TransactionSynchronizationManager.isSynchronizationActive()) {ConnectionHolder holderToUse = conHolder;if (holderToUse == null) {holderToUse = new ConnectionHolder(con);}else {holderToUse.setConnection(con);}holderToUse.requested();TransactionSynchronizationManager.registerSynchronization(new ConnectionSynchronization(holderToUse, dataSource));holderToUse.setSynchronizedWithTransaction(true);if (holderToUse != conHolder) {TransactionSynchronizationManager.bindResource(dataSource, holderToUse);}}return con;}… }

它首先查看當(dāng)前是否存在事務(wù)管理上下文,并嘗試從事務(wù)管理上下文獲取連接,如果獲取失敗,直接從數(shù)據(jù)源中獲取連接。在獲取連接后,如果當(dāng)前擁有事務(wù)上下文,則將連接綁定到事務(wù)上下文中。

我們?cè)谇鍐?1 的 JdbcUserService 中,使用 DataSourceUtils.getConnection() 替換直接從數(shù)據(jù)源中獲取連接的代碼:

清單 6. JdbcUserService.java:使用 DataSourceUtils 獲取數(shù)據(jù)連接
public void logon(String userName) {try {//Connection conn = jdbcTemplate.getDataSource().getConnection();//①使用DataSourceUtils獲取數(shù)據(jù)連接Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";jdbcTemplate.update(sql, System.currentTimeMillis(), userName);Thread.sleep(1000); } catch (Exception e) {e.printStackTrace();} }

重新運(yùn)行代碼,得到如下的執(zhí)行結(jié)果:

清單 7. 輸出日志
連接數(shù) [active:idle]-[0:0] 連接數(shù) [active:idle]-[1:0] 連接數(shù) [active:idle]-[0:1] 連接數(shù) [active:idle]-[1:0] 連接數(shù) [active:idle]-[0:1]

對(duì)照清單 4 的輸出日志,我們可以看到已經(jīng)沒有連接泄漏的現(xiàn)象了。一個(gè)執(zhí)行線程在運(yùn)行 JdbcUserService#logon() 方法時(shí),只占用一個(gè)連接,而且方法執(zhí)行完畢后,該連接馬上釋放。這說(shuō)明通過 DataSourceUtils.getConnection() 方法確實(shí)獲取了方法所在事務(wù)上下文綁定的那個(gè)連接,而不是像原來(lái)那樣從數(shù)據(jù)源中獲取一個(gè)新的連接。

使用 DataSourceUtils 獲取數(shù)據(jù)連接也可能造成泄漏!

是否使用 DataSourceUtils 獲取數(shù)據(jù)連接就可以高枕無(wú)憂了呢?理想很美好,但現(xiàn)實(shí)很殘酷:如果 DataSourceUtils 在沒有事務(wù)上下文的方法中使用 getConnection() 獲取連接,依然會(huì)造成數(shù)據(jù)連接泄漏!

保持代碼清單 6 的代碼不變,調(diào)整 Spring 配置文件,將清單 3 中 Spring AOP 事務(wù)增強(qiáng)配置的代碼注釋掉,重新運(yùn)行清單 6 的代碼,將得到如下的輸出日志:

清單 8. 輸出日志
連接數(shù) [active:idle]-[0:0] 連接數(shù) [active:idle]-[1:1] 連接數(shù) [active:idle]-[1:1] 連接數(shù) [active:idle]-[2:1] 連接數(shù) [active:idle]-[2:1]

我們通過下表對(duì)數(shù)據(jù)源連接的占用和泄漏情況進(jìn)行描述:

表 2. 執(zhí)行過程數(shù)據(jù)源連接占用情況
時(shí)間執(zhí)行線程 1執(zhí)行線程 2數(shù)據(jù)源連接activeidleleak
T0未啟動(dòng)未啟動(dòng)000
T1正在執(zhí)行方法未啟動(dòng)110
T2執(zhí)行完畢未啟動(dòng)111
T3執(zhí)行完畢正式執(zhí)行方法211
T4執(zhí)行完畢執(zhí)行完畢212

仔細(xì)對(duì)照表 1 的執(zhí)行過程,我們發(fā)現(xiàn)在 T1 時(shí),有事務(wù)上下文時(shí)的 active 為 2,idle 為 0,而此時(shí)由于沒有事務(wù)管理,則 active 為 1 而 idle 也為 1。這說(shuō)明有事務(wù)上下文時(shí),需要等到整個(gè)事務(wù)方法(即 logon())返回后,事務(wù)上下文綁定的連接才釋放。但在沒有事務(wù)上下文時(shí),logon() 調(diào)用 JdbcTemplate 執(zhí)行完數(shù)據(jù)操作后,馬上就釋放連接。

在 T2 執(zhí)行線程完成 logon() 方法的執(zhí)行后,有一個(gè)連接沒有被釋放(active),所以發(fā)生了連接泄漏。到 T4 時(shí),兩個(gè)執(zhí)行線程都完成了 logon() 方法的調(diào)用,但是出現(xiàn)了兩個(gè)未釋放的連接。

要堵上這個(gè)連接泄漏的漏洞,需要對(duì) logon() 方法進(jìn)行如下的改造:

清單 9.JdbcUserService.java:手工釋放獲取的連接
public void logon(String userName) {Connection conn = null;try {conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";jdbcTemplate.update(sql, System.currentTimeMillis(), userName);Thread.sleep(1000);// ①} catch (Exception e) {e.printStackTrace();}finally {// ②顯式使用DataSourceUtils釋放連接DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());} }

在 ② 處顯式調(diào)用?DataSourceUtils.releaseConnection()?方法釋放獲取的連接。特別需要指出的是:一定不能在 ① 處釋放連接!因?yàn)槿绻?logon() 在獲取連接后,① 處代碼前這段代碼執(zhí)行時(shí)發(fā)生異常,則①處釋放連接的動(dòng)作將得不到執(zhí)行。這將是一個(gè)非常具有隱蔽性的連接泄漏的隱患點(diǎn)。

JdbcTemplate 如何做到對(duì)連接泄漏的免疫

分析 JdbcTemplate 的代碼,我們可以清楚地看到它開放的每個(gè)數(shù)據(jù)操作方法,首先都使用 DataSourceUtils 獲取連接,在方法返回之前使用 DataSourceUtils 釋放連接。

來(lái)看一下 JdbcTemplate 最核心的一個(gè)數(shù)據(jù)操作方法 execute():

清單 10.JdbcTemplate#execute()
public <T> T execute(StatementCallback<T> action) throws DataAccessException {//① 首先根據(jù)DataSourceUtils獲取數(shù)據(jù)連接Connection con = DataSourceUtils.getConnection(getDataSource());Statement stmt = null;try {Connection conToUse = con;…h(huán)andleWarnings(stmt);return result;}catch (SQLException ex) {JdbcUtils.closeStatement(stmt);stmt = null;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);}finally {JdbcUtils.closeStatement(stmt);//② 最后根據(jù)DataSourceUtils釋放數(shù)據(jù)連接DataSourceUtils.releaseConnection(con, getDataSource());} }

在 ① 處通過 DataSourceUtils.getConnection() 獲取連接,在 ② 處通過 DataSourceUtils.releaseConnection() 釋放連接。所有 JdbcTemplate 開放的數(shù)據(jù)訪問方法最終都是通過?execute(StatementCallback<T> action)執(zhí)行數(shù)據(jù)訪問操作的,因此這個(gè)方法代表了 JdbcTemplate 數(shù)據(jù)操作的最終實(shí)現(xiàn)方式。

正是因?yàn)?JdbcTemplate 嚴(yán)謹(jǐn)?shù)墨@取連接,釋放連接的模式化流程保證了 JdbcTemplate 對(duì)數(shù)據(jù)連接泄漏問題的免疫性。所以,如有可能盡量使用 JdbcTemplate,HibernateTemplate 等這些模板進(jìn)行數(shù)據(jù)訪問操作,避免直接獲取數(shù)據(jù)連接的操作。

使用 TransactionAwareDataSourceProxy

如果不得已要顯式獲取數(shù)據(jù)連接,除了使用 DataSourceUtils 獲取事務(wù)上下文綁定的連接外,還可以通過 TransactionAwareDataSourceProxy 對(duì)數(shù)據(jù)源進(jìn)行代理。數(shù)據(jù)源對(duì)象被代理后就具有了事務(wù)上下文感知的能力,通過代理數(shù)據(jù)源的 getConnection() 方法獲取的連接和使用 DataSourceUtils.getConnection() 獲取連接的效果是一樣的。

下面是使用 TransactionAwareDataSourceProxy 對(duì)數(shù)據(jù)源進(jìn)行代理的配置:

清單 11.applicationContext.xml:對(duì)數(shù)據(jù)源進(jìn)行代理
<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"p:defaultAutoCommit="false"/><!-- ①對(duì)數(shù)據(jù)源進(jìn)行代理--> <bean id="dataSourceProxy" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"p:targetDataSource-ref="dataSource"/><!-- ②直接使用數(shù)據(jù)源的代理對(duì)象--> <bean id="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSourceProxy"/><!-- ③直接使用數(shù)據(jù)源的代理對(duì)象--> <bean id="jdbcManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"p:dataSource-ref="dataSourceProxy"/>

對(duì)數(shù)據(jù)源進(jìn)行代理后,我們就可以通過數(shù)據(jù)源代理對(duì)象的 getConnection() 獲取事務(wù)上下文中綁定的數(shù)據(jù)連接了。

因此,如果數(shù)據(jù)源已經(jīng)進(jìn)行了 TransactionAwareDataSourceProxy 的代理,而且方法存在事務(wù)上下文,那么清單 1 的代碼也不會(huì)生產(chǎn)連接泄漏的問題。

其它數(shù)據(jù)訪問技術(shù)的等價(jià)類

理解了 Spring JDBC 的數(shù)據(jù)連接泄漏問題,其中的道理可以平滑地推廣到其它框架中去。Spring 為每個(gè)數(shù)據(jù)訪問技術(shù)框架都提供了一個(gè)獲取事務(wù)上下文綁定的數(shù)據(jù)連接(或其衍生品)的工具類和數(shù)據(jù)源(或其衍生品)的代理類。

DataSourceUtils 的等價(jià)類

下表列出了不同數(shù)據(jù)訪問技術(shù)對(duì)應(yīng) DataSourceUtils 的等價(jià)類:

表 3. 不同數(shù)據(jù)訪問框架 DataSourceUtils 的等價(jià)類
數(shù)據(jù)訪問技術(shù)框架連接 ( 或衍生品 ) 獲取工具類
Spring JDBCorg.springframework.jdbc.datasource.DataSourceUtils
Hibernateorg.springframework.orm.hibernate3.SessionFactoryUtils
iBatisorg.springframework.jdbc.datasource.DataSourceUtils
JPAorg.springframework.orm.jpa.EntityManagerFactoryUtils
JDOorg.springframework.orm.jdo.PersistenceManagerFactoryUtils

TransactionAwareDataSourceProxy 的等價(jià)類

下表列出了不同數(shù)據(jù)訪問技術(shù)框架下 TransactionAwareDataSourceProxy 的等價(jià)類:

表 4. 不同數(shù)據(jù)訪問框架 TransactionAwareDataSourceProxy 的等價(jià)類
數(shù)據(jù)訪問技術(shù)框架連接 ( 或衍生品 ) 獲取工具類
Spring JDBCorg.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
Hibernateorg.springframework.orm.hibernate3.LocalSessionFactoryBean
iBatisorg.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
JPA無(wú)
JDOorg.springframework.orm.jdo.
TransactionAwarePersistenceManagerFactoryProxy

小結(jié)

在本文中,我們通過剖析了解到以下的真相:

    • 使用 Spring JDBC 時(shí)如果直接獲取 Connection,可能會(huì)造成連接泄漏。為降低連接泄漏的可能,盡量使用 DataSourceUtils 獲取數(shù)據(jù)連接。也可以對(duì)數(shù)據(jù)源進(jìn)行代理,以便將其擁有事務(wù)上下文的感知能力;
    • 可以將 Spring JDBC 防止連接泄漏的解決方案平滑應(yīng)用到其它的數(shù)據(jù)訪問技術(shù)框架中。

?

?

?

?

轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/3832949.html

總結(jié)

以上是生活随笔為你收集整理的Spring 事务管理高级应用难点剖析--转的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 电车痴汉在线观看 | 日韩中文第一页 | 男人添女人下部高潮全视频 | 国产日产精品一区二区三区 | 成年人晚上看的视频 | 成人一级生活片 | 成人精品一区二区三区四区 | 日韩免费在线视频 | 无码人妻精品一区二区三区不卡 | 久久久18禁一区二区三区精品 | 色妇av| 亚洲爱色 | 国产免费麻豆 | 黄网站免费视频 | 亚洲人久久 | 理论片国产 | 亚洲日本在线观看视频 | 欧美激情一区二区三区 | 伊人365| 亚洲永久在线观看 | 最新黄色网页 | 超碰人人超碰 | 久久久久久久av | 三上悠亚久久 | 一级黄色片网址 | 五月婷久久| 亚洲精品911 | 欧美精品一区在线发布 | 熟睡侵犯の奶水授乳在线 | 一区二区日韩在线观看 | 在线免费观看中文字幕 | www国产一区 | 色综合狠狠 | 少妇性bbb搡bbb爽爽爽欧美 | 中国丰满熟妇xxxx性 | 亚洲欧美网 | 一区二区不卡在线观看 | 中国国产黄色片 | 色乱码一区二区三在线看 | 国产精品传媒 | 成人免费在线看片 | 18男女无套免费视频 | 免费精品视频一区二区三区 | 伊人影院在线观看 | 麻豆传媒在线播放 | 久久99精品久久久久久噜噜 | 亚洲专区一区二区三区 | 亚洲综合色av | 奇米第四色影视 | ww黄色| 97干在线视频 | 久草毛片 | 综合久久影院 | 在线日韩亚洲 | 国产小视频在线免费观看 | 九七久久| 午夜免费大片 | 成人欧美一级特黄 | 亚洲免费自拍 | 亚洲一区二区三区高清视频 | 欧美一级性 | 亚洲美女屁股眼交3 | 成人免费看aa片 | 影音先锋二区 | 无码精品视频一区二区三区 | 色丁香六月 | av免费不卡 | 一本一道无码中文字幕精品热 | 色综合九九 | 午夜整容室 | 中文字幕成人动漫 | 国产一区二区麻豆 | 成人性生活毛片 | 女同动漫免费观看高清完整版在线观看 | 国产精品videossex久久发布 | 91中文字幕在线视频 | 日本亚洲色图 | 国产高清在线观看视频 | 成人精品视频在线观看 | 激情五月综合色婷婷一区二区 | 免费看黄色片子 | 欧美高清hd19 | 国产精品视屏 | 人人九九| 性欧美丰满熟妇xxxx性 | 午夜视频一区二区 | 亚洲精品综合 | www.亚洲人| 成人性生生活性生交3 | 美景之屋电影免费高清完整韩剧 | 欧美88av | 精品99久久久久成人网站免费 | 一边吃奶一边摸做爽视频 | 全黄性性激高免费视频 | 久久国语精品 | 国产精品网站视频 | 性综艺节目av在线播放 | 青青草原国产 | 青青草国产一区 |