用 Spring 框架指定自定义隔离级别
许多 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 事务管理器的事务代理的错误定义
清单 1 中定义了两个 bean。第一个 bean 的定义指定了应用程序将使用的事务管理器。正如您能看到的那样,这个 bean 依赖于另一个叫做
jtaTransactionManager
的 bean,而这个 bean 的定义依赖于您正在使用的应用服务器。例如,对于 IBM WebSphere Application Server 来说,这个 bean 的定义是这样的:清单 1 中第二个 bean(称为
orderService
)包含一个服务对象的事务代理定义,该服务对象实现了一个名为 OrderService
的接口。这个代理为三个方法声明了三个事务性定义:save()
、delete()
和 find()
。由于 “序列化” 和 “未提交读” 被指定为这些方法的隔离级别,那么期望这些就是在运行时获得的隔离级别是符合逻辑的。然而,请注意该代理定义包含了对 JTA 事务管理器的引用。如果用这个配置运行应用程序,您也许会十分惊诧。只要执行了 OrderService
实现的 save()
、delete()
或 find()
方法,就会出现这样一个异常:出现这个错误是因为 JTA 事务管理器不支持自定义隔离级别。当使用 JTA 事务管理器时,事务代理的 bean 定义会和清单 2 中的类似:
清单 2. 使用 JTA 事务管理器的事务代理的正确定义
请注意,和 清单 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 实例的定义
第二步是要确保所有的数据访问对象使用
JdbcOperationsImpl
类的一个实例来与数据库进行通信,而不是 JdbcTemplate
实例。这是很明显的,因为 JdbcTemplate
类实现 JdbcOperations
接口。不需要改变数据访问对象中一行代码;只需要改变 Spring 配置文件中每个数据访问对象的配置。例如,最初的 OrderDAO
数据访问对象的定义是这样的:请将
OrderDAO
数据访问对象的定义改成这样:现在,
JdbcOperationsImpl
类中的所有访问存储资源(如 batchUpdate()
或 execute()
方法)的方法都调用一个名为 getJdbcTemplate()
的方法,如清单 4 所示:在这段代码中,getJdbcTemplate()
方法查询 Spring 应用程序的上下文以获取相应的 JdbcTemplate
实例。请注意,使用了 jdbcTemplate
的 bean id
来查询上下文。同样,请注意如果在 getJdbcTemplate()
获取 JdbcTemplate
对象时发生错误,将返回对默认 JdbcTemplate
对象的引用。defaultJdbcTemplate
对象是使用 “已提交读” 隔离级别的 JdbcOperationsImpl
类的 JdbcTemplate
实例变量。JdbcOperationsImpl
类使用这个实例变量作为后备解决方案,以防相应的 JdbcTemplate
实例不能从应用程序的上下文中获取。(当发生这种情况时,会在日记中记一个警告。)此类的构造函数期望将默认的 JdbcTemplate
实例作为一个参数,如清单 5 所示:
清单 5. JdbcOperationsImpl 类的构造函数
从清单 6 中可见,只要要求应用程序的上下文返回标识为
jdbcTemplate
的对象,就会调用 IsolationLevelUtil
类的 getJdbcTemplate()
方法:清单 6. jdbcTemplate bean 的定义
第三步是用 清单 6 显示的定义更新 Spring 配置文件,并定义 IsolationLevelUtil
类的实现,如清单 7 所示:
清单 7. IsolationLevelUtil 类的实现
IsolationLevelUtil
类的 getJdbcTemplate()
方法返回和当前执行线程关联在一起的 JdbcTemplate
实例。名为 threadJdbcTemplate
的本地线程变量被用于保持线程和 JdbcTemplate
实例间的关联。您也许想知道为什么 JdbcOperationsImpl
类的 getJdbcTemplate()
方法没有显式地调用 IsolationLevelUtil
的 getJdbcTemplate()
方法。尽管这个方法会起作用,但更好的设计是让这两个类保持解耦。例如,如果想要实现一种不同的机制来获取和执行中的用例相应的 JdbcTemplate
实例,只需要改变 Spring 配置文件,而不是 JdbcOperationsImpl
类。
|
如果您正在思考哪个组件将相应的 JdbcTemplate
实例设置为 IsolationLevelUtil
类上的本地线程变量,您的思路是正确的。为此,这个值必须在线程执行的前期已经设置好了。否则,将返回 NULL
值。所以,第四步是编写一个负责设置名为 threadJdbcTemplate
的本地线程变量的组件。请将这个组件实现为一个名为 IsolationLevelAdvice
的面向方面的 advice,如清单 8 所示。这个 advice 在用例开始执行前即被应用。
清单 8. IsolationLevelAdvice 类的实现
在该应用程序中,每个服务对象实现都需要此类的实例。
|
第五步是要在 Spring 配置文件中定义这个类的一个 bean 定义,该 bean 将和 OrderService
实现类关联起来,如清单 9 所示:
清单 9.针对 OrderService 实现的隔离 advice 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 的定义
第七步也是最后的一步是要定义应用程序所需的 JdbcTemplate
实例。清单 11 显示了每个实例的定义。每个 JdbcTemplate
定义都有一个对不同数据源对象的引用。由于有四个隔离级别,所以需要四个数据源定义和四个 JdbcTemplate
定义。清单 11 也显示了这些数据源定义:
清单 11. JdbcTemplate 和数据源对象的定义
图 2 中的类图撷取了这些类中存在的关系,定义这些类是为了实现我所描述过的解决方案:
图 2. 本文解决方案的类图
在这个类图中显示的大多数关系并没有定义在 Java 源代码中,而是在 Spring 配置文件中。(这对 Spring 用户来说并不奇怪。)同样,如果将我探讨过的 Spring bean 的定义和该类图中的实体作比较,很容易看出,在 图 2 中被标识为 orderServiceIsolationAdvice
、rrTemplate
和 rcTemplate
的类在本质上并不是 Java 类。这三个类中的每个类都有一个 Spring bean 的定义(而不是 Java 类文件)。为在类图中传达这个信息,我使用了在 IsolationLevelAdvice
类和 orderServiceIsolationAdvice
间以及在 JdbcTemplate
类和 rrTemplate
及 rcTemplate
间的 “绑定关系”。orderServiceIsolationAdvice
、rrTemplate
和 rcTemplate
实体只不过是通过将其模板类的参数和实际值绑定起来从而实例化其相应的 “模板类” 的具体对象。
下载 这些类的完整的源代码,您需要这些类来实现我在本文中演示的解决方案。
如 果在技术需求中声明了在执行使用分布式事务的用例过程中应该使用相同的隔离级别,尽管 JTA 事务管理器不支持自定义隔离级别,但您的应用程序能够满足此项需求。本文提供了实现此目标的一种方法,即使用 Spring 的依赖项-注入功能来保持类的解耦。第一眼看去,该实现似乎有点复杂,但您会意识到它很直白且相当简单。在执行用例时的任何时刻访问数据库,它让您在一种 可配置的方式下使用相同的隔离级别。处理业务逻辑和持久性逻辑的 Java 代码并未改变。相反,使用在 Spring 配置文件中的设置,所有 “神奇的事情” 都在运行时发生。这就是该解决方案的设计中的主要优点之一:使实现应用程序业务逻辑和持久性逻辑的类从确保在执行用例过程中使用的相同隔离级别的类和组件 中解耦出来。
作者要感谢 Coraly Romero-Principe,有了她的帮助,这篇文章才可能面世。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
源代码 | j-isolation.zip | 4KB | HTTP |
关于下载方法的信息 |
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- Spring 框架:从该项目的站点中学习更多有关 Spring 框架的知识。
- “Introduction to the Spring framework”(Rod Johnson,TheServerSide.com,2005 年 5 月):极好的关于 Spring 框架的介绍性文章。
- “Understanding JTS -- An introduction to transactions”、“理解 JTS ― 幕后魔术” 和 “理解 JTS ― 平衡安全性和性能” (Brian Goetz,developerWorks,2002 3 月 — 5 月):Java theory and practice 系列中的这些文章探讨了 Java 事务领域。
- “Understanding JTA -- the Java Transaction API” (DevX,2002):关于分布式事务的好文章。
- Core J2EE Patterns - Session Facade:阅读有关 Session Facade 设计模式。
- developerWorks:数百篇有关 Java 编程各个方面的文章。
讨论
- 通过参与 developerWorks blogs 加入 developerWorks 社区。
Ricardo Olivieri 是 IBM Global Services 的一位软件工程师。他的专业领域包括 WebSphere Application Server 的企业 Java 应用程序的设计和开发、WebSphere Application Server 的管理和配置以及分布式软件架构。在过去几年里,Ricardo 把兴趣放在学习开发源码项目上,比如 Drools、Spring、WebWork、Hibernate 和 JasperReports。他是通过认证的 Java 开发人员和 WebSphere Application Server 管理员。他从 University of Puerto Rico Mayaguez Campus 获得计算机工程学士学位。 |