- 浏览: 212487 次
- 性别:
- 来自: 深圳
文章分类
最新评论
-
jongde1:
Axure太难学了,分享mockplus工具,有兴趣可以去了解 ...
Axure RP 原型设计工具 -
di1984HIT:
这里面提到了好几种解决办法。
Spring AOP对日志记录、Exception日志记录 -
di1984HIT:
学习一下。
spring struts2 零配置 -
di1984HIT:
不错,不错啊
Struts2防止表单重复提交 -
di1984HIT:
kettle怎么样啊。
Kettle初探
Spring 事务管理高级应用难点剖析: 第 2 部分
简介: 本文是“Spring 事务管理高级应用难点剖析”系列文章的第 2 部分,作者将继续深入剖析在实际 Spring 事务管理应用中容易遇见的一些难点,包括混合使用多种数据访问技术(如 Spring JDBC + Hibernate)的事务管理问题,以及通过 Spring AOP 增强的 Bean 存在的一些比较特殊的情况。
联合军种作战的混乱
Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色,各有千秋。像 Hibernate 是非常优秀的 ORM 实现方案,但对底层 SQL 的控制不太方便;而 iBatis 则通过模板化技术让您方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 莫属了,但是它也是最底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下,才能给出答案。所以在一个应用中,往往采用多个数据访问技术:一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术,两者珠联璧合,形成联合军种,共同御敌。
但是,这种联合军种如何应对事务管理的问题呢?我们知道 Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协作,如何工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。
其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果您采用了一个高端 ORM 技术(Hibernate,JPA,JDO),同时采用一个 JDBC 技术(Spring JDBC,iBatis),由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程让前者的会话封装后者的连接。所以,我们只要直接采用前者的事务管理器就可以了。下表给出了混合数据访问技术所对应的事务管理器:
表 1. 混合数据访问技术的事务管理器
混合数据访问技术 事务管理器
ORM 技术框架 JDBC 技术框架
Hibernate Spring JDBC 或 iBatis HibernateTransactionManager
JPA Spring JDBC 或 iBatis JpaTransactionManager
JDO Spring JDBC 或 iBatis JdoTransactionManager
由于一般不会出现同时使用多个 ORM 框架的情况(如 Hibernate + JPA),我们不拟对此命题展开论述,只重点研究 ORM 框架 + JDBC 框架的情况。Hibernate + Spring JDBC 可能是被使用得最多的组合,下面我们通过实例观察事务管理的运作情况。
清单 1.User.java:使用了注解声明的实体类
-----------------------------------------
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;
}
-----------------------------------------
再来看下 UserService 的关键代码:
清单 2.UserService.java:使用 Hibernate 数据访问技术
-----------------------------------------
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 {
@Autowired
private HibernateTemplate hibernateTemplate;
@Autowired
private ScoreService scoreService;
public void logon(String userName) {
System.out.println("logon method...");
updateLastLogonTime(userName); //①使用Hibernate数据访问技术
scoreService.addScore(userName, 20); //②使用Spring JDBC数据访问技术
}
public void updateLastLogonTime(String userName) {
System.out.println("updateLastLogonTime...");
User user = hibernateTemplate.get(User.class,userName);
user.setLastLogonTime(System.currentTimeMillis());
hibernateTemplate.flush(); //③请看下文的分析
}
}
-----------------------------------------
在①处,使用 Hibernate 操作数据,而在②处调用 ScoreService#addScore(),该方法内部使用 Spring JDBC 操作数据。
在③处,我们显式调用了 flush() 方法,将 Session 中的缓存同步到数据库中,这个操作将即时向数据库发送一条更新记录的 SQL 语句。之所以要在此显式执行 flush() 方法,原因是:默认情况下,Hibernate 要在事务提交时才将数据的更改同步到数据库中,而事务提交发生在 logon() 方法返回前。如果所有针对数据库的更改都使用 Hibernate,这种数据同步延迟的机制不会产生任何问题。但是,我们在 logon() 方法中同时采用了 Hibernate 和 Spring JDBC 混合数据访问技术。Spring JDBC 无法自动感知 Hibernate 一级缓存,所以如果不及时调用 flush() 方法将数据更改同步到数据库,则②处通过 Spring JDBC 进行数据更改的结果将被 Hibernate 一级缓存中的更改覆盖掉,因为,一级缓存在 logon() 方法返回前才同步到数据库!
ScoreService 使用 Spring JDBC 数据访问技术,其代码如下:
清单 3.ScoreService.java:使用 Spring JDBC 数据访问技术
-----------------------------------------
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{
@Autowired
private 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);
//① 查看此处数据库激活的连接数
BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();
System.out.println("激活连接数量:"+basicDataSource.getNumActive());
}
}
-----------------------------------------
Spring 关键的配置文件代码如下所示:
清单 4. applicationContext.xml 事务配置代码部分
-----------------------------------------
<!-- 使用Hibernate事务管理器 -->
<bean id="hiberManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"/>
<!-- 对所有继承BaseService类的公用方法实施事务增强 -->
<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>
-----------------------------------------
启动 Spring 容器,执行 UserService#logon() 方法,可以查看到如下的执行日志:
清单 5. 代码运行日志
-----------------------------------------
12:38:57,062 (AbstractPlatformTransactionManager.java:365) - Creating new transaction
with name [user.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
12:38:57,093 (SessionImpl.java:220) - opened session at timestamp: 12666407370
12: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 HibernateTemplate
12:38:57,187 (HibernateTemplate.java:397) - Found thread-bound Session
for HibernateTemplate
Hibernate: 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 update
2010-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
激活连接数量: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 ⑤
-----------------------------------------
仔细观察这段输出日志,在①处 UserService#logon() 开启一个新的事务,在②处 ScoreService#addScore() 方法加入到①处开启的事务上下文中。③处的输出是 ScoreService#addScore() 方法内部的输出,汇报此时数据源激活的连接数为 1,这清楚地告诉我们 Hibernate 和 JDBC 这两种数据访问技术在同一事务上下文中“共用”一个连接。在④处,提交 Hibernate 事务,接着在⑤处触发调用底层的 Connection 提交事务。
从以上的运行结果,我们可以得出这样的结论:使用 Hibernate 事务管理器后,可以混合使用 Hibernate 和 Spring JDBC 数据访问技术,它们将工作于同一事务上下文中。但是使用 Spring JDBC 访问数据时,Hibernate 的一级或二级缓存得不到同步,此外,一级缓存延迟数据同步机制可能会覆盖 Spring JDBC 数据更改的结果。
由于混合数据访问技术的方案的事务同步而缓存不同步的情况,所以最好用 Hibernate 完成读写操作,而用 Spring JDBC 完成读的操作。如用 Spring JDBC 进行简要列表的查询,而用 Hibernate 对查询出的数据进行维护。如果确实要同时使用 Hibernate 和 Spring JDBC 读写数据,则必须充分考虑到 Hibernate 缓存机制引发的问题:必须充分分析数据维护逻辑,根据需要,及时调用 Hibernate 的 flush() 方法,以免覆盖 Spring JDBC 的更改,在 Spring JDBC 更改数据库时,维护 Hibernate 的缓存。
可以将以上结论推广到其它混合数据访问技术的方案中,如 Hibernate+iBatis,JPA+Spring JDBC,JDO+Spring JDBC 等。
特殊方法成漏网之鱼
由于 Spring 事务管理是基于接口代理或动态字节码技术,通过 AOP 实施事务增强的。虽然,Spring 还支持 AspectJ LTW 在类加载期实施增强,但这种方法很少使用,所以我们不予关注。
对于基于接口动态代理的 AOP 事务增强来说,由于接口的方法是 public 的,这就要求实现类的实现方法必须是 public 的(不能是 protected,private 等),同时不能使用 static 的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施 AOP 增强,也不能进行 Spring 事务增强了。
基于 CGLib 字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行 AOP 增强植入的。由于使用 final、static、private 修饰符的方法都不能被子类覆盖,相应的,这些方法将不能被实施 AOP 增强。所以,必须特别注意这些修饰符的使用,以免不小心成为事务管理的漏网之鱼。
下面通过具体的实例说明基于 CGLib 字节码动态代理无法享受 Spring AOP 事务增强的特殊方法。
清单 6.UserService.java:4 个不同修饰符的方法
-----------------------------------------
package user.special;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserService {
//① private方法因访问权限的限制,无法被子类覆盖
private void method1() {
System.out.println("method1");
}
//② final方法无法被子类覆盖
public final void method2() {
System.out.println("method2");
}
//③ static是类级别的方法,无法被子类覆盖
public static void method3() {
System.out.println("method3");
}
//④ public方法可以被子类覆盖,因此可以被动态字节码增强
public void method4() {
System.out.println("method4");
}
}
-----------------------------------------
Spring 通过 CGLib 动态代理技术对 UserService Bean 实施 AOP 事务增强的配置如下所示:
清单 7.applicationContext.xml:对 UserService 用 CGLib 实施事务增强
----------
<?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">
<!-- 省略声明数据源及DataSourceTransactionManager事务管理器-->
…
<aop:config proxy-target-class="true">
<!-- ①显式使用CGLib动态代理 -->
<!-- ②希望对UserService所有方法实施事务增强 -->
<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 动态代理技术,在 ② 处通过 AspjectJ 切点表达式表达 UserService 所有的方法,希望对 UserService 所有方法都实施 Spring AOP 事务增强。
在 UserService 添加一个可执行的方法,如下所示:
清单 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");
}
}
-----------------------------------------
在运行 UserService 之前,将 Log4J 日志级别设置为 DEBUG,运行以上代码查看输出日志,如下所示:
-----------
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 method4
17:24:10,953 (AbstractPlatformTransactionManager.java:365)
- Creating new transaction with name [user.special.UserService.method4]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
17: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 transaction
17:24:11,125 (DataSourceUtils.java:312)
- Returning JDBC Connection to DataSource
after method4
-----------------------------------------
观察以上输出日志,很容易发现 method1~method3 这 3 个方法都没有被实施 Spring 的事务增强,只有 method4 被实施了事务增强。这个结果刚才验证了我们前面的论述。
我们通过下表描述哪些特殊方法将成为 Spring AOP 事务增强的漏网之鱼:
表 2. 不能被 Spring AOP 事务增强的方法
动态代理策略 不能被事务增强的方法
基于接口的动态代理 除 public 外的其它所有的方法,此外 public static 也不能被增强
基于 CGLib 的动态代理 private、static、final 的方法
不过,需要特别指出的是,这些不能被 Spring 事务增强的特殊方法并非就不工作在事务环境下。只要它们被外层的事务方法调用了,由于 Spring 的事务管理的传播特殊,内部方法也可以工作在外部方法所启动的事务上下文中。我们说,这些方法不能被 Spring 进行 AOP 事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依就可以顺利地传播到这些方法中。
这些不能被 Spring 事务增强的方法和可被 Spring 事务增强的方法唯一的区别在 “是否可以主动启动一个新事务”:前者不能而后者可以。对于事务传播行为来说,二者是完全相同的,前者也和后者一样不会造成数据连接的泄漏问题。换句话说,如果这些“特殊方法”被无事务上下文的方法调用,则它们就工作在无事务上下文中;反之,如果被具有事务上下文的方法调用,则它们就工作在事务上下文中。
对于 private 的方法,由于最终都会被 public 方法封装后再开放给外部调用,而 public 方法是可以被事务增强的,所以基本上没有什么问题。在实际开发中,最容易造成隐患的是基于 CGLib 的动态代理时的“public static”和“public final”这两种特殊方法。原因是它们本身是 public 的,所以可以直接被外部类(如 Web 层的 Controller 类)调用,只要调用者没有事务上下文,这些特殊方法也就以无事务的方式运作。
小结
在本文中,我们通过剖析了解到以下的真相:
混合使用多个数据访问技术框架的最佳组合是一个 ORM 技术框架(如 Hibernate 或 JPA 等)+ 一个 JDBC 技术框架(如 Spring JDBC 或 iBatis)。直接使用 ORM 技术框架对应的事务管理器就可以了,但必须考虑 ORM 缓存同步的问题;
Spring AOP 增强有两个方案:其一为基于接口的动态代理,其二为基于 CGLib 动态生成子类的代理。由于 Java 语法的特性,有些特殊方法不能被 Spring AOP 代理,所以也就无法享受 AOP 织入带来的事务增强。
在下一篇文章中,笔者将继续分析 Spring 事务管理的以下难点:
直接获取 Connection 时,哪些情况会造成数据连接的泄漏,以及如何应对;
除 Spring JDBC 外,其它数据访问技术数据连接泄漏的应对方案。
原文:http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts2/index.html
简介: 本文是“Spring 事务管理高级应用难点剖析”系列文章的第 2 部分,作者将继续深入剖析在实际 Spring 事务管理应用中容易遇见的一些难点,包括混合使用多种数据访问技术(如 Spring JDBC + Hibernate)的事务管理问题,以及通过 Spring AOP 增强的 Bean 存在的一些比较特殊的情况。
联合军种作战的混乱
Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色,各有千秋。像 Hibernate 是非常优秀的 ORM 实现方案,但对底层 SQL 的控制不太方便;而 iBatis 则通过模板化技术让您方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 莫属了,但是它也是最底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下,才能给出答案。所以在一个应用中,往往采用多个数据访问技术:一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术,两者珠联璧合,形成联合军种,共同御敌。
但是,这种联合军种如何应对事务管理的问题呢?我们知道 Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协作,如何工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。
其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果您采用了一个高端 ORM 技术(Hibernate,JPA,JDO),同时采用一个 JDBC 技术(Spring JDBC,iBatis),由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程让前者的会话封装后者的连接。所以,我们只要直接采用前者的事务管理器就可以了。下表给出了混合数据访问技术所对应的事务管理器:
表 1. 混合数据访问技术的事务管理器
混合数据访问技术 事务管理器
ORM 技术框架 JDBC 技术框架
Hibernate Spring JDBC 或 iBatis HibernateTransactionManager
JPA Spring JDBC 或 iBatis JpaTransactionManager
JDO Spring JDBC 或 iBatis JdoTransactionManager
由于一般不会出现同时使用多个 ORM 框架的情况(如 Hibernate + JPA),我们不拟对此命题展开论述,只重点研究 ORM 框架 + JDBC 框架的情况。Hibernate + Spring JDBC 可能是被使用得最多的组合,下面我们通过实例观察事务管理的运作情况。
清单 1.User.java:使用了注解声明的实体类
-----------------------------------------
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;
}
-----------------------------------------
再来看下 UserService 的关键代码:
清单 2.UserService.java:使用 Hibernate 数据访问技术
-----------------------------------------
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 {
@Autowired
private HibernateTemplate hibernateTemplate;
@Autowired
private ScoreService scoreService;
public void logon(String userName) {
System.out.println("logon method...");
updateLastLogonTime(userName); //①使用Hibernate数据访问技术
scoreService.addScore(userName, 20); //②使用Spring JDBC数据访问技术
}
public void updateLastLogonTime(String userName) {
System.out.println("updateLastLogonTime...");
User user = hibernateTemplate.get(User.class,userName);
user.setLastLogonTime(System.currentTimeMillis());
hibernateTemplate.flush(); //③请看下文的分析
}
}
-----------------------------------------
在①处,使用 Hibernate 操作数据,而在②处调用 ScoreService#addScore(),该方法内部使用 Spring JDBC 操作数据。
在③处,我们显式调用了 flush() 方法,将 Session 中的缓存同步到数据库中,这个操作将即时向数据库发送一条更新记录的 SQL 语句。之所以要在此显式执行 flush() 方法,原因是:默认情况下,Hibernate 要在事务提交时才将数据的更改同步到数据库中,而事务提交发生在 logon() 方法返回前。如果所有针对数据库的更改都使用 Hibernate,这种数据同步延迟的机制不会产生任何问题。但是,我们在 logon() 方法中同时采用了 Hibernate 和 Spring JDBC 混合数据访问技术。Spring JDBC 无法自动感知 Hibernate 一级缓存,所以如果不及时调用 flush() 方法将数据更改同步到数据库,则②处通过 Spring JDBC 进行数据更改的结果将被 Hibernate 一级缓存中的更改覆盖掉,因为,一级缓存在 logon() 方法返回前才同步到数据库!
ScoreService 使用 Spring JDBC 数据访问技术,其代码如下:
清单 3.ScoreService.java:使用 Spring JDBC 数据访问技术
-----------------------------------------
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{
@Autowired
private 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);
//① 查看此处数据库激活的连接数
BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();
System.out.println("激活连接数量:"+basicDataSource.getNumActive());
}
}
-----------------------------------------
Spring 关键的配置文件代码如下所示:
清单 4. applicationContext.xml 事务配置代码部分
-----------------------------------------
<!-- 使用Hibernate事务管理器 -->
<bean id="hiberManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"/>
<!-- 对所有继承BaseService类的公用方法实施事务增强 -->
<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>
-----------------------------------------
启动 Spring 容器,执行 UserService#logon() 方法,可以查看到如下的执行日志:
清单 5. 代码运行日志
-----------------------------------------
12:38:57,062 (AbstractPlatformTransactionManager.java:365) - Creating new transaction
with name [user.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
12:38:57,093 (SessionImpl.java:220) - opened session at timestamp: 12666407370
12: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 HibernateTemplate
12:38:57,187 (HibernateTemplate.java:397) - Found thread-bound Session
for HibernateTemplate
Hibernate: 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 update
2010-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
激活连接数量: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 ⑤
-----------------------------------------
仔细观察这段输出日志,在①处 UserService#logon() 开启一个新的事务,在②处 ScoreService#addScore() 方法加入到①处开启的事务上下文中。③处的输出是 ScoreService#addScore() 方法内部的输出,汇报此时数据源激活的连接数为 1,这清楚地告诉我们 Hibernate 和 JDBC 这两种数据访问技术在同一事务上下文中“共用”一个连接。在④处,提交 Hibernate 事务,接着在⑤处触发调用底层的 Connection 提交事务。
从以上的运行结果,我们可以得出这样的结论:使用 Hibernate 事务管理器后,可以混合使用 Hibernate 和 Spring JDBC 数据访问技术,它们将工作于同一事务上下文中。但是使用 Spring JDBC 访问数据时,Hibernate 的一级或二级缓存得不到同步,此外,一级缓存延迟数据同步机制可能会覆盖 Spring JDBC 数据更改的结果。
由于混合数据访问技术的方案的事务同步而缓存不同步的情况,所以最好用 Hibernate 完成读写操作,而用 Spring JDBC 完成读的操作。如用 Spring JDBC 进行简要列表的查询,而用 Hibernate 对查询出的数据进行维护。如果确实要同时使用 Hibernate 和 Spring JDBC 读写数据,则必须充分考虑到 Hibernate 缓存机制引发的问题:必须充分分析数据维护逻辑,根据需要,及时调用 Hibernate 的 flush() 方法,以免覆盖 Spring JDBC 的更改,在 Spring JDBC 更改数据库时,维护 Hibernate 的缓存。
可以将以上结论推广到其它混合数据访问技术的方案中,如 Hibernate+iBatis,JPA+Spring JDBC,JDO+Spring JDBC 等。
特殊方法成漏网之鱼
由于 Spring 事务管理是基于接口代理或动态字节码技术,通过 AOP 实施事务增强的。虽然,Spring 还支持 AspectJ LTW 在类加载期实施增强,但这种方法很少使用,所以我们不予关注。
对于基于接口动态代理的 AOP 事务增强来说,由于接口的方法是 public 的,这就要求实现类的实现方法必须是 public 的(不能是 protected,private 等),同时不能使用 static 的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施 AOP 增强,也不能进行 Spring 事务增强了。
基于 CGLib 字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行 AOP 增强植入的。由于使用 final、static、private 修饰符的方法都不能被子类覆盖,相应的,这些方法将不能被实施 AOP 增强。所以,必须特别注意这些修饰符的使用,以免不小心成为事务管理的漏网之鱼。
下面通过具体的实例说明基于 CGLib 字节码动态代理无法享受 Spring AOP 事务增强的特殊方法。
清单 6.UserService.java:4 个不同修饰符的方法
-----------------------------------------
package user.special;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserService {
//① private方法因访问权限的限制,无法被子类覆盖
private void method1() {
System.out.println("method1");
}
//② final方法无法被子类覆盖
public final void method2() {
System.out.println("method2");
}
//③ static是类级别的方法,无法被子类覆盖
public static void method3() {
System.out.println("method3");
}
//④ public方法可以被子类覆盖,因此可以被动态字节码增强
public void method4() {
System.out.println("method4");
}
}
-----------------------------------------
Spring 通过 CGLib 动态代理技术对 UserService Bean 实施 AOP 事务增强的配置如下所示:
清单 7.applicationContext.xml:对 UserService 用 CGLib 实施事务增强
----------
<?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">
<!-- 省略声明数据源及DataSourceTransactionManager事务管理器-->
…
<aop:config proxy-target-class="true">
<!-- ①显式使用CGLib动态代理 -->
<!-- ②希望对UserService所有方法实施事务增强 -->
<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 动态代理技术,在 ② 处通过 AspjectJ 切点表达式表达 UserService 所有的方法,希望对 UserService 所有方法都实施 Spring AOP 事务增强。
在 UserService 添加一个可执行的方法,如下所示:
清单 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");
}
}
-----------------------------------------
在运行 UserService 之前,将 Log4J 日志级别设置为 DEBUG,运行以上代码查看输出日志,如下所示:
-----------
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 method4
17:24:10,953 (AbstractPlatformTransactionManager.java:365)
- Creating new transaction with name [user.special.UserService.method4]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
17: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 transaction
17:24:11,125 (DataSourceUtils.java:312)
- Returning JDBC Connection to DataSource
after method4
-----------------------------------------
观察以上输出日志,很容易发现 method1~method3 这 3 个方法都没有被实施 Spring 的事务增强,只有 method4 被实施了事务增强。这个结果刚才验证了我们前面的论述。
我们通过下表描述哪些特殊方法将成为 Spring AOP 事务增强的漏网之鱼:
表 2. 不能被 Spring AOP 事务增强的方法
动态代理策略 不能被事务增强的方法
基于接口的动态代理 除 public 外的其它所有的方法,此外 public static 也不能被增强
基于 CGLib 的动态代理 private、static、final 的方法
不过,需要特别指出的是,这些不能被 Spring 事务增强的特殊方法并非就不工作在事务环境下。只要它们被外层的事务方法调用了,由于 Spring 的事务管理的传播特殊,内部方法也可以工作在外部方法所启动的事务上下文中。我们说,这些方法不能被 Spring 进行 AOP 事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依就可以顺利地传播到这些方法中。
这些不能被 Spring 事务增强的方法和可被 Spring 事务增强的方法唯一的区别在 “是否可以主动启动一个新事务”:前者不能而后者可以。对于事务传播行为来说,二者是完全相同的,前者也和后者一样不会造成数据连接的泄漏问题。换句话说,如果这些“特殊方法”被无事务上下文的方法调用,则它们就工作在无事务上下文中;反之,如果被具有事务上下文的方法调用,则它们就工作在事务上下文中。
对于 private 的方法,由于最终都会被 public 方法封装后再开放给外部调用,而 public 方法是可以被事务增强的,所以基本上没有什么问题。在实际开发中,最容易造成隐患的是基于 CGLib 的动态代理时的“public static”和“public final”这两种特殊方法。原因是它们本身是 public 的,所以可以直接被外部类(如 Web 层的 Controller 类)调用,只要调用者没有事务上下文,这些特殊方法也就以无事务的方式运作。
小结
在本文中,我们通过剖析了解到以下的真相:
混合使用多个数据访问技术框架的最佳组合是一个 ORM 技术框架(如 Hibernate 或 JPA 等)+ 一个 JDBC 技术框架(如 Spring JDBC 或 iBatis)。直接使用 ORM 技术框架对应的事务管理器就可以了,但必须考虑 ORM 缓存同步的问题;
Spring AOP 增强有两个方案:其一为基于接口的动态代理,其二为基于 CGLib 动态生成子类的代理。由于 Java 语法的特性,有些特殊方法不能被 Spring AOP 代理,所以也就无法享受 AOP 织入带来的事务增强。
在下一篇文章中,笔者将继续分析 Spring 事务管理的以下难点:
直接获取 Connection 时,哪些情况会造成数据连接的泄漏,以及如何应对;
除 Spring JDBC 外,其它数据访问技术数据连接泄漏的应对方案。
原文:http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts2/index.html
发表评论
-
Replace constructor-arg by annotation
2013-05-24 04:34 1104replace constructor-arg by anno ... -
spring struts2 零配置
2013-05-23 18:04 6861.ssh(struts2.3.8+spring3.2+hei ... -
使用Spring进行统一日志管理 + 统一异常管理
2013-03-15 16:23 1229使用Spring进行统一日志管理 + 统一异常管理 统一日志 ... -
Spring 配置(事务和日志)
2013-03-15 16:11 1851Spring的那些配置(事务和日志) Spring整合Iba ... -
Spring 自定义注解实现操作日志记录功能
2013-03-15 09:55 2345Spring 自定义注解实现操作日志记录功能 最近项目组长分 ... -
Spring AOP对日志记录、Exception日志记录
2013-03-14 18:14 6503Spring AOP对日志记录、Exception日志记录 ... -
Struts2防止表单重复提交
2013-03-14 10:18 916Struts2防止表单重复提交 最近开发中涉及到了表单重复提 ... -
Spring事务配置的五种方式
2013-03-05 10:08 736Spring事务配置的五种方式 前段时间对Spring的事 ... -
spring 异常与事务
2013-03-01 11:21 1830Service层捕获异常,并抛出RuntimeExceptio ... -
如何将基于 Struts、Spring 和 Hibernate 的应用从 Tomcat 迁移到 WebSphere Application Server
2012-12-21 10:28 1193引言 现在很多的企业都 ... -
Spring 事务管理高级应用难点剖析: 第 3 部分
2011-04-26 17:29 804Spring 事务管理高级应用 ... -
Spring 事务管理高级应用难点剖析: 第 1 部分
2011-04-26 17:05 799Spring 事务管理高级应用难点剖析: 第 1 部分 简介 ...
相关推荐
总的来说,Spring 事务管理的高级应用不仅涉及到事务的启动、提交和回滚,还涵盖了事务的传播行为和隔离级别,这些都是保证软件系统数据一致性的重要工具。了解并合理利用这些特性,可以帮助开发者编写出更健壮、...
本篇文章主要探讨的是Spring事务管理在高级应用中的难点和一些常见误解。 首先,一些开发者误认为使用Spring事务管理必须按照传统的Web、Service和DAO三层架构来组织代码。他们倾向于在每一层都定义接口和实现类,...
Spring最成功,最吸引人的地方莫过于轻量级的声明式事务管理,仅此一点,它就宣告了重量级EJB容器的覆灭。Spring声明式事务管理将开发者从繁复的事务管理代码中解脱出来,专注于业务逻辑的开发上,这是一件可以被拿...
在本文中,我们将深入探讨Spring事务管理的一些高级应用难点,特别是涉及多线程场景下的事务处理。 首先,Spring事务管理器利用了ThreadLocal来在每个线程中保存数据访问基础设施,如数据库连接。ThreadLocal是一种...
第10章:对实际应用中Spring事务管理各种疑难问题进行透彻的剖析,让读者对Spring事务管理不再有云遮雾罩的感觉。 第11章:讲解了如何使用Spring JDBC进行数据访问操作,我们还重点讲述了LOB字段处理、主键产生...
第10章 Spring的事务管理难点剖析 10.1 DAO和事务管理的牵绊 10.1.1 JDBC访问数据库 10.1.2 Hibernate访问数据库 10.2 应用分层的迷惑 10.3 事务方法嵌套调用的迷茫 10.3.1 Spring事务传播机制回顾 10.3.2 相互嵌套...
第10章:对实际应用中Spring事务管理各种疑难问题进行透彻的剖析,让读者对Spring事务管理不再有云遮雾罩的感觉。 第11章:讲解了如何使用Spring JDBC进行数据访问操作,我们还重点讲述了LOB字段处理、主键产生...
第10章 Spring的事务管理难点剖析 10.1 DAO和事务管理的牵绊 10.1.1 JDBC访问数据库 10.1.2 Hibernate访问数据库 10.2 应用分层的迷惑 10.3 事务方法嵌套调用的迷茫 10.3.1 Spring事务传播机制回顾 10.3.2 相互嵌套...
面试必考之HashMap源码分析与实现 探索JVM底层奥秘ClassLoader源码分析与案例讲解 面试必备技能之Dubbo企业实战 ...互联网系统垂直架构之Session解决方案 分库分表之后分布式下如何...无处不在的Spring AOP事务及踩过的坑
2. **AOP**:Spring的AOP模块允许开发者定义横切关注点,如日志、事务管理等,将它们与业务逻辑分离,提高代码的可复用性和可维护性。 3. **MVC框架**:Spring MVC为Web应用提供了模型-视图-控制器架构,简化了Web...
“Spring 事务管理高级应用难点剖析”系列文章,聚焦于Spring框架的事务管理,这是Java企业级应用开发中的关键部分。这部分内容可能涵盖了事务的ACID特性,以及Spring中的声明式和编程式事务管理,对于处理并发和...
利用 Spring Boot 的事务管理机制和 Spring Security 进行用户权限管理,保证多用户并发操作时的数据一致性及安全。 - **系统性能优化** 采用缓存技术、数据库索引优化等方式提高系统响应速度;同时采用前后端...
主要课程内容 ...Spring IoC 源码深度剖析 设计⾮常优雅 设计模式 注意:原则、⽅法和技巧 Spring AOP ⾼级应⽤ 声明式事务控制 Spring AOP 源码深度剖析 必要的笔记、必要的图、通俗易懂的语⾔化解知识难点
- **Spring源码分析**:深入剖析Spring框架内部机制,如IOC容器、AOP实现原理等。 - **MyBatis源码分析**:解读MyBatis的工作流程、执行过程及SQL映射机制。 - **事务管理**:讨论非传统事务处理方式——柔性事务...
SSH是一个流行的企业级Web应用开发框架,它结合了Struts的MVC设计模式、Spring的依赖注入和事务管理以及Hibernate的对象关系映射功能,提供了一套完整的解决方案。 1. Struts2:作为MVC框架,Struts2负责处理HTTP...
开发环境:IDEA,Tomcat,MySQL,Redis 项目构建:Maven 软件环境:SSM(SpringMVC,Spring,...三、难点分析 MySQL:事务 + 行级锁 多用户秒杀 ——> Update库存数量 四、功能模块 秒杀接口暴露(Exposer,封装的
### 基于SpringBoot的高校办公室行政事务管理系统 #### 一、系统概述与背景 在信息化快速发展的今天,高效地管理和处理各种行政事务成为各高校办公室的重要任务之一。随着业务量的增加以及数据处理复杂性的提升,...
2. **Spring**:Spring框架是企业级Java应用的基石,它提供了依赖注入(DI)和面向切面编程(AOP)等功能,简化了应用的配置和管理。在音像店租赁系统中,Spring可能被用来管理对象的生命周期,提供事务处理,并协助...
- **事务管理**: 支持本地和分布式事务管理。 ##### 5.2 Hibernate优势 - **提高生产力**: 大大减少了数据库操作的编码工作量。 - **跨数据库支持**: 支持多种数据库系统,降低迁移成本。 - **社区活跃**: 拥有庞大...