16.3.4编程式事务划分
开发者可以在应用程序的更高级别上对事务进行标定,而不用考虑低级别的数据访问执行了多少操作。这样不会对业务服务的实现进行限制;只需要定义一个Spring的PlatformTransactionManager
即可。当然,PlatformTransactionManager
可以从多处获取,但最好是通过setTransactionManager(..)
方法以Bean来注入,正如ProductDAO
应该由setProductDao(..)
方法配置一样。下面的代码显示Spring应用程序上下文中的事务管理器和业务服务的定义,以及业务方法实现的示例:
<bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<bean id="myProductService" class="product.ProductServiceImpl">
<property name="transactionManager" ref="myTxManager"/>
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
public class ProductServiceImpl implements ProductService {
private TransactionTemplate transactionTemplate;
private ProductDao productDao;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public void increasePriceOfAllProductsInCategory(final String category) {
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// do the price increase...
}
});
}
}
Spring的TransactionInterceptor
允许任何检查的应用异常到callback
代码中去,而TransactionTemplate
还会非受检异常触发进行回调。TransactionTemplate
则会因为非受检异常或者是由应用标记事务回滚(通过TransactionStatus
)。TransactionInterceptor
也是一样的处理逻辑,但是同时还允许基于方法配置回滚策略。
16.3.5事务管理策略
无论是TransactionTemplate
或者是TransactionInterceptor
都将实际的事务处理代理到PlatformTransactionManager
实例上来进行处理的,这个实例的实现可以是一个HibernateTransactionManager
(包含一个Hibernate的SessionFactory
通过使用ThreadLocal
的Session
),也可以是JatTransactionManager
(代理到容器的JTA子系统)。开发者甚至可以使用一个自定义的PlatformTransactionManager
的实现。现在,如果应用有需求需要需要部署分布式事务的话,只是一个配置变化,就可以从本地Hibernate事务管理切换到JTA。简单地用Spring的JTA事务实现来替换Hibernate事务管理器即可。因为引用的PlatformTransactionManager
的是通用事务管理API,事务管理器之间的切换是无需修改代码的。
对于那些跨越了多个Hibernate会话工厂的分布式事务,只需要将JtaTransactionManager
和多个LocalSessionFactoryBean
定义相结合即可。每个DAO之后会获取一个特定的SessionFactory
引用。如果所有底层JDBC数据源都是事务性容器,那么只要使用JtaTransactionManager
作为策略实现,业务服务就可以划分任意数量的DAO和任意数量的会话工厂的事务。
无论是HibernateTransactionManager
还是JtaTransactionManager
都允许使用JVM级别的缓存来处理Hibernate,无需基于容器的事务管理器查找,或者JCA连接器(如果开发者没有使用EJB来实例化事务的话)。
HibernateTransactionManager
可以为指定的数据源的Hibernate JDBC的Connection
转成为纯JDBC的访问代码。如果开发者仅访问一个数据库,则开发者完全可以不使用JTA,通过Hibernate和JDBC数据访问进行高级别事务划分。如果开发者已经通过LocalSessionFactoryBean
的dataSource
属性与DataSource
设置了传入的SessionFactory
,HibernateTransactionManager
会自动将Hibernate事务公开为JDBC事务。或者,开发者可以通过HibernateTransactionManager
的dataSource
属性的配置以确定公开事务的类型。
16.3.6对比由容器管理的和本地定义的资源
开发者可以在不修改一行代码的情况下,在容器管理的JNDISessionFactory
和本地定义的SessionFactory
之间进行切换。是否将资源定义保留在容器中,还是仅仅留在应用中,都取决于开发者使用的事务策略。相对于Spring定义的本地SessionFactory
来说,手动注册的JNDISessionFactory
没有什么优势。通过Hibernate的JCA连接器来发布一个SessionFactory
只会令代码更符合J2EE服务标准,但是并不会带来任何实际的价值。
Spring对事务支持不限于容器。使用除JTA之外的任何策略配置,事务都可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下,Spring的单一资源本地事务支持是一种轻量级和强大的替代JTA的解决方案。当开发者使用本地EJB无状态会话Bean来驱动事务时,即使只访问单个数据库,并且只使用无状态会话Bean来通过容器管理的事务来提供声明式事务,开发者的代码依然是依赖于EJB容器和JTA的。同时,以编程方式直接使用JTA也需要一个J2EE环境的。 JTA不涉及JTA本身和JNDI DataSource实例方面的容器依赖关系。对于非Spring,JTA驱动的Hibernate事务,开发者必须使用Hibernate JCA连接器或开发额外的Hibernate事务代码,并为JVM级缓存正确配置TransactionManagerLookup
。
Spring驱动的事务可以与本地定义的HibernateSessionFactory
一样工作,就像本地JDBC DataSource访问单个数据库一样。但是,当开发者有分布式事务的要求的情况下,只能选择使用Spring JTA事务策略。JCA连接器是需要特定容器遵循一致的部署步骤的,而且显然JCA支持是需要放在第一位的。JCA的配置需要比部署本地资源定义和Spring驱动事务的简单web应用程序需要更多额外的的工作。同时,开发者还需要使用容器的企业版,比如,如果开发者使用的是WebLogic Express的非企业版,就是不支持JCA的。具有跨越单个数据库的本地资源和事务的Spring应用程序适用于任何基于J2EE的Web容器(不包括JTA,JCA或EJB),如Tomcat,Resin或甚至是Jetty。此外,开发者可以轻松地在桌面应用程序或测试套件中重用中间层代码。
综合前面的叙述,如果不使用EJB,请尽量使用本地的SessionFactory
设置和Spring的HibernateTransactionManager
或JtaTransactionManager
。开发者能够得到了前面提到的所有好处,包括适当的事务性JVM级缓存和分布式事务支持,而且没有容器部署的不便。只有必须配合EJB使用的时候,JNDI通过JCA连接器来注册HibernateSessionFactory
才有价值。
16.3.7Hibernate的虚假应用服务器警告
在某些具有非常严格的XADataSource
实现的JTA环境(目前只有一些WebLogic Server和WebSphere版本)中,当配置Hibernate时,没有考虑到JTA的 PlatformTransactionManager
对象,可能会在应用程序服务器日志中显示虚假警告或异常。这些警告或异常经常描述正在访问的连接不再有效,或者JDBC访问不再有效。这通常可能是因为事务不再有效。例如,这是WebLogic的一个实际异常:
java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.
开发者可以通过配置令Hibernate意识到Spring中同步的JTAPlatformTransactionManager
实例的存在,这样即可消除掉前面所说的虚假警告信息。开发者有以下两种选择:
- 如果在应用程序上下文中,开发者已经直接获取了JTA
PlatformTransactionManager
对象(可能是从JNDI到JndiObjectFactoryBean
或者<jee:jndi-lookup>
标签),并将其提供给Spring的JtaTransactionManager
(其中最简单的方法就是指定一个引用bean将此JTAPlatformTransactionManager
实例定义为LocalSessionFactoryBean
的jtaTransactionManager
属性的值)。 Spring之后会令PlatformTransactionManager
对象对Hibernate可见。 - 更有可能开发者无法获取JTA
PlatformTransactionManager
实例,因为Spring的JtaTransactionManager
是可以自己找到该实例的。因此,开发者需要配置Hibernate令其直接查找JTAPlatformTransactionManager
。开发者可以如Hibernate手册中所述那样通过在Hibernate配置中配置应用程序服务器特定的TransactionManagerLookup
类来执行此操作。
本节的其余部分描述了在PlatformTransactionManager
对Hibernate可见和PlatformTransactionManager
对Hibernate不可见的情况下发生的事件序列:
当Hibernate未配置任何对JTAPlatformTransactionManager
的进行查找时,JTA事务提交时会发生以下事件:
- JTA事务提交
- Spring的
JtaTransactionManager
与JTA事务同步,所以它被JTA事务管理器通过afterCompletion
回调调用。 - 在其他活动中,此同步令Spring通过Hibernate的
afterTransactionCompletion
触发回调(用于清除Hibernate缓存),然后在Hibernate Session上调用close()
,从而令Hibernate尝试close()
JDBC连接。 - 在某些环境中,因为事务已经提交,应用程序服务器会认为
Connection
不可用,导致Connection.close()
调用会触发警告或错误。
当Hibernate配置了对JTAPlatformTransactionManager
进行查找时,JTA事务提交时会发生以下事件:
- JTA事务准备提交
- Spring的
JtaTransactionManager
与JTA事务同步,所以JTA事务管理器通过beforeCompletion
方法来回调事务。 - Spring确定Hibernate与JTA事务同步,并且行为与前一种情况不同。假设Hibernate Session需要关闭,Spring将会关闭它。
- JTA事务提交。
- Hibernate与JTA事务同步,所以JTA事务管理器通过
afterCompletion
方法回调事务,可以正确清除其缓存。
16.4 JPA
Spring JPA在org.springframework.orm.jpa
包中已经可用,Spring JPA用了Hibernate集成相似的方法来提供更易于理解的JPA支持,与此同时,了解了JPA底层实现,可以理解更多的Spring JPA特性。
16.4.1 Spring中JPA配置的三个选项
Spring JPA支持提供了三种配置JPAEntityManagerFactory
的方法,之后通过EntityManagerFactory
来获取对应的实体管理器。
LocalEntityManagerFactoryBean
通常只有在简单的部署环境中使用此选项,例如在独立应用程序或者进行集成测试时,才会使用这种方式。
LocalEntityManagerFactoryBean
创建一个适用于应用程序且仅使用JPA进行数据访问的简单部署环境的EntityManagerFactory
。工厂bean会使用JPAPersistenceProvider
自动检测机制,并且在大多数情况下,仅要求开发者指定持久化单元的名称:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="myPersistenceUnit"/>
</bean>
</beans>
这种形式的JPA部署是最简单的,同时限制也很多。开发者不能引用现有的JDBCDataSource
bean定义,并且不支持全局事务。而且,持久化类的织入(weaving)(字节码转换)是特定于提供者的,通常需要在启动时指定特定的JVM代理。该选项仅适用于符合JPA Spec的独立应用程序或测试环境。
从JNDI中获取EntityManagerFactory
在部署到J2EE服务器时可以使用此选项。检查服务器的文档来了解如何将自定义JPA提供程序部署到服务器中,从而对服务器进行比默认更多的个性化定制。
从JNDI获取EntityManagerFactory
(例如在Java EE环境中),只需要在XML配置中加入配置信息即可:
<beans>
<jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>
此操作将采用标准J2EE引导:J2EE服务器自动检测J2EE部署描述符(例如web.xml)中persistence-unit-ref条目和持久性单元(实际上是应用程序jar中的META-INF/persistence.xml文件),并为这些持久性单元定义环境上下文位置。
在这种情况下,整个持久化单元部署(包括持久化类的织入(weaving)(字节码转换))都取决于J2EE服务器。JDBC DataSource
通过META-INF/persistence.xml文件中的JNDI位置进行定义; 而EntityManager
事务与服务器JTA子系统集成。 Spring仅使用获取的EntityManagerFactory
,通过依赖注入将其传递给应用程序对象,通常通过JtaTransactionManager
来管理持久性单元的事务。
如果在同一应用程序中使用多个持久性单元,则这种JNDI检索的持久性单元的bean名称应与应用程序用于引用它们的持久性单元名称相匹配,例如@PersistenceUnit
和@PersistenceContext
注释。
LocalContainerEntityManagerFactoryBean
在基于Spring的应用程序环境中使用此选项来实现完整的JPA功能。这包括诸如Tomcat的Web容器,以及具有复杂持久性要求的独立应用程序和集成测试。
LocalContainerEntityManagerFactoryBean
可以完全控制EntityManagerFactory
的配置,同时适用于需要细粒度定制的环境。 LocalContainerEntityManagerFactoryBean
会基于persistence.xml
文件,dataSourceLookup
策略和指定的loadTimeWeaver
来创建一个PersistenceUnitInfo
实例。因此,可以在JNDI之外使用自定义数据源并控制织入(weaving)过程。以下示例显示LocalContainerEntityManagerFactoryBean
的典型Bean定义:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="someDataSource"/>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
</bean>
</beans>
下面的例子是一个典型的persistence.xml文件:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
<mapping-file>META-INF/orm.xml</mapping-file>
<exclude-unlisted-classes/>
</persistence-unit>
</persistence>
<exclude-unlisted-classes />
标签表示不会进行注解实体类的扫描。指定的显式true
值 –<exclude-unlisted-classes>true</exclude-unlisted-classes/>
– 也意味着不进行扫描。<exclude-unlisted-classes> false</exclude-unlisted-classes>
则会触发扫描;但是,如果开发者需要进行实体类扫描,建议开发者简单地省略<exclude-unlisted-classes>
元素。
LocalContainerEntityManagerFactoryBean
是最强大的JPA设置选项,允许在应用程序中进行灵活的本地配置。它支持连接到现有的JDBCDataSource
,支持本地和全局事务等。但是,它对运行时环境施加了需求,其中之一就是如果持久性提供程序需要字节码转换,就需要有织入(weaving)能力的类加载器。
此选项可能与J2EE服务器的内置JPA功能冲突。在完整的J2EE环境中,请考虑从JNDI获取EntityManagerFactory
。或者,在开发者的LocalContainerEntityManagerFactoryBean
定义中指定一个自定义persistenceXmlLocation
,例如META-INF/my-persistence.xml,并且只在应用程序jar文件中包含有该名称的描述符。因为J2EE服务器仅查找默认的META-INF/persistence.xml文件,所以它会忽略这种自定义持久性单元,从而避免了与Spring驱动的JPA设置之间发生冲突。 (例如,这适用于Resin 3.1)
何时需要加载时间织入?
并非所有JPA提供商都需要JVM代理。Hibernate就是一个不需要JVM代理的例子。如果开发者的提供商不需要代理或开发者有其他替代方案,例如通过定制编译器或
Ant
任务在构建时应用增强功能,则不用使用加载时间编织器。
LoadTimeWeaver
是一个Spring提供的接口,它允许以特定方式插入JPAClassTransformer
实例,这取决于环境是Web容器还是应用程序服务器。 通过代理挂载ClassTransformers
通常性能较差。代理会对整个虚拟机进行操作,并检查加载的每个类,这是生产服务器环境中最不需要的额外负载。
Spring为各种环境提供了一些LoadTimeWeaver
实现,允许ClassTransformer
实例仅适用于每个类加载器,而不是每个VM。
有关LoadTimeWeaver
的实现及其设置的通用或定制的各种平台(如Tomcat,WebLogic,GlassFish,Resin和JBoss)的更多了解,请参阅AOP章节中的Spring配置一节。
如前面部分所述,开发者可以使用@EnableLoadTimeWeaving
注解或者load-time-weaver
XML元素来配置上下文范围的LoadTimeWeaver
。所有JPALocalContainerEntityManagerFactoryBeans
都会自动拾取这样的全局织入器。这是设置加载时间织入器的首选方式,为平台(WebLogic,GlassFish,Tomcat,Resin,JBoss或VM代理)提供自动检测功能,并将织入组件自动传播到所有可以感知织入者的Bean:
<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
开发者也可以通过LocalContainerEntityManagerFactoryBean
的loadTimeWeaver
属性来手动指定专用的织入器:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</property>
</bean>
无论LTW如何配置,使用这种技术,依赖于仪器的JPA应用程序都可以在目标平台(例如:Tomcat)中运行,而不需要代理。这尤其重要的是当主机应用程序依赖于不同的JPA实现时,因为JPA转换器仅应用于类加载器级,彼此隔离。
处理多个持久化单元
例如,对于依赖存储在类路径中的各种JARS中的多个持久性单元位置的应用程序,Spring将PersistenceUnitManager
作为中央仓库来避免可能昂贵的持久性单元发现过程。默认实现允许指定多个位置,这些位置将通过持久性单元名称进行解析并稍后检索。(默认情况下,搜索classpath下的META-INF/persistence.xml文件。)
<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
<property name="persistenceXmlLocations">
<list>
<value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
<value>classpath:/my/package/**/custom-persistence.xml</value>
<value>classpath*:META-INF/persistence.xml</value>
</list>
</property>
<property name="dataSources">
<map>
<entry key="localDataSource" value-ref="local-db"/>
<entry key="remoteDataSource" value-ref="remote-db"/>
</map>
</property>
<!-- if no datasource is specified, use this one -->
<property name="defaultDataSource" ref="remoteDataSource"/>
</bean>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitManager" ref="pum"/>
<property name="persistenceUnitName" value="myCustomUnit"/>
</bean>
在默认实现传递给JPA provider之前,是允许通过属性(影响全部持久化单元)或者通过PersistenceUnitPostProcessor
以编程(对选择的持久化单元进行)进行对PersistenceUnitInfo
进行自定义的。如果没有指定PersistenceUnitManager
,则由LocalContainerEntityManagerFactoryBean
在内部创建和使用。