如果您正在构建一个应用程序,该应用程序要求在执行用例时在全局事务中具有自定义隔离级别,您可能已经发现这是一件困难的事,因为 Java™ Transaction API 并不提供对自定义隔离级别的支持。幸运地是,Spring 框架允许您设计在全局事务中使用自定义隔离级别的 Web 和企业应用程序,但这却不是一件容易的事。在本文中,Ricardo Olivieri 用 7 个详细的步骤演示了这一过程。
许多 Java Enterprise Edition(EE)应用程序在执行用户请求时都会访问多处资源。例如,应用程序也许需要将一条消息放到一个面向消息的中间件队列中,并在相同的事务上下文中更新数据库行。可以通过使用应用服务器提供的 Java Transaction API(JTA)事务管理器和兼容 XA 的驱动程序连接到数据资源来实现这一任务。但应用程序的需求也许会在执行一个用例时调用全局事务中的自定义隔离级别(custom isolation level) —— JTA 事务管理器并不支持自定义隔离级别。如果正在使用 Spring 框架,出这个原因,如果为 Spring 配置文件中的全局事务指定一个自定义隔离级别,将会抛出一个异常。
本文展示了一种能够 使用 Spring 来指定全局事务中的自定义隔离级别的方法。如果您部署应用程序的应用服务器,允许在定义数据源的位置指定作为数据库访问的隔离级别值,那么该方法都是有效的。为从本文中获益,您应该熟悉 Spring 框架并理解如何在 Spring 配置文件中定义事务代理及面向方面的 advice。在对应用服务器熟悉的前提下,也假设您熟悉 Java EE 设计模式和全局/分布式事务的概念。
软件应用程序的需求也许做了这样的规定(这里的许多技术超出了本文讨论范围),即在执行一个给定用例的过程中,必须将相同的隔离级别使用到所有的数据访问中。需求也许还这样规定,在一个用例实现中只要访问了两项或超过两项的外部资源,该应用程序就应该使用全局事务。例如,作为用例实现的一部分,应用程序也许会查询两个不同的数据库表并将一条消息放到消息队列中。针对这个用例的设计也许需要使用 “已提交读” 隔离级别来执行两个数据库 READ 操作。但也需要在执行不同的 用例时,应用程序会使用不同的隔离级别(如 “可重复读”)来执行这两个相同数据库的 READ 操作。在这两个用例的执行中,应用程序执行相同的数据库操作和部分相同的代码段,但却必须使用不同的隔离级别。
您可以分别为两个 READ 操作定义方法,并以要使用的隔离级别作为参数。这些方法的调用者会依据执行中的用例来指定相应的隔离级别。但即使这种方法会起作用,将这种逻辑包含在 Java 代码中并不是最佳方法,且维护代码会很困难。表面上看,利用 Spring 框架的功能似乎是更好的方法。Spring 是一个强大的框架,这在很大程度上是由于其为应用程序定义事务的强大功能。Spring 让您用一种清晰的方式指定事务属性,如隔离级别、传播行为和异常处理行为(例如,当抛出特定的异常时,事务是否应该自动回滚)。但缺乏对指定自定义隔离级别的支持是 JTA 是一块软肋,如下列场景所说明的那样。
|
使用 JTA 事务管理器的新手或只对它了解一点的开发人员也许想要为服务对象(如 OrderService)(参见 什么是服务对象?)的实现定义(在 Spring 配置文件中)一个事务代理,如清单 1 所示:
清单 1. 使用 JTA 事务管理器的事务代理的错误定义
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<constructor-arg>
<ref local="jtaTransactionManager" />
</constructor-arg>
</bean>
<bean id="orderService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager" />
</property>
<property name="proxyInterfaces">
<list>
<value>sample.services.OrderService</value>
</list>
</property>
<property name="target">
<ref local="orderServiceTarget" />
</property>
<property name="transactionAttributes">
<props>
<prop key="save*">PROPAGATION_REQUIRED, ISOLATION_SERIALIZABLE</prop>
<prop key="delete*">PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED</prop>
<prop key="find*">PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED,
readOnly</prop>
</props>
</property>
</bean>
|
清单 1 中定义了两个 bean。第一个 bean 的定义指定了应用程序将使用的事务管理器。正如您能看到的那样,这个 bean 依赖于另一个叫做 jtaTransactionManager 的 bean,而这个 bean 的定义依赖于您正在使用的应用服务器。例如,对于 IBM WebSphere Application Server 来说,这个 bean 的定义是这样的:
<bean id="jtaTransactionManager"
class="org.springframework.transaction.jta.WebSphereTransactionManagerFactoryBean"
singleton="true" />
|
|
清单 1 中第二个 bean(称为 orderService)包含一个服务对象的事务代理定义,该服务对象实现了一个名为 OrderService 的接口。这个代理为三个方法声明了三个事务性定义:save()、delete() 和 find()。由于 “序列化” 和 “未提交读” 被指定为这些方法的隔离级别,那么期望这些就是在运行时获得的隔离级别是符合逻辑的。然而,请注意该代理定义包含了对 JTA 事务管理器的引用。如果用这个配置运行应用程序,您也许会十分惊诧。只要执行了 OrderService 实现的 save()、delete() 或 find() 方法,就会出现这样一个异常:
org.springframework.transaction.InvalidIsolationLevelException: JtaTransactionManager does not support custom isolation levels at org.springframework.transaction.jta.JtaTransactionManager.applyIsolationLevel( JtaTransactionManager.java:617) at org.springframework.transaction.jta.JtaTransactionManager.doJtaBegin( JtaTransactionManager.java:595) at org.springframework.transaction.jta.JtaTransactionManager.doBegin( JtaTransactionManager.java:559) at org.springframework.transaction.support.AbstractPlatformTransactionManager. getTransaction(AbstractPlatformTransactionManager.java:234) ... |
出现这个错误是因为 JTA 事务管理器不支持自定义隔离级别。当使用 JTA 事务管理器时,事务代理的 bean 定义会和清单 2 中的类似:
清单 2. 使用 JTA 事务管理器的事务代理的正确定义
<bean id="orderService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager" />
</property>
<property name="proxyInterfaces">
<list>
<value>sample.services.OrderService</value>
</list>
</property>
<property name="target">
<ref local="orderServiceTarget" />
</property>
<property name="transactionAttributes">
<props>
<prop key="save*">PROPAGATION_REQUIRED, ISOLATION_DEFAULT</prop>
<prop key="delete*">PROPAGATION_REQUIRED, ISOLATION_DEFAULT</prop>
<prop key="find*">PROPAGATION_REQUIRED, ISOLATION_DEFAULT,readOnly
</prop>
</props>
</property>
</bean>
|
请注意,和 清单 1 惟一的区别是,现在所有的隔离级别都被设置为 ISOLATION_DEFAULT。如果要用 清单 2 中的事务配置执行一个应用程序,该代码会顺利运行。然而,您很可能想知道当执行 save()、delete() 或 find() 方法时,使用哪个隔离级别。这个问题的答案取决于 “其依赖项”。隔离级别依赖于用于与数据库通信的数据源。
图 1 中的序列图说明了在执行 save() 方法时,OrderService 实现对象和两个数据访问对象(DAO)的交互。(正如从您的经验中得出的那样,DAO 主要用于将业务逻辑从存储访问/持久性代码中分离出来。)
图 1. OrderService 实现的 save() 方法的序列图
从 这里 看全图。
在执行 OrderService 实现的 save() 方法时使用的隔离级别由在 OrderDAO 和 CustomerDAO 数据访问对象中引用的数据源所声明。例如,如果 OrderDAO 被配置为从定义为具有 “未提交读” 隔离级别的数据源中获取连接,而 CustomerDAO 被配置为使用定义为具有 “序列化” 隔离级别的数据源,然后在通过 OrderDAO 对象访问数据时, save() 方法会使用 “未提交读” 隔离级别,而在通过 CustomerDAO 访问数据时,使用 “序列化” 隔离级别。但如果再回过头来看 清单 1,就会发现这并不是预期的目的。相反,在一个用例执行中,单个的隔离级别将被用于所有的数据访问(如 save()、delete() 或 find() 方法),即使不同的用例执行相同的数据库操作,并且对数据访问对象执行相同的调用集。继续读下去,看看如何实现这一目标。
|
|
该解决方案是一个由 7 个步骤组成的过程,在此过程中利用了名为 JdbcOperations 的 Spring 接口,该接口可以在 org.springframework.jdbc.core 包中找到。正如 Spring 文档中所描述的那样,该接口能被轻易地模拟或保存。第一步是要创建一个名为 JdbcOperationsImpl 的类,该类实现 JdbcOperations 接口。该类也实现 ApplicationContextAware 接口。
JdbcOperations 接口需要许多数据库访问操作的实现。当然,您不应该(也不应该想要)编写如此低层的代码。相反,此类的目的仅仅是作为一个代理,该代理将所有的数据访问调用转发至一个 org.springframework.jdbc.core.JdbcTemplate 实例。
您也许会回想起之前用 Spring 编写数据访问代码的经历,可以轻易地通过将一个 javax.sql.DataSource 实例传给 JdbcTemplate 的构造函数将其实例化。请记住,本文假设您正在使用一个应用服务器,该服务器将数据源定义作为隔离级别值的占位符。为在执行用例时使用相同的隔离级别,必须在执行该用例时,使用相同的 JdbcTemplate 实例来跨越所有的数据访问对象。换言之,依赖于执行中的用例,数据访问对象需要获得对 JdbcTemplate 实例的引用,该实例与(通过其 DataSource 对象)相应的隔离级别值相关联。
ApplicationContextAware 接口需要 setApplicationContext() 方法的一个实现,该方法将实现类的访问提供给 Spring 应用程序的上下文。正如稍后将会看到的那样,访问 Spring 的上下文是必需的,因为 JdbcOperationsImpl 使用它来获取 bean(通过其 ID)。JdbcOperationsImpl 类的 bean 定义如清单 3 所示:
清单 3. JdbcOperationsImpl 实例的定义
<bean id="jdbcOperations"
class="application.storage.JdbcOperationsImpl" singleton="true">
<constructor-arg index="0">
<!-- Reference to a JdbcTemplate instance with a
"read committed" isolation level -->
<ref local="rcJdbcTemplate" />
</constructor-arg>
</bean>
|
|
第二步是要确保所有的数据访问对象使用 JdbcOperationsImpl 类的一个实例来与数据库进行通信,而不是 JdbcTemplate 实例。这是很明显的,因为 JdbcTemplate 类实现 JdbcOperations 接口。不需要改变数据访问对象中一行代码;只需要改变 Spring 配置文件中每个数据访问对象的配置。例如,最初的 OrderDAO 数据访问对象的定义是这样的:
<bean id="orderDAO"
class="sample.dao.OrderDAOImpl" singleton="true">
<property name="jdbcOperations">
<ref local="jdbcTemplate" />
</property>
</bean>
|
请将 OrderDAO 数据访问对象的定义改成这样:
<bean id="orderDAO"
class="sample.dao.OrderDAOImpl" singleton="true">
<property name="jdbcOperations">
<ref local="jdbcOperations" />
</property>
</bean>
|
现在,JdbcOperationsImpl 类中的所有访问存储资源(如 batchUpdate() 或 execute() 方法)的方法都调用一个名为 getJdbcTemplate() 的方法,如清单 4 所示:
清单 4. JdbcOperationsImpl 类中 getJdbcTemplate() 方法的实现
private JdbcTemplate getJdbcTemplate() {
try {
return (JdbcTemplate) applicationContext.getBean("jdbcTemplate");
} catch (ClassCastException e) {
logger.warn(
"Using default JdbcTemplate instance.", e);
return defaultJdbcTemplate;
}
}
|
在这段代码中,getJdbcTemplate() 方法查询 Spring 应用程序的上下文以获取相应的 JdbcTemplate 实例。请注意,使用了 jdbcTemplate 的 bean id 来查询上下文。同样,请注意如果在 getJdbcTemplate() 获取 JdbcTemplate 对象时发生错误,将返回对默认 JdbcTemplate 对象的引用。defaultJdbcTemplate 对象是使用 “已提交读” 隔离级别的 JdbcOperationsImpl 类的 JdbcTemplate 实例变量。JdbcOperationsImpl 类使用这个实例变量作为后备解决方案,以防相应的 JdbcTemplate 实例不能从应用程序的上下文中获取。(当发生这种情况时,会在日记中记一个警告。)此类的构造函数期望将默认的 JdbcTemplate 实例作为一个参数,如清单 5 所示:
清单 5. JdbcOperationsImpl 类的构造函数
public JdbcOperationsImpl(JdbcTemplate defaultJdbcTemplate) {
super();
this.defaultJdbcTemplate = defaultJdbcTemplate;
}
|
从清单 6 中可见,只要要求应用程序的上下文返回标识为 jdbcTemplate 的对象,就会调用 IsolationLevelUtil 类的 getJdbcTemplate() 方法:
清单 6. jdbcTemplate bean 的定义
<bean id="jdbcTemplate"
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass">
<value>application.services.IsolationLevelUtil</value>
</property>
<property name="targetMethod">
<value>getJdbcTemplate</value>
</property>
<property name="singleton">
<value>false</value>
</property>
</bean>
|
|
第三步是用 清单 6 显示的定义更新 Spring 配置文件,并定义 IsolationLevelUtil 类的实现,如清单 7 所示:
清单 7. IsolationLevelUtil 类的实现
public class IsolationLevelUtil {
private static final ThreadLocal threadJdbcTemplate = new ThreadLocal();
private IsolationLevelUtil() {
super();
}
public static JdbcTemplate getJdbcTemplate() {
JdbcTemplate jdbcTemplate = (JdbcTemplate) threadJdbcTemplate.get();
return jdbcTemplate;
}
public static void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
threadJdbcTemplate.set(jdbcTemplate);
}
}
|
IsolationLevelUtil 类的 getJdbcTemplate() 方法返回和当前执行线程关联在一起的 JdbcTemplate 实例。名为 threadJdbcTemplate 的本地线程变量被用于保持线程和 JdbcTemplate 实例间的关联。您也许想知道为什么 JdbcOperationsImpl 类的 getJdbcTemplate() 方法没有显式地调用 IsolationLevelUtil 的 getJdbcTemplate() 方法。尽管这个方法会起作用,但更好的设计是让这两个类保持解耦。例如,如果想要实现一种不同的机制来获取和执行中的用例相应的 JdbcTemplate 实例,只需要改变 Spring 配置文件,而不是 JdbcOperationsImpl 类。
|
如果您正在思考哪个组件将相应的 JdbcTemplate 实例设置为 IsolationLevelUtil 类上的本地线程变量,您的思路是正确的。为此,这个值必须在线程执行的前期已经设置好了。否则,将返回 NULL 值。所以,第四步是编写一个负责设置名为 threadJdbcTemplate 的本地线程变量的组件。请将这个组件实现为一个名为 IsolationLevelAdvice 的面向方面的 advice,如清单 8 所示。这个 advice 在用例开始执行前即被应用。
清单 8. IsolationLevelAdvice 类的实现
public class IsolationLevelAdvice implements MethodInterceptor {
private Map methodJdbcTemplateMap;
private JdbcTemplate defaultJdbcTemplate;
public IsolationLevelAdvice(Map methodJdbcTemplateMap,
JdbcTemplate defaultJdbcTemplate) {
super();
this.defaultJdbcTemplate = defaultJdbcTemplate;
this.methodJdbcTemplateMap = methodJdbcTemplateMap;
}
public Object invoke(MethodInvocation invocation) throws Exception {
boolean set = false;
try {
Method method = invocation.getMethod();
set = setThreadJdbcTemplate(method);
Object rval = invocation.proceed();
return rval;
} finally {
if (set) {
unsetThreadJdbcTemplate();
}
}
}
public boolean setThreadJdbcTemplate(Method method) {
boolean set = false;
if (IsolationLevelUtil.getJdbcTemplate() == null) {
JdbcTemplate jdbcTemplate = null;
String methodName = method.getName();
Iterator methodPatterns = methodJdbcTemplateMap.keySet().iterator();
while (methodPatterns.hasNext()) {
String methodPattern = (String) methodPatterns.next();
if (Pattern.matches(methodPattern, methodName)) {
jdbcTemplate = (JdbcTemplate)
methodJdbcTemplateMap.get(methodPattern);
break;
}
}
if (jdbcTemplate == null) {
jdbcTemplate = defaultJdbcTemplate;
}
IsolationLevelUtil.setJdbcTemplate(jdbcTemplate);
set = true;
}
return set;
}
public void unsetThreadJdbcTemplate() {
IsolationLevelUtil.setJdbcTemplate(null);
}
}
|
在该应用程序中,每个服务对象实现都需要此类的实例。
|
第五步是要在 Spring 配置文件中定义这个类的一个 bean 定义,该 bean 将和 OrderService 实现类关联起来,如清单 9 所示:
清单 9.针对 OrderService 实现的隔离 advice bean 的定义
<bean id="orderServiceIsolationAdvice"
class="application.services.IsolationLevelAdvice" singleton="true">
<constructor-arg index="0">
<map>
<entry key="save.*">
<ref local="rrJdbcTemplate" />
</entry>
<entry key="delete.*">
<ref local="rcJdbcTemplate" />
</entry>
<entry key="find.*">
<ref local="rcJdbcTemplate" />
</entry>
</map>
</constructor-arg>
<constructor-arg index="1">
<ref local="rcJdbcTemplate" />
</constructor-arg>
</bean>
|
清单 9 中 bean 的定义显示了 IsolationLevelAdvice 类的实例的构造函数将一个对象映射表作为第一个参数。这个映射表使用字符串匹配模式作为定义在 OrderService 接口中方法的名称的键。这些模式中的每一个都被映射到一个 JdbcTemplate 实例中,该实例具有必须用于用例执行的隔离级别。构造函数的第二个参数指定 JdbcTemplate 实例,使用该实例是为了防止没有 JdbcTemplate 对象被映射到已经调用的方法中。如果在 清单 8 中仔细观察这个类的实现,会看到 IsolationLevelAdvice 实例将在运行时使用反射来确定要在 OrderService 实现对象上调用哪个方法。在确定了将执行的方法的名称后,该 advice 实例查询 methodJdbcTemplateMap 实例变量(methodJdbcTemplateMap 对象是对这个类的构造函数中第一个参数的引用)来确定在执行该用例时要使用哪个 JdbcTemplate。
|
第六步是要指定 IsolationLevelAdvice bean(被标识为 orderServiceIsolationAdvice)和 OrderService 实现对象间的关联。清单 10 中显示的 bean 定义通过让 Spring 容器(被 IsolationLevelAdvice 实例标识为 orderServiceIsolationAdvice)充当 OrderService 类实现的 advice 正好完成这项任务:
清单 10. 针对 OrderService 实现的 AOP 代理 bean 的定义
<bean id="orderServiceTarget" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>application.services.OrderService</value>
</property>
<property name="interceptorNames">
<value>orderServiceIsolationAdvice</value>
</property>
<property name="target">
<ref bean="orderServiceImpl" />
</property>
</bean>
|
|
第七步也是最后的一步是要定义应用程序所需的 JdbcTemplate 实例。清单 11 显示了每个实例的定义。每个 JdbcTemplate 定义都有一个对不同数据源对象的引用。由于有四个隔离级别,所以需要四个数据源定义和四个 JdbcTemplate 定义。清单 11 也显示了这些数据源定义:
清单 11. JdbcTemplate 和数据源对象的定义
<!-- "Serializable" isolation level - JdbcTemplate -->
<bean id="sJdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate" singleton="true">
<property name="dataSource">
<ref local="sDataSource" />
</property>
</bean>
<!-- "Read repeatable" isolation level - JdbcTemplate -->
<bean id="rrJdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate" singleton="true">
<property name="dataSource">
<ref local="rrDataSource" />
</property>
</bean>
<!-- "Read committed" isolation level - JdbcTemplate -->
<bean id="rcJdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate" singleton="true">
<property name="dataSource">
<ref local="rcDataSource" />
</property>
</bean>
<!-- "Read uncommitted" isolation level - JdbcTemplate -->
<bean id="ruJdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate" singleton="true">
<property name="dataSource">
<ref local="ruDataSource" />
</property>
</bean>
<!-- "Serializable" isolation level - data source -->
<bean id="sDataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>java:comp/env/jdbc/s_ds</value>
</property>
</bean>
<!-- "Repeatable read" isolation level - data source -->
<bean id="rrDataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>java:comp/env/jdbc/rr_ds</value>
</property>
</bean>
<!-- "Read committed" isolation level - data source -->
<bean id="rcDataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>java:comp/env/jdbc/rc_ds</value>
</property>
</bean>
<!-- "Read uncommitted" isolation level - data source -->
<bean id="ruDataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>java:comp/env/jdbc/ru_ds</value>
</property>
</bean>
|
图 2 中的类图撷取了这些类中存在的关系,定义这些类是为了实现我所描述过的解决方案:
图 2. 本文解决方案的类图
在 这里 查看全图。
在这个类图中显示的大多数关系并没有定义在 Java 源代码中,而是在 Spring 配置文件中。(这对 Spring 用户来说并不奇怪。)同样,如果将我探讨过的 Spring bean 的定义和该类图中的实体作比较,很容易看出,在 图 2 中被标识为 orderServiceIsolationAdvice、rrTemplate 和 rcTemplate 的类在本质上并不是 Java 类。这三个类中的每个类都有一个 Spring bean 的定义(而不是 Java 类文件)。为在类图中传达这个信息,我使用了在 IsolationLevelAdvice 类和 orderServiceIsolationAdvice 间以及在 JdbcTemplate 类和 rrTemplate 及 rcTemplate 间的 “绑定关系”。orderServiceIsolationAdvice、rrTemplate 和 rcTemplate 实体只不过是通过将其模板类的参数和实际值绑定起来从而实例化其相应的 “模板类” 的具体对象。
下载 这些类的完整的源代码,您需要这些类来实现我在本文中演示的解决方案。
本文介绍了一种在Spring框架中实现自定义事务隔离级别的方法,适用于需要在同一事务中使用特定隔离级别的应用场景。通过7个详细步骤,展示了如何利用Spring的特性来解决JTA不支持自定义隔离级别的限制。
756

被折叠的 条评论
为什么被折叠?



