在使用Hibernate的应用中, Spring的对DAO对象通常的事务管理特别应该引起关注。它的目的就是分离数据访问和事务处理,使事务性业务对象不与任何特殊的数据访问或者事务策略绑在一起,从而不影响业务对象的可复用性。这种划分既可以经由事务模板(TransactionTemplate)用编程的方式实现,也可以经由面向方面(AOP)事务拦截器(TransactionTemplate)用声明的方式实现。无论是本地的Hibernate / JDBC事务,还是JTA事务都支持对象外的事务策略,这对于本地的无状态会话Bean(Stateless Session Beans)是一个非常有用的选择。
Spring的HibernateTemplate类提供了一个简单的方式实现了Hibernate-based DAO对象而不必关心如何获得Hibernate的Session实例,也不必关心多方参与的事务处理。无需使用try-catch块,也无需进行事务检查。一个简单的Hibernate访问方法就完全解决了些麻烦! 无论是在多个DAO接口还是在多方事务的情况下,Spring使得多种DAO对象无缝地协同工作。例如:某些DAO对象可能是基于plain JDBC的实现,更适合于经由Spring的JdbcTemplate来避免手动的异常处理。
你可以单独地使用许多Spring特性,因为Spring的所有对象都是设计成可复用的JavaBean对象的集合。也不要因为Spring可以提供一个完整的应该框架而气馁!使用其他的Spring特性时,应用配置概念是一个附加的特性,并不是一个必须的特性。无论如何,当你要决定去构建一个象Spring这样的内在的基础架构的时候,在使用Spring的路途上没有什么范围上的限制。
1. 介绍: 资源管理
典型的业务应用系统常常由于重复的资源管理代码而导致混乱。许多项目试着用自己的方法来解决这个问题,有时要为此付出失败的代价,Spring针对适当的资源管理提倡了一种引人注目的简单方法:即经由模板来倒置控制(Inversion of control),例如:基础类使用回调接口,或者应用AOP拦截器。其基础核心是适当的资源处理和将特殊的API异常转换为一个unchecked的基础异常。
Spring引入了一个DAO异常层适用于任何数据访问策略。对于直接的JDBC,JdbcTemplate类关注于连接处理,并且关注于对SQLException转换为适当的DataAccessException,包括对特殊的数据库SQL错误转换为有意义的异常。 经由不同的事务管理对象,Spring支持JTA和JDBC事务。Spring 也提供对Hibernate和JDO的支持,它的这种支持由与JdbcTemplate类的作用相类似的HibernateTemplate类和JdoTemplate类, 以及HibernateInterceptor类、JdoInterceptor类,还有Hibernate、JDO 事务管理类组成。
最主要的目的是要使应用的层次分明,为此将数据访问和事务处理同应用对象分离开来。所有的业务对象都不再依赖数据访问或者事务策略。不再有硬编码的资源查找代码,不再有难以替换的单例对象,也不再需要定制服务注册。
所有的单独的数据访问特性均无需依赖于Spring,可以单独使用,无需让Spring知道,同时也可以通过Spring的应用配置(提供基于XML的配置和对普通JavaBean实例的交叉引用)来进行装配。在一个典型的Spring应用中,大部分重要的对象都是普通的JavaBean:数据访问模板对象(data access templates)、数据访问对象(使用数据访问模板对象的对象)、事务管理对象及业务对象(使用数据访问对象和事务对象的对象),web表示分解对象、web控制对象(使用业务对象的对象)等等。
2. 应用配置中的资源定义
为了避免应用对象将资源查找的代码硬编码,Spring允许在应用配置中将一个如JDBC DataSource或者Hibernate SessionFactory定义为一个Bean。应用对象如果需要访问资源只需要通过Bean引用(DAO定义在下一部分说明)接受先前定义的实例的引用。以下的内容引用自一个应用配置定义,显示了如何建立一个JDBC DataSource和一个Hibernate的SessionFactory:
<beans> .JndiObjectFactoryBean"> .LocalSessionFactoryBean"> .MySQLDialect</prop> |
注意选择是用JNDI来定位数据源还是从一个象Jakarta Commons DBCP BasicDataSource这样的本地定义取得一个数据源,只是一个改变配置的事:
<bean id="myDataSource" .dbcp.BasicDataSource" destroy-method="close"> |
你也可以使用一个JNDI查找SessionFactory,但是通常对于EJB环境之外的应用来说并不是需要的(参考"container resources vs local resources"部分的讨论)。
3. 倒置控制(Inversion of Control): 模板和回调
模板的基本编程模式就象你将在下面看到那样,至于方法就如同任何定制的数据访问对象或者业务的对象的方法一样。除了需要向其提供一个Hibernate的SessionFactory之外,再没有对周围执行对象的信赖的限制。虽然最好是从一个Spring的应用配置中经由一个简单setSessionFactory bean的属性设置使用Bean引用来获得它,但随后你可以从任何地方获得它。随后的引用片段包括一段在Spring应用配置中对DAO定义的配置,其中引用了在其前面定义的SessionFactory,和一段DAO方法的实现的例子。
<beans> <bean id="myProductDao" class="product.ProductDaoImpl"> <property name="sessionFactory"> <ref bean="mySessionFactory"/> </property> </bean> ... </beans> |
public class ProductDaoImpl implements ProductDao { private SessionFactory sessionFactory; public void setSessionFactory(SessionFactorysessionFactory) { this.sessionFactory = sessionFactory; } public List loadProductsByCategory(final Stringcategory) { HibernateTemplate hibernateTemplate = new HibernateTemplate(this.sessionFactory); return (List) hibernateTemplate.execute( new HibernateCallback() { public Object doInHibernate(Session session) throwsHibernateException { List result = session.find( "from test.Product product where product.category=?", category, Hibernate.STRING); // do some further stuff with the result list return result; } } ); } } |
一个回调的实现可以被有效地用在任何Hibernate数据访问中。在任何情况下都由HibernateTemplate来管理Session的开闭和自动的多方事务。模板实例是线程安全和可重用的,因此它们可以做为其他类的变量。
对于简单的单步的动作,象find, load, saveOrUpdate或者delete的调用,HibernateTemplate提供更为便利的选择以代替象一行的回调的执行。此外,Spring提供了一个方便的基本类,就是HibernateDaoSupport类,它提供了setSessionFactory方法来接受一个SessionFactory,同时提供了getSessionFactory和getHibernateTemplate方法供其继承类使用。将这些结合起来,允许对于典型的需求给出了非常简单的DAO实现:
public class ProductDaoImpl extends HibernateDaoSupport implementsProductDao { public List loadProductsByCategory(String category) { return getHibernateTemplate().find( "from test.Product product where product.category=?", category, Hibernate.STRING); } } |
4. 应用一个AOP拦截器代替一个模板
除使用HibernateTemplate之外的另一个选择就是使用Spring的AOP HibernateInterceptor。用直接在一个委托的try/catch块中编写Hibernate代码,配合相应的在应用配置中分别的拦截器配置来代替执行回调。下面的片段显示了一个Spring应用配置中的DAO, interceptor和proxy的各自的定义,同时给出了一个DAO方法实现的例子:
<beans> ... <bean id="myHibernateInterceptor" class="org.springframework.orm.hibernate.HibernateInterceptor"> <property name="sessionFactory"> <ref bean="mySessionFactory"/> </property> </bean> <bean id="myProductDaoTarget" class="product.ProductDaoImpl"> <property name="sessionFactory"> <ref bean="mySessionFactory"/> </property> </bean> <bean id="myProductDao" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>product.ProductDao</value> </property> <property name="interceptorNames"> <list> <value>myHibernateInterceptor</value> <value>myProductDaoTarget</value> </list> </property> </bean> ... </beans> |
public class ProductDaoImpl extends HibernateDaoSupportimplements ProductDao { public List loadProductsByCategory(final String category)throws MyException { Session session = SessionFactoryUtils.getSession(getSessionFactory(), false); try { List result = session.find( "from test.Product product where product.category=?", category, Hibernate.STRING); if (result == null) { throw new MyException("invalid search result"); } return result; } catch (HibernateException ex) { throw SessionFactoryUtils.convertHibernateAccessException(ex); } } } |
这个方法将只在有一个与它配合的HibernateInterceptor时才能正常工作,HibernateInterceptor为它负责在方法调用前线程绑定Session的开启和方法调用后的关闭。getSession方法调用中的"false"标志是要确认Session必须是已经存在的,如果没有发现任何一个Session,SessionFactoryUtils将会为其创建一个。如果已经有一个Session句柄绑定在本线程上,比如是由一个HibernateTransactionManager事务绑定的,在任何情况下SessionFactoryUtils会自动接入这个Session。HibernateTemplate在底层也使用SessionFactoryUtils,与以上说的方式基本是一样的。
HibernateInterceptor的主要益处是它允许在数据访问代码中抛出checked application exception,而HibernateTemplate由于受限于回调只能在其中抛出unchecked exceptions。注意到这点我们可以推迟各自的检验,同时在回调后抛出应用异常。拦截方式的主要缺点是它需要在配置中进行特殊的配置。HibernateTemplate在大多数情况下都是一种简单好用的方法。
5. 程序事务划分
在这种底层的数据访问服务之上,事务处理可以在更高的应用层被划分 ,形成一些操作。这里除了需要一个Spring的PlatformTransactionManager对象外,对于周围运行的业务对象也没有任何限制。同样的,其后你可以从任何地方获得它们,但是经由Bean引用的方式通过setTransactionManage方法获得更为适合,象productDAO要经由一个setProductDao方法获得一样。下面的引用片段显示了在一个Spring应用配置中的事务管理对象和业务对象的定义,并且还提供了一个业务方法实现的例子:
<beans> ... <bean id="myTransactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"> <ref bean="mySessionFactory"/> </property> </bean> <bean id="myProductService" class="product.ProductServiceImpl"> <property name="transactionManager"> <ref bean="myTransactionManager"/> </property> <property name="productDao"> <ref bean="myProductDao"/> </property> </bean> </beans> |
public class ProductServiceImpl implements ProductService { private PlatformTransactionManager transactionManager; private ProductDao productDao; public void setTransactionManager(PlatformTransactionManagertransactionManager) { this.transactionManager = transactionManager; } public void setProductDao(ProductDao productDao) { this.productDao = productDao; } public void increasePriceOfAllProductsInCategory(final Stringcategory) { TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager); transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); transactionTemplate.execute( new TransactionCallbackWithoutResult() { public void doInTransactionWithoutResult(TransactionStatusstatus) { List productsToChange = productDAO.loadProductsByCategory(category); ... } } ); } } |
6. 声明性事务划分
我们还可以选择使用Spring的AOP TransactionInterceptor通过在应用配置中定义拦截器配置来代替事务划分代码的事务处理方式。这允许我们保持业务对象独立于每个业务对象中重复的事务划分代码。此外,事务行为和隔离层次的变化可以通过一个配置文件来改变而不需要对业务对象的实现造成影响。
<beans> ... <bean id="myTransactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"> <ref bean="mySessionFactory"/> </property> </bean> <bean id="myTransactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager"> <ref bean="myTransactionManager"/> </property> <property name="transactionAttributeSource"> <value> product.ProductService.increasePrice*=PROPAGATION_REQUIRED product.ProductService.someOtherBusinessMethod=PROPAGATION_MANDATORY </value> </property> </bean> <bean id="myProductServiceTarget" class="product.ProductServiceImpl"> <property name="productDao"> <ref bean="myProductDao"/> </property> </bean> <bean id="myProductService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>product.ProductService</value> </property> <property name="interceptorNames"> <list> <value>myTransactionInterceptor</value> <value>myProductServiceTarget</value> </list> </property> </bean> </beans> |
public class ProductServiceImpl implements ProductService { private ProductDao productDao; public void setProductDao(ProductDao productDao) { this.productDao = productDao; } public void increasePriceOfAllProductsInCategory(final Stringcategory) { List productsToChange = this.productDAO.loadProductsByCategory(category); ... } } |
如同使用HibernateInterceptor一样,TransactionInterceptor允许任何checked application exception从回调代码中抛出,而TransactionTemplate受回调限制在其内部抛出unchecked exceptions,在出现一个unchecked application exception的情况时,TransactionTemplate将引发一个回滚或者这个事务由应用(通过事务状态)标记为回滚。TransactionInterceptor默认情况也是同样的行为,但是允许为每一个方法制定回滚策略。
建立声明性事务的一个便利的方式是使用TransactionProxyFactoryBean,特别是如果没有其他AOP拦截器的话,TransactionProxyFactoryBean将联合定义为代理的自身与一个特殊的目标Bean的事务配置。这将减少一个代理Bean对应一个目标Bean的配置情况。此外,你不必指定哪个接口或者哪个类必须定义事务方法。
<beans> ... <bean id="myTransactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"> <ref bean="mySessionFactory"/> </property> </bean> <bean id="myProductServiceTarget" class="product.ProductServiceImpl"> <property name="productDao"> <ref bean="myProductDao"/> </property> </bean> <bean id="myProductService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref bean="myTransactionManager"/> </property> <property name="target"> <ref bean="myProductServiceTarget"/> </property> <property name="transactionAttributes"> <props> <prop key="increasePrice*">PROPAGATION_REQUIRED</prop> <prop key="someOtherBusinessMethod">PROPAGATION_MANDATORY</prop> </props> </property> </bean> </beans> |
7. 事务管理策略
对于Hibernate应用来说,无论是TransactionTemplate还是TransactionInterceptor都是委托验实际的事务处理给PlatformTransactionManager实例,可以是一个HibernateTransactionManager(由一个单一的Hibernate的SessionFactory,使用一个ThreadLocal Session)或者可以是一个JtaTransactionManager(代理容器的JTA子系统)。甚至你可以使用一个自定义的PlatformTransactionManager实现。
如果选择从本地Hibernate事务管理转为由JTA来进行事务管理,例如:当你的应用的部署面对分布的事务需求时,也仅仅是改变一下配置的事。只要简单地将Hibernate的事务管理换为JTA事务实现即可。所有的事务划分和数据访问无需做任何变动仍可以继续工作,因为他们使用的都是普通的事务管理API。
对于分布式的事务会跨越多个Hibernate的session factories,仅仅是联合JtaTransactionManager与多个LocalSessionFactoryBean定义作为事务策略。你的每一个DAO将通过它们各自的Bean属性得到一个特殊的SessionFactory的引用。如果这一切都是在下面的JDBC数据源是事务容器,一个业务对象可以划分事务跨越很多DAO和很多session factories而无需做特别的处理,对于使用JtaTransactionManager做为事务策略也是一样的。
<beans> <bean id="myDataSource1" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>jdbc/myds1</value> </property> </bean> <bean id="myDataSource2" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>jdbc/myds2</value> </property> </bean> <bean id="mySessionFactory1" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="mappingResources"> <list> <value>product.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop> </props> </property> <property name="dataSource"> <ref bean="myDataSource1"/> </property> </bean> <bean id="mySessionFactory2" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="mappingResources"> <list> <value>inventory.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">net.sf.hibernate.dialect.OracleDialect</prop> </props> </property> <property name="dataSource"> <ref bean="myDataSource2"/> </property> </bean> <bean id="myTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/> <bean id="myProductDao" class="product.ProductDaoImpl"> <property name="sessionFactory"> <ref bean="mySessionFactory1"/> </property> </bean> <bean id="myInventoryDao" class="product.InventoryDaoImpl"> <property name="sessionFactory"> <ref bean="mySessionFactory2"/> </property> </bean> <bean id="myProductServiceTarget" class="product.ProductServiceImpl"> <property name="productDao"> <ref bean="myProductDao"/> </property> <property name="inventoryDao"> <ref bean="myInventoryDao"/> </property> </bean> <bean id="myProductService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref bean="myTransactionManager"/> </property> <property name="target"> <ref bean="myProductServiceTarget"/> </property> <property name="transactionAttributes"> <props> <prop key="increasePrice*">PROPAGATION_REQUIRED</prop> <prop key="someOtherBusinessMethod">PROPAGATION_MANDATORY</prop> </props> </property> </bean> </beans> |
无论是HibernateTransactionManager还是JtaTransactionManager允许适当的对Hibernate的在JVM层次的缓存处理-不需要容器-提供特殊的事务查找或者JCA连接器(只要不使用EJB发起事务)。另外,HibernateTransactionManager能输出JDBC连接供通常的JDBC访问代码使用。这样就允许在高层次上的事务划分是混合了Hibernate与JDBC而不要JTA的,只要只是访问一个数据库就可以!
8. 使用Spring管理应用的Bean
一个Spring应用配置定义可以被多种配置实现所加载,从FileSystemXmlApplicationContext和ClassPathXmlApplicationContext到XmlWebApplicationContext。这就允许在各种环境下重用Spring管理的数据访问和业务对象。默认情况下,一个Web应用将有它自己的定义在“WEB-INF/applicationContext.xml”中的根配置。
在任何一个Spring应用中,一个应用配置定义在一个XML格式的文件中用来对应用的所有有关的Bean进行装配,从Hibernate的session factory到自定义的数据访问和业务对象(象上面所有的Bean那样)。他们中的大多数不需要Spring容器知道他们,甚至即使是与其他Bean合作时也一样,因为他们只是简单的JavaBean之间的协作。下面的Bean定义可能是一个Spring Web 的MVC配置中用来访问业务对象的配置的一部分。
<bean id="myProductList" class="product.ProductListController"> <property name="productService"> <ref bean="myProductService"/> </property> </bean> |
Spring的Web控制器经由Bean引用拥有它们需要的所有的业务和数据访问对象,因此它们无需在应用配置中做任何手工的Bean查找。但是当使用Spring管理的Beans用于Struts或者是在EJB实现,或者一个applet中时常常是需要必须手工查找一个Bean的。因此Spring的Bean可以被用在任何地方。也许只是需要是一应用配置的引用,或者经由一个web容器的Servlet配置属性,或者从一个文件中或者类路径的资源中创建它。
ApplicationContext context =WebApplicationContextUtils.getWebApplicationContext(servletContext); ProductService productService = (ProductService) context.getBean("myProductService"); |
ApplicationContext context = new FileSystemXmlApplicationContext("C:/myContext.xml"); ProductService productService = (ProductService) context.getBean("myProductService"); |
ApplicationContext context = new ClassPathXmlApplicationContext("myContext.xml"); ProductService productService = (ProductService) context.getBean("myProductService"); |
9. 容器资源VS本地资源
Spring的资源管理允许简单地在一个JNDI SessionFactory和一个本地SessionFactory间做选择,同样允许在一个JNDI DataSource与本地DataSource间做选择,而无需改变应用的一行代码。在容器中保存资源定义还是在应用本地保存,主要是一个事务策略方面的事。比较一个Spring定义的本地SessionFactory与一个手工注册的JNDI SessionFactory没有任何益处。如果经由Hibernate的JCA连接器注册,才会有加入JTA事务的明显益处,特别是对EJB。
一个重要的Spring事务提供的好处是它不与任何容器绑定。定义包括JTA在内的策略,它都可以独立工作或者在一个试验环境中工作。特别是对典型的一个数据库的事务来说,对于JTA这是一个非常轻量的和强大的选择。当使用本地EJB SLSB的事务时,你将同时依赖EJB容器和JTA-即使你只是访问一个数据库,即使只是使用SLSBs经由CMT来声明事务。选择使用 JTA编程也需要一个J2EE环境。
就JTA自身和JNDI数据源来说JTA不只是包括容器依赖。对于不使用Spring的JTA驱动的Hibernate事务,你必须使用HibernateJCA连接器或者在合适的JVM缓冲层专门写Hibernate的事务代码配置JTA事务。在只访问一个数据库的情况下,Spring驱动的事务可以与一个本地定义的Hibernate的SessionFactory配合良好,就如同与一个本地JDBC数据源相配合一样。因此当面对分布的事务需求时,你只需要转换为Spring的JTA事务策略即可。
要注意一个JCA连接器需要特别的容器的部署步骤,并且显然首先得支持JCA。这比使用本地资源定义和Spring驱动事务来部署一个简单的Web应用有更多的争议。而且你常常需要企业版本的容器支持,象WebLogic Express就不提供JCA。一个只用一个数据库的使用本地资源和事务的Spring应用可以在任何J2EE的Web容器中工作,Web容器不必支持JTA, JCA和EJB,如:Tomcat, Resin甚至最小的Jetty。另外,这样一个中间层就可以很容易地在桌面应用或者在测试套件中被重用。
所有考虑过的事情包括:如果你不使用EJB,坚持使用本地SessionFactory,使用SpringHibernateTransactionManager或者JtaTransactionManager,你将获得包括适当处理的JVM层的缓存和分布事务的所有益处,而无需引起任何关于容器部署的争论。经由JCA连接器的一个Hibernate的SessionFactory的JNDI注册只是在使用EJB的情况中才会有明显的附加值。
10. Skeletons和例子
配置使用Spring和HIbernate的一个J2EE的Web应用的注释和细节最好去看看在Spring Framework的例子中的“典型的Web应用”Skeletons,它给出了适合于JDBC 和 Hibernate应用的多种数据源及事务管理的配置项,仔细看一下事务拦截器的配置,它也同样向你展示了如何配置AOP拦截器。
在Spring的1.0 M2版中,例子Petclinic提供了JDBC和Hibernate的DAO实现和应用配置的选择。Petclinic
可以作为一个可工作的简单应用说明如何在一个Spring web 应用中使用Hibernate,同样也包括根据不同的事务策略来声明事务划分。