Spring基于代理的AOP框架很适合处理一般的中间件和特定应用的问题。 然而,有时,更强大的AOP方案也是需要的,例如,如果我们需要给一个类增加 额外的字段, 或者通知(Advise)一个不是由Spring IoC容器创建的细粒度的对象。
We recommend the use of AspectJ in such cases. Accordingly, as of version 1.1, Spring provides a powerful integration with AspectJ.
因为Spring很好的整合了AspectJ,所以这种情况下我们推荐使用AspectJ。
Spring/AspectJ集成最重要的部分是允许Spring的依赖注射来配置AspectJ的aspect。 这给方面(aspect)和对象(object)带来了类似的好处。例如:
Aspect不需要使用特定的配置机制;它们可以使用和整个应用相同的、一致的方法来配置。
•Aspect可以依赖应用对象。例如,一个权限aspect可以依赖一个权限管理对象,稍后我们可以看到例子。
可以通过相关的Spring上下文(Context)获得一个aspect的引用(reference),这可以允许动态的aspect配置。
AspectJ 的方式可以通过设值方式(setter)的注入Java Bean的属性,也可以通过实现Spring的生命周期接口来实现,例如实现 BeanFactoryAware。
值得注意的是,AspectJ不能使用构造器注入方式和方法注入方式, 这是由于aspect没有类似对象的构造器那样可以调用方法的原因。大多数情况下,AspectJ的aspect是单例的,每个类装载器一个实例, 这个单一的实例负责通知(advising)多个对象实例。
Spring IoC容器不能实例化aspect,因为,aspect没有可调用的构造器。 但是,它可以使用AspectJ为所有aspect定义的静态方法aspectOf()获得一个 aspect的引用,并且,能够把依赖注入aspect。
考虑一个关于安全的aspect,它依赖于一个安全管理对象。这个aspect应用于 Account类中实例变量balance的所有的值变化。(我们不能够以同样方法使 用Spring AOP做到这点。)
AspectJ中aspect的代码(Spring/AspectJ的一个例子),显示如下。注意, 对SecurityManager接口的依赖在Java Bean的属性中说明。
public aspect BalanceChangeSecurityAspect { private SecurityManager securityManager; public void setSecurityManager(SecurityManager securityManager) { this.securityManager = securityManager; } private pointcut balanceChanged() : set(int Account.balance); before() : balanceChanged() { this.securityManager.checkAuthorizedToModify(); } }
我们配置这个aspect的方式和普通类是一样的。注意,我们设置属性引用 的方式是完全相同的。注意,我们必须使用factory-method属性来指定使用 aspectOf()静态方法”创建”这个aspect。实际上,是定位(locating), 而非创建(creating)这个aspect,但Spring容器不关心这些。
<bean id="securityAspect" class="org.springframework.samples.aspectj.bank.BalanceChangeSecurityAspect" factory-method="aspectOf" > <property name="securityManager"> <ref local="securityManager"/> </property> </bean>
我们不需要在Spring配置中做任何事情去定位(target)这个aspect。 在这个aspect的控制适用地方的AspectJ代码里,包含了切入点(pointcut) 的信息。这样它甚至能够适用于不被Spring IoC容器管理的对象。
在Spring将来的版本中,我们计划支持在Spring XML或者其它的Bean定义 文件中使用AspectJ的切入点表达式来定位Spring通知(advice)。这将允许 AspectJ切入点模型的某些能力被应用在Spring基于代理的AOP框架。这将可以在 纯Java中工作,并且不需要AspectJ编译器。仅仅AspectJ切入点相关的方法调用的 子集是可用的。
这个特性计划在Spring 1.2版本中提供,它有赖于AspectJ的增强。
这个特性替代了我们先前为Spring创建一个切入点表达式语言的计划。
在将来的Spring版本中(可能是1.2),我们将加入一些Spring的服务作为 AspectJ的aspect,像声明性事务服务。这将允许AspectJ的用户使用这些服务, 而无需潜在地依赖于Spring的AOP框架,甚至,不必依赖Spring IoC容器。
AspectJ的用户可能比Spring用户对这个特性更感兴趣。
Spring提供了一致的事务管理抽象。这个抽象是Spring最重要的抽象之一, 它有如下的优点:
为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、iBATIS数据库层 和JDO
提供比大多数事务API更简单的,易于使用的编程式事务管理API
整合Spring数据访问抽象
支持Spring声明式事务管理
传统上,J2EE开发者有两个事务管理的选择: 全局事务或 局部事务。全局事务由应用服务器管理,使用JTA。局部 事务是和资源相关的:例如,一个和JDBC连接关联的事务。这个选择有深刻的含义。 全局事务可以用于多个事务性的资源(需要指出的是多数应用使用单一事务性 的资源)。使用局部事务,应用服务器不需要参与事务管理,并且不能帮助确保 跨越多个资源的事务的正确性。
全局事务有一个显著的不利方面,代码需要使用JTA:一个笨重的API(部分是 因为它的异常模型)。此外,JTA的UserTransaction通常需 要从JNDI获得,这意味着我为了JTA需要同时使用JNDI和JTA。 显然全部使用全局事务限制了应用代码的重用性,因为JTA通常只在应用服 务器的环境中才能使用。
使用全局事务的比较好的方法是通过EJB的CMT (容器管理的事务): 声明式事务管理的一种形式(区别于编程式事务管理 )。EJB的CMT不需要任何和事务相关的JNDI查找,虽然使用EJB本身 肯定需要使用JNDI。它消除大多数——不是全部——书写Java代码控制事务的需求。 显著的缺点是CMT绑定在JTA和应用服务器环境上,并且只有我们选择 使用EJB实现业务逻辑,或者至少处于一个事务化EJB的外观(Facade)后 才能使用它。EJB有如此多的诟病,当存在其它声明式事务管理时, EJB不是一个吸引人的建议。
局部事务容易使用,但也有明显的缺点:它们不能用于多个事务性资 源,并且趋向侵入的编程模型。例如,使用JDBC连接事务管理的代码不能用于 全局的JTA事务中。
Spring解决了这些问题。它使应用开发者能够使用在任何环境 下使用一致的编程模型。你可以只写一次你的代码,这在不同环境 下的不同事务管理策略中很有益处。Spring同时提供声明式和编程式事务管理。
使用编程式事务管理,开发者直接使用Spring事务抽象,这个抽象可以使用在任何 底层事务基础之上。使用首选的声明式模型,开发者通常书写很少的事务相关代 码,因此不依赖Spring或任何其他事务API。
Spring事务抽象的关键是事务策略的概念。
这个概念由 org.springframework.transaction.PlatformTransactionManager 接口体现,如下:
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }
这首先是一个SPI接口,虽然它也可以在编码中使用。注意按照Spring的哲学, 这是一个接口。因而如果需要它可以很容易地被模拟和 桩化。它也没有和一个查找策略如JNDI捆绑在一起:PlatformTransactionManager 的实现定义和其他Spring IoC容器中的对象一样。这个好处使得即使使用JTA,也 是一个很有价值的抽象:事务代码可以比直接使用JTA更加容易测试。
继续Spring哲学,TransactionException是unchecked的。 低层的事务失败几乎都是致命。很少情况下应用程序代码可以从它们 中恢复,不过应用开发者依然可以捕获并处理 TransactionException。
getTransaction()根据一个类型为 TransactionDefinition的参数返回一个 TransactionStatus对象。返回的 TransactionStatus对象可能代表一个新的或已经存在的事 务(如果在当前调用堆栈有一个符合条件的事务)。
如同J2EE事务上下文一样,一个TransactionStatus也是和执 行的线程关联的。
TransactionDefinition接口指定:
事务隔离:当前事务和其它事务的隔离的程度。 例如,这个事务能否看到其他事务未提交的写数据?
事务传播:通常在一个事务中执行的 所有代码都会在这个事务中运行。但是,如果一个事务上下文已经存在, 有几个选项可以指定一个事务性方法的执行行为:例如,简单地在现有的 事务中运行(大多数情况);或者挂起现有事务,创建一个新的事务。 Spring提供EJB CMT中熟悉的事务传播选项。
事务超时: 事务在超时前能运行多 久(自动被底层的事务基础设施回滚)。
只读状态: 只读事务不修改任何数 据。只读事务在某些情况下(例如当使用Hibernate时)可可是一种非常有用的优化。
这些设置反映了标准概念。如果需要,请查阅讨论事务隔离层次和其他核心事 务概念的资源:理解这些概念在使用Spring和其他事务管理解决方案时是非常关键的。
TransactionStatus接口为处理事务的代码提供一个简单 的控制事务执行和查询事务状态的方法。这个概念应该是熟悉的,因为它们在所 有的事务API中是相同的:
public interface TransactionStatus { boolean isNewTransaction(); void setRollbackOnly(); boolean isRollbackOnly(); }
但是使用Spring事务管理时,定义 PlatformTransactionManager的实现是基本方式。在好的Spring 风格中,这个重要定义使用IoC实现。
PlatformTransactionManager实现通常需要了解它们工作 的环境:JDBC、JTA、Hibernate等等。
下面来自Spring范例jPetstore中的 dataAccessContext-local.xml,它展示了一个局部 PlatformTransactionManager实现是如何定义的。它将和JDBC一起工作。
我们必须定义JDBC数据源,然后使用DataSourceTransactionManager,提供给 它的一个数据源引用。
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"><value>${jdbc.driverClassName}</value></property> <property name="url"><value>${jdbc.url}</value></property> <property name="username"><value>${jdbc.username}</value></property> <property name="password"><value>${jdbc.password}</value></property> </bean>
PlatformTransactionManager定义如下:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource"><ref local="dataSource"/></property> </bean>
如果我们使用JTA,如同范例中dataAccessContext-jta.xml, 我们需要使用通过JNDI获得的容器数据源,和一个JtaTransactionManager实 现。JtaTransactionManager不需要知道数据源,或任何其他特定资源,因为它将 使用容器的全局事务管理。
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"><value>jdbc/jpetstore</value></property> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
我们可以很容易地使用Hibernate局部事务,如同下面Spring的PetClinic 示例应用中的例子的一样。
在这种情况下,我们需要定义一个Hibernate的LocalSessionFactory,应用程 序将使用它获得Hibernate的会话。
数据源bean定义和上面例子类似,这里不再罗列(如果这是容器数据源,它应该是非事务的,因为Spring会管理事务, 而不是容器)。
这种情况下,“transactionManager” bean的类型是HibernateTransactionManager。 和DataSourceTransactionManager拥有一个数据源的引用一样, HibernateTransactionManager需要一个SessionFactory的引用。
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="dataSource"><ref local="dataSource"/></property> <property name="mappingResources"> <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">${hibernate.dialect}</prop> </props> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"><ref local="sessionFactory"/></property> </bean>
使用Hibernate和JTA事务,我们可以简单地使用JtaTransactionManager, 就象JDBC或任何其他资源策略一样。
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
注意任何资源的JTA配置都是这样的,因为它们都是全局事务。
在所有这些情况下,应用程序代码不需要任何更改。我们可以仅仅更改配置 来更改管理事务的方式,即使这些更改意味这从局部事务转换到全局事务或者相反 的转换。如果不使用全局事务,你需要采用一个特定的编码规范。幸运的是它非常简单 。你需要以一个特殊的方式获得连接资源或者会话资源,允许相关的 PlatformTransactionManager实现跟踪连接的使用,并且当需要时应用事务管理。
例如,如果使用JDBC,你不应该调用一个数据源的 getConnection()方法,而必须使用Spring的 org.springframework.jdbc.datasource.DataSourceUtils类,如下:
Connection conn = DataSourceUtils.getConnection(dataSource);
这将提供额外的好处,任何SQLException都被Spring的 CannotGetJdbcConnectionException封装起来,它属于Spring的unchecked 的DataAccessException的类层次。这给你比 简单地从SQLException获得更多的信息,并且确保跨数据 库,甚至跨越不同持久化技术的可移植性。
没有Spring事务管理的情况下,这也能很好地工作,因此无论使用 Spring事务管理与否,你都可以使用它。
当然,一旦你使用Spring的JDBC支持或Hibernate支持,你将不想使用 DataSourceUtils或其他帮助类,因为与直接使用相关API相比,你将更乐意使用Spring的抽象。 例如,如果你使用Spring的JdbcTemplate或jdbc.object包来简化使用JDBC, 正确的数据库连接将自动取得,你不需要书写任何特殊代码。
Spring提供两种方式的编程式事务管理
使用TransactionTemplate
直接使用一个PlatformTransactionManager实现
我们通常推荐使用第一种方式。
第二种方式类似使用JTA UserTransaction API (虽然异常处理少一点麻烦)。
TransactionTemplate采用和其他Spring模板 ,如JdbcTemplate和 HibernateTemplate,一样的方法。它使用回调方法,把应用 程序代码从处理取得和释放资源中解脱出来(不再有try/catch/finally)。如同 其他模板,TransactionTemplate是线程安全的。
必须在事务上下文中执行的应用代码看起来像这样,注意使用 TransactionCallback可以返回一个值:
Object result = tt.execute(new TransactionCallback() { public Object doInTransaction(TransactionStatus status) { updateOperation1(); return resultOfUpdateOperation2(); } });
如果没有返回值,使用TransactionCallbackWithoutResult, 如下:
tt.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { updateOperation1(); updateOperation2(); } });
回调中的代码可以调用TransactionStatus对象的 setRollbackOnly()方法来回滚事务。
想要使用TransactionTemplate的应用类必须能访问一 个PlatformTransactionManager:通常通过一个JavaBean属 性或构造函数参数暴露出来。
使用模拟或桩化的PlatformTransactionManager,单元测试 这些类很简单。没有JNDI查找和静态魔术代码:它只是一个简单的接口。和平常一样, 你可以使用Spring简化单元测试。
你也可以使用 org.springframework.transaction.PlatformTransactionManager 直接管理事务。简单地通过一个bean引用给你的bean传递一个你使用的 PlatformTransactionManager实现。然后, 使用TransactionDefinition和 TransactionStatus对象就可以发起事务,回滚和提交。
DefaultTransactionDefinition def = new DefaultTransactionDefinition() def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransactionDefinition(def); try { // execute your business logic here } catch (MyException ex) { transactionManager.rollback(status); throw ex; } transactionManager.commit(status);
Spring也提供了声明式事务管理。这是通过Spring AOP实现的。
大多数Spring用户选择声明式事务管理。这是最少影响应用代码的选择, 因而这是和非侵入性的轻量级容器的观念是一致的。从考虑EJB CMT和Spring声明式事务管理的相似以及不同之处出发是很有益的。 它们的基本方法是相似的:都可以指定事务管理到单独的方法;如果需要可以在事务上 下文调用setRollbackOnly()方法。不同之处如下:
不象EJB CMT绑定在JTA上,Spring声明式事务管理可以在任何环境下使用。 只需更改配置文件,它就可以和JDBC、JDO、Hibernate或其他的事务机制一起工作
Spring可以使声明式事务管理应用到普通Java对象,不仅仅是特殊的类,如EJB
Spring提供声明式回滚规则:EJB没有对应的特性, 我们将在下面讨论这个特性。回滚可以声明式控制,不仅仅是编程式的
Spring允许你通过AOP定制事务行为。例如,如果需要,你可以在事务 回滚中插入定制的行为。你也可以增加任意的通知,就象事务通知一样。使用 EJB CMT,除了使用setRollbackOnly(),你没有办法能 够影响容器的事务管理
Spring不提供高端应用服务器提供的跨越远程调用的事务上下文传播。如 果你需要这些特性,我们推荐你使用EJB。然而,不要轻易使用这些特性。通常我 们并不希望事务跨越远程调用
回滚规则的概念是很重要的:它们使得我们可以指定哪些异常应该发起自 动回滚。我们在配置文件中,而不是Java代码中,以声明的方式指定。因此,虽然我们仍 然可以编程调用TransactionStatus对象的 setRollbackOnly()方法来回滚当前事务,多数时候我们可以 指定规则,如MyApplicationException应该导致回滚。 这有显著的优点,业务对象不需要依赖事务基础设施。例如,它们通常不需要引 入任何Spring API,事务或其他任何东西。
EJB的默认行为是遇到系统异常(通常是运行时异常), EJB容器自动回滚事务。EJB CMT遇到应用程序异常 (除了java.rmi.RemoteException外的checked异常)时不 会自动回滚事务。虽然Spring声明式事务管理沿用EJB的约定(遇到unchecked 异常自动回滚事务),但是这是可以定制的。
按照我们的测试,Spring声明式事务管理的性能要胜过EJB CMT。
通常通过TransactionProxyFactoryBean设置Spring事务代理。我们需 要一个目标对象包装在事务代理中。这个目标对象一般是一个普通Java对象的bean。当我 们定义TransactionProxyFactoryBean时,必须提供一个相关的 PlatformTransactionManager的引用和事务属性。 事务属性含有上面描述的事务定义。
<bean id="petStore" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="target"><ref bean="petStoreTarget"/></property> <property name="transactionAttributes"> <props> <prop key="insert*">PROPAGATION_REQUIRED,-MyCheckedException</prop> <prop key="update*">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_REQUIRED,readOnly</prop> </props> </property> </bean>
事务代理会实现目标对象的接口:这里是id为petStoreTarget的bean。(使用 CGLIB也可以实现具体类的代理。只要设置proxyTargetClass属性为true就可以。 如果目标对象没有实现任何接口,这将自动设置该属性为true。通常,我们希望面向接口而不是 类编程。)使用proxyInterfaces属性来限定事务代理来代 理指定接口也是可以的(一般来说是个好想法)。也可以通过从 org.springframework.aop.framework.ProxyConfig继承或所有AOP代理工厂共享 的属性来定制TransactionProxyFactoryBean的行为。
这里的transactionAttributes属性定义在 org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource 中的属性格式来设置。这个包括通配符的方法名称映射是很直观的。注意 insert*的映射的值包括回滚规则。添加的-MyCheckedException 指定如果方法抛出MyCheckedException或它的子类,事务将 会自动回滚。可以用逗号分隔定义多个回滚规则。-前缀强制回滚,+前缀指定提 交(这允许即使抛出unchecked异常时也可以提交事务,当然你自己要明白自己 在做什么)。
TransactionProxyFactoryBean允许你通过 “preInterceptors”和“postInterceptors”属性设置“前”或“后”通知来提供额外的 拦截行为。可以设置任意数量的“前”和“后”通知,它们的类型可以是 Advisor(可以包含一个切入点), MethodInterceptor或被当前Spring配置支持的通知类型 (例如ThrowAdvice, AfterReturningtAdvice或BeforeAdvice, 这些都是默认支持的)。这些通知必须支持实例共享模式。如果你需要高级AOP特 性来使用事务,如有状态的maxin,那最好使用通用的 org.springframework.aop.framework.ProxyFactoryBean, 而不是TransactionProxyFactoryBean实用代理创建者。
也可以设置自动代理:配置AOP框架,不需要单独的代理定义类就可以生成类的 代理。
更多信息和实例请参阅AOP章节。
无论你是是否是AOP专家,都可以更有效地使用Spring的 声明式事务管理。但是,如果你想成为Spring AOP的高级用户,你会发现整合声明 式事务管理和强大的AOP性能是非常容易的。TransactionProxyFactoryBean非常有用,当事 务代理包装对象时,它使你可以完全控制代理。如果你需要用一致的方式(例如,一个 样板文件,“使所有的方法事务化”)包装大量的bean,使用一个 BeanFactoryPostProcessor的一个实现, BeanNameAutoProxyCreator,可以提供另外一种方法, 这个方法在这种简单的情况下更加简单。
重述一下,一旦ApplicationContext读完它的初始化信息,它将初始化所有实 现BeanPostProcessor接口的bean,并且让它们后处理 ApplicationContext中所有其他的bean。所以使用这种机制,一个正 确配置的BeanNameAutoProxyCreator可以用来后处 理所有ApplicationContext中所有其他的bean(通过名称来识别),并且把它 们用事务代理包装起来。真正生成的事务代理和使用 TransactionProxyFactoryBean生成的基本一致,这里不再 讨论。
让我们看下面的配置示例:
<!-- Transaction Interceptor set up to do PROPOGATION_REQUIRED on all methods --> <bean id="matchAllWithPropReq" class="org.springframework.transaction.interceptor.MatchAlwaysTransactionAttributeSource"> <property name="transactionAttribute"><value>PROPAGATION_REQUIRED</value></property> </bean> <bean id="matchAllTxInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="transactionAttributeSource"><ref bean="matchAllWithPropReq"/></property> </bean> <!-- One BeanNameAutoProxyCreator handles all beans where we want all methods to use PROPOGATION_REQUIRED --> <bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="interceptorNames"> <list> <idref local="matchAllTxInterceptor"/> <idref bean="hibInterceptor"/> </list> </property> <property name="beanNames"> <list> <idref local="core-services-applicationControllerSevice"/> <idref local="core-services-deviceService"/> <idref local="core-services-authenticationService"/> <idref local="core-services-packagingMessageHandler"/> <idref local="core-services-sendEmail"/> <idref local="core-services-userService"/> </list> </property> </bean>
假设我们在ApplicationContext中已经有一个TransactionManager的实例 ,我们首先要做的使创建一个 TransactionInterceptor实例。 根据通过属性传递的TransactionAttributeSource接口的一个实现, ransactionInterceptor决定哪个 方法被拦截。这个例子中,我们希望处理匹配 所有方法这种最简单的情况。这个不是最有效的方式,但设置非常迅速,因为 我可以使用预先定义的匹配所有方法的 MatchAlwaysTransactionAttributeSource类。如果我 们需要特定的方式,可以使用 MethodMapTransactionAttributeSource, NameMatchTransactionAttributeSource或 AttributesTransactionAttributeSource。
现在我们已经有了事务拦截器,我们只需把它交给我们定义的 BeanNameAutoProxyCreator实例中,这样AppliactonContext中 定义的6个bean以同样的方式被封装。你可以看到,这 比用TransactionProxyFactoryBean以一种方式单独封装6个bean简洁很 多。封装第7个bean只需添加一行配置。
你也许注意到可以应用多个拦截器。在这个例子中,我们还应用了一个 前面定义的HibernateInterceptor (bean id=hibInterceptor),它为我们管理 Hibernare的会话。
有一件事值得注意,就是在TransactionProxyFactoryBean, 和BeanNameAutoProxyCreator切换时bean的命名。 第一种情况,你只需给你想要包装的bean一个类似myServiceTarget的id, 给代理对象一个类似myService的id,然后所有代理对象的用户只需引用代理对象, 如myService(这些是通用命名规范, 要点是目标对象要有和代理对象不同的名称,并且它们都要在ApplicationContext中可用)。然而, 使用BeanNameAutoProxyCreator时, 你得命名目标对象为myService。 然后当BeanNameAutoProxyCreator后处理目标对象 并生成代理时,它使得代理以和原始对象的名称被插入到 ApplicationContext中。从这一点看,只有代理(含有被包装的对象)在ApplicationContext中可用。
如果你只有很少的事务操作,使用编程式事务管理才是个好主意。例如, 如果你有一个WEB应用需要为某些更新操作提供事务,你可能不想用Spring或其 他技术设置一个事务代理。使用 TransactionTemplate可能是个很好的方法。
另一方面,如果你的应用有大量的事务操作,声明式事务管理就很有价值。它使得事务管理从业务逻辑分离, 并且Spring中配置也不困难。使用Spring,而不是EJB CMT,声明式事务管理配置的成本极大地降低。
Spring的事务管理能力--尤其声明式事务管理--极大地改变了J2EE应用程序需要 应用服务器的传统想法。
尤其,你不需要应用服务器仅仅为了通过EJB声明事务。事实上,即使你拥 有强大JTA支持的应用服务器,你也可以决定使用Spring声明式事务管理提供比 EJB CMT更强大更高效的编程模型。
只有需要支持多个事务资源时,你才需要应用服务器的JTA支持。许多应用没有 这个需求。例如许多高端应用使用单一的,具有高度扩展性的数据库,如Oracle 9i RAC。
当然也许你需要应用服务器的其它功能,如JMS和JCA。但是如果你只需使用 JTA,你可以考虑开源的JTA实现,如JOTM(Spring整合了JOTM)。但是, 2004年早期,高端的应用服务器提供更健壮的XA资源支持。
最重要一点,使用Spring,你可以选择何时将你的应用迁移到完整 应用服务器。使用EJB CMT或JTA都必须书写代码使用局部事务, 例如JDBC连接的事务,如果以前需要全局的容器管理的事务,还要面临着繁重 的改写代码的过程,这些日子一去不复返了。使用Spring只有配置需要改变,你的代码 不需要修改。
开发着需要按照需求仔细的使用正确的 PlatformTransactionManager实现。
理解Spring事务抽象时如何和JTA全局事务一起工作是非常重要的。使用得当, 就不会有任何冲突:Spring仅仅提供一个简单的,可以移植的抽象层。
如果你使用全局事务,你必须为你的所有事务操作使用Spring的 org.springframework.transaction.jta.JtaTransactionManager。 否则Spring将试图在象容器数据源这样的资源上执行局部事务。这样的局部事务没有任何 意义,好的应用服务器会把这作为一个错误。
attribute 或者叫annotation的扩充。
是对程序元素:通常为类和/或方法的举例来说,我们可以象下面一样给一个类添加元数据:
/** * Normal comments * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute() */ public class PetStoreImpl implements PetStoreFacade, OrderService {
我们也可以添加元数据到一个方法上:
/** * Normal comments * @@org.springframework.transaction.interceptor.RuleBasedTransactionAttribute () * @@org.springframework.transaction.interceptor.RollbackRuleAttribute (Exception.class) * @@org.springframework.transaction.interceptor.NoRollbackRuleAttribute ("ServletException") */ public void echoException(Exception ex) throws Exception { .... }
这两个例子都使用了Jakarta Commons Attributes的格式。
源代码级的元数据随着Microsoft的.NET平台的发布被介绍给大众,它使用了源代码级的attribute 来控制事务,缓冲池(pooling)和一些其他的行为。
这种方法的价值已经被J2EE社区的人们认识到了。举例来说,跟EJB中清一色使用的传统的XML部署描述文 件比起来它要简单很多。XML描述文件适合于把一些东西从程序源代码中提取出来,一些重要的企业级设 定——特别是事务特性——本来属于程序代码。并不像EJB规范中设想的那样,调整一个方法的事务特性根本 没有什么意义。
虽然元数据attribute主要用于框架的基础架构来描述应用程序的类需要的业务, 但是也可以在运行时查询元数据attribute。这是与XDoclet这样的解决方案的关键区别,XDoclet 主要把元数据作为生成代码的一种方式,比如生成EJB类。
这一段包括了几个解决方案:
JSR-175:标准的Java元数据实现,在Java 1.5中提供。 但是我们现在就需要一个解决方案,通常情况下可能还需要一个外观(facade)。
XDoclet:成熟的解决方案,主要用于代码生成
其它不同的开源attribute实现,在JSR-175的发布 悬而未决的情况下,它们当中的Commons Attributes看来是最有前途的。所有的这些实现都需要一 个特定的前编译或后编译的步骤。
为了与它提供的其他重要概念的抽象相一致,Spring提供了一个对元数据实现的外观(facade), 以org.springframework.metadata.Attributes这个接口的形式来表示。
这样一个外观很有价值,因为下面几个原因:
目前还没有一个标准的元数据解决方案。 Java 1.5版本会提供一个,但是在Spring1.0版本的时候, Java 1.5仍是beta版本。而且,至少两年内还是需要对1.3和1.4版本的应用程序提供元数据支持。 现在Spring打算提供一些可以工作的解决方案: 在一个重要的环境下等待1.5,并不是个好的选择.
目前的元数据API,例如Commons Attributes(被Spring 1.0使用), 测试起来很困难。Spring提供了一个简单的更容易模拟的元数据接口。
即使当Java 1.5在语言级别提供了对元数据的支持时,提供了一个如此的抽象仍然是有价值的:
JSR-175的元数据是静态的。它是在编译时与某一个类关联,而在部署环境下是不可改变的。 这里会需要多层次的元数据,以支持在部署时重载某些attribute的值--举例来说, 在一个XML文件中定义用于覆盖的attribute。
JSR-175的元数据是通过Java反射API返回的。这使得在测试时无法模拟元数据。 Spring提供了一个简单的接口来允许这种模拟。
虽然Spring在Java 1.5达到它的GA版本之前将支持JSR-175,但仍会继续提供一个attribute抽象API。
Spring的Attributes接口看起来是这样的:
public interface Attributes { Collection getAttributes(Class targetClass); Collection getAttributes(Class targetClass, Class filter); Collection getAttributes(Method targetMethod); Collection getAttributes(Method targetMethod, Class filter); Collection getAttributes(Field targetField); Collection getAttributes(Field targetField, Class filter); }
这是个最普通不过的命名者接口。JSR-175能提供更多的功能,比如定义在方法参数上的attributes。 在1.0版本时,Spring目的在于提供元数据的一个子集,使得能象EJB或.NET一样提供有效的声明式企业级服务。 1.0版本以后,Spring将提供更多的元数据方法。
注意到该接口像.NET一样提供了Object类型的attibute。 这使得它区别于一些仅提供String类的attribute的attribute系统, 比如Nanning Aspects和JBoss 4(在DR2版本时)。支持Object类型的attribute有一个显著的优点。 它使attribute含有类层次,还可以使attribute能够灵活的根据它们的配置参数起作用。
对于大多数attribute提供者来说,attribute类的配置是通过构造函数参数或JavaBean的属性完成的。Commons Attributes同时支持这两种方式。
同所有的Spring抽象API一样,Attributes是一个接口。 这使得在单元测试中模拟attribute的实现变得容易起来。
虽然为其他元数据提供者来说,提供org.springframework.metadata.Attributes 接口的实现很简单,但是目前Spring只是直接支持Jakarta Commons Attributes。
Commons Attributes 2.0 (http://jakarta.apache.org/commons/sandbox/attributes/) 是一个功能很强的attribute解决方案。它支持通过构造函数参数和JavaBean属性来配置attribute, 也提供了更好的attribute定义的文档。(对JavaBean属性的支持是在Spring team的要求下添加的。)
我们已经看到了两个Commons Attributes的attribute定义的例子。通常,我们需要解释一下:
Attribute类的名称。 这可能是一个FQN,就像上面的那样。如果相关的attribute类已经被导入, 就不需要FQN了。你也可以在attibute编译器的设置中指定attribute的包名。
任何必须的参数化,可以通过构造函数参数或者JavaBean属性完成。
Bean的属性如下:
/** * @@MyAttribute(myBooleanJavaBeanProperty=true) */
把构造函数参数和JavaBean属性结合在一起也是可以的(就像在Spring IoC中一样)。
因为,并不象Java 1.5中的attribute一样,Common Attributes没有和Java语言本身结合起来, 因此需要运行一个特定的attribute编译的步骤作为整个构建过程的一部分。
为了在整个构建过程中运行Commmons Attributes,你需要做以下的事情。
1.复制一些必要的jar包到$ANT_HOME/lib目录下。 有四个必须的jar包,它们包含在Spring的发行包里:
Commons Attributes编译器的jar包和API的jar包。
来自于XDoclet的xjavadoc.jar
来自于Jakarta Commons的commons-collections.jar
2.把Commons Attributes的ant任务导入到你的项目构建脚本中去,如下:
<taskdef resource="org/apache/commons/attributes/anttasks.properties"/>
3.接下来,定义一个attribute编译任务,它将使用Commons Attributes的attribute-compiler任务 来“编译”源代码中的attribute。这个过程将生成额外的代码至destdir属性指定的位置。 在这里我们使用了一个临时目录:
<target name="compileAttributes" > <attribute-compiler destdir="${commons.attributes.tempdir}" > <fileset dir="${src.dir}" includes="**/*.java"/> </attribute-compiler> </target>
运行javac命令编译源代码的编译目标任务应该依赖于attribute编译任务,还需要编译attribute时生成至 目标临时目录的源代码。如果在attribute定义中有语法错误,通常都会被attribute编译器捕获到。 但是,如果attribute定义在语法上似是而非,却使用了一些非法的类型或类名, 编译所生成的attribute类可能会失败。在这种情况下,你可以看看所生成的类来确定错误的原因。
Commons Attributes也提供对Maven的支持。请参考Commons Attributes的文档得到进一步的信息。虽然attribute编译的过程可能看起来复杂,实际上是一次性的耗费。一旦被创建后,attribute的编译是递增式的, 所以通常它不会减慢整个构建过程。一旦编译过程完成后, 你可能会发现本章中描述的attribute的使用将节省在其他方面的时间。
如果需要attribute的索引支持(目前只在Spring的以attribute为目标的web控制器中需要,下面会讨论到), 你需要一个额外的步骤,执行在包含编译后的类的jar文件上。在这步可选的步骤中, Commons Attributes将生成一个所有在你源代码中定义的attribute的索引,以便在运行时进行有效的查找。 该步骤如下:
<attribute-indexer jarFile="myCompiledSources.jar"> <classpath refid="master-classpath"/> </attribute-indexer>可以到Spring jPetStore例程下的attributes目录下察看关于该构建过程的例子。 你可以使用它里面的构建脚本,并修改该脚本以适应你自己的项目。
如果你的单元测试依赖于attribute,尽量使它依赖于Spring对于Attribute的抽象,而不是Commons Attributes。 这不仅仅为了更好的移植性——举例来说,你的测试用例将来仍可以工作如果你转换至Java 1.5的attributes—— 它也简化了测试。Commons Attributes是静态的API,而Spring提供的是一个容易模拟的元数据接口。
元数据attributes最重要的用处是和Spring AOP的联合。 提供类似于.NET风格的编程模式,声明式的服务会被自动提供给声明了元数据attribute的应用对象。 这样的元数据attribute可以像在声明式事务管理一样被框架直接支持,也可以是自定义的.
这就是AOP和元数据attribute配合使用的优势所在。
基于Spring AOP自动代理功能实现。配置可能象这样:
<bean id="autoproxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> </bean> <bean id="transactionAttributeSource" class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource" autowire="constructor"> </bean> <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor" autowire="byType"> </bean> <bean id="transactionAdvisor" class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor" autowire="constructor" > </bean> <bean id="attributes" class="org.springframework.metadata.commons.CommonsAttributes" />
这里的基本概念和AOP章节中关于自动代理的讨论一致。
最重要的bean的定义名称为autoproxy和 transactionAdvisor。要注意bean的实际名称并不重要; 关键是它们的类。
所定义的自动代理bean org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator 将自动通知(“自动代理”)当前工厂类中的所有符合Advisor实现的bean实例, 这个类并不知道attribute,而是依赖符合的Advisors的切入点。这些切入点知道attribute的内容。
因而我们只是需要一个AOP的advisor来提供基于attribute的声明式事务管理。
也可以添加任意的自定义Advisor实现,它们将被自动计算并应用。 (如果有必要的话,你可以使用切入点除了符合自动代理配置的attribute,而且满足一定规则的Advisor。)
最后,attributes bean使用的是Commons Attributes的实现。 也可以用org.springframework.metadata.Attributes的另一个实现代替。
源代码级的attribute的最普通用法是提供了类似.NET的声明式事务管理。一旦定义好上面的bean定义, 你就能定义任意数目的需要声明式事务的应用对象了。只有那些拥有事务attribute的类或方法会被有事务的通知。 除了定义你需要的事务attribute以外,其他什么都不用做。
不象在.NET中那样,你可以在类或方法级别指定事务attribute。 如果指定了类级别的attribute,它的所有方法都会“继承”它。 方法中定义的attribute将完全覆盖类上定义的attribute。
再者,象.NET中一样,你可以通过在类上指定attribute增加缓冲池行为。 Spring能把该行为应用到任何普通Java对象上。你只需在需要缓冲的业务对象上指定一个缓冲池的attribute,如下:
/** * @@org.springframework.aop.framework.autoproxy.target.PoolingAttribute (10) * * @author Rod Johnson */ public class MyClass {
你需要通常的自动代理的基本配置。 然后指定一个支持缓冲池的TargetSourceCreator,如下所示。 由于缓冲池会影响目标对象的创建,我们不能使用一个通常的advice。 注意如果一个类有缓冲池的attribute,即使没有任何advisors应用到该类,缓冲池也会对该类起作用。
<bean id="poolingTargetSourceCreator" class="org.springframework.aop.framework.autoproxy.metadata.AttributesPoolingTargetSourceCreator" autowire="constructor" > </bean>
对应的自动代理bean定义需要指定一系列的“自定义的目标源生成器”,包括支持缓冲池的目标源生成者。 我们可以修改上面的例子来引入这个属性,如下:
<bean id="autoproxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> > <property name="customTargetSourceCreators"> <list> <ref local="poolingTargetSourceCreator" /> </list> </property> </bean>
就像通常在Spring中使用元数据一样,这是一次性的耗费:一旦创建完成后, 在其它业务对象上使用缓冲池是非常容易的。
对于很少需要缓冲池技术的地方,很少需要缓冲大量业务对象,这是就值得斟酌了。 因此这个功能好像也不是经常使用。详细请参考org.springframework.aop.framework.autoproxy包的Javadoc。 除了以最少的代码量使用Commons Pool以外,其它的缓冲池实现也是可以的。
由于自动代理底层架构的灵活性,我们甚至还可以超越.NET元数据attribute的功能。
我们可以定义一些自定义的attribute来提供任何种类的行为。为了达到这个目的,你需要:
定义你的自定义attribute类
定义一个Spring AOP Advisor,自定义attributes的出现的时候触发它的切入点。
把Advisor当作一个bean的定义添加到一个包含普通自动代理基础架构的应用上下文中。
把attribute添加到普通Java对象上。
有几个潜在的领域中你可能想这么做,比如自定义的声明式安全管理,或者可能的缓存。
这是一个功能很强的机制,它能显著减少某些工程中的配置工作。但是,要记住它在底层是依赖AOP的。 你在应用的使用的Advisors越多,运行时配置将会越复杂。 (如果你想查看object上应用了哪些通知, 可以看一下关于org.springframework.aop.framework.Advised的参考。它使你能够检查相关的Advisors。)Spring 1.0中的元数据的另一个主要用法是为简化Spring MVC web配置提供了一个选择。
Spring MVC提供了灵活的处理器映射: 将外来的请求映射到控制器(或其它的处理器)实例上。 通常上处理器映射被配置在相应的Spring DispatcherServlet的xxx-servlet.xml文件中。
把这些配置定义在DispatcherServlet的配置文件中通常是个不错的选择。它提供了最大的灵活性。 特别的是:
Controller实例是显式的被Spring IoC通过XML bean定义来管理。
该映射位于controller的外面, 所以相同controller的实例可以在同一个DispatcherServlet中被赋予不同的映射或者在不同的配置中重用。
Spring MVC能够支持在任何规则上的映射,而不是大多数其它框架中仅有的将请求URL映射到控制器。
然而,对于每个controller来说,我们同时需要一个处理器映射(通常是一个处理器映射的XML bean定义), 和一个控制器自身的一个XML映射描述。
Spring提供了一个基于源代码级attribute的更简单的方法,对于一些简单场景来说是一个吸引人的选择。
这一段描述的方法很适合一些简单的MVC场景。但它牺牲了一些Spring MVC的功能, 比如根据不同的映射使用相同的控制器,把映射基于除了请求的URL之外的规则。在这种方法下,控制器上标记了一个或多个类级的元数据attribute, 每一个attribute指定了它们应该映射的一个URL。
下面的例子展示了这种方法。在每一种情况下,我们都有一个依赖于Cruncher类的业务对象控制器。 和往常一样,这种依赖性将通过依赖注射来解决。 Cruncher必须作为一个bean定义出现在相关的DispatcherServlet的XML文件中,或在一个父上下文中。
我们把一个attribute设置到控制器类上,并指定应该映射的URL。 我们可以通过JavaBean属性或构造函数参数来表达依赖关系。这种映射必须通过自动装配来解决: 那就是说,在上下文中必须只能有一个Crucher类型的业务对象。
/** * Normal comments here * @author Rod Johnson * @@org.springframework.web.servlet.handler.metadata.PathMap("/bar.cgi") */ public class BarController extends AbstractController { private Cruncher cruncher; public void setCruncher(Cruncher cruncher) { this.cruncher = cruncher; } protected ModelAndView handleRequestInternal( HttpServletRequest arg0, HttpServletResponse arg1) throws Exception { System.out.println("Bar Crunching c and d =" + cruncher.concatenate("c", "d")); return new ModelAndView("test"); } }
为了使这个自动映射起作用,我们需要添加下面的配置到相关的xxxx-servlet.xml文件中, 这些配置是用来指定相关attribute的处理器映射的。 这个特殊的处理器映射可以处理任意数量的象上面一样带有attribute的控制器。 Bean的id(“commonsAttributesHandlerMapping”)并不重要。关键是它的类型:
<bean id="commonsAttributesHandlerMapping" class="org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping" />
现在我们并不需要一个象上面例子中的Attributes bean的定义, 因为这个类直接利用Commons Attributes API,而不是通过Spring的元数据抽象。
现在我们不再需要对每一个控制器提供XML配置。控制器被自动映射到指定的URL上。 控制器得益于IoC,使用了Spring的自动装配功能。举例来说, 上述的简单控制器中的“cruncher”属性上的依赖性会自动在当前的web应用中解决. Setter方法和构造函数依赖注射都是可以的,每一个都是零配置。
构造函数注册的一个例子,同时演示了多个URL路径:
/** * Normal comments here * @author Rod Johnson * * @@org.springframework.web.servlet.handler.metadata.PathMap("/foo.cgi") * @@org.springframework.web.servlet.handler.metadata.PathMap("/baz.cgi") */ public class FooController extends AbstractController { private Cruncher cruncher; public FooController(Cruncher cruncher) { this.cruncher = cruncher; } protected ModelAndView handleRequestInternal( HttpServletRequest arg0, HttpServletResponse arg1) throws Exception { return new ModelAndView("test"); } }
这种方法有着下列的好处:
显著的减少配置工作量。每一次我们增加一个控制器时,我们不需要XML的配置。 就像attribute驱动的事务管理一样,一旦有了基础架构后,添加更多的应用类将会很简单。
我们保留了Spring IoC配置控制器的能力。
不过该方法有如下的限制:
这个方法使构建过程更加复杂,不过开销是一次性的。 我们需要一个attribute编译步骤和一个建立attribute的索引步骤。 但是,一旦这些步骤就位后,这将不再是一个问题。
虽然将来会增加对其他attribute提供者的支持,但目前只支持Commons Attributes。
只有“根据类型的自动装配”的依赖注射才支持这样的控制器。但是, 这妨碍这些控制器对Structs Actions(在框架中并没有IoC的支持), 和值得斟酌的WebWork Actions(只有基本的IoC支持,并且IoC逐步受到关注)的领先。
依赖自动魔法的IoC解决方案可能是令人困惑的。
由于根据类型的自动装配意味着必须依赖一种指定类型,如果我们使用AOP就需要小心了。 在通常使用TransactionProxyFactoryBean的情况下,举例来说, 我们对一个业务接口例如Cruncher有两个实现: 一个为原始的普通Java对象的定义,另一个是带有事务的AOP代理。这将无法工作, 因为所有者应用上下文无法确切的解析这种类型依赖。解决方法是使用AOP的自动代理, 建立起自动代理的基本架构,这样就只有一个Crucher的实现, 同时该实现是自动被通知的。 因此这种方法和上面的以attribute为目标声明式服务能很好的一起工作。 因为attribute编译过程必须恰当处理web控制器的定位,所有这就很容易被创建
不象其它的元数据的功能,当前只有Commons Attributes的一种实现: org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping。 这个限制是因为我们不仅需要attribute编译,还需要attribute索引: 根据attribute API来查询所有拥有PathMap attribute的类。虽然将来可能会提供, 但目前在org.springframework.metadata.Attributes的抽象接口上还没有提供索引。 (如果你需要增加另一个attribute实现的支持,并且必须支持索引, 你可以简单的继承AbstractPathMapHandlerMapping, CommonsPathMapHandlerMapping的父类, 用你选择的attribute API来实现其中两个protected的虚方法。)
因此我们在构建过程中需要两个额外的步骤:attribute编译和建立attribute索引。 上面已经展示了attributer索引建立器的使用方法。要注意到目前Commons Attributes仍需要一个Jar文件来建立索引。
如果你开始使用控制器元数据映射方法,你可以在任何一个地方转换至经典的Spring XML映射方法。 所以你不需要拒绝这种选择。正因为这个原因,我发现我经常在开始一个web应用时使用元数据映射.对元数据attribute的其它应用看来正在流行。到2004年3月时, 一个为Spring建立的基于attribute的验证包正在开发中。 当考虑到潜在的多次使用时,attribute解析的一次性开销看起来更加吸引人了.
Spring提供的JDBC抽象框架由core, datasource,object和 support四个不同的包组成。
就和它名字的暗示一样,org.springframework.jdbc.core包里定义了提供核心功能的类。 其中有各种SQLExceptionTranslator和DataFieldMaxValueIncrementer的实现以及一个用于JdbcTemplate的DAO基础类。
org.springframework.jdbc.datasource包里有一个用以简化数据源访问的工具类, 以及各种数据源的简单实现,以被用来在J2EE容器之外不经修改地测试JDBC代码。 这个工具类提供了从JNDI获得连接和可能用到的关闭连接的静态方法。 它支持绑定线程的连接,比如被用于DataSourceTransactionManager。
接着,org.springframework.jdbc.object包里是把关系数据库的查询, 更新和存储过程封装为线程安全并可重用对象的类。 这中方式模拟了JDO,尽管查询返回的对象理所当然的“脱离”了数据库连接。 这个JDBC的高层抽象依赖于org.springframework.jdbc.core包中所实现的底层抽象。
最后在org.springframework.jdbc.support包中你可以找到 SQLException的转换功能和一些工具类。
在JDBC调用中被抛出的异常会被转换成在定义org.springframework.dao包中的异常。 这意味着使用Spring JDBC抽象层的代码不需要实现JDBC或者RDBMS特定的错误处理。 所有的转换后的异常都是unchecked,它允许你捕捉那些你可以恢复的异常, 并将其余的异常传递给调用者。
这是在JDBC核心包中最重要的类。它简化了JDBC的使用, 因为它处理了资源的建立和释放。 它帮助你避免一些常见的错误,比如忘了总要关闭连接。它运行核心的JDBC工作流, 如Statement的建立和执行,而只需要应用程序代码提供SQL和提取结果。这个类执行SQL查询, 更新或者调用存储过程,模拟结果集的迭代以及提取返回参数值。它还捕捉JDBC的异常并将它们转换成 org.springframework.dao包中定义的通用的,能够提供更多信息的异常体系。
使用这个类的代码只需要根据明确定义的一组契约来实现回调接口。 PreparedStatementCreator回调接口创建一个由该类提供的连接所建立的PreparedStatement, 并提供SQL和任何必要的参数。CallableStatementCreateor实现同样的处理, 只是它创建了CallableStatement。RowCallbackHandler接口从数据集的每一行中提取值。
这个类可以直接通过数据源的引用实例化,然后在服务中使用, 也可以在ApplicationContext中产生并作为bean的引用给服务使用。 注意:数据源应当总是作为一个bean在ApplicationContext中配置, 第一种情况它被直接传递给服务,第二种情况给JdbcTemplate。 因为这个类把回调接口和SQLExceptionTranslator接口作为参数表示,所以没有必要为它定义子类。 这个类执行的所有SQL都会被记入日志。
为了从数据库获得数据,我们需要得到数据库的连接。 Spring采用的方法是通过一个数据源。 数据源是JDBC规范的一部分,它可以被认为是一个通用的连接工厂。 它允许容器或者框架将在应用程序代码中隐藏连接池和事务的管理操作。 开发者将不需要知道连接数据库的任何细节,那是设置数据源的管理员的责任。 虽然你很可能在开发或者测试的时候需要兼任两种角色,但是你并不需要知道实际产品中的数据源是如何配置的。
使用Spring的JDBC层,你可以从JNDI得到一个数据源,也可以通过使用Spring发行版提供的实现自己配置它。 后者对于脱离Web容器的单元测试是十分便利的。 我们将在本节中使用DriverManagerDataSource实现,当然以后还会提到其他的一些的实现。 DriverManagerDataSource和传统的方式一样获取JDBC连接。 为了让DriverManager可以装载驱动类,你必须指定JDBC驱动完整的类名。 然后你必须提供相对于各种JDBC驱动的不同的URL。你必须参考你所用的驱动的文档,以获得需要使用的正确参数。 最后,你还必须提供用来连接数据库的用户名和密码 下面这个例子说明如何配置DriverManagerDataSource:
DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName( "org.hsqldb.jdbcDriver"); dataSource.setUrl( "jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername( "sa"); dataSource.setPassword( "");
SQLExceptionTranslator是一个需要实现的接口, 它被用来处理SQLException和我们的数据访问异常org.springframework.dao.DataAccessException之间的转换。
实现可以是通用的(比如使用JDBC的SQLState值),也可以为了更高的精确度特殊化 (比如使用Oracle的ErrorCode)。
SQLErrorCodeSQLExceptionTranslator 是SQLExceptionTranslator的实现,它被默认使用。比供应商指定的SQLState更为精确。 ErrorCode的转换是基于被保存在SQLErrorCodes型的JavaBean中的值。 这个类是由SQLErrorCodesFactory建立并填充的,就像它的名字说明的, SQLErrorCodesFactory是一个基于一个名为"sql-error-codes.xml"的配置文件的内容建立SQLErrorCodes的工厂。 这个文件带有供应商的码一起发布,并且是基于DatabaseMetaData信息中的DatabaseProductName,适合当前数据库的码会被使用。
SQLErrorCodeSQLExceptionTranslator使用以下的匹配规则:
使用子类实现的自定义转换。要注意的是这个类本身就是一个具体类,并可以直接使用, 在这种情况下,将不使用这条规则。
使用ErrorCode的匹配。在默认情况下,ErrorCode是从SQLErrorCodesFactory得到的。 它从classpath中寻找ErrorCode,并把从数据库metadata中得到的数据库名字嵌入它们。
如果以上规则都无法匹配,那么是用SQLStateSQLExceptionTranslator作为默认转换器。
SQLErrorCodeSQLExceptionTranslator可以使用以下的方式继承:
public class MySQLErrorCodesTransalator extends SQLErrorCodeSQLExceptionTranslator { protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) { if (sqlex.getErrorCode() == -12345) return new DeadlockLoserDataAccessException(task, sqlex); return null; } }
在这个例子中,只有特定的ErrorCode'-12345'被转换了,其他的错误被简单的返回,由默认的转换实现来处理。要使用自定义转换器时,需要通过setExceptionTranslator 方法将它传递给JdbcTemplate,并且在所有需要使用自定义转换器的数据访问处理中使用这个JdbcTemplate 下面是一个如何使用自定义转换器的例子:
// create a JdbcTemplate and set data source JdbcTemplate jt = new JdbcTemplate(); jt.setDataSource(dataSource); // create a custom translator and set the datasource for the default translation lookup MySQLErrorCodesTransalator tr = new MySQLErrorCodesTransalator(); tr.setDataSource(dataSource); jt.setExceptionTranslator(tr); // use the JdbcTemplate for this SqlUpdate SqlUpdate su = new SqlUpdate(); su.setJdbcTemplate(jt); su.setSql("update orders set shipping_charge = shipping_charge * 1.05"); su.compile(); su.update();
这个自定义的转换器得到了一个数据源,因为我们仍然需要默认的转换器在sql-error-codes.xml中查找ErrorCode。
要执行一个SQL,几乎不需要代码。你所需要的全部仅仅是一个数据源和一个JdbcTemplate。 一旦你得到了它们,你将可以使用JdbcTemplate提供的大量方便的方法。 下面是一个例子,它显示了建立一张表的最小的但有完整功能的类。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAStatement { private JdbcTemplate jt; private DataSource dataSource; public void doExecute() { jt = new JdbcTemplate(dataSource); jt.execute("create table mytable (id integer, name varchar(100))"); } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
除了execute方法,还有大量的查询方法。其中的一些被用来执行那些只返回单个值的查询。 也许你需要得到合计或者某一行中的一个特定的值。如果是这种情况,你可以使用queryForInt, queryForLong或者queryForObject。 后者将会把返回的JDBC类型转换成参数中指定的Java类。如果类型转换无效,那么将会抛出一个InvalidDataAccessApiUsageException。 下面的例子有两个查询方法,一个查询得到int,另一个查询得到String。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class RunAQuery { private JdbcTemplate jt; private DataSource dataSource; public int getCount() { jt = new JdbcTemplate(dataSource); int count = jt.queryForInt("select count(*) from mytable"); return count; } public String getName() { jt = new JdbcTemplate(dataSource); String name = (String) jt.queryForObject("select name from mytable", java.lang.String.class); return name; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
除了得到单一结果的查询方法之外,还有一些方法,可以得到一个包含查询返回的数据的List。 其中最通用的一个方法是queryForList,它返回一个List, 其中每一项都是一个表示字段值的Map。 你用下面的代码在上面的例子中增加一个方法来得到一个所有记录行的List:
public List getList() { jt = new JdbcTemplate(dataSource); List rows = jt.queryForList("select * from mytable"); return rows; }
返回的List会以下面的形式: [{name=Bob, id=1}, {name=Mary, id=2}].
还有很多更新的方法可以供你使用。我将展示一个例子,说明通过某一个主键更新一个字段。 在这个例子里,我用了一个使用榜定参数的SQL Statement。大多数查询和更新的方法都有这个功能。 参数值通过对象数组传递。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAnUpdate { private JdbcTemplate jt; private DataSource dataSource; public void setName(int id, String name) { jt = new JdbcTemplate(dataSource); jt.update("update mytable set name = ? where id = ?", new Object[] {name, new Integer(id)}); } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
这个辅助类提供从JNDI获取连接和在必要时关闭连接的方法。它支持线程绑定的连接, 比如被用于DataSourceTransactionManager。
注意:方法getDataSourceFromJndi用以针对那些不使用BeanFactory或者ApplicationContext 的应用。对于后者,建议在factory中配置你的bean甚至JdbcTemplate的引用: 使用JndiObjectFactoryBean可以从JNDI中得到一个DataSource 并将DataSource的引用给别的bean。要切换到另一个DataSource 就仅仅是一个配置的问题:你甚至可以用一个非JNDI的DataSource来替换 FactoryBean的定义!
实现这个接口的类可以提供一个关系数据库的连接。 它继承javax.sql.DataSource接口,使用它可以知道在一次数据库操作后, 是否需要关闭连接。如果我们需要重用一个连接,那么它对于效率是很有用的。
这个SmartDataSource的实现封装了单个在使用后不会关闭的连接。 所以很明显,它没有多线程的能力。
如果客户端代码想关闭这个认为是池管理的连接,比如使用持久化工具的时候, 需要将suppressClose设置成true。这样会返回一个禁止关闭的代理来接管物理连接。 需要注意的是,你将无法将不再能将这个连接转换成本地Oracle连接或者类似的连接。
它的主要作用是用来测试。例如,它可以很容易的让测试代码脱离应用服务器测试,而只需要一个简易的JNDI环境。 和DriverManagerDataSource相反,它在所有的时候都重用一个连接, 以此来避免建立物理连接过多的消耗。
这个SmartDataSource的实现通过bean的属性配置JDBC驱动, 并每次都返回一个新的连接。
它对于测试或者脱离J2EE容器的独立环境都是有用的, 它可以作为不同的ApplicationContext中的数据源bean, 也可以和简易的JNDI环境一起工作。被认为是池管理的Connection.close()操作 的调用只会简单的关闭连接,所以任何使用数据源的持久化代码都可以工作。
这个PlatformTransactionManager的实现是对于单个JDBC数据源的。 从某个数据源绑定一个JDBC连接到一个线程,可能允许每个数据源一个线程连接。
应用程序的代码需要通过DataSourceUtils.getConnection(DataSource)取得JDBC连接代替 J2EE标准的方法DataSource.getConnection。这是推荐的方法, 因为它会抛出org.springframework.dao中的unchecked的异常代替SQLException。 Framework中所有的类都使用这种方法,比如JdbcTemplate。 如果不使用事务管理,那么就会使用标准的方法,这样他就可以在任何情况下使用。
支持自定义的隔离级,以及应用于适当的JDBC statement查询的超时。 要支持后者,应用程序代码必须使用JdbcTemplate或者对每一个创建的statement 都调用DataSourceUtils.applyTransactionTimeout。
因为它不需要容器支持JTA,在只有单个资源的情况下, 这个实现可以代替JtaTransactionManager。 如果你坚持需要的连接的查找模式,两者间的切换只需要更换配置。 不过需要注意JTA不支持隔离级。
org.springframework.jdbc.object包由一些允许你 以更面向对象的方式访问数据库的类组成。你可以执行查询并获得一个包含业务对象的List, 这些业务对象关系数据的字段值映射成它们的属性。你也可以执行存储过程,更新,删除和插入操作。
这是一个表示SQL查询的可重用的而且线程安全的对象。 子类必须实现newResultReader()方法来提供一个对象,它能在循环处理ResultSet的时候保存结果。 这个类很少被直接使用,而使用它的子类MappingSqlQuery,它提供多得多的方法 将数据行映射到Java类。MappingSqlQueryWithParameters 和UpdatableSqlQuery是继承SqlQuery的另外两个实现。
MappingSqlQuery是一个可以重用的查询对象, 它的子类必须实现抽象方法mapRow(ResultSet, int)来把JDBC ResultSet的每一行转换成对象。
在所有的SqlQuery实现中,这个类是最常使用并且也是最容易使用的。
下面是一个自定义查询的简单例子,它把customer表中的数据映射成叫做Customer的Java类。
private class CustomerMappingQuery extends MappingSqlQuery { public CustomerMappingQuery(DataSource ds) { super(ds, "SELECT id, name FROM customer WHERE id = ?"); super.declareParameter(new SqlParameter("id", Types.INTEGER)); compile(); } public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { Customer cust = new Customer(); cust.setId((Integer) rs.getObject("id")); cust.setName(rs.getString("name")); return cust; } }
我们为customer查询提供一个构建方法,它只有数据源这一个参数。 在构建方法中,我们调用超类的构建方法,并将数据源和将要用来查询取得数据的SQL作为参数。 因为这个SQL将被用来建立PreparedStatement,所以它可以包含?来绑定执行时会得到的参数。 每一个参数必须通过declareParameter方法并传递给它一个SqlParameter来声明。 SqlParameter有一个名字和一个在java.sql.Types定义的JDBC类型。 在所有的参数都定义完后,我们调用compile方法建立随后会执行的PreparedStatement。
我们来看一段代码,来实例化这个自定义查询对象并执行:
public Customer getCustomer(Integer id) { CustomerMappingQuery custQry = new CustomerMappingQuery(dataSource); Object[] parms = new Object[1]; parms[0] = id; List customers = custQry.execute(parms); if (customers.size() > 0) return (Customer) customers.get(0); else return null; }
在例子中的这个方法通过一个参数id得到customer。在建立了CustomerMappingQuery 类的一个实例后,我们再创建一个数组,用来放置所有需要传递的参数。 在这个例子中只有一个Integer的参数需要传递。 现在我们使用这个数组执行查询,我们会得到一个List包含着Customer对象, 它对应查询返回结果的每一行。在这个例子中如果有匹配的话,只会有一个实体。
这个RdbmsOperation子类表示一个SQL更新操作。就像查询一样, 更新对象是可重用的。和所有的RdbmsOperation对象一样,更新可以有参数并定义在SQL中。
类似于查询对象中的execute()方法,这个类提供很多update()的方法。
这个类是具体的。通过SQL设定和参数声明,它可以很容易的参数化,虽然他也可以子例化 (例如增加自定义方法)。
import java.sql.Types; import javax.sql.DataSource; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.object.SqlUpdate; public class UpdateCreditRating extends SqlUpdate { public UpdateCreditRating(DataSource ds) { setDataSource(ds); setSql("update customer set credit_rating = ? where id = ?"); declareParameter(new SqlParameter(Types.NUMERIC)); declareParameter(new SqlParameter(Types.NUMERIC)); compile(); } /** * @param id for the Customer to be updated * @param rating the new value for credit rating * @return number of rows updated */ public int run(int id, int rating) { Object[] params = new Object[] { new Integer(rating), new Integer(id)}; return update(params); } }
这是RDBMS存储过程的对象抽象的超类。它是一个抽象类,它的执行方法都是protected的, 以避免被直接调用,而只能通过提供更严格形式的子类调用。
继承的sql属性是RDBMS中存储过程的名字。虽然这个类中提供的其他功能在JDBC3.0中也十分的重要, 但最值得注意的是JDBC3.0中的使用命名的参数。
下面是一段例子程序,它调用Oracle数据库提供的函数sysdate()。 要使用存储过程的功能,你必须创建一个继承StoredProcedure的类. 这里没有任何输入参数,但需要使用SqlOutParameter类声明一个date型的输出参数。 execute()方法会返回一个使用名字作为key来映射每一个被声明的输出参数的实体的Map。
import java.sql.Types; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.sql.DataSource; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.datasource.*; import org.springframework.jdbc.object.StoredProcedure; public class TestSP { public static void main(String[] args) { System.out.println("DB TestSP!"); TestSP t = new TestSP(); t.test(); System.out.println("Done!"); } void test() { DriverManagerDataSource ds = new DriverManagerDataSource(); ds.setDriverClassName("oracle.jdbc.driver.OracleDriver"); ds.setUrl("jdbc:oracle:thin:@localhost:1521:mydb"); ds.setUsername("scott"); ds.setPassword("tiger"); MyStoredProcedure sproc = new MyStoredProcedure(ds); Map res = sproc.execute(); printMap(res); } private class MyStoredProcedure extends StoredProcedure { public static final String SQL = "sysdate"; public MyStoredProcedure(DataSource ds) { setDataSource(ds); setFunction(true); setSql(SQL); declareParameter(new SqlOutParameter("date", Types.DATE)); compile(); } public Map execute() { Map out = execute(new HashMap()); return out; } } private static void printMap(Map r) { Iterator i = r.entrySet().iterator(); while (i.hasNext()) { System.out.println((String) i.next().toString()); } } }
SQL "function"封装返回单一行结果的查询。默认的情况返回一个int,当然我们可以重载它, 通过额外返回参数得到其他类型。这和使用JdbcTemplate的 queryForXxx方法很相似。使用SqlFunction的好处是 不用必须在后台建立JdbcTemplate。
这个类的目的是调用SQL function,使用像"select user()"或者"select sysdate from dual" 得到单一的结果。它不是用来调用复杂的存储功能也不是用来使用CallableStatement 来调用存储过程或者存储功能。对于这类的处理应当使用StoredProcedure或者SqlCall。
这是一个具体的类,它通常不需要子类化。使用这个包的代码可以通过声明SQL和参数创建这个类型的对象, 然后能重复的使用run方法执行这个function。下面是一个得到一张表的行数的例子:
public int countRows() { SqlFunction sf = new SqlFunction(dataSource, "select count(*) from mytable"); sf.compile(); return sf.run(); }
Spring在资源管理,DAO实现支持以及实物策略等方面提供了与Hibernate, JDO和iBATIS SQL映射的集成。 对Hibernate,Spring使用了很多IoC的方便的特性提供了一流的支持,帮助你处理很多典型的Hibernate整合的问题。所有的这些都遵守Spring通用的事务和DAO异常体系.
当您选择使用O/R映射来创建数据访问应用程序的时候,Spring的增加部分就会向您提供重要的支持。首先你应该了解的是,一旦你使用了Spring对O/R映射的支持,你不需要亲自作所有的事情。在决定花费力气,冒着风险建造类似的内部底层结构之前,我们都建议您考虑和利用Spring的解决方案。不管你使用的是何种技术,大部分的O/R映射支持都可以以library样式被使用,因为所有的东西都是被设计成一组可重复利用的JavaBeans。在ApplicationContext和BeanFactory中使用更是提供了配置和部署简单的好处,因此,这一章里的大多数例子都是在ApplicationContext中配置。
使用Spring构建你的ORM应用的好处包括:
避免绑定特殊的技术,允许mix-and-match的实现策略。 虽然Hibernate非常强大,灵活,开源而且免费,但它还是使用了自己的特定的API。此外有人也许会争辩:iBatis更轻便而且在不需要复杂的O/R映射策略的应用中使用也很优秀。能够选择的话,使用标准或抽象的API来实现主要的应用需求,通常是更好的。尤其,当你可能会因为功能,性能或其他方面的原因而需要切换到另一个实现的时候。举例来说,Spring对Hibernate事务和异常的抽象,以及能够让你轻松交换mapper和DAO对象(实现数据访问功能)的IoC机制,这两个特性可以让你在不牺牲Hibernate性能的情况下,在你的应用程序中隔离Hibernate的相关代码。处理DAO的高层次的service代码不需要知道DAO的具体实现。这个方法可以很容易使用mix-and-match方案互不干扰地实现数据访问层(比如在一些地方用Hibernate,一些地方使用JDBC,其他地方使用iBatis),mix-and-match有利于处理遗留下来的代码以及利用各种技术(JDBC,Hibernate,iBatis)的长处.
测试简单. Spring的IoC使得很容易替换掉不同的实现,Hibernate SessionFacotory的位置,datasource, 事务管理, 映射对象的实现。这样就很容易隔离测试持久化相关代码的各个部分。
通用的资源管理。 Spring的application context能够处理诸如Hibernate 的SessionFactory, JDBC的datasource,iBatis的SQLMaps配置对象以及其他相关资源的定位和配置。这使得这些配置的值很容易被管理和修改。Spring提供了有效,简单和安全的Hibernate Session处理。一般的使用Hibernate的代码则需要使用同一个Hibernate Session对象以确保有效和恰当地事务处理。而Spring让我们可以很容易透明地创建和绑定一个session到当前线程;你可以使用以下两种办法之一:声明式的AOP方法拦截器,或通过使用一个外部的template包装类在Java代码层次实现。这样,Spring就解决了在很多Hibernate论坛上出现的使用问题。
异常包装。 Spring能够包装Hibernate异常,把它们从专有的,checked exception变为一组抽象的runtime exception。这样你就可以仅仅在恰当的层处理大部分的不可恢复的异常,使你避免了很多讨厌的catch/throw以及异常声明。你还是可以在你需要的地方捕捉和处理异常。回想一下JDBC异常(包括与DB相关的方言)被转变为同样的异常体系,这就意味着你可以在一致的编程模型中处理JDBC操作。
综合的事务管理 。 Spring允许你包装你的ORM代码,通过使用声明式的AOP方法拦截器或者在代码级别使用外部的template包装类。不管使用哪一种,事务相关的语义都会为你处理,万一有异常发生也会帮你做适当的事务操作(比如rollback)。就象我们下面要讨论的一样,你能够使用和替换各种transaction managers,却不会使你的Hibernate相关的代码受到影响。更好的是,JDBC相关的代码可以完全和Hibernate代码integrate transactionaly。这对于处理那些没有用Hibernate或iBatis实现的功能非常有用。
典型的应用经常会被重复的资源管理代码搞胡乱。 很多项目尝试创造自己的方案解决这个问题,有时会为了编程方便牺牲适当的故障处理。 对于恰当的资源处理Spring提倡令人瞩目的简单的解决方案:使用templating的IoC, 比如基础的class和回调接口,或者提供AOP拦截器。基础的类负责固定的资源处理, 以及将特定的异常转换为unchecked异常体系。Spring引进了DAO异常体系,可适用于任何数据访问策略。 对于直接使用JDBC的情况,前面章节提到的JdbcTemplate类负责处理connection, 正确地把SQLException 变为DataAccessException 体系(包括将与数据库相关的SQL错误代码变成有意义的异常类)。 它同时支持JTA和JDBC事务,通过它们各自的Spring transaction managers。 Spring同样也提供了对Hibernate和JDO的支持:一个HibernateTemplate/JdoTemplate类似于 JdbcTemplate,HibernateInterceptor/JdoInterceptor,以及一个Hibernate/JDO transaction manager。主要的目的是:能够清晰地划分应用层次而不管使用何种数据访问和事务技术;使应用对象之间的耦合松散。业务对象(BO)不再依赖于数据访问和事务策略;不再有硬编码的资源lookup;不再有难于替换的singletons ;不再有自定义的服务注册。一个简单且坚固的方案连接了应用对象,并且使它们可重用尽可能地不依赖容器。虽然所有的数据访问技术都能独立使用,但是与Spring application context结合更好一些,它提供了基于xml的配置和普通的与Spring 无关的JavaBean实例。在典型的Spring app中,很多重要的对象都是JavaBeans:数据访问template,数据访问对象(使用template),transaction managers, 业务对象(使用数据访问对象和transaction managers),web view resolvers, web controller(使用业务对象)等等。
为了避免将应用对象贴紧硬编码的资源lookup, Spring允许你像定义普通bean一样在application context中定义诸如 JDBC DataSource,Hibernate SessionFactory 的资源。 需要访问这些资源的应用对象只需要持有这些预定义实例的引用。 下面的代码演示如何创建一个JDBC DataSource和Hibernate SessionFactory : :
<beans> <bean id="myDataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/myds</value> </property> </bean> <bean id="mySessionFactory" 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="myDataSource"/> </property> </bean> ... </beans>
你可以将一个JNDI定位的DataSource换为一个本地定义的如DBCP的BasicDataSource,如下面的代码: :
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"> <value>org.hsqldb.jdbcDriver</value> </property> <property name="url"> <value>jdbc:hsqldb:hsql://localhost:9001</value> </property> <property name="username"> <value>sa</value> </property> <property name="password"> <value></value> </property> </bean>
当然你也可以把本地的SessionFactory换为JNDI定位的, 但是如果不是在EJB上下文中,这是不需要的。(查看“容器资源 vs 本地资源”一节)
对于可以成为定制的数据访问对象或业务对象的方法来说, 基本的模板编程模型看起来像下面所示的代码那样。 对于外部对象没有任何实现特定接口的要求,它只需要提供一个Hibernate的SessionFactory。 它可以从任何地方得到,比较适宜的方法是作为从Spring 的application context中得到的bean引用: 通过简单的setSessionFactory这个bean属性setter。下面的代码显示了在application context中一个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(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public List loadProductsByCategory(final String category) { HibernateTemplate hibernateTemplate = new HibernateTemplate(this.sessionFactory); return (List) hibernateTemplate.execute( new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException { List result = session.find( "from test.Product product where product.category=?", category, Hibernate.STRING); // do some further stuff with the result list return result; } } ); } }
一个callback的实现能在任何的Hibernate数据访问中有效使用。 HibernateTemplate会确保Session正确的打开和关闭,并且会自动参与事务。 Template实例是线程安全的,可重用的,因此可以作为外部类的实例变量而被保持。 对于简单的一步操作,例如简单的find,load, saveOrUpdate,或者delete调用, HibernateTemplate提供了可选的简单方法可以用来替换这种一行的callback实现。 此外,Spring提供了一个简便的HibernateDaoSupport基类, 这个基类提供了setSessionFactory方法来接受一个SessionFactory,getSessionFactory 以及getHibernateTemplate方法是子类使用的。这样对于典型的需求就可以有很简单的DAO实现:
public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao { public List loadProductsByCategory(String category) { return getHibernateTemplate().find( "from test.Product product where product.category=?", category, Hibernate.STRING); } }
Spring的AOP HibernateInterceptor是作为HibernateTemplate的替换, 它在一个委托的try/catch块中直接写Hibernate代码, 以及在application context中的一个interceptor定义,从而代替callback的实现。 下面演示了在application context中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 HibernateDaoSupport implements 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才能工作。 getSession方法中的”false”标志为了确保Session已经存在, 否则SessionFactoryUtils会创建一个新的。 如果线程中已经存在一个 SessionHolder比如一个HibernateTransactionManager事务 ,那SessionFactoryUtils 就肯定会自动参与进来。HibernateTemplate 在内部使用SessionFactoryUtils, 他们是一样的底层结构。 HibernateInterceptor最主要的优点是允许在数据访问代码中抛出checked应用异常, 而HibernateTemplate在回调中严格限制只能抛出未检查异常。 值得注意的是我们可以把各种check以及抛出应用异常推迟到回调之后。 Interceptor的主要缺点是它需要在context进行特殊的装配。HibernateTemplate的简便方法在很多场景下更简单些
在这些低层次的数据访问服务之上,可以在应用的高层次上划分事务, 让事务横跨多个操作。这里对于相关的业务对象(BO)同样没有实现接口的限制, 它只需要一个Spring的PlatformTransactionManager。同SessionFactory一样, 这个manager可以来自任何地方, 但是最好是作为一个经由setTransactionManager 方法设置的bean引用, 例如,用setProductDao方法来设置productDAO. 下面演示了在Srping application context中一个transaction manager和一个业务对象的定义, 以及具体的业务方法是如何实现的:
<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(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } public void setProductDao(ProductDao productDao) { this.productDao = productDao; } public void increasePriceOfAllProductsInCategory(final String category) { TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager); transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); transactionTemplate.execute( new TransactionCallbackWithoutResult() { public void doInTransactionWithoutResult(TransactionStatus status) { List productsToChange = productDAO.loadProductsByCategory(category); ... } } ); } }
作为可选的,我们可以使用Spring的AOP TransactionInterceptor来替换事务划分的手工代码, 这需要在application context中定义interceptor。 这个方案使得你可以把业务对象从每个业务方法中重复的事务划分代码中解放出来。 此外,像传播行为和隔离级别等事务概念能够在配置文件中改变,而不会影响业务对象的实现。
<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 String category) { List productsToChange = this.productDAO.loadProductsByCategory(category); ... } ... }
同HibernateInterceptor一样,TransactionInterceptor 允许任何checked的应用异常在callback代码中抛出, 而TransactionTemplate 在callback中严格要求unchecked异常。 TransactionTemplate 会在一个unchecked异常抛出时触发一个rollback, 或者当事务被应用程序通过TransactionStatus标记为rollback-only。 TransactionInterceptor默认进行同样的操作,但是它允许对每个方法配置rollback方针。 一个简便可选的创建声明式事务的方法是:TransactionProxyFactoryBean, 特别是在没有其他AOP interceptor牵扯到的情况下。对一个特定的目标bean, TransactionProxyFactoryBean用事务配置自己联合proxy定义。 这样就把配置工作减少为配置一个目标bean以及一个 proxy bean的定义(少了interceptor的定义)。 此外你也不需要指定事务方法定义在哪一个接口或类中。
<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>
TransactionTemplate 和TransactionInterceptor 都是将真正的事务处理代理给一个PlatformTransactionManager实例, 比如在Hibernate应用中它可以是一个HibernateTransactionManager (对于单独一个的Hibernat SessionFactory, 实质上使用一个ThreadLocal的Session)或一个JtaTransactionManager (代理给容器的JTA子系统)。 你甚至可以使用自定义的PlatformTransactionManager的实现。 所以呢,如果你的应用需要分布式事务的时候, 将原来的Hibernate事务管理转变为JTA之类的,只不过是改变配置文件的事情。 简单地,将Hibernate transaction manager替换为Spring的JTA transaction实现。 事务的划分和数据访问代码则不需要改变,因为他们使用的是通用的事务管理API。 对于横跨多个Hibernate SessionFacotry的分布式事务, 只需简单地将JtaTransactionManager 同多个LocalSessionFactoryBean 定义结合起来作为一个事务策略。 你的每一个DAO通过bean属性得到各自的SessionFactory引用。 如果所有的底层JDBC datasource都是支持事务的容器, 那么只要一个业务对象使用了 JtaTransactionManager策略, 它就可以横跨多个DAO和多个session factories来划分事务,而不需要特殊的对待.
<beans> <bean id="myDataSource1" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/myds1</value> </property> </bean> <bean id="myDataSource2" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/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事务管理器的lookup或JCA连接器(只要事务不是用EJB发起的), 从而考虑到在适当JVM级别上的cache处理。 而且HibernateTransactionManager 能够为普通的JDBC访问代码输出JDBC Connection。 这就可以使得混合的Hibernate/JDBC数据访问可以不用JTA而在高层次上进行事务划分, 只要它们使用的是同一个数据库!
注释, 对于使用TransactionProxyFactoryBean 声明性划分事务属于可选方式,请参考for an alternative approach to using 第 7.4.1 节 “BeanNameAutoProxyCreator,另一种声明方式”.
一个Spring application context的定义能够被很多种不同的上下文实现所读取, 比如FileSystemXmlApplicationContext 和 ClassPathXmlApplicationContext以及XmlWebApplicationContext。 默认的情况下,一个web应用程序会从”WEB-INF/applicationContext.xml”中得到root context。 在所有的Spring应用中,XML文件中定义的application context会把所有相关的application beans连接起来, 包括Hibernate session factory以及数据访问和业务对象(就像上面定义的那些bean)。 它们中的大部分都不会意识到被Spring容器所管理,甚至在同其他bean合作的时候, 因为它们仅仅遵循JavaBeans的规范。一个bean属性及可以表示值参数,也可以是其他的合作bean。 下面的bean定义能够作为Spring web MVC context的一部分,在最基础的 application context中访问business beans。
<bean id="myProductList" class="product.ProductListController"> <property name="productService"> <ref bean="myProductService"/> </property> </bean>
Spring web controller所需要的所有业务或数据访问对象都是通过bean引用提供的, 所以在应用得上下文中就不再需要手动的bean lookup了。但是在与Struts一起使用, 或在一个EJB实现中甚至一个applet中使用,你仍然可以自己手动地look up一个bean (意思大概是Spring不会影响你原来的使用方式)。因此,Spring beans事实上能够在任何地方支持。 你只需要一个application context的引用,你可以在一个web 应用中通过一个servlet context的属性得到, 或者手动地从一个文件或class path resource创建实例。
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");
Spring的资源管理考虑到了在JNDI SessionFactory和local的SessionFactory之间的简单切换, 对于JNDI DataSource也是这样的,不需要修改一行代码。 把资源定义放在容器中还是放在应用程序本地中主要是由使用的事务策略决定的。 同Spring定义的本地SessionFactory相比,一个手动注册的JNDI SessionFactory并不会提供任何多余的好处。 如果通过Hibernate的JCA连接器进行注册,对于参与JTA事务有明显的好处, 尤其在EJB中。而Spring的transaction支持的一个重要好处就是不依赖于任何一个容器。 使用非JTA的策略配置,程序将会在独立的或测试的环境下同样正常工作。尤其在典型的单数据库事务情况下, 这将是JTA的轻便和强大的替换方案。当使用一个本地的EJB SLSB来驱动事务时, 尽管你可能只访问一个数据库而且仅仅通过CMT使用SLSBs的声明性事务,你仍然要依赖于EJB容器和JTA。 编程式使用JTA的替换方案依然需要J2EE环境。JTA不仅仅在自己这一方面而且在JNDI DataSource方面, 引入了容器依赖。对于非Spring的,JTA驱动的Hibernate事务,为了在适当的JVM层次上做caching, 你必须使用Hibernate JCA连接器或者额外的Hibernate事务代码及配置好的JTA事务。 Spring驱动的事务能够很好地使用本地定义的Hibernate SessionFacotry工作,就像使用本地的JDBC DataSource工作( 当然访问的必须是一个单独的数据库)。因此你只需要在面对分布式事务需求的时候退回到Spring的JTA事务策略。 必须要注意,一个JCA连接器需要特定容器的部署步骤,而且首先要支持JCA。 这要比使用本地资源定义和Spring驱动事务来部署一个简单的Web应用麻烦多了。 而且你通常需要企业版本的容器,比如WebLogic的Express版本并不提供JCA。 一个仅使用本地资源并且事务仅跨越一个数据库的Spring应用将会在任何一种J2EE Web容器中工作 (不需要JTA,JCA或者EJB),比如Tomcat,Resin, 甚至普通的Jetty。 更多的是,这样的中间层可以在桌面应用或测试用例中简单地重用。综上所述:如果你不使用EJB, 坚持使用本地SessionFactory的创建和Spring的HibernateTransactionManager 或JtaTransactionManager。 你可得到包括适当的JVM层次上的caching以及分布式事务在内的所有好处,却不会有任何容器部署的麻烦。 通过JCA连接器的Hibernate SessionFactory 的JNDI注册仅仅在EJB中使用会带来好处。
Spring通过org.springframework.orm.ibatis包来支持iBATIS SqlMaps 1.3.x和2.0。 iBATIS的支持非常类似于Hibernate的支持,它支持同样的template样式的编程, 而且同Hibernate一样,iBatis的支持也是和Spring的异常体系一起工作,他会让你喜欢上Spring所有的IoC特性。
Spring对iBATIS SqlMaps1.3和2.0都提供了支持。首先让我们来看一看两个之间的区别。
表 11.1. iBATIS SqlMaps supporting classes for 1.3 and 2.0
Creation of SqlMap | SqlMapFactoryBean | SqlMapClientFactoryBean |
Template-style helper class | SqlMapTemplate | SqlMapClientTemplate |
Callback to use MappedStatement | SqlMapCallback | SqlMapClientCallback |
Super class for DAOs | SqlMapDaoSupport | SqlMapClientDaoSupport |
使用iBATIS SqlMaps涉及创建包含语句和结果对应关系的配置文件。 Spring会负责使用SqlMapFactoryBean或SqlMapClientFactoryBean来读取这些配置文件, 其中后一个类是同SqlMaps2.0联合使用的。
public class Account { private String name; private String email; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getEmail() { return this.email; } public void setEmail(String email) { this.email = email; } }
假设我们要映射这个类,我们就要创建如下的SqlMap。通过使用查询,我们稍后可以用email addersses来取得users. Account.xml:
<sql-map name="Account"> <result-map name="result" class="examples.Account"> <property name="name" column="NAME" columnIndex="1"/> <property name="email" column="EMAIL" columnIndex="2"/> </result-map> <mapped-statement name="getAccountByEmail" result-map="result"> select ACCOUNT.NAME, ACCOUNT.EMAIL from ACCOUNT where ACCOUNT.EMAIL = #value# </mapped-statement> <mapped-statement name="insertAccount"> insert into ACCOUNT (NAME, EMAIL) values (#name#, #email#) </mapped-statement> </sql-map>
在定义完SqlMap之后,我们必须为iBATIS创建一个配置文件 (sqlmap-config.xml):
<sql-map-config> <sql-map resource="example/Account.xml"/> </sql-map-config>
iBATIS会从classpath读取资源,所以要确保Account.xml文件在classpath上面。
用Spring,我们现在能够很容易地通过 SqlMapFactoryBean创建SqlMap:
<bean id="sqlMap" class="org.springframework.orm.ibatis.SqlMapFactoryBean"> <property name="configLocation"><value>WEB-INF/sqlmap-config.xml</value></property> </bean>
SqlMapDaoSupport类是类似于HibernateDaoSupport 和JdbcDaoSupport的支持类。让我们实现一个DAO:
public class SqlMapAccountDao extends SqlMapDaoSupport implements AccountDao { public Account getAccount(String email) throws DataAccessException { Account acc = new Account(); acc.setEmail(); return (Account)getSqlMapTemplate().executeQueryForObject("getAccountByEmail", email); } public void insertAccount(Account account) throws DataAccessException { getSqlMapTemplate().executeUpdate("insertAccount", account); } }
正如你所看到的,我们使用SqlMapTemplate来执行查询。Spring会在创建如下所示的SqlMapAccountDao的时候已经使用SqlMapFactoryBean为我们初始化了SqlMap。一切都准备就绪了:
<!-- for more information about using datasource, have a look at the JDBC chapter --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"><value>${jdbc.driverClassName}</value></property> <property name="url"><value>${jdbc.url}</value></property> <property name="username"><value>${jdbc.username}</value></property> <property name="password"><value>${jdbc.password}</value></property> </bean> <bean id="accountDao" class="example.SqlMapAccountDao"> <property name="dataSource"><ref local="dataSource"/></property> <property name="sqlMap"><ref local="sqlMap"/></property> </bean>
在使用iBATIS的应用程序中增加声明式事务管理是相当的简单。 基本上你所需要做的唯一事情是: 在你的application context中增加一个transaction manager并且使用诸如 TransactionProxyFactoryBean声明式地设定你的事务界限。 关于这方面的更多情况可以在第 7 章 事务管理中找到。
TODO elaborate!