8.6.2 使用XML Schema配置事务策略

Spring同时支持编程式事务策略声明式事务策略,通常都推荐采用声明式事务策略。使用声明式事务策略的优势十分明显。

  • 声明式事务能大大降低开发者的代码书写量,而且声明式事务几乎不影响应用的代码。因此,无论底层事务策略如何变化,应用程序都无须任何改变。
  • 应用程序代码无须任何事务处理代码,可以更专注于业务逻辑的实现
  • Spring则可对任何POJO的方法提供事务管理,而且Spring的声明式事务管理无须容器的支持,可在任何环境下使用。
  • EJBCMT无法提供声明式回滚规则;而通过配置文件, Spring可指定事务在遇到特定异常时自动回滚。 Spring不仅可以在代码中使用setRollbackOnly回滚事务,也可以在配置文件中配置回滚规则。
  • 由于Spring采用AOP的方式管理事务,因此,可以在事务回滚动作中插入用户自己的动作,而不仅仅是执行系统默认的回滚。

本节不打算全面介绍Spring的各种事务策略,因此这里不会介绍编程式事务。如果读者需要更全面地了解Spring事务的相关方面,请自行参阅Spring官方参考手册。
SpringXML Schema方式提供了简洁的事务配置策略, Spring提供了tx:命名空间来配置事务管理。**tx:命名空间下提供了<tx:advice>元素来配置事务增强处理,一旦使用该元素配置了事务增强处理,就可直接使用<aop:advisor>元素启用自动代理了**。

配置<tx:advice>元素时除了需要transaction-manager属性指定事务管理器之外,还需要配置一个<attributes>子元素,该子元素里又可包含多个<method>子元素。
<tx:advice>元素的属性、子元素的关系如图8.19所示。
这里有一张图片
从图8.19可以看出,配置<tx:advice>元素的重点就是配置<method>子元素,实际上每个<method>子元素都为一批方法指定了所需的事务定义,包括事务传播属性、事务隔离属性、事务超时属性、只读事务、对指定异常回滚、对指定异常不回滚等。

method元素

如图8.19所示,配置<method>子元素可以指定如下几个属性:

属性 描述
name 必选属性,与该事务语义关联的方法名。该属性支持使用通配符,例如get*"handle*on*Event
propagation 指定事务传播行为,该属性值可为Propagation枚举类的任一枚举值,各枚举值的含义下面立即介绍。该属性的默认值为Propagation.REQUIRED
isolation 指定事务隔离级别,该属性值可为Isolation枚举类的任一枚举值,各枚举值的具体含义可参考API文档。该属性的默认值为Isolation.DEFAULT
timeout 指定事务超时的时间(以秒为单位),指定-1意味着不超时,该属性的默认值是-1
read-only 指定事务是否只读。该属性的默认值是false
rollback-for 指定触发事务回滚的异常类(应使用全限定类名),该属性可指定多个异常类,多个异常类之间以英文逗号隔开。
no-rollback-for 指定不触发事务回滚的异常类(应使用全限定类名),该属性可指定多个异常类,多个异常类之间以英文逗号隔开。

method元素的propagation属性值

<method>子元素的propagation属性用于指定事务传播行为, Spring支持的事务传播行为如下。

propagation属性值 描述
PROPAGATION_MANDATORY 要求调用该方法的线程必须处于事务环境中,否则抛出异常。
PROPAGATION_NESTED 即使执行该方法的线程已处于事务环境中,也依然启动新的事务,方法在嵌套的事务里执行;即使执行该方法的线程并未处于事务环境中,也启动新的事务,然后执行该方法,此时与PROPAGATION_REQUIRED相同。
PROPAGATION_NEVER 不允许调用该方法的线程处于事务环境中,如果调用该方法的线程处于事务环境中,则抛出异常。
PROPAGATION_NOT_SUPPORTED 如果调用该方法的线程处于事务环境中,则先暂停当前事务,然后执行该方法。
PROPAGATION_REQUIRED 要求在事务环境中执行该方法,如果当前执行线程已处于事务环境中,则直接调用;如果当前执行线程不处于事务环境中,则启动新的事务后执行该方法。
PROPAGATION_REQUIRES_NEW 该方法要求在新的事务环境中执行,如果当前执行线程已处于事务环境中,则先暂停当前事务,启动新事务后执行该方法;如果当前调用线程不处于事务环境中,则启动新的事务后执行方法。
PROPAGATION_SUPPORTS 如果当前执行线程处于事务环境中,则使用当前事务,否则不使用事务。

程序示例

本示例使用NewsDaoImpl组件来测试Spring的事务功能,程序将使用<tx:advice>元素来配置事务增强处理,再使用<aop:advisor>为容器中的一批Bean配置自动事务代理。

NewsDaoImpl.java

NewsDaoImpl组件包含一个insert()方法,该方法同时插入两条记录,但插入的第二条记录将会违反唯一键约束,从而引发异常。下面是NewsDaoImpl类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.crazyit.app.dao.impl;

import javax.sql.DataSource;
import org.crazyit.app.dao.NewsDao;
import org.springframework.jdbc.core.JdbcTemplate;

public class NewsDaoImpl implements NewsDao
{
private DataSource ds;
public void setDs(DataSource ds)
{
this.ds = ds;
}
public void insert(String title, String content)
{
JdbcTemplate jt = new JdbcTemplate(ds);
jt.update("insert into news_inf"
+ " values(null , ? , ?)"
, title , content);
// 两次插入的数据违反唯一键约束
jt.update("insert into news_inf"
+ " values(null , ? , ?)"
, title , content);
// 如果没有事务控制,则第一条记录可以被插入
// 如果增加事务控制,将发现第一条记录也插不进去。
}
}

配置文件

下面是本应用示例所使用的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 定义数据源Bean,使用C3P0数据源实现,并注入数据源的必要信息 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://localhost/spring?useSSL=true"
p:user="root"
p:password="root"
p:maxPoolSize="40"
p:minPoolSize="2"
p:initialPoolSize="2"
p:maxIdleTime="30"/>
<!-- 配置JDBC数据源的局部事务管理器,使用DataSourceTransactionManager 类 -->
<!-- 该类实现PlatformTransactionManager接口,是针对采用数据源连接的特定实现-->
<!-- 配置DataSourceTransactionManager时需要依注入DataSource的引用 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!-- 配置一个业务逻辑Bean -->
<bean id="newsDao" class="org.crazyit.app.dao.impl.NewsDaoImpl"
p:ds-ref="dataSource"/>
<!-- 配置事务增强处理Bean,指定事务管理器 -->
<tx:advice id="txAdvice"
transaction-manager="transactionManager">
<!-- 用于配置详细的事务语义 -->
<tx:attributes>
<!-- 所有以'get'开头的方法是read-only的 -->
<tx:method name="get*" read-only="true" timeout="8"/>
<!-- 其他方法使用默认的事务设置,指定超时时长为5秒 -->
<tx:method name="*" isolation="DEFAULT"
propagation="REQUIRED" timeout="5"/>
</tx:attributes>
</tx:advice>
<!-- AOP配置的元素 -->
<aop:config>
<!-- 配置一个切入点,匹配org.crazyit.app.dao.impl包下
所有以Impl结尾的类里、所有方法的执行 -->
<aop:pointcut id="myPointcut"
expression="execution(* org.crazyit.app.dao.impl.*Impl.*(..))"/>
<!-- 指定在myPointcut切入点应用txAdvice事务增强处理 -->
<aop:advisor advice-ref="txAdvice"
pointcut-ref="myPointcut"/>
</aop:config>
</beans>

配置文件详解

引入命名空间

配置文件中使用XML Schema启用了Spring配置文件的tx:aop:两个命名空间:

1
2
3
4
5
6
7
8
9
10
<beans
...
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
...
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

配置事务增强处理

程序中的:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 配置事务增强处理Bean,指定事务管理器 -->
<tx:advice id="txAdvice"
transaction-manager="transactionManager">
<!-- 用于配置详细的事务语义 -->
<tx:attributes>
<!-- 所有以'get'开头的方法是read-only的 -->
<tx:method name="get*" read-only="true" timeout="8"/>
<!-- 其他方法使用默认的事务设置,指定超时时长为5秒 -->
<tx:method name="*" isolation="DEFAULT"
propagation="REQUIRED" timeout="5"/>
</tx:attributes>
</tx:advice>

这段XML代码配置了一个事务增强处理,配置<tx:advice>元素时只需指定一个transaction-manager属性,该属性的默认值是"transactionManager"
如果事务管理器Bean( PlatformTransactionManager实现类)的名字是transactionManager,则配置<tx:advice>元素时完全可以省略transaction-manager属性。如果为事务管理器Bean指定了其他名字,则需要为<tx:advice.>元素指定transaction-manager属性。

确保事务增强处理在合适的切入点被织入

配置文件中最后一段XML代码:

1
2
3
4
5
6
7
8
9
10
<!-- AOP配置的元素 -->
<aop:config>
<!-- 配置一个切入点,匹配org.crazyit.app.dao.impl包下
所有以Impl结尾的类里、所有方法的执行 -->
<aop:pointcut id="myPointcut"
expression="execution(* org.crazyit.app.dao.impl.*Impl.*(..))"/>
<!-- 指定在myPointcut切入点应用txAdvice事务增强处理 -->
<aop:advisor advice-ref="txAdvice"
pointcut-ref="myPointcut"/>
</aop:config>

<aop:config>定义,它确保由txAdvice切面定义的事务增强处理能在合适的切入点被织入。上面这段代码先定义了一个切入点,它匹配org.crazyit.app.dao.impl包下所有以Impl结尾的类所包含的所有方法,该切入点被命名为myPointcut然后用一个<aop:advisor>把这个切入点与txAdvice绑定在一起,表示当myPointcut执行时, txAdvice定义的增强处理将被织入。

提示<aop:advisor>元素是一个很奇怪的东西,标准的AOP机制里并没有所谓的Advisor", Advisor的作用非常简单:将Advice和切入点(既可通过pointcut-ref指定个已有的切入点,也可通过pointcut指定切入点表达式)绑定在一起,保证Advice所包含的增强处理将在对应的切入点被织入。
使用这种配置策略时,无须专门为每个业务Bean配置事务代理, Spring AOP会自动为所有匹配切入点表达式的业务组件生成代理,程序可以直接请求容器中的newsDao Bean,该Bean的方法已经具有了事务性——因为该Bean的实现类位于org.crazyit.app.dao.impl包下,且以Impl结尾,和poIntcut切入点匹配。

主程序

本示例的主程序非常简单,直接获取newsDao这个Bean,并调用它的insert()方法,可以看到该方法已经具有了事务性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package lee;

import org.springframework.context.support.*;
import org.springframework.context.*;
import org.crazyit.app.dao.*;
public class SpringTest
{
public static void main(String[] args)
{
// 创建Spring容器
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
// 获取事务代理Bean
NewsDao dao = (NewsDao) ctx.getBean("newsDao", NewsDao.class);
// 执行插入操作
dao.insert("疯狂Java", "轻量级Java EE企业应用实战");
}
}

上面的配置文件直接获取容器中的newsDao这个Bean,因为Spring AOP会为该Bean自动织入事务增强处理的方式,所以newsDao这个bean里的所有方法都具有事务性。
运行上面的程序,将出现一个异常,因为有了事务控制,出现异常的insert()方法所执行的两条SQL语句将全部回滚。

事务代理的业务方法

当使用<tx:advisor>为目标Bean生成事务代理之后, Spring AOP将会把负责事务操作的增强处理织入目标Bean的业务方法中。在这种情况下,事务代理的业务方法将如图8.20所示
这里有一张图片
当采用<aop:advisor>元素将Advice和切入点绑定时,实际上是由Spring提供的Bean后处理器完成的。 Spring提供了BeanNameAutoProxyCreatorDefaultAdvisorAutoProxyCreator这两个Bean后处理器,它们都可以对容器中的Bean执行后处理(为它们织入切面中包含的增强处理)。在配置<aop:advisor>元素时传入一个txAdvice事务增强处理后,Bean后处理器将为所有Bean实例里匹配切入点的方法织入事务操作的增强处理。

为不同的业务逻辑方法指定不同的事务策略

在声明式事务策略下, Spring也允许为不同的业务逻辑方法指定不同的事务策略,如下面的配置文件所示。

在遇到特定checked异常时自动回滚

如果想让事务在遇到特定的checked异常时自动回滚,则可借助于rollback-for属性。
在默认情况下,只有当方法引发运行时异常和unchecked异常时, Spring事务机制才会自动回滚事务。也就是说,只有当抛出一个RuntimeException或其子类实例,或Error对象时, Spring才会自动回滚事务。如果事务方法拋出checked异常,则事务不会自动回滚
通过使用rollback-for属性可强制Spring遇到特定checked异常时自动回滚事务,下面的XML配置片段示范了这种用法。

遇到特定runtime异常时强制不回滚

如果想让Spring遇到特定runtime异常时强制不回滚事务,则可通过no-rollback-for属性来指定如下面的配置片段所示。

8.6 Spring的事务

Spring的事务管理不需要与任何特定的事务API耦合。对不同的持久层访问技术,编程式事务提供了一致的事务编程风格,通过模板化操作一致性地管理事务。声明式事务基于Spring AOP实现,但并不需要开发者真正精通AOP技术,亦可容易地使用Spring的声明式事务管理。

8.6.1 Spring支持的事务策略

Java EE应用的传统事务有两种策略:全局事务局部事务

  • 全局事务由应用服务器管理,需要底层服务器的JTA支持。

  • 局部事务和底层所采用的持久化技术有关,

    • 当采用JDBC持久化技术时,需要使用Connection对象来操作事务;
    • 而采用Hibernate持久化技术时,需要使用Session对象来操作事务。
  • 全局事务可以跨多个事务性资源(典型例子是关系数据库和消息队列);

  • 使用局部事务,应用服务器不需要参与事务管理,因此不能保证跨多个事务性资源的事务的正确性。

当然,实际上大部分应用都使用单一的事务性资源。
图8.18对比了JTA全局事务、JDBC局部事务、 Hibernate事务的事务操作代码。
这里有一张图片
从图8.18可以看岀,当采用传统的事务编程策略时,程序代码必然和具体的事务操作代码耦合,这样造成的后果是:当应用需要在不同的事务策略之间切换时,开发者必须手动修改程序代码。如果使用Spring事务管理策略,就可以改变这种现状。

PlatformTransactionManager接口

Spring事务策略是通过PlatformTransactionManager接口体现的,该接口是Spring事务策略的核心。该接口的源代码如下:

1
2
3
4
5
6
7
8
9
public interface PlatformTransactionManager
{
//平台无关的获得事务的方法
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
//平台无关的事务提交方法
void commit(TransactionStatus status) throws TransactionException;
//平台无关的事务回滚方法
void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager是一个与任何事务策略分离的接口,随着底层不同事务策略的切换,应用必须采用不同的实现类。 PlatformTransactioManager接口没有与任何事务性资源捆绑在一起,它可以适应于任何的事务策略,结合SpringloC容器,可以向PlatformTransactionManager注入相关的平台特性。
Platform Transaction Manager接口有许多不同的实现类,应用程序面向与平台无关的接口编程,当底层采用不同的持久层技术时,系统只需使用不同的PlatformTransactionManager实现类即可—而这种切换通常由Spring容器负责管理,应用程序既无须与具体的事务API耦合,也无须与特定实现类耦合,从而将应用和持久化技术、事务API彻底分离开来
Spring的事务机制是一种典型的策略模式, PlatformTransactionManager代表事务管理接口,但它并不知道底层到底如何管理事务,它只要求事务管理需要提供开始事务( getTransaction())、提交事务( commit())和回滚事务( rollback())三个方法,但具体如何实现则交给其实现类来完成——不同的实现类则代表不同的事务管理策略。
即使使用容器管理的JTA,代码也依然无须执行JNDI查找,无须与特定的JTA资源耦合在一起通过配置文件,JTA资源传给PlatformTransactionManager的实现类。因此,程序的代码可在JTA事务管理和非JTA事务管理之间轻松切换。

Spring本身没有任何事务支持,它只是负责包装底层的事务,应用程序面向PlatformTransactionManager接口编程时, Spring在底层负责将这些操作转换成具体的事务操作代码,因此应用的底层支持怎样的事务策略,那么Spring就可支持怎样的事务策略。 Spring事务管理的优势是将应用从具体的事务API中分离出来,而不是真正提供事务管理的底层实现。

PlatformTransactionManager接口内,包含一个getTransaction(TransactionDefinition definition)方法,该方法根据TransactionDefinition参数返回一个TransactionStatus对象。 TransactionStatus对象表示一个事务, TransactionStatus被关联在当前执行的线程上。
getTransaction(Transaction Definition definitio)返回的TransactionStatus对象,可能是一个新的事务,也可能是一个已经存在的事务对象。如果当前执行的线程已经处于事务管理下,则返回当前线程的事务对象;否则,系统将新建一个事务对象后返回。

TransactionDefinition接口

TransactionDefinition接口定义了一个事务规则,该接口必须指定如下几个属性值。

  1. 事务隔离:当前事务和其他事务的隔离程度。例如,这个事务能否看到其他事务未提交的数据
  2. 事务传播:通常,在事务中执行的代码都会在当前事务中运行。但是,如果一个事务上下文已经存在,有几个选项可指定该事务性方法的执行行为。例如,在大多数情况下,简单地在现有的事务上下文中运行;或者挂起现有事务,创建一个新的事务。 Spring提供EJB CMT( ContainManagerTransaction,容器管理事务)中所有的事务传播选项。
  3. 事务超时:事务在超时前能运行多久,也就是事务的最长持续时间。如果事务一直没有被提交或回滚,将在超出该时间后,系统自动回滚事务。
  4. 只读状态:只读事务不修改任何数据。在某些情况下(例如使用Hibernate时),只读事务是非常有用的优化。

TransactionStatus接口

TransactionStatus代表事务本身,它提供了简单的控制事务执行和查询事务状态的方法,这些方法在所有的事务API中都是相同的。 TransactionStatus接口的源代码如下:

1
2
3
4
5
6
7
8
9
public interface TransactionStatus
{
//判断事务是否为新建的事务
boolean isNewTransaction();
//设置事务回滚
void setRollbackOnly();
//查询事务是否已有回滚标志
boolean isRollbackOnly();
}

Spring具体的事务管理由PlatformTransactionManager的不同实现类来完成。在Spring容器中配置PlatformTransactionManager Bean时,必须针对不同的环境提供不同的实现类。

程序示例

下面提供了不同的持久层访问环境及其对应的PlatformTransactionManager实现类的配置。

JDBC数据源配置示例

JDBC数据源的局部事务管理器的配置文件如下:

JTA配置示例

容器管理的JTA全局事务管理器的配置文件如下:

从上面的配置文件来看,当配置JtaTransactionManager全局事务管理策略时,只需指定事务管理器实现类即可,无须传入额外的事务性资源。这是因为全局事务的JTA资源由Java EE服务器提供,而Spring容器能自行从Java EE服务器中获取该事务性资源,所以无须使用依赖注入来配置。

Hibernate配置示例

当采用Hibernate持久层访问策略时,局部事务策略的配置文件如下:

如果底层采用Hibernate持久层技术,但事务采用JTA全局事务,则Spring配置文件如下:

从上面的配置文件可以看出,不论采用哪种持久层访问技术,只要使用JTA全局事务, Spring事务管理的配置就完全一样,因为它们采用的都是全局事务管理策略。
当采用JTA全局事务策略时,实际上需要底层应用服务器的支持,而不同应用服务器所提供的JTA全局事务可能存在细节上的差异,因此实际配置全局事务管理器时可能需要使用JtaTransactionManager的子类,如WebLogicJtaTransactionManager(Orcale提供的WebLogic)WebSphereUowTransactionManager(IBM提供的WebSphere)等,它们分别对应于不同的应用服务器
从上面的配置文件可以看出,当应用程序采用Spring事务管理策略时,应用程序无须与具体的事务策略耦合,应用程序只要面向PlatformTransactionManager策略接口编程, ApplicationContext将会根据配置文件选择合适的事务策略实现类。

实际上, Spring提供了如下两种事务管理方式。

  1. 编程式事务管理:即使使用Spring的编程式事务,程序也可直接获取容器中的transactionManager Bean,该Bean总是PlatformTransactionManager的实例,所以可以通过该接口提供的三个方法来开始事务、提交事务和回滚事务。
  2. 声明式事务管理:无须在Java程序中书写任何事务操作代码,而是通过在XML文件中为业务组件配置事务代理(AOP代理的一种),AOP为事务代理所织入的增强处理也由Spring提供—在目标方法执行之前,织入开始事务;在目标方法执行之后,织入结束事务

不论采用何种持久化策略, Spring都提供了一致的事务抽象,因此,应用开发者能在仼何环境下使用一致的编程模型。无须更改代码,应用就可在不同的事务管理策略中切换。
当使用编程式事务时,开发者使用的是Spring事务抽象(面向PlatformTransactionManager接口编程),而无须使用任何具体的底层事务APISpring的事务管理将代码从底层具体的事务API中抽象出来,该抽象能以任何底层事务为基础。

提示:Spring的编程式事务还可通过TransactionTemplate类来完成,该类提供了一个execute(TransactionCallback action)方法,可以以更简捷的方式来进行事务操作.

当使用声明式事务时,开发者无须书写任何事务管理代码,不依赖Spring或仼何其他事务APISpring的声明式事务无须任何额外的容器支持, Spring容器本身管理声明式事务。使用声明式事务策略,可以让开发者更好地专注于业务逻辑的实现。

Spring所支持的事务策略非常灵活, Spring的事务策略允许应用程序在不冋的事务策略之间自由切换,即使需要在局部事务策略和全局事务策略之间切换,也只需要修改配置文件即可,而应用程序的代码无须任何改变。这种灵活的设计,正是面向接口编程带来的优势.

8.5.3 使用@CacheEvict清除缓存

@CacheEvict注解修饰的方法可用于清除缓存,使用@CacheEvict注解时可指定如下属性。

属性 描述
value 必需属性。用于指定该方法用于清除哪个缓存区的数据
key 通过SpEL表达式显式指定缓存的key
allEntries 该属性指定是否清空整个缓存区
beforeInvocation 该属性指定是否在执行方法之前清除缓存。默认是在方法成功完成之后才清除缓存。
condition 该属性指定一个SpEL表达式,只有当该表达式为true时才清除缓存。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\CacheEvict
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

UserServiceImpl类增加两个方法,分别用于清空指定缓存和清空缓存。下面是UserServiceImpl类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.crazyit.app.service.impl;

import org.crazyit.app.domain.User;
import org.crazyit.app.service.UserService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service("userService")
@Cacheable(value = "users")
public class UserServiceImpl implements UserService
{
public User getUsersByNameAndAge(String name, int age)
{
System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
return new User(name, age);
}
public User getAnotherUser(String name, int age)
{
System.out.println("--正在执行findAnotherUser()查询方法--");
return new User(name, age);
}
// 指定根据name、age参数清除缓存
@CacheEvict(value = "users")
public void evictUser(String name, int age)
{
System.out.println("--正在清空" + name + " , " + age + "对应的缓存--");
}
// 指定清除user缓存区所有缓存数据
@CacheEvict(
value = "users",
allEntries = true)
public void evictAll()
{
System.out.println("--正在清空整个缓存--");
}
}

上面程序中
第一个@CacheEvict注解只是指定了value="users"一个属性,这表明该注解用于清除users缓存区中的数据,程序将会根据传入的nameage参数清除对应的数据。
第二个@CacheEvict注解则指定了allEntries=true,这表明该方法将会清空整个users缓存区。
使用如下主程序来测试它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package lee;

import org.crazyit.app.service.UserService;
import org.crazyit.app.domain.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 调用us对象的2个带缓存的方法,系统会缓存两个方法返回的数据
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
User u2 = us.getAnotherUser("猪八戒", 400);
// 调用evictUser()方法清除缓存中指定的数据
us.evictUser("猪八戒", 400);
// 由于前面根据"猪八戒", 400缓存的数据已经被清除了,
// 因此下面代码会重新执行,方法返回的数据将被再次缓存。
User u3 = us.getAnotherUser("猪八戒", 400); // ①
System.out.println(u2 == u3); // 输出false
// 由于前面前面已经缓存了参数为"孙悟空", 500的数据,
// 因此下面代码不会重新执行,直接利用缓存中的数据。
User u4 = us.getAnotherUser("孙悟空", 500); // ②
System.out.println(u1 == u4); // 输出true
// 清空整个缓存。
us.evictAll();
// 由于整个缓存都已经被清空,因此下面两行代码都会重新执行
User u5 = us.getAnotherUser("孙悟空", 500);
User u6 = us.getAnotherUser("猪八戒", 400);
System.out.println(u1 == u5); // 输出false
System.out.println(u3 == u6); // 输出false
}
}

上面程序中us.evictUser("猪八戒", 400);这行代码只是清除了缓存区中"猪八戒",400"对应的数据,因此上面程序在①号代码处需要重新执行该方法;但在②号代码处将可直接利用缓存区中的数据,无须重新执行该方法。
程序中第二行粗体字代码清空了整个缓存,因此第二行粗体字代码以后的两次调用方法都需要重新执行(因为缓存区中已经没数据了)。
执行该程序可以看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
--正在执行findUsersByNameAndAge()查询方法--
--正在执行findAnotherUser()查询方法--
--正在清空猪八戒 , 400对应的缓存--
--正在执行findAnotherUser()查询方法--
false
true
--正在清空整个缓存--
--正在执行findAnotherUser()查询方法--
--正在执行findAnotherUser()查询方法--
false
false

8.5.2 使用@Cacheable执行缓存

@Cacheable注解可用于修饰类或修饰方法,

  • 当使用@Cacheable注解修饰类时,用于告诉Spring在类级别上进行缓存,此时程序调用该类的实例的任何方法时都需要缓存,而且共享同一个缓存区;
  • 当使用@Cacheable注解修饰方法时,用于告诉Spring在方法级别上进行缓存,此时只有当程序调用该方法时才需要缓存。

1. 类级别的缓存

使用@Cacheable注解修饰类时,就可控制Spring在类级别进行缓存,这样当程序调用该类的任意方法时,只要传入的参数相同, Spring就会使用缓存。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\EhCache
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

假设本示例有如下UserServiceImpl组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
@Service("userService")
// 指定将数据放入users缓存区
@Cacheable(value = "users")
public class UserServiceImpl implements UserService
{
public User getUsersByNameAndAge(String name, int age)
{
System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
return new User(name, age);
}
public User getAnotherUser(String name, int age)
{
System.out.println("--正在执行findAnotherUser()查询方法--");
return new User(name, age);
}
}

上面程序中的粗体字代码指定对UserServiceImpl进行类级别的缓存,这样程序调用该类的任意方法时,只要传入的参数相同, Spring就会使用缓存。
此处所指的缓存的意义是:当程序第一次调用该类的实例的某个方法时, Spring缓存机制会将该方法返回的数据放入指定缓存区——就是@Cacheable注解的value属性值所指定的缓存区(注意此处指定将数据放入users缓存区,这就要求前面为缓存管理器配置过名为users的缓存区)。以后程序调用该类的实例的任何方法时,只要传入的参数相同, Spring将不会真正执行该方法,而是直接利用缓存区中的数据
例如如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 第一次调用us对象的方法时会执行该方法,并缓存方法的结果
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
// 第二次调用us对象的方法时直接利用缓存的数据,并不真正执行该方法
User u2 = us.getAnotherUser("孙悟空", 500);
System.out.println(u1 == u2); // 输出true
}
}

上面程序中的两行粗体字代码先后调用了UserServiceImpl的两个不同方法,但由于程序传入的方法参数相同,因此Spring不会真正执行第二次调用的方法,而是直接复用缓存区中的数据。
编译、运行该程序,可以看到如下输出:

1
2
--正在执行findUsersByNameAndAge()查询方法--
true

从上面输出结果可以看出,程序并未真正指定第二次调用getAnotherUser方法。

类级别缓存默认以方法的参数作为key来缓存方法返回的数据

由此可见,类级别的缓存默认以所有方法参数作为key来缓存方法返回的数据——同一个类不管调用哪个方法,只要调用方法时传入的参数相同, Spring都会直接利用缓存区中的数据。

@Cacheable注解的属性

使用@Cacheable时可指定如下属性。

属性 描述
value 必需属性。该属性可指定多个缓存区的名字,用于指定将方法返回值放入指定的缓存区内。
key 通过SpEL表达式显式指定缓存的key
condition 该属性指定一个返回boolean值的SpEL表达式,只有当该表达式返回true时, Spring才会缓存方法返回值
unless 该属性指定一个返回boolean值的spEL表达式,当该表达式返回true时, Spring不缓存方法返回值

@CachePut注解

@Cacheable注解功能类似的还有一个@CachePut注解,@CachePut注解同样会让Spring将方法返回值放入缓存区。与@Cacheable不同的是,@CachePut修饰的方法不会读取缓存区中的数据——这意味着不管缓存区是否已有数据,@CachePut总会告诉Spring要重新执行这些方法,并再次将方法返回值放入缓存区.

@Cacheable的key属性详解

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\key
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

例如,将上面程序中UserServiceImpl的注解改为如下形式

1
2
3
4
5
6
7
8
9
10
@Service("userService")
...
@Cacheable(
key = "#name",
value = "users"
)
public class UserServiceImpl implements UserService
{
...
}

上面的粗体字代码显式指定以name参数作为缓存的key,这样只要调用的方法具有相同的name参数, Spring缓存机制就会生效。使用如下主程序来测试它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 第一次调用us对象的方法时会执行该方法,并缓存方法的结果
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
// 指定使用name作为缓存key,因此主要两次调用方法的name参数相同
// 缓存机制就会生效
User u2 = us.getAnotherUser("孙悟空", 400);
System.out.println(u1 == u2); // 输出true
}
}

上面程序两次调用方法时传入的参数并不完全相同,只有name参数相同,但由于前面使用@Cacheable注解时显式指定了key="#name",这就意味着缓存使用name参数作为缓存的key,因此上面两次调用方法将依然只执行第一次调用,第二次调用将直接使用缓存的数据,不会真正执行该方法。

@Cacheable注解的condition属性unless属性详解

condition属性与unless属性的功能基本相似,但规则恰好相反:

  • condtion指定的条件为true时,Spring缓存机制才会执行缓存;
  • unless指定的条件为true时, Spring缓存机制就不执行缓存。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\condition
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

例如,将程序中UserServiceImpl类中的注解改为如下形式

1
2
3
4
5
6
7
...
@Service("userService")
@Cacheable(value = "users" , condition="#age<100")
public class UserServiceImpl implements UserService
{
...
}

上面代码显式指定Spring缓存生效的条件是#age<100,这样只要调用方法时age参数小于, Spring缓存机制就会生效。使用如下主程序来测试它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 调用方法时age参数不小于100,因此不会缓存,
// 所以下面两次方法调用都会真正执行这些方法。
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
User u2 = us.getAnotherUser("孙悟空", 500);
System.out.println(u1 == u2); // 输出false
// 调用方法时age参数小于100,因此会缓存,
// 所以下面第二次方法调用时不会真正执行该方法,而是直接使用缓存数据。
User u3 = us.getUsersByNameAndAge("孙悟空", 50);
User u4 = us.getAnotherUser("孙悟空", 50);
System.out.println(u3 == u4); // 输出true
}
}

上面程序中前两行代码调用方法时age参数大于100,因此前两行代码不会使用缓存。但程序后面两行代码调用方法时age参数小于100,因此后面两行代码会使用缓存。编译、运行该示例,可以看到如下输出:

1
2
3
4
5
--正在执行findUsersByNameAndAge()查询方法--
--正在执行findAnotherUser()查询方法--
false
--正在执行findUsersByNameAndAge()查询方法--
true

2. 方法级别的缓存

使用@Cacheable修饰方法时,就可控制Spring在方法级别进行缓存,这样当程序调用该方法时,只要传入的参数相同, Spring就会使用缓存

例如,将前面的UserDaoImpl改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
@Service("userService")
public class UserServiceImpl implements UserService
{
@Cacheable(value = "users1")
public User getUsersByNameAndAge(String name, int age)
{
System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
return new User(name, age);
}
@Cacheable(value = "users2")
public User getAnotherUser(String name, int age)
{
System.out.println("--正在执行findAnotherUser()查询方法--");
return new User(name, age);
}
}

上面两行粗体字代码指定getUsersByNameAndAgegetAnotherUser这两个方法分别使用不同的缓存区,这意味着这两个方法都会缓存,但由于它们使用了不同的缓存区,因此它们不能共享缓存数据。
上面程序需要分别使用users1users2两个缓存区,因此还需要在ehcache.xml文件中配置这两个缓存区,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="gbk"?>
<ehcache>
<diskStore path="java.io.tmpdir" />
<!-- 配置默认的缓存区 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- 配置名为users1的缓存区 -->
<cache name="users1"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600" />
<cache name="users2"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600" />
</ehcache>

使用如下主程序来测试它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 第一次调用us对象的方法时会执行该方法,并缓存方法的结果
//代码1
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
// 由于getAnotherUser()方法使用另一个缓存区,
// 因此无法使用getUsersByNameAndAge()方法缓存区的数据。
//代码2
User u2 = us.getAnotherUser("孙悟空", 500);
System.out.println(u1 == u2); // 输出false
// getAnotherUser("孙悟空", 500)已经执行过一次,故下面代码使用缓存
//代码3
User u3 = us.getAnotherUser("孙悟空", 500);
System.out.println(u2 == u3); // 输出true
}
}

上面程序中的代码1和代码2分别调用了不同的方法,由于这两个方法分别使用不同的缓存区,因此它们不能共享缓存,所以代码2也需要真正执行。代码3与代码2调用的是同一个方法,而且方法参数相同,因此代码3会直接使用缓存中保存的方法返回值.
运行上面程序,将看到如下输出:

1
2
3
4
--正在执行findUsersByNameAndAge()查询方法--
--正在执行findAnotherUser()查询方法--
false
true

8.5 Spring的缓存机制

Spring 3.1新增了一种全新的缓存机制,这种缓存机制与Spring容器无缝地整合在一起,可以对容器中的任意BeanBean的方法增加缓存。 Spring的缓存机制非常灵活,它可以对容器中的仼意BeanBean的任意方法进行缓存,因此这种缓存机制可以在Java EE应用的任何层次上进行缓存。
Hibernate SessionFactory级别的二级缓存相比, Spring缓存的级别更高, Spring缓存可以在控制器组件或业务逻辑组件级别上进行缓存,这样应用完全无须重复调用底层的DAO(数据访问对象,通常基于Hibernate等技术实现)组件的方法
Spring缓存同样不是一种具体的缓存实现方案,它底层同样需要依赖EhCacheGuava等具体的缓存工具。但这也正是Spring缓存机制的优势,应用程序只要面向Spring缓存API编程,应用底层的缓存实现可以在不同的缓存实现之间自由切换,应用程序无须任何改变,只要对配置文件略作修改即可。

8.5.1 启用Spring缓存

Spring配置文件专门为缓存提供了一个cache:命名空间,为了启用Spring缓存,需要在配置文件中导入cache:命名空间。导入cache:命名空间与前面介绍的导入util:context:命名空间的方式完全一样。
导入cache:命名空间之后,启用Spring缓存还要两步。

  1. Spring配置文件中添加<cache:annotation-driven cache-manager="缓存管理器ID">,该元素指定Spring根据注解来启用Bean级别方法级别的缓存.
  2. 针对不同的缓存实现配置对应的缓存管理器。

对于上面两步,其中第1步非常简单,使用<cache:annotation-driven>元素时可通过cache-manager属性显式指定容器中缓存管理器的id;该属性的默认值为cacheManager,也就是说,如果将容器中缓存管理器的id设为cacheManager,则可省略<cache:annotation-driven>元素中的cache-manager属性。
第2步则略微有点麻烦,由于Spring底层可使用大部分主流的Java缓存工具,而不同的缓存工具所需的配置也不同,因此略微有点麻烦。下面以Spring内置的缓存实现和EhCache为例来介绍Spring缓存的配置.

1. Spring内置缓存实现的配置

需要说明的是,** Spring内置的缓存实现只是一种内存中的缓存,并非真正的缓存实现**,因此通常只能用于简单的测试环境,不建议在实际项目中使用Spring内置的缓存实现
Spring内置的缓存实现使用SimpleCacheManager作为缓存管理器,使用SimpleCacheManager配置缓存非常简单,直接在Spring容器中配置该Bean,然后通过<property>元素驱动该缓存管理器执行setCaches()方法来设置缓存区即可。
SimpleCacheManager是一种内存中的缓存区,底层直接使用了JDKConcurrentMap来实现缓存,SimpleCacheManager使用了ConcurrentMapCacheFactoryBean作为缓存区,每个ConcurrentMapCacheFactoryBean配置一个缓存区。

项目示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\SimpleCache
└─src\
├─beans.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

例如,如下代码即可配置Spring内置缓存的缓存管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 配置Spring内置的缓存管理器 -->
<bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
<!-- 配置缓存区 -->
<property name="caches">
<set>
<!-- 下面列出多个缓存区,p:name用于为缓存区指定名字 -->
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name="default" />
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name="users" />
</set>
</property>
</bean>

上面配置文件使用SimpleCacheManager配置了Spring内置的缓存管理器,并为该缓存管理器配置了两个缓存区:defaultusers,这些缓存区的名字很重要,因为后面使用注解驱动缓存时需要根据缓存区的名字来将缓存数据放入指定缓存区内。
在实际应用中,开发者可以根据自己的需要,配置更多的缓存区,一般来说,应用有多少个组件需要缓存,程序就应该配置多少个缓存区。
从上面的配置文件可以看出,由于Spring内置提供的缓存实现本身就是基于JDKConcurrentMap来实现的,所有数据都直接缓存在内存中,因此配置起来非常简单。但Spring内置的缓存一般只能作为测试使用,在实际项目中不推荐使用这种缓存

2. EhCache缓存实现的配置

引入EhCache需要的jar包

在配置EhCache缓存实现之前,首先需要将EhCache缓存的JAR包添加到项目的类加载路径中,此处可直接将前文介绍Hibernate二级缓存时使用的EhCacheJAR包复制过来使用即可。
只要将Hibernate解压路径下lib/loptional/lehcache/路径下的ehcache2.10.3jarslf4japi-1.7.7jar复制到项目类加载路径下即可。其中ehcache-2.10.3jarEhCache的核JAR包,而slf4j-api-1.7.7jar则是该缓存工具所使用的日志工具.
为了使用EhCache,同样需要在应用的类加载路径下添加一个ehcache.xm配置文件。例如,使用如下ehcache.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="gbk"?>
<ehcache>
<diskStore path="java.io.tmpdir" />
<!-- 配置默认的缓存区 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- 配置名为users的缓存区 -->
<cache name="users"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600" />
</ehcache>

上面的配置文件同样配置了两个缓存区,其中:

  • 第一个是用于配置匿名的、默认的缓存区,
  • 第二个才是配置了名为users的缓存区。如果需要,读者完全可以将<cache>元素复制多个,用于配置多个有名字的缓存区。这些缓存区的名字同样很重要,后面使用注解驱动缓存时需要根据缓存区的名字来将缓存数据放入指定缓存区内。

提示:
ehcache.xml文件中的<defaultCache>元素和<cache>元素所能接受的属性,在前文介绍Hibernate二级缓存时已经有详细说明,此处不再赘述。
Spring使用EhCacheCacheManager作为EhCache缓存实现的缓存管理器,因此只要该对象配置在Spring容器中,它就可作为缓存管理器使用,但EhCacheCacheManager底层需要依赖一个net.sf.ehcache.CacheManager作为实际的缓存管理器。
为了将net.sf.ehcache.CacheManager纳入Spring容器的管理之下, Spring提供了EhCacheManagerFactoryBean工厂Bean,该工厂Bean实现了FactoryBean<CacheManager>接口。当程序把EhCacheManagerFactoryBean部署在Spring容器中,并通过Spring容器请求获取该工厂Bean时,实际返回的是它的产品—也就是CacheManager对象。
因此,为了在Spring配置文件中配置基于EhCache的缓存管理器,只要Spring配置文件中增加如下两段配置即可。

1
2
3
4
5
6
7
8
9
<!-- 配置EhCache的CacheManager 通过configLocation指定ehcache.xml文件的位置 -->
<bean id="ehCacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
p:configLocation="classpath:ehcache.xml" p:shared="false" />
<!-- 配置基于EhCache的缓存管理器 并将EhCache的CacheManager注入该缓存管理器Bean -->
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheCacheManager"
p:cacheManager-ref="ehCacheManager">
</bean>

上面配置文件中配置的

  • 第一个Bean是一个工厂Bean,它用于配置EhCacheCacheManager;
  • 第二个Bean才是为Spring缓存配置的基于EhCache的缓存管理器,该缓存管理器需要依赖于CacheManager,因此程序将第一个Bean注入到第二个Bean中。

配置好上面任意一种缓存管理器之后,接下来就可使用注解来驱动Spring将缓存数据存入指定缓存区了

8.4.6 基于XML配置文件的管理方式 3. 配置切入点

类似于@AspectJ方式,允许定义切入点来重用切入点表达式,XML配置方式也可通过定义切入点来重用切入点表达式。

Spring提供了<aop:pointcut>元素来定义切入点。

  • 当把<aop:pointcut>元素作为<aop:config>的子元素定义时,表明该切入点可被多个切面共享;
  • 当把<aop:pointcut>元素作为<aop:aspect>的子元素定义时,表明该切入点只能在该切面中有效。

配置<aop:pointcut>元素时通常需要指定如下两个属性:

属性 描述
id 指定该切入点的标识名。
expression 指定该切入点关联的切入点表达式。

定义切入点

如下配置片段定义了一个简单的切入点:

1
2
3
<!-- 定义一个切入点:myPointcut 通过expression指定它对应的切入点表达式 -->
<aop:pointcut id="myPointcut"
expression="execution(* org.crazyit.app.service.impl.*.*(..))" />

上面的配置片段既可作为<aop:config>的子元素,用于配置全局切入点;
也可作为<aop:aspect>的子元素,用于配置仅对该切面有效的切入点。

在XML配置中引用在注解定义的切入点

除此之外,如果要在XML配置中引用使用注解定义的切入点,在<aop:pointcut>元素中指定切入点表达式时还有另外一种用法,看如下配置片段:

1
2
3
4
5
6
7
<aop:config>
...
<!-- 直接引用org.crazyit.SystemArchitecture类中用注解定义的切入点 -->
<aop:pointcut id="myPointcut"
expression="org.crazyit.SystemArchitecture.myPointcut()"/>
...
</aop:config>

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\XML-AfterThrowing
└─src\
├─beans.xml
├─lee\
│ └─BeanTest.java
└─org\
└─crazyit\
└─app\
├─aspect\
│ └─RepairAspect.java
└─service\
├─Hello.java
├─impl\
│ ├─HelloImpl.java
│ └─WorldImpl.java
└─World.java

下面的示例程序定义了一个AfterThrowing增强处理,包含该增强处理的切面类如下。

1
2
3
4
5
6
7
8
9
10
11
12
package org.crazyit.app.aspect;

public class RepairAspect
{
// 定义一个普通方法作为Advice方法
// 形参ex用于访问目标方法中抛出的异常
public void doRecoveryActions(Throwable ex)
{
System.out.println("目标方法中抛出的异常:" + ex);
System.out.println("模拟Advice对异常的修复...");
}
}

与前面的切面类完全类似,该Java类就是一个普通的Java类。下面的配置文件将负责配置该Bean实例,并将该Bean实例转换成切面Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="GBK"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 定义一个普通Bean实例,该Bean实例将被作为Aspect Bean -->
<bean id="afterThrowingAdviceBean"
class="org.crazyit.app.aspect.RepairAspect" />
<bean id="hello" class="org.crazyit.app.service.impl.HelloImpl" />
<bean id="world" class="org.crazyit.app.service.impl.WorldImpl" />
<aop:config>
<!-- 定义一个切入点:myPointcut 通过expression指定它对应的切入点表达式 -->
<aop:pointcut id="myPointcut"
expression="execution(* org.crazyit.app.service.impl.*.*(..))" />
<aop:aspect id="afterThrowingAdviceAspect"
ref="afterThrowingAdviceBean">
<!-- 定义一个AfterThrowing增强处理,指定切入点 以切面Bean中的doRecoveryActions()方法作为增强处理方法 -->
<aop:after-throwing
pointcut-ref="myPointcut" method="doRecoveryActions"
throwing="ex" />
</aop:aspect>
</aop:config>
</beans>

上面配置文件中配置了一个全局切入点myPointcut,这样其他切面Bean就可多次复用该切入点了。上面的配置文件在配置<aop:after-throwing>元素时,使用pointcut-ref引用了一个已有的myPointcut这个切入点。

8.4.6 基于XML配置文件的管理方式 2. 配置增强处理

与使用@AspectJ完全一样,使用XML一样可以配置BeforeAfterAfterReturningAfterThrowingAround五种增强处理,而且完全支持和@AspectJ完全一样的语义。
使用XML配置增强处理分别依赖于如下几个元素。

XML元素 描述
<aop:before> 配置 Before增强处理。
<aop:after> 配置Afer增强处理。
<aop:after-returning> 配置AfterReturning增强处理。
<aop:after-throwing> 配置AfterThrowing增强处理
<aop:around> 配置 Around增强处理。

上面这些元素都不支持使用子元素,但通常可指定如下属性。

属性 描述
pointcut或者pointcut-ref pointcut属性指定一个切入表达式, pointcut-ref属性指定已有的切入点名称, Spring将在匹配该表达式的连接点时织入该增强处理。通常pointcutpointcut-ref两个属性只需使用其中之一。
method 该属性指定一个方法名,指定将切面Bean的该方法转换为增强处理。
throwing 该属性只对<after-throwing>元素有效,用于指定一个形参名, AfteThrowing增强处理方法可通过该形参访问目标方法所抛出的异常。
returning 该属性只对< after-returning…>元素有效,用于指定一个形参名, Afterreturning增强处理方法可通过该形参访问目标方法的返回值

既然应用选择使用XML配置方式来配置增强处理,所以切面类里定义切面、切入点和增强处理的注解全都可删除了。

XML配置方式中的切入点指示符

当定义切入点表达式时,XML配置方式和@AspectJ注解方式支持完全相同的切入点指示符,一样可以支持executionwithinargsthistargetbean等切入点指示符。

XML配置方式中的组合运算符

XML配置方式和@AspectJ注解方式一样支持组合切入点表达式,但XML配置方式不再使用简单的&&!作为组合运算符(因为直接在XML文件中需要使用实体引用来表示它们),而是使用如下三个组合运算符:and(相当于&&)、or(相当于)和not(相当于!)。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\XML-config
└─src\
├─beans.xml
├─lee\
│ └─BeanTest.java
└─org\
└─crazyit\
└─app\
├─aspect\
│ ├─FourAdviceTest.java
│ └─SecondAdviceTest.java
└─service\
├─Hello.java
├─impl\
│ ├─HelloImpl.java
│ └─WorldImpl.java
└─World.java

下面的程序定义了一个简单的切面类,该切面类只是将前面@Aspect示例中切面类的全部注解删除后的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package org.crazyit.app.aspect;

import org.aspectj.lang.*;
import java.util.Arrays;

public class FourAdviceTest
{
public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable
{
System.out.println("Around增强:执行目标方法之前,模拟开始事务...");
// 访问执行目标方法的参数
Object[] args = jp.getArgs();
// 当执行目标方法的参数存在,
// 且第一个参数是字符串参数
if (args != null && args.length > 0
&& args[0].getClass() == String.class)
{
// 修改目标方法调用参数的第一个参数
args[0] = "【增加的前缀】" + args[0];
}
// 执行目标方法,并保存目标方法执行后的返回值
Object rvt = jp.proceed(args);
System.out.println("Around增强:执行目标方法之后,模拟结束事务...");
// 如果rvt的类型是Integer,将rvt改为它的平方
if (rvt != null && rvt instanceof Integer)
rvt = (Integer) rvt * (Integer) rvt;
return rvt;
}
public void authority(JoinPoint jp)
{
System.out.println("②Before增强:模拟执行权限检查");
// 返回被织入增强处理的目标方法
System.out.println(
"②Before增强:被织入增强处理的目标方法为:" + jp.getSignature().getName());
// 访问执行目标方法的参数
System.out
.println("②Before增强:目标方法的参数为:" + Arrays.toString(jp.getArgs()));
// 访问被增强处理的目标对象
System.out.println("②Before增强:被织入增强处理的目标对象为:" + jp.getTarget());
}
public void log(JoinPoint jp, Object rvt)
{
System.out.println("AfterReturning增强:获取目标方法返回值:" + rvt);
System.out.println("AfterReturning增强:模拟记录日志功能...");
// 返回被织入增强处理的目标方法
System.out.println("AfterReturning增强:被织入增强处理的目标方法为:"
+ jp.getSignature().getName());
// 访问执行目标方法的参数
System.out.println(
"AfterReturning增强:目标方法的参数为:" + Arrays.toString(jp.getArgs()));
// 访问被增强处理的目标对象
System.out.println("AfterReturning增强:被织入增强处理的目标对象为:" + jp.getTarget());
}
public void release(JoinPoint jp)
{
System.out.println("After增强:模拟方法结束后的释放资源...");
// 返回被织入增强处理的目标方法
System.out.println(
"After增强:被织入增强处理的目标方法为:" + jp.getSignature().getName());
// 访问执行目标方法的参数
System.out.println("After增强:目标方法的参数为:" + Arrays.toString(jp.getArgs()));
// 访问被增强处理的目标对象
System.out.println("After增强:被织入增强处理的目标对象为:" + jp.getTarget());
}
}

上面的FourAdviceTest.java几乎是一个POJO类,该Java类的4个方法的第一个参数都是JoinPoint类型,之所以将4个方法的第一个参数定义为JoinPoint类型,是为了访问连接点的相关信息,当然Spring AOP只支持使用方法执行作为连接点,所以这里使用JoinPoint只是为了获取目标方法的方法名、参数值等信息
除此之外,本示例程序中还定义了如下一个简单的切面类。

1
2
3
4
5
6
7
8
9
10
11
package org.crazyit.app.aspect;

public class SecondAdviceTest
{
// 定义Before增强处理
public void authority(String aa)
{
System.out.println("①号Before增强:模拟执行权限检查");
System.out.println("目标方法的第一个参数为:" + aa);
}
}

上面切面类的authority方法里多了一个String aa的形参,则应用试图通过aa这个形参来访间目标方法的参数值,这需要在配置该切面Bean时使用args切入点指示符。
本应用中的Spring配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="GBK"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 定义一个普通Bean实例,该Bean实例将被作为Aspect Bean -->
<bean id="fourAdviceBean"
class="org.crazyit.app.aspect.FourAdviceTest" />
<!-- 再定义一个普通Bean实例,该Bean实例将被作为Aspect Bean -->
<bean id="secondAdviceBean"
class="org.crazyit.app.aspect.SecondAdviceTest" />
<bean id="hello" class="org.crazyit.app.service.impl.HelloImpl" />
<bean id="world" class="org.crazyit.app.service.impl.WorldImpl" />
<aop:config>
<!-- 将fourAdviceBean转换成切面Bean 切面Bean的新名称为:fourAdviceAspect 指定该切面的优先级为2 -->
<aop:aspect id="fourAdviceAspect"
ref="fourAdviceBean" order="2">
<!-- 定义一个After增强处理 直接指定切入点表达式 以切面Bean中的release()方法作为增强处理方法 -->
<aop:after
pointcut="execution(* org.crazyit.app.service.impl.*.*(..))"
method="release" />
<!-- 定义一个Before增强处理 直接指定切入点表达式 以切面Bean中的authority()方法作为增强处理方法 -->
<aop:before
pointcut="execution(* org.crazyit.app.service.impl.*.*(..))"
method="authority" />
<!-- 定义一个AfterReturning增强处理 直接指定切入点表达式 以切面Bean中的log()方法作为增强处理方法 -->
<aop:after-returning
pointcut="execution(* org.crazyit.app.service.impl.*.*(..))"
method="log" returning="rvt" />
<!-- 定义一个Around增强处理 直接指定切入点表达式 以切面Bean中的processTx()方法作为增强处理方法 -->
<aop:around
pointcut="execution(* org.crazyit.app.service.impl.*.*(..))"
method="processTx" />
</aop:aspect>
<!-- 将secondAdviceBean转换成切面Bean 切面Bean的新名称为:secondAdviceAspect 指定该切面的优先级为1,该切面里的增强处理将被优先织入 -->
<aop:aspect id="secondAdviceAspect"
ref="secondAdviceBean" order="1">
<!-- 定义一个Before增强处理 直接指定切入点表达式 以切面Bean中的authority()方法作为增强处理方法
且该参数必须为String类型(由authority方法声明中aa参数的类型决定) -->
<aop:before
pointcut="execution(* org.crazyit.app.service.impl.*.*(..)) and args(aa,..)"
method="authority" />
</aop:aspect>
</aop:config>
</beans>

上面的配置文件中依次配置了fourAdviceBeansecondAdviceBeanhelloworld这4个Bean,它们没有丝毫特别之处,完全可以像管理普通Bean一样管理它们。
上面配置文件中的aop:config元素中的代码用于将fourAdviceBean转换成一个切面Bean,并将该Bean里包含的4个方法转换成4个增强处理。

  • 当配置fourAdviceAspect切面时,为其指定了order="2",这将意味着该切面里的增强处理的织入顺序为2;
  • 而配置secondAdviceAspect切面时,为其指定了order="1",
  • 所以Spring AOP将优先织入secondAdviceAspect里的增强处理,再织入fourAdviceAspect里的增强处理。

8.4.6 基于XML配置文件的管理方式 1. 配置切面

定义切面使用<aop:aspect>元素,使用该元素来定义切面时,其实质是将一个已有的Spring Bean转换成切面Bean,所以需要先定义一个普通的Spring Bean

使用aop:aspect元素将普通Bean转为切面Bean

因为切面Bean可以当成一个普通的Spring Bean来配置,所以完全可以为该切面Bean配置依赖注入。当切面Bean定义完成后,通过在<aop:aspect>元素中使用ref属性来引用该Bean,就可将该Bean转换成一个切面Bean了。

配置<aop:aspect>元素时可以指定如下三个属性。

属性 描述
id 定义该切面的标识名
ref 用于将ref属性所引用的普通Bean转换为切面Bean
order 指定该切面Bean的优先级,该属性的作用与前面@AspectJ中的@Order注解、 Ordered接口的作用完全一样, order属性值越小,该切面对应的优先级越高

上面配置文件中的粗体字代码将Spring容器中的idafterAdviceBeanBean转换为一个切面Bean,该切面BeanidafterAdviceAspect

可以使用依赖注入来管理切面Bean

由于Spring支持将切面Bean当成普通Bean来管理,所以完全可以利用依赖注入来管理切面Bean,管理切面Bean的属性值、依赖关系等。

8.4.6 基于XML配置文件的管理方式

除了前面介绍的基于JDK1.5的注解方式来定义切面、切入点和增强处理外, Spring AOP也允许直接使用XML配置文件来定义管理它们
如果应用中没有使用JDK1.5,那就只能选择使用XML配置方式了, Spring2提供了一个aop:命名空间来定义切面、切入点和增强处理
实际上,使用XML配置方式与前面介绍的@AspectJ方式的实质是一样的,同样需要指定相关信息:配置切面切入点增强处理所需要的信息完全一样,只是提供这些信息的位置不同而已。使用XML配置方式时是通过XML文件来提供这些信息的;而使用@AspectJ方式时则通过注解来提供这些信息。
相比之下,使用XML配置方式有如下几个优点。

  1. 如果应用没有使用JDK1.5以上版本,那么应用只能使用XML配置方式来管理切面、切入点和增强处理等。
  2. 采用XML配置方式时对早期的Spring用户来说更加习惯,而且这种方式允许使用纯粹的POJO来支持AOP。当使用AOP作为工具来配置企业服务时,XML会是一个很好的选择。
  3. 当使用XML风格时,可以在配置文件中清晰地看出系统中存在哪些切面

使用XML配置方式,存在如下几个缺点。

  1. 使用XML配置方式不能将切面、切入点、增强处理等封装到一个地方。如果需要査看切面、切入点、增强处理,必须同时结合Java文件和XML配置文件来査看;但使用@AspectJ时,则只需一个单独的类文件即可看到切面、切入点和增强处理的全部信息。
  2. XML配置方式比@AspectJ方式有更多的限制:仅支持"singleton"切面Bean,不能在XML中组合多个命名连接点的声明。

除此之外,@AspectJ切面还有一个优点,就是能被Spring AOPAspectJ同时支持,如果有一天需要将应用改为使用Aspect.来实现AOP,使用@Aspect将非常容易迁移到基于Aspect.的AOP实现中。相比之下,选择使用@AspectJ风格会有更大的吸引力。
Spring配置文件中,所有的切面、切入点和增强处理都必须定义在<aop:config>元素内部。<beans>元素下可以包含多个<aop:config>元素,一个<aop:config>可以包含pointcutadvisoraspect元素,且这三个元素必须按照这顺序来定义。关于<aop:config>元素所包含的子元素如图8.17所示。

这里的图太模糊了,先不贴图

aop:config元素配置和自动代理不能混用

使用<aop:config>方式进行配置时,可能与Spring的自动代理方式相冲突,例如使用<aop:aspect-autoproxy>或类似方式显式启用了自动代理,则可能会导致出现问题(如有些增强处理没有被织入)。因此建议:要么全部使用<aop:config>配置方式,要么全部使用自动代理方式,不要两者混合使用。

8.4.5 基于注解的 零配置 方式 10. 组合切入点表达式

Spring支持使用如下三个逻辑运算符来组合切入点表达式。

逻辑运算符 描述
&& 要求连接点同时匹配两个切入点表达式。
|| 只要连接点匹配任意一个切入点表达式
! 要求连接点不匹配指定的切入点表达式。

回忆前面定义切入点表达式时使用了如下片段:

1
pointcut="execution(* org.crazyit.app.service.impl.*.*(..))&&args(food,time)"

上面pointcut属性指定的切入点表达式需要匹配如下两个条件。

  1. 匹配org.crazyit.app.service.impl包下任意类中任意方法的执行。
  2. 被匹配的方法的第一个参数类型必须是food的类型,第二个参数类型必须是time的类型(food和time的类型由增强处理方法来决定)

实际上,上面的pointcut切入点表达式由两个表达式组成,而且使用&&来组合这两个表达式,所以要求同时满足这两个切入点表达式的要求。