Advanced AOP

这一章将更详细的讨论Spring AOP中可用的功能以及如何在真实世界的应用程序中使用AOP。首先是@AspectJ,Spring 2.5 可以使用新的方法构建aspect;他可以将使用指定注释注释的类自动转换为Spring AOP的aspect。@AspectJ可以使你更清楚简单的定义aspect。因为使用@AspectJ aspect是Spring的bean,你可以充分的利用DI。然后将介绍Introduction,前面曾经简单的提到过,它允许你使用类似拦截的概念动态的为任何对象添加接口实现。Introduction之后。我们将讨论如何使用ProxyFactoryBean,这个类是Spring AOP的核心,他可以影响你的应用程序的行为。我们将探讨直接调用和代理调用之间的不同。接下来将会讨论与AspectJ整合,获得一个全功能的静态编译的AOP实现。AspectJ的功能要远远超过Spring AOP,但是AspectJ更加复杂。就像前面章节中提到的,在你需要使用Spring AOP中缺少的功能时,AspectJ是非常好的选择(通常是需要不同的pointcut类型)。最后将讨论如何进行面向方面的编程(aspect-oriented pargramming)。我们将忽略通常的应用(比如安全方面或者logging),提供更加实际的例子。

@AspectJ

@AspectJ和AspectJ没有任何关系,他只是一组Spring用来解析pointcut和advice的Java 5的注释。也就是说@AspectJ方面(aspect)不依赖与AspectJ;它完全用于Spring AOP。@AspectJ可以非常容易并且方便的创建aspects。例,

Description: Simple @AspectJ Aspect

01 @Aspect
02 public class LoggingAspect {
03   
04     @Around("execution(* com.apress.prospring2.ch06.simple.TestBean.*(..))")
05     public Object log(ProceedingJoinPoint pjp) throws Throwable {
06         System.out.println("Before");
07         Object ret = pjp.proceed();
08         System.out.println("After");
09         return ret;
10     }
11   
12 }

第一个@AspectJ告诉Spring将这个bean当做是一个方面(aspect),从中提取pointcut和advice。下面的@Around结合AspectJ表达式表示log()是一个通知以及这个通知将应用于什么样的pointcut。

Description: The TestBean and Sample Application for the Aspect

01 // TestBean.java
02 public class TestBean {
03   
04     public void work() {
05         System.out.println("work");
06     }
07   
08     public void stop() {
09         System.out.println("stop");
10     }
11 }
01 public class LoggingAspectDemo {
02   
03     public static void main(String[] args) {
04         ApplicationContext ac = new ClassPathXmlApplicationContext(
05             "/META-INF/spring/ataspectjdemo1-context.xml"
06         );
07         TestBean testBean = (TestBean) ac.getBean("test");
08         testBean.work();
09         testBean.stop();
10     }
11 }

最后是配置文件:

Description: ataspectjdemo1-context.xml

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="test" class="com.apress.prospring2.ch06.simple.TestBean"/>
11     <bean class="com.apress.prospring2.ch06.simple.LoggingAspect"/>
12     <aop:aspectj-autoproxy />
13 </beans>

注意配置文件的高亮部分,首先定义了命名空间aop。接下来将LoggingAspect定义为正规的Spring Bean,最后使用<aop:aspectj-autoproxy>,这个元素负责post-processing所有的bean,这表示定义的bean中至少有一块是advice。运行这个例子结果如下

1 Before
2 work
3 After
4 Before
5 stop
6 After

注意这个方面是一个正规的Spring bean,这意味着我们可以利用Spring设置它的依赖对象。下面的例子修改LoggingAspect以便包含自定义消息,

Description: ImprovedLoggingAspect

01 @Aspect
02 public class ImprovedLoggingAspect {
03     private String beforeMessage;
04     private String afterMessage;
05     @Around("execution(* com.apress.prospring2.ch06.simple.TestBean.*(..))")
06     public Object log(ProceedingJoinPoint pjp) throws Throwable {
07         System.out.println(String.format(this.beforeMessage,
08         pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));
09         Object ret = pjp.proceed();
10         System.out.println(String.format(this.afterMessage,
11         pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));
12         return ret;
13     }
14     @PostConstruct
15     public void initialize() {
16         Assert.notNull(this.beforeMessage,
17             "The [beforeMessage] property of [" + getClass().getName() +
18             "] must be set.");
19         Assert.notNull(this.afterMessage,
20             "The [afterMessage] property of [" + getClass().getName() +
21             "] must be set.");
22     }
23     public void setBeforeMessage(String beforeMessage) {
24         this.beforeMessage = beforeMessage;
25     }
26     public void setAfterMessage(String afterMessage) {
27         this.afterMessage = afterMessage;
28     }
29 }

这里我们将这个aspect当做一个普通的Spring bean。我们定义了两个属性和一个使用@PostConstruct注释的方法(可以根据本例的方法名或者简单的添加System.out.println(“Initing…”);发现这个注释用来表示初始化方法)。下面是配置文件:

Description: Modified ApplicationContext Configuration File

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="test" class="com.apress.prospring2.ch06.simple.TestBean"/>
11     <bean class="com.apress.prospring2.ch06.simple.ImprovedLoggingAspect">
12         <property name="beforeMessage" value="Before %s %s"/>
13         <property name="afterMessage" value="After %s %s"/>
14     </bean>
15     <aop:aspectj-autoproxy />
16 </beans>

下面是程序运行结果:

1 Before work []
2 work
3 After work []
4 Before stop []
5 stop
6 After stop []

@AspectJ Aspects in Detail

下面将介绍如何创建pointcut以及advice。首先是pointcut;下面将使用@pointcut表示被@Pointcut注释的方法。使用@pointcut表达式表示@Pointcut注释中使用的代码。如下面代码片段,

1 @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean.*(..))")
2 private void testBeanExecution(){ }

上面的代码中testBeanExecution方法是一个@pointcut,“execution(* com.apress.prospring2.ch06.simple.TestBean.*(..))”表示@pointcut表达式。

Pointcuts

上面的例子我们将一个around advice应用于一个pointcut。我们指定这个pointcut表达是常量,如果我们希望使用相同的pointcut创建其他的advice,我们需要复制这个表达式常量,为了避免重复,可以使用@Pointcut注释创建一个pointcut。例,

Description: Using the @Pointcut Annotation

01 @Aspect
02 public class LoggingAspectPC {
03   
04     private String beforeMessage;
05     private String afterMessage;
06   
07     @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean.*(..))")
08     private void testBeanExecution() { }
09   
10     @Around("testBeanExecution()")
11     public Object log(ProceedingJoinPoint pjp) throws Throwable {
12         System.out.println(String.format(this.beforeMessage,
13         pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));
14         Object ret = pjp.proceed();
15         System.out.println(String.format(this.afterMessage,
16         pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));
17         return ret;
18     }
19   
20     @PostConstruct
21     public void initialize() {
22         Assert.notNull(this.beforeMessage,
23             "The [beforeMessage] property of [" + getClass().getName() +
24             "] must be set.");
25         Assert.notNull(this.afterMessage,
26             "The [afterMessage] property of [" + getClass().getName() +
27             "] must be set.");
28     }
29   
30     public void setBeforeMessage(String beforeMessage) {
31         this.beforeMessage = beforeMessage;
32     }
33   
34     public void setAfterMessage(String afterMessage) {
35         this.afterMessage = afterMessage;
36     }
37 }

上面高亮部分我们创建了@pointcut。现在可以使用相同的@pointcut创建其他的advice了,

01 @Aspect
02 public class LoggingAspectPC {
03     @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean.*(..))")
04     private void testBeanExecution() { }
05     @Around("testBeanExecution()")
06     public Object log(ProceedingJoinPoint pjp) throws Throwable {
07         ...
08     }
09     @After("testBeanExecution()")
10     public void afterCall(JoinPoint jp) {
11         System.out.println("After");
12     }
13     ....
14 }

注意高亮的部分我们在两个advice块上使用了相同的@pointcut。不过需要注意这个@pointcut是私有的,这意味着我们只能在这个类中使用它。下面我们来延伸这个例子:我们将创建一个公共的@pointcut集合然后将他们应用与我们的方面(aspect)。因为@pointcut是一个使用@Pointcut注释的简单方法,所以我们可以创建SystemPointcuts类,如下,

Description: The SystemPointcuts Class

01 public final class SystemPointcuts {
02   
03     private SystemPointcuts() {
04     }
05   
06     @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean2.*(..))")
07     public void testBeanExecution() { }
08   
09     @Pointcut("within(com.apress.prospring2.ch06.simple.TestBean2)")
10     public void fromTestBeanExecution() { }
11 }

注意我们将这个类声明为final并且使用私有的构造函数,因为我们只是希望这个类包含@pointcut方法,我们需要阻止创建这个类实例的可能。下面将显示如何使用这个类,

Description: Usage of the Pointcuts from the SystemPointcuts Class

01 @Aspect
02 public class PointcutDemoAspect {
03   
04     @Around("SystemPointcuts.testBeanExecution()")
05     public Object log(ProceedingJoinPoint pjp) throws Throwable {
06         System.out.println("Before");
07         Object ret = pjp.proceed();
08         System.out.println("After");
09         return ret;
10     }
11   
12     @Around("SystemPointcuts.fromTestBeanExecution()")
13     public Object inTestBean(ProceedingJoinPoint pjp) throws Throwable {
14         System.out.println("In Test Bean>");
15         Object ret = pjp.proceed();
16         System.out.println("<");
17         return ret;
18     }
19 }

上面高亮部分显示我们如何使用SystemPointcuts类中的@pointcut方法。

Description: UML class diagram of TestBean2 and SimpleBean

这张图片显示不出来,不知道为什么,参考书上的吧。

接着创建ApplicationContext配置文件定义test bean和simple bean。

Description: ApplicationContext Configuration File

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="test" class="com.apress.prospring2.ch06.simple.TestBean2">
11         <property name="simpleBean" ref="simple"/>
12     </bean>
13     <bean id="simple" class="com.apress.prospring2.ch06.simple.SimpleBean"/>
14     <bean class="com.apress.prospring2.ch06.simple.PointcutDemoAspect"/>
15     <aop:aspectj-autoproxy/>
16 </beans>

这个示例程序演示了Spring正确的通过@pointcut方法应用通知。

Description: Example Application for the SystemPointcuts Class

01 public class PointcutDemo {
02     public static void main(String[] args) {
03         ApplicationContext ac = new ClassPathXmlApplicationContext(
04             "/META-INF/spring/ataspectjdemo2-context.xml"
05         );
06         TestBean2 testBean = (TestBean2) ac.getBean("test");
07         SimpleBean simpleBean = (SimpleBean) ac.getBean("simple");
08         testBean.work();
09         testBean.stop();
10         simpleBean.sayHello();
11     }
12 }

运行这个示例程序Spring将正确的创建通知的bean。并且正确的应用的@pointcut表达式。下面完整的讨论@pointcut表达式

Pointcut Expressions

虽然@AspectJ pointcut表达式支持AspectJ语法,但是Spring AOP并不支持全部的AspectJ pointcut。下表总结了Spring AOP中可以使用的AspectJ pointcut,

Expression Description
execution Matches method execution join points; you can specify the package, class and method name, its visibility, return type, and type of arguments. This is the most widely used pointcut expression. For example, execution(* com.apress..TestBean. *(..) means to execute any method in TestBean in the com.apress subpackage with any return type and any arguments.
within Matches join points when executed from the declared type. For example, within(com.apress..TestBean) matches calls made from methods of TestBean.
this Matches join points when executed from the declared type. For example, within(com.apress..TestBean) matches calls made from methods of TestBean.
this Matches join points by comparing the type of bean reference (the AOP proxy) with the specified type. For example, this(SimpleBean) will match only calls from a bean of type SimpleBean.
target Matches join points by comparing the type of bean being invoked with the specified type. target(SimpleBean) will match only calls made to a bean of type SimpleBean, for example.
args Matches join points by comparing the method argument types with the specified argument types. As an example, args(String, String) will match only methods with two arguments of type String.
@Target Matches join points by checking that the target class of the invocation has the specified annotation. @target(Magic), for example, will match calls to methods from classes with the @Magic annotation.
@args Similar to args, @args checks the annotations on the method arguments instead of their types. An example of @args is @args(NotNull), which would match all methods with a single argument with the @NotNull annotation.
@within Similar to within, this expression matches join points when executed from a type with the specified annotation. An example of the @within expression might be @within(Magic), which will match calls from within bean of type with the @Magic annotation.
@annotation Matches join points by checking the annotation on the method to be called with the specified annotation. For example, @annotation(Magic) will match calls to all methods with the @Magic annotation.
bean Matches join points by comparing the bean’s ID (or names); you can also use wildcards in the bean name pattern. An example might be bean(“simple”), which will match join points in the bean with ID or name simple.

你可以使用||(disjunction)、&&(conjunction)操作符合并pointcut表达式,或者使用!{not)操作符否定表达式。你可以在@pointcut方法的pointcut表达式中使用这些操作符,

Description: Combining AspectJ Pointcut Expressions

1 execution(* com.apress.prospring2.ch06.simple.TestBean2.*(..)) && within(com.apress.prospring2..*)

你也可以选择使用@Pointcut注释的方法和其他使用@Pointcut注释的方法或者表达式进行合并。

Description: Combining the @Pointcut-Annotated Methods

01 public final class SystemPointcuts {
02   
03     private SystemPointcuts() {
04     }
05   
06     @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean2.*(..))")
07     private void testBeanExec() { }
08   
09     @Pointcut("within(com.apress.prospring2..*)")
10     private void withinProSpringPackage() { }
11   
12     @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean2.*(..)) &&" + "within(com.apress.prospring2..*)")
13     public void same1() { }
14   
15     @Pointcut("execution(* com.apress.prospring2.ch06.simple.TestBean2.*(..)) &&" + "withinProSpringPackage()")
16     public void same2() { }
17   
18     @Pointcut("testBeanExec() && withinProSpringPackage()")
19     public void same3() { }
20 }

上面的例子有两个私有的@pointcut-testBeanExec和withinProSpringPackage,并且在公共的@pointcut same2和same3上应用私有的@pointcuts。

Exploring the Pointcut Expressions

这里有十种类型的pointcut表达式,每种类型都有其特定的语法。即使表达式看起来非常简单,他也会在你使用完整系列的语法时变得相当复杂。

The execution Expression

Spring使用的execution pointcut表达式与AspectJ的execution语法相同,下面是语法定义。

Description: Formal Definition of the Execution Pointcut Expression Syntax

1 execution(modifiers-pattern?
2     ret-type-pattern
3     declaring-type-pattern? name-pattern(param-pattern)
4     throws-pattern?)

问号标记(question mark,?)表示表达式条目是可选的;换句话说我们可以省略他。下面我们尝试分析我们使用过的

1 * com.apress.prospring2.ch06.simple.TestBean2.*(..)

表达式: *号表示任何返回类型(ret-type-pattern),接着是完整的类名(declaring-type-pattern)。紧跟着类名是*(..),他的意思是具有任何名字、任何数量(包括0)任何类型的参数。因为我们没有指定modifiers-pattern和throws-pattern,所以Spring AOP将匹配任何作用域修饰的方法以及抛出任何类型异常的方法。

声明一个匹配在com.apress.prospring2.ch06子包下的任何方法(任何返回值,任何参数,任何异常)需要创建下面的表达式,

01 * com.apress.prospring2.ch06..*.*(..)。这个表达式告诉我么那匹配任何参数(最后的*(..)),任何返回值类型(第一个*),在任何类中(中间的那个*),在任何以com.apress.prospring2.ch06(注意在包名和类名之间的..)开始的包中。下面是一个更现实的例子,</p>
02 <i>Description: A More Realistic Pointcut Expression</i>
03 [java]
04 public final class SystemPointcuts {
05   
06     private SystemPointcuts() {
07     }
08   
09     @Pointcut("execution(* com.apress.prospring2.ch06..*.*(..)) &&" +
10         "!execution(* com.apress.prospring2.ch06..*.set*(..)) &&" +
11         "!execution(* com.apress.prospring2.ch06..*.get*(..))")
12     public void serviceExecution() { }
13     ...
14 }

上面的表达式表示我们可以使用这个pointcut使com.apress.prospring2.ch06.services的所有类中除了setter和getter方法之外的所有方法获得通知。

Tip其实可以使用@Transactional注释将方法标记为业务方法(transactional),这比使用@pointcuts(这个东西指使用@Pointcut注释的方法)更加简单)

The within Expression

within表达式的语法比execution表达式语法简单的多,例,

Description: Syntax of the within Pointcut Expression

1 within(declaring-type-pattern)

在within表达式中你可以使用通常用的..和*号通配符;声明一个匹配com包和他子包中的所有类的被调用的所有方法的pointcut,你需要创建的表达式为“within(com..*)”。

The this Expression

this pointcut表达式语法和within表达式语法类似。唯一的不同是无法使用..和*通配符:this pointcut语义上就像是表示在匹配任何表达式指定类的对象上执行的任何方法,所以我们不能用它匹配任何包和他的子包中的任何类。所以他允许的语法是this(class-name),例如this(com.apress.prospring2.ch06.simple.TestBean2)。

The target Expression

target表达式语法和this表达式语法完全相同。因为target表达式定义了一个匹配与表达式指定类型相匹配的对象上执行任何方法的pointcut。同样不能使用通配符。因此语法是target(class-name),例,target(com.apress.prospring2.ch06.simple.SimpleBean)。

注意:target和this的区别在于target针对目标对象类型以及他的子类,而this是针对代理对象类型以及他的子类的,假设现在有一个接口Wminterface,这个接口定义了一个wm()方法,现在Bean1实现这个接口。在ApplicationContext文件中使用Bean1类型定义一个名字为bean1的bean,然后通过getBean(bean1)获得这个bean。应该是Wminterface类型的,如果用Bean1类型会出现问题(不了解Spring创建bean的机制,但是这就恰恰是我需要的类型),然后可以为两个advice(比如after advice)定义pointcut,@target(Bean1)和@this(Bean1)。运行例子的话将会发现应用于target(Bean1)切入点的advice将被应用。这是目前为止发现他们之间的差异,以后发现以后补充。不过应该只有这点不同了,因为我发现Spring参考中对于他们的定义也是分别用proxy和target object这两个术语来描述的。见spring 2.5 reference第135页。

The args Expression

args表达式的语法是args(type-pattern? (, type-pattern)*);也就是说我们可以指定0个,1个或者多个type-pattern表达式。需要特别注意的一个问题是execution表达式中的argument-pattern将对形参类型进行匹配。而args表达式则对实参类型进行匹配。例,

Description: The SimpleBean Class

1 public class SimpleBean {
2     public void sayHello() {
3         System.out.println("Hello");
4     }
5     public void x(CharSequence a, String b) {
6     }
7 }

切入点execution(* Simple.*(CharSequence, String))将匹配所有对x(“A”, “B”)的调用,因为表达式中的类型名和形参相匹配。但是切入点execution(* SimpleBean.*(String, String))不会和任何方法匹配,即使为方法传递两个String类型的参数。如果希望使用String类型的实参创建一个能够匹配对所有x(CharSequence, String)调用的pointcut,则需要args(String, String)。其实就是说execution表达式中的argument-pattern部分匹配形参类型,而args的type-pattern可以在运行时对实参类型进行匹配。

The @target Expression

@target表达式是需要完成类型名的另外一个例子,并且他需要的类型名应该是@interface(也就是注释类)。例,@target(Magic)或@target(org.springframework.transaction.annotation.Transactional),Magic和Transactional都是使用@Retention(RetentionPolicy.RUNTIME)注释的@interface,并且@target注释包含ElementType.TYPE。他将匹配使用这些注释注释的类的所有方法调用;匹配使用特定annotation注释的方法调用(pointcut)请使用@annotation表达式。

The @within Expression

@within表达式需要完整的@interface类型名,他匹配所有以该@interface注释的目标对象的所有方法。例,@within(StartsTransaction)

The @annotation Expression

@annotation表达式将匹配任何使用它指定的@interface注释的方法。例如,@annotation(Transactional)将匹配所有以Transactional注释的方法。

The @args Expression

@args表达式和args表达式类似;他们之间的不同在于@args匹配的是运行时传递给被调用方法的参数类型是否是使用它指定的@interface注释的。例如定义@args(Magic)切入点和Bean的方法wm(MessageWriter),而MessageWriter是使用@Magic注释的。

The bean Expression

这种表达式是Spring专用的;他将匹配指定的bean id或者name中的所有方法调用。你可以在bean name中使用*通配符。例,匹配所有simple bean中的方法调用可以创建bean(simple)表达式;匹配所有id或者name以Service结束的bean可以创建bean(*Service)表达式。

Using the @Pointcut in XML

因为使用@Pointcut注释定义pointcuts是Spring AOP的特有的,所以我们甚至可以在XML配置文件中使用它们。如果我们希望把所有在TestBean2中的执行的方法都归纳为一种业务,我们可以在<aop:advisor …/>元素中使用SystemPointcuts.testBeanExecution()。下面是配置文件,

Description: Using the @Pointcuts in the XML Configuration File

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:tx="http://www.springframework.org/schema/tx"
05     xmlns:aop="http://www.springframework.org/schema/aop"
06     xsi:schemaLocation="
07         http://www.springframework.org/schema/beans
08         http://www.springframework.org/schema/beans/spring-beans.xsd
09         http://www.springframework.org/schema/aop
10         http://www.springframework.org/schema/aop/spring-aop.xsd
11         http://www.springframework.org/schema/tx
12         http://www.springframework.org/schema/tx/spring-tx.xsd">
13     <bean id="test" class="com.apress.prospring2.ch06.simple.TestBean2">
14         <property name="simpleBean" ref="simple"/>
15     </bean>
16     <bean id="simple" class="com.apress.prospring2.ch06.simple.SimpleBean"/>
17     <aop:config>
18         <aop:advisor advice-ref="tx-advice"
19             pointcut="com.apress.prospring2.ch06.simple.SystemPointcuts.testBeanExecution()"/>
20     </aop:config>
21     <bean id="transactionManager"
22         class="com.apress.prospring2.ch06.simple.NoopTransactionManager"/>
23     <tx:advice id="tx-advice" transaction-manager="transactionManager">
24         <tx:attributes>
25             <tx:method name="*" propagation="REQUIRED"/>
26         </tx:attributes>
27     </tx:advice>
28 </beans>

不必过于关心transactionManager bean和tx:advice;上面代码中重要的部分是高亮显示的部分。我们使用<aop:adviosr …/>元素定义了一个advisor;这个advisor引用SystemPointcuts类中的@Pointcut注释的方法;他的advice是tx:advice。关于声名式事务支持在第16章讨论,现在只需要知道这些方法的执行将成为事务的一部分以及如果方法执行没有抛出异常则advice将被提交到事务就足够了。如果目标方法抛出异常则事务将进行回滚操作。

Types of Advice

现在我们知道了如何创建pointcut表达式(无论是使用@Pointcut注释方法还是在XML配置文件中直接使用pointcut表达式),下面我们将开始了解我们可以使用的advice类型。目前我们已经知道怎么使用around advice(在上面的@AspectJ例子和XML配置文件中的tx:advice)。下面将讨论advice所有的基本类型(before,after,after throwing和around)。

Before Advice

他在方法体(method body)被处理之前执行,但是除非我们抛出异常,否则都将无法避免执行目标方法。这使得before advices适合进行访问控制:我们检查调用者是否允许调用目标,如果不则抛出异常。下面的UML显示了我们用来讨论before advice的类。

Description: UML class diagram for the before advice classes
pro-spring-2-5_advanced-aop_uml-class-diagram-for-the-before-advice-classes

我们现在希望只能在我们logged之后调用StockService的方法。实际上我们将决定保护所有con.apress.prospring2.ch06.services包中的类,以便未来我们不需要在担心安全问题。下面是一个before advice的简单实现,

Description: naive Implementation of the Before Advice

1 @Aspect
2 public class BeforeAspect {
3     @Before("execution(* com.apress.prospring2.ch06.services.*.*(..))")
4     public void beforeLogin() throws Throwable {
5         if (SecurityContext.getCurrentUser() == null)
6             throw new RuntimeException("Must login to call this method.");
7     }
8 }

上面的实现大致看起来很好,没有问题,为了测试advice,我们讲解下面的XML配置文件,

Description: The XML Configuration File for the Before Advice

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService" class="com.apress.prospring2.ch06.services.UserService"/>
11     <bean id="stockService"
12         class="com.apress.prospring2.ch06.services.StockService"/>
13     <bean class="com.apress.prospring2.ch06.before.BeforeAspect"/>
14     <aop:aspectj-autoproxy />
15 </beans>

我们将BeforeAspect定义为Spring bean,并且使用<aop:aspectj-autoproxy/>元素,我们使用下面的代码访问被通知的服务。

Description: Sample Appliction fore the Before Advice

1 public class BeforeDemo {
2     public static void main(String[] args) {
3         ApplicationContext ac = new ClassPathXmlApplicationContext(
4             "/META-INF/spring/beforedemo1-context.xml"
5         );
6         StockService stockService = (StockService) ac.getBean("stockService");
7         System.out.println(stockService.getStockLevel("ABC"));
8     }
9 }

当我们运行应用程序的时候,他将会失败并抛出带有”Must login to call this method”消息的RuntimeException。哈哈,工作正常!下面开始测试在login之后是否可以调用service方法,我们添加获得UserService bean和对login方法的调用,例,

Description: Code Fragment to Get the UserService and login

1 UserService userService = (UserService) ac.getBean("userService");
2 userService.login("janm");

现在,问题出现了,login方法也是属于services包中的类;因此他也会被beforeLogin通知。因此我们没有办法Login,所以我们需要修改我们的pointcut表达式以便执行login方法。虽然我们可以直接写pointcut表达式,但是这里我们创建两个私有的@pointcut表达式并且在before advice的pointcut表达式中使用它们,

Description: Change to the BeforeAspect

01 @Aspect
02 public class BeforeAspect {
03   
04     @Pointcut("execution(* com.apress.prospring2.ch06.services.*.*(..))")
05     private void serviceExecution() { }
06   
07     @Pointcut("execution(* com.apress.prospring2.ch06.services.UserService.login(..))")
08     private void loginExecution() { }
09   
10     @Before("serviceExecution() && !loginExecution()")
11     public void beforeLogin() throws Throwable {
12         if (SecurityContext.getCurrentUser() == null)
13             throw new RuntimeException("Must login to call this method.");
14     }
15 }

现在我们可以成功的运行上面的例子了,我们允许在没有login的情况下调用UserService.login方法,但是我们必须login之后才能调用其他的方法。

After Returning Advice

after returning advice在目标方法正常完成后执行。“正常完成(finish normally)”表示方法没有抛出任何异常。通常使用after returning advice完成某些核查工作。例,

Descrption: After Returning Advice

1 @Aspect
2 public class AfterAspect {
3   
4     @AfterReturning("execution(* com.apress.prospring2.ch06.services.*.*(..))")
5     public void auditCall() {
6         System.out.println("After method call");
7     }
8 }

这个aspect定义了通知所有在com.apress.prospring2.ch06.service包中的类的所有方法调用的after returning advice。

Description: XML Configuration File for the Sample Application

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService" class="com.apress.prospring2.ch06.services.UserService"/>
11     <bean id="stockService"
12         class="com.apress.prospring2.ch06.services.StockService"/>
13     <bean class="com.apress.prospring2.ch06.afterreturning.AfterAspect"/>
14     <aop:aspectj-autoproxy />
15 </beans>

我们定义了两个service bean(userService和stockService)并且还定义了一个AfterAspect bean。匿名AfterAspect bean包含了我们的after returning advice方法auditCall()。我们将在例子中使用这个配置文件。

Description: Sample Application for After Returning Advice

01 public class AfterDemo {
02     public static void main(String[] args) {
03         ApplicationContext ac = new ClassPathXmlApplicationContext(
04             "/META-INF/spring/afterreturningdemo1-context.xml"
05         );
06         UserService userService = (UserService) ac.getBean("userService");
07         userService.login("janm");
08         StockService stockService = (StockService) ac.getBean("stockService");
09         System.out.println(stockService.getStockLevel("ABC"));
10     }
11 }

运行这个程序产生的结果将证明我们的after returning advice-auditCall工作正常。现在问题是我们的after returning advice不知道任何关于调用的信息,只知道匹配的方法正常执行完毕。无法了解是哪个方法,不知道他的返回值。我们可以使用JoinPoint参数或者使用@AfterReturning annotation的返回字段修改advice。例,

Description: Using the JoinPoint Argument

1 @Aspect
2 public class AfterAspect {
3   
4     @AfterReturning("execution(* com.apress.prospring2.ch06.services.*.*(..))")
5     public void auditCall(JoinPoint jp) {
6         System.out.println("After method call of " + jp);
7     }
8 }

我们可以使用JoinPoint参数作为接口查找方法返回后的一些信息:其中可能最感兴趣的信息是我们可以从JoinPoint中获得当前执行的方法和他的参数。唯一比较郁闷的事情使我们无法通过JoinPoint获得返回值。为了获得返回值我们必须停止使用JoinPoint参数并且明确的设置@AfterReturning注释的returning()和argNames()属性。例,

Description: Using the returning() and argNames() Properties

01 @Aspect
02 public class AfterAspect {
03   
04     @AfterReturning(
05         pointcut = "execution(* com.apress.prospring2.ch06.services.*.*(..))",
06         returning = "ret", argNames = "ret")
07     public void auditCall(Object ret) {
08         System.out.println("After method call of " + ret);
09         if (ret instanceof User) {
10             ((User)ret).setPassword("****");
11         }
12     }
13 }

注意我们通过定义@AfterReturning注释的属性(pointcut(),returning(),argNames())来创建功能更强大的after returning advice。我们可以看到方法调用的返回值。我们不能直接修改返回值,但是如果返回类型允许,我们可以改变返回值的属性值。这非常像试图修改函数参数的值并把修改后的值传递给正在执行的代码。

After Throwing Advice

After Throwing advice在目标方法抛出异常后执行。我们可以利用after trhowing advice方位抛出的异常。通过这类advice的参数我们还能对异常类型进行过滤。

Description: IOException After Throwing Advice

1 @Aspect
2 public class AfterThowingAspect {
3     @AfterThrowing(
4     pointcut = "execution(* com.apress.prospring2.ch06.services.*.*(..))",
5         throwing = "ex", argNames = "ex")
6     public void logException(IOException ex) {
7         System.out.println("After method call of " + ex);
8     }
9 }

请注意上面代码中的高亮部分:将@AfterThrowing通知的throwing()属性值指定为将要接收的异常,并且将符合下面声明的参数名的至指定为argNames()属性值。现在运行下面的应用程序将会失败,因为如果StockService.getStockLevel(String)方法的参数为null则他将抛出NullPointerException异常。

Description: Sample Application for the After Throwing Advice

01 public class AfterThrowingDemo {
02     public static void main(String[] args) {
03         ApplicationContext ac = new ClassPathXmlApplicationContext(
04             "/META-INF/spring/afterthrowingdemo1-context.xml"
05         );
06         UserService userService = (UserService) ac.getBean("userService");
07         userService.login("janm");
08         StockService stockService = (StockService) ac.getBean("stockService");
09         System.out.println(stockService.getStockLevel(null));
10     }
11 }

虽然上面的例子创建了after throwing advice(并且在afterthrowingdemo1-context.xml中正确的配置了bean,但是advice不会被执行,因为我们捕捉的异常类型是IOException而不是NullPointerException。如果将advice的参数改为Exception,然后运行改程序,将产生下面结果,

1 ...
2 Exception in thread "main" java.lang.NullPointerException
3 After method call of java.lang.NullPointerException
4 at com.apress.prospring2.ch06.services.StockService.getStockLevel(StockService.java:9)
5 ...

After Advice

最终的after advice是after,或者说最后的通知。他不管目标发放是否正常完成(或者抛出异常)都会被执行。不过他没有办法访问方法的返回值或者任何抛出的异常。通常使用finally advice释放资源。类似于Java中的try / catch / finally的结构。

Around Advice

历史上功能做强大、最复杂的advice就是这个了。他在目标方法调用周围(前后)执行。因此,这个advice至少需要一个参数,并且必须有返回值。参数指定被调用的目标,返回值指定目标的返回值,无论这个返回值出自哪里(暗示可以修改返回值,偷笑~)。你将总是在事务管理方面的代码中发现around advice:advice将被应用到事务开始时并且在处理目标调用之前。如果目标正常返回,则advice提交事务;如果发生任何异常,他将完成回滚(rollback)操作。其他的例子还有在缓存中使用这类advice。

Description: Around Advice for Caching

01 @Aspect
02 public class CachingAspect {
03   
04     private Map<MethodAndArguments, Object> cache =
05         Collections.synchronizedMap( new HashMap<MethodAndArguments, Object>());
06     private Object nullValue = new Object();
07   
08     private static class MethodAndArguments {
09         private Object target;
10         private Object[] arguments;
11         private MethodAndArguments(Object target, Object[] arguments) {
12             this.target = target;
13             this.arguments = arguments;
14         }
15   
16         public boolean equals(Object o) {
17             if (this == o) return true;
18             if (o == null || getClass() != o.getClass()) return false;
19             MethodAndArguments that = (MethodAndArguments) o;
20             return Arrays.equals(arguments, that.arguments) &&
21                 target.equals(that.target);
22         }
23   
24         public int hashCode() {
25             int result;
26             result = target.hashCode();
27             result = 31 * result + Arrays.hashCode(arguments);
28             return result;
29         }
30     }

上面的缓存实现肯定不是企业级的,但是对于演示Around advice来说已经足够了。他使用一个ProceddingJointpoint作为参数并且返回一个Object;这个参数将给我们任何关于目标方法执行的信息,并且调用目标方法的代码将接受从advice中返回的返回值。

你可能想知道我们是否必须调用目标方法或者是否可以调用多次;答案是YES。我们可以在这个advice中做任何我们想做的事情:我们可以再通知中重新进行处理,尝试多次调用目标方法。或者完全的跳过目标方法的调用。

Argument Binding

你可能会发现有时需要从Advice中访问传递给目标的参数值。前面我们看到了如何访问返回值;实际上你也可以使用JoinPoint.getArgs()访问参数。不过你也可能觉得需要做的工作很多,因为获得的参数一个Object[],我们可能需要进行边界检查以及类型转换。幸运的是@AspectJ支持binding,我们可以在通知中从绑定目标的参数。例,

Descrption: An Aspect with Bound Arguments

01 @Aspect
02 public class BindingAspect {
03     @Around(value =
04         "execution(* com.apress.prospring2.ch06.services.StockService.*(..)) " +
05         "&& args(cutoffDate, minimumDiscount)",
06         argNames = "pjp, cutoffDate, minimumDiscount")
07     public Object discountEnforcement(ProceedingJoinPoint pjp, Date cutoffDate,
08         BigDecimal minimumDiscount) throws Throwable {
09   
10         return pjp.proceed();
11     }
12 }

这里的discountEnforcement通知将在StockService类执行任何方法时执行,args从句将决定这个方法需要两个参数,这两个参数分别是Date类型的cutoffDate参数和BigDecimal类型的minimumDiscount参数(可以从advice方法的参数推导出来)。除了从调用目标中得到这两个参数之外,advice方法还接收一个ProceedingJoinPoint实例。对了,还要注意一下@Around注释的argNames()属性,他明确的指定了advice方法的参数名-pjp,cutoffDate和minimumDiscount。Spring AOP将利用argNames()决定哪个参数从pointcut表达式中获得绑定的值。

Note:使用argNames()的原因是,一旦Java code编译成为字节码(bytecode),就不可能确定参数名了;我们只能获得它的索引和类型。不过我们可以求助于pointcut和bind expressions;argNames()方法表现了将参数名映射(解析)到索引一种机制。

其实如果JointPoint(或者他的子接口)是advice方法的第一个参数,就可以从argNames()属性中省略他,但是不推荐这么做。因为稍微不小心就会忘记JoinPoint需要作为第一个参数;况且一些IDE会在argNames不匹配advice方法的参数时报错。

Description: IntelliJ IDEA reporing a problem with argNames()
intellij-idea-reporting-a-problem-with-argnames-method

如果你觉得argNames用起来不方便,没有指定他的话,Spring AOP将使用class文件中的debug信息决定参数名。这种能力通常不是默认开启的,但是你可以使用javac的-g:vars指示开启参数名debug信息。这样做除了可以允许Spring AOP确定参数名之外,它通常会少许的增加编译后的*.class文件尺寸,但这通常不会有什么根本上的问题。

使用-g:vars指示编译的*.class文件有一个比较棘手的问题:反编译后产生的代码更加易读以及编译器没有办法应用任何优化移除无用的参数。如果你编写的是开源项目,第一个问题就没什么了;而第二个问题如果你能够在编写代码的时候小心一些,不要使用没有的参数,也不会造成任何问题。如果你的class文件不包含必须的debuging信息,Spring将尝试使用参数类型去匹配他们。在上面的例子中,Spring可以很容易的做这样的匹配:ProceedingJoinPoint和Date以及BigDecimal包含完全不同的类型信息,Date和BigDecimal也是完全不同的类。如果Spring无法安全的使用参数类型进行匹配(例如,被通知的方法也有一个ProceedingJoinPoint的参数),他将抛出AmbiguousBindingException。

Introductions

在Spring AOP可用的功能集中,Introductions是非常重要的一部分。使用introductions,你可以动态的向现有的对象添加新的功能。在Spring中,你可以向一个已有对象中引入(introduce)任何接口的实现。对于为什么这种功能非常有用,为什么你已经可以在开发时简单的添加函数还需要动态的在运行时添加新的函数你可能会觉得奇怪。其实这个问题的答案非常简单,当你需要动态添加切面函数时使用传统的OO编程会比较困难。

Spring文档给出了两个典型的使用introduction的例子:object locking和modification detection。在Spring文档中的object locking例子中,使用了一个接口,Lockable,这个接口定义了用于锁定或解锁对象以及查询对象状态的方法。这个接口使应用程序可以锁定一个对象以便保证这个对象的内部状态不能被修改。你可以为你希望能够被锁定的类实现这个接口。可是这样的结果将导致在很多不同的class中有很多重复的代码。当然,你也可以将实现重构只一个抽象的基类,but you lose your one short(one-short:一次性的) at concrete inheritance(不知道什么意思,莫非是暗指可以在基类中重载这些方法,所以可能会导致一些混乱什么的。),并且你仍然需要在任何需要修改对象状态的方法中检查锁定状态。显然的,这并不是一个完美的解决方案,并且可能引起潜在的bug和造成维护的困难。

使用introductions,你可以克服所有上面列出的问题。你可以在单一的类中集中实现Lockable并且在运行时使任何对象采用这个Lockable实现。不仅仅是对象采用这个Lockable实现,还会使他变为Lockable的一个实例并且通过Lockable接口的instanceof测试,即使这个类没有实现Lockable接口。

introduction实际上是一个方法拦截器(method interceptor)的扩展,他可以拦截对象上所有需要和introduction配合的方法。使用这个功能,你可以在调用任何setter方法之前检查锁定状态,并且在对象被锁定时抛出一个异常。所有有关的代码将被囊括在单独的一个地方,并且任何Lockable对象都不需要知道相关细节。

Introductions是在应用程序中提供声明式服务的关键。例如,如果你建立一个需要Lockable接口的应用程序,使用introductions,你可以声明式的定义那些对象需要和Lockable相互配合。

关于Lockable例子的实现查阅Spring文档(reference)。下面将把注意力集中在Spring文档中未实现的例子-object modification detection。

Call Tracking with Introductions

对象修改检测(Object modification detection)在很多情况下都是非常有用的技术。典型的是你可以在持久化对象数据时通过修改检测避免不必要的数据库访问。如果一个对象传递给了一个方法进行修改,但是他并未被修改,这时发送一个更新数据库的语句是没有什么意义的。这种情况下使用修改检测可以增加应用程序吞吐量,特别是当数据库已经有大量负载的情况下或者在远程网络中进行通信操作时。

不幸的是这样的功能非常难以实现,因为你必须为每一个可以修改对象状态的方法添加用来检查对象状态是否真的被修改了的状态检查。当你考虑检查值是否被真的改变时,你会发现你至少需要为每个方法添加八行代码。你可以把这些代码重构为单一的方法,但是你让然要在你需要进行检查的方法中调用这些方法。在一个典型的应用程序中总是有许多不同的类需要修改检查,并且维护类似的应用程序将变得十分困难。

这明显的就是使用introductions的地方。我们不想使所有需要进行修改检测的类都继承某一个基础的实现,这样会浪费他们唯一的继承机会(只能继承自一个类),也不想为每一个修改状态的方法添加检查代码。这里应该使用introductions,我们就可以提供一个灵活的解决方案进行修改检查而不需要编写那些重复的,容易出错的代码。

下面的例子我们将使用introductions建立统计框架。CallTracker接口包含所有的修改检查逻辑,他的实现将引入到适当的对象中,通过拦截逻辑自动完成状态修改检查。

The CallTracker Interface

这个方案的中心是CallTracker接口,我们的应用程序使用它去跟踪自身的状况。我们不去研究应用程序如何使用CallTracker;而是集中注意力研究introduction的实现。

Description: The CallTracker Interface

01 public interface CallTracker {
02   
03     void markNormal();
04   
05     void markFailing();
06   
07     int countNormalCalls();
08   
09     int countFailingCalls();
10   
11     String describe();
12 }

这里没有什么特别的地方,mark*方法简单的增加正常调用和失败调用的计数,count*方法返回适当的计数值。

Creating a Mix in

下一步是创建CallTracker的实现和需要引入的对象;这种行为暗含组合(mix in)的意思。

Description: The DefaultCallTracker Class

01 public class DefaultCallTracker implements CallTracker {
02   
03     private int normalCalls;
04     private int failingCalls;
05   
06     public void markNormal() {
07         this.normalCalls++;
08     }
09   
10     public void markFailing() {
11         this.failingCalls++;
12     }
13   
14     public int countNormalCalls() {
15         return this.normalCalls;
16     }
17   
18     public int countFailingCalls() {
19         return this.failingCalls;
20     }
21   
22     public String describe() {
23         return toString();
24     }
25   
26     @Override
27     public String toString() {
28         final StringBuilder sb = new StringBuilder();
29         sb.append("DefaultCallTracker");
30         sb.append("{normalCalls=").append(normalCalls);
31         sb.append(", failingCalls=").append(failingCalls);
32         sb.append('}');
33         return sb.toString();
34     }
35 }

上面例子中首先需要注意的事情就是CallTracker的实现有两个私有的normalCalls和failingCalls字段组成。这个例子强调了为什么你需要为每一个被通知的对象都创建一个组合实例:the mix in introduces not only methods to the object but also a state。(看不太明白,不过大致意思应该是说不仅仅组合引入的方法到对象,而且还有引入的状态。)如果你在很多不同的对象中组合单一实例,你同样共享了实例的状态,这意味着所有的对象第一次显示他们被修改的时候可能已经被修改过了。(哈哈,不知道对不对)

Creating the Mix-In Aspect

下一步是创建包含after returning和after throwing advice的aspect以及组合声明。

Descrption: Creating Advice for the Mix-In

01 @Aspect
02 public class CallTrackerAspect {
03   
04     @Pointcut("execution(* com.apress.prospring2.ch06.services.*.*(..))")
05     private void serviceCall() { }
06   
07     @DeclareParents(
08         value = "com.apress.prospring2.ch06.services.*",
09         defaultImpl = DefaultCallTracker.class)
10     public static CallTracker mixin;
11   
12     @AfterReturning(
13     value = "serviceCall() && this(tracker)",
14         argNames = "tracker")
15     public void normalCall(CallTracker tracker) {
16         tracker.markNormal();
17     }
18   
19     @AfterThrowing(
20         value = "serviceCall() && this(tracker)",
21         throwing = "t",
22         argNames = "tracker, t")
23     public void failingCall(CallTracker tracker, Throwable t) {
24         tracker.markFailing();
25     }
26 }

这个aspect中新的代码是注释mixin字段的@DeclareParents;他声明所有com.apress.prospring2.ch06.services中的类都是用DefaultCallTracker实现引入CallTracker.

下面我们定义@pointcut(切入点方法)serviceCall()(因为我们在两个不同的advice中使用它)

然后我们使用serviceCall() && this(tracker)创建after returning advice。serviceCall()引用@pointcut,this(tracker)将匹配所有实现了CallTracker接口的对象中的所有方法的执行(因为tracker表示advice method参数中的CallTracker)。Spring AOP将绑定tracker参数到已经通知的对象,因此我们可以在通知方法(advice body)中使用tracker参数。

最后,我们使用相同的切入点表达式(serviceCall() && this(tracker))创建after throwing advice;Spring AOP将绑定tracker参数到failingCall advice方法的参数。除了CallTracker参数,我们还在@AfterThrowing注释定义了我们希望接受的异常作为failingCall方法的第二个参数。Spring AOP从参数类型中推断出异常的类型(本例是Throwable)

Putting It All Together

现在组合introduces的aspect和适当的advice,我们可以使用UserService和StockService bean创建一个简单的应用程序。

Description: The Introductions Sample Application

01 public class IntroductionDemo {
02   
03     public static void main(String[] args) {
04         ApplicationContext ac = new ClassPathXmlApplicationContext(
05             "/META-INF/spring/introductionsdemo1-context.xml"
06         );
07         UserService userService = (UserService) ac.getBean("userService");
08         describeTracker(userService);
09         userService.login("janm");
10         userService.setAdministratorUsername("x");
11         describeTracker(userService);
12         StockService stockService = (StockService) ac.getBean("stockService");
13         describeTracker(stockService);
14         try {
15             stockService.getStockLevel(null);
16         } catch (Exception ignored) {
17         }
18         System.out.println(stockService.getStockLevel("ABC"));
19         stockService.applyDiscounts(new Date(), new BigDecimal("10.0"));
20         describeTracker(stockService);
21     }
22   
23     private static void describeTracker(Object o) {
24         CallTracker t = (CallTracker)o;
25         System.out.println(t.describe());
26     }
27 }

上面使用以前的例子中的userService和stockService bean:我们调用userService.login(“janm”),接着调用userService.setAdministratorUsername(“x”);对于stockBean bean的调用是一样的。不过我们现在可以将userService和stockService转换为CallTracker;他们现在都实现了新引入的接口。配置文件:

Description: XML Configuration File for the Introductions Demonstration

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10   
11     <bean id="userService"
12         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
13   
14     <bean id="stockService"
15         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
16   
17     <bean class="com.apress.prospring2.ch06.introductions.CallTrackerAspect"/>
18   
19     <aop:aspectj-autoproxy />
20   
21 </beans>

Spring AOP将分别代理DefaultUserService和DefaultStockService。下面将详细的研究一下DefaultUserService:他只实现了UserService接口,但是之后应用CallTrackerAspect的userService bean的类型是JdkDynamicAopProxy-他不是DefaultUserService的实例。它同时实现了UserService接口和CallTracker混合接口。他将拦截所有的方法调用并将针对UserService接口的调用委托给DefaultUserService。这个代理也将创建使用@DeclareParents注释定义的DefaultCallTracker的实例并将所有针对CallTracker接口的调用委托给该实例。下面是Demo程序的运行结果。

01 before userService.login("janm"):
02 DefaultCallTracker{normalCalls=0, failingCalls=0}
03   
04 after userService.setAdministratorUsername("x"):
05 DefaultCallTracker{normalCalls=2, failingCalls=0}
06   
07 before stockService.getStockLevel(null):
08 DefaultCallTracker{normalCalls=0, failingCalls=0}
09 193734
10   
11 after stockService.applyDiscounts(...):
12 DefaultCallTracker{normalCalls=2, failingCalls=1}

上面的输出验证了CallTracker接口被引入所有的com.apress.prospring2.ch06.services包中的类并且我们可以从任何通知到的类中获得DefaultCallTracker的实例。

Introductions Summary

Introductions是Spring AOP中的一个非常强大的功能;它不仅仅是允许你扩展已经存在的方法的功能,而是允许动态的扩展对象实现的接口。使用introductions是 实现你的应用程序结合已经定义的接口横切逻辑的完美方法。一般情况下这种类型的逻辑通常使用声明式的而不是编程式的。

因为introductions通过代理工作,它增加确定数量的性能损失,并且所有代理的方法都会考虑被通知,因为代理需要在运行时将每个调用路由至合适的目标对象。无论如何,使用introductions你可以实现很多类型的服务,这些性能损失只是减少实现服务所需代码的代价,他还通过完全集中服务逻辑为你的应用程序增加稳定性和可维护性。

The Aspect Life Cycle

到现在为止我们创建的aspect都是单例的(singletons),更准确的说Spring将只使用一个@Aspect注释的bean并且使用这个aspect的单一实例通知(advice)所有的目标。例,

Descrption: Simple Singleton Aspect

01 @Aspect
02 public class CountingAspect {
03   
04     private int count;
05   
06     @Before("execution(* com.apress.prospring2.ch06.services.*.*(..))")
07     public void count() {
08         this.count++;
09         System.out.println(this.count);
10     }
11 }

下面是Demo应用程序代码清单。他获得userService和stockService bean并且调用两次userService.login()方法,接着调用stockService.getStockLevel()方法,

Description: Sample Application for the Singleton Aspect

01 public class LifecycleDemo {
02   
03     public static void main(String[] args) {
04         ApplicationContext ac = new ClassPathXmlApplicationContext(
05             "/META-INF/spring/lifecycledemo1-context.xml"
06         );
07         UserService userService = (UserService) ac.getBean("userService");
08         StockService stockService = (StockService) ac.getBean("stockService");
09         for (int i = 0; i < 2; i++) {
10             userService.login("janm");
11         }
12         stockService.getStockLevel("A");
13     }
14 }

下面是配置文件,他简单的声明了userService,stockService和CountingAspect beans

Description: XML Configuration for the LifecycleDemo Application

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService"
11     class="com.apress.prospring2.ch06.services.DefaultUserService"/>
12     <bean id="stockService"
13     class="com.apress.prospring2.ch06.services.DefaultStockService"/>
14     <bean class="com.apress.prospring2.ch06.lifecycle.CountingAspect"/>
15     <aop:aspectj-autoproxy />
16 </beans>

现在运行这个应用程序,他将打印1,2,3。这证明了aspect只有单一的实例。如果我们希望为不同的目标保持状态,我们需要改变aspect bean的作用域为prototype。如果我们按照下面修改XML配置文件中的CountingAspect bean的定义,

1 <bean class="com.apress.prospring2.ch06.lifecycle.CountingAspect"
2     scope="prototype"/>

然后运行应用程序将打印1,2,1:现在应用程序将为每个目标对象维护一个aspect的实例。像这样执行apsect,我们可以明确的在@Aspect注释中使用perthis表达式:

1 @Aspect("perthis(execution("+
2                 "* com.apress.prospring2.ch06.services.UserService.*(..)))")
3 public class PertargetCountingAspect {
4 }

这样修改的结果将导致Spring AOP为每一个被通知的对象创建一个新的aspect实例。

Spring AOP支持的其他生命周期策略是pertarget。他和perthis类似,除了他是为匹配joinpoints的每一个唯一的目标对象创建一个新的aspect实例

Framework Services for AOP

到目前为止我们都是使用Spring支持的@AspectJ创建方面(aspect)。我们依赖基本的Spring AOP框架服务处理被注释的bean并且使用这些bean。如果你无法使用注释(annotations)(可能你使用的是1.5版本前的JDK或者你不喜欢他们),你可以使用XML配置中的aop命名空间。这部分将讨论XML配置并且揭开Spring AOP的面纱以便研究如何将@AspectJ注释的bean转换为Spring AOP可用的其他的格式

Creating Our First Aspect Using the aop Namespace

首先让我们只使用XML配置文件创建一个aspect。

Description: XML Configuration File

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService"
11         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
12     <bean id="stockService"
13         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
14     <bean id="aspectBean" class="com.apress.prospring2.ch06.xml.AspectBean"/>
15     <aop:config>
16         <aop:pointcut id="serviceCall"
17             expression="execution(* com.apress.prospring2.ch06.services.*.*(..))"/>
18         <aop:aspect id="firstAspect" ref="aspectBean">
19             <aop:before method="logCall" pointcut-ref="serviceCall"/>
20         </aop:aspect>
21     </aop:config>
22 </beans>

上面高亮部分的代码声明了匹配con.apress.prospring2.ch06.services包中所有类的使用任何参数的任何方法调用的pointcut。和我们使用@AspectJ编写的代码类似;下面是AspectBean需要的代码,

Description: AspectBean Code

1 public class AspectBean {
2   
3     public void logCall(JoinPoint jp){
4         System.out.println(jp);
5     }
6 }

这些代码和使用@AspectJ也非常相似,唯一不同的是这里没有任何注释。下面是DEMO程序

Description: Sample Application for the XML aop Support

01 public class XmlDemo1 {
02   
03     public static void main(String[] args) {
04         ApplicationContext ac = new ClassPathXmlApplicationContext(
05             "/META-INF/spring/xmldemo1-context.xml"
06         );
07         UserService userService = (UserService) ac.getBean("userService");
08         StockService stockService = (StockService) ac.getBean("stockService");
09         userService.login("janm");
10         stockService.getStockLevel("A");
11     }
12 }

这个DEMO程序的代码和使用 使用@AspectJ创建的aspect的代码一样;换句话说使用XML配置是完全透明的,调用代码完全不需要知道它调用的是接收通知的bean。下面是运行结果,

1 > userService.login("janm"):
2     org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint: execution(login)
3   
4 > stockService.getStockLevel("A)":
5     org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint: execution(getStockLevel)

我们可以看到userService和stockService被正确的通知和AspectBean.logCall()方法的执行。

Pointcuts in the aop Namespace

现在来研究一下定义pointcut的配置条目。要定义一个pointcut,你需要使用<aop:config>元素的子元素<aop:pointcut …/>。下面是上面例子配置文件中AOP定义部分

Description: Aop Section of the XML Configuration File

1 ...
2     <aop:config>
3         <aop:pointcut id="serviceCall"
4         expression="execution(* com.apress.prospring2.ch06.services.*.*(..))"/>
5         <aop:aspect id="firstAspect" ref="aspectBean">
6             <aop:before method="logCall" pointcut-ref="serviceCall"/>
7         </aop:aspect>
8     </aop:config>
9 ...

上面代码中的高亮部分定义了一个id为serviceCall的poincut,在下面定义的before advice firstAspect中使用这个pointcut。像使用@AspectJ一样,我们可以引用一个使用pointcut表达式定义的@pointcut(也就是pointcut方法)。例,

Description: @pointcut Definition

1 public final class Pointcuts {
2   
3     private Pointcuts() {
4     }
5   
6     @Pointcut("execution(* com.apress.prospring2.ch06.services.*.*(..))")
7     public void serviceExecution() { }
8 }

我们可以在XML配置文件中使用Pointcuts.serviceExecution()这个@pointcut。如,

1 <aop:pointcut id="serviceCall"
2     expression="com.apress.prospring2.ch06.xml.Pointcuts.serviceExecution()"/>

这样做唯一的缺点是实际上还是使用了注释。我们相信最好的选择是在你的应用程序中只使用一种风格的配置方式。混合使用@AspectJ和XML配置很快就会使你的管理变得十分复杂。

回到XML配置,你可以使用Spring AOP支持的任何pointcut表达式。

Creating Advice Using the aop Namespace

在XML配置文件中创建Advice和使用@AspectJ类似;唯一的不同是表现aspect的bean是Spring简单bean并且配置信息分别在XML文件和aspect bean的Java代码中。aop命名空间和@AspectJ一样强大。上个例子中我们已经看到了简单的before advice;下面将详细的研究XML advice。就像@AspectJ advice,XML dvice需要一个pointcut。你可以使用advice定义中的pointcut-ref属性引用一个已经存在的pointcut,或者使用advice定义中的pointcut属性直接指定pointcut表达式。例,

Descrption: Using pointcut-ref and pointcut Attributes

1 ...
2 <aop:aspect id="firstAspect" ref="aspectBean">
3     <aop:before method="logCall" pointcut-ref="serviceCall"/>
4 </aop:aspect>
5 <aop:aspect id="firstAspect" ref="aspectBean">
6     <aop:before method="logCall"
7         pointcut="execution(* com.apress.prospring2.ch06.services.*.*(..))"/>
8 </aop:aspect>
9 ...

上面代码的第一部分引用一个使用<pointcut>元素声明的已经存在的pointcut;第二部分直接指定了pointcut表达式。

Before Advice

声明before advice,我们需要创建<aspect>元素的子元素<before>。<before>元素的属性分别是method,pointcut或pointcut-ref,和arg-names。你必须设置method和pointcut或者pointcut-ref属性。method属性是指你在<aspect>元素中引用的bean的公共方法名;他的参数可以使不同类型,从没有参数到只有JoinPoint,到具有所有你在pointcut表达式中绑定的参数。如果你使用参数绑定。你应该考虑使用arg-names属性帮助Spring AOP框架正确的表示参数的边界。

After Returning Advice

创建after returning advice需要创建<advice>元素的子元素<after-returning>。就像before advice,你需要设置method和pointcut或pointcut-ref属性。大部分时候你可能想要知道返回值;为了获得返回值需要设置returning属性为该通知方法的Object参数名。最终,你同样可以使用arg-names属性指定通过method属性指定的方法的参数名。下面的例子将创建一个审核任何方法调用并且检查一些返回值的advice。

Description: Using the After Returning Advice

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService"
11         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
12   
13     <bean id="stockService"
14         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
15   
16     <bean id="aspectBean" class="com.apress.prospring2.ch06.xml.AspectBean"/>
17   
18     <aop:config>
19         <aop:aspect id="firstAspect" ref="aspectBean">
20             <aop:after-returning method="auditCall"
21                 pointcut="execution(* com.apress.prospring2.
22                     ch06.services.*.*(..)) &amp;&amp; target(target)"
23                 arg-names="target, ret"
24             returning="ret"/>
25         </aop:aspect>
26     </aop:config>
27 </beans>

注意pointcut表达式指定services包中的任何类的任何方法调用并且绑定调用目标到一个名为target的参数。我们同样在AspectBean.auditCall()方法中接收目标的返回值。关注arg-names属性,你可以清楚的看到auditCall方法需要两个参数,并且因为我们想要接收任何目标类型和任何返回值类型,所以这些参数的类型必须是Object。下面是AspectBean.auditCall的代码,

Description: The AspectBean auditCall Method

1 public class AspectBean {
2   
3     public void auditCall(Object target, Object ret) {
4         System.out.println("After method call of " + ret);
5         if (ret instanceof User) {
6             ((User)ret).setPassword("****");
7         }
8     }
9 }

现在我们的auditCall方法有两个参数:target和ret。注意XML配置文件是按照正确的顺序指定的参数名。这里十分容易出错。<after-returning>元素给出了和@AfterReturning注释一样的一些配置选选项。

After Throwing Advice

现在你知道after throwing advice仅在当与之匹配的方法抛出异常时执行。我们可以使用<aspect>元素的子元素<after-throwing>创建after throwing advice。你至少需要设置method,pointcut或pointcut-ref和throwing属性。大部分时候同样会设置arg-names属性。例,

Description: After Throwing Advice in XML

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10   
11     <bean id="userService"
12         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
13   
14     <bean id="stockService"
15         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
16   
17     <bean id="aspectBean" class="com.apress.prospring2.ch06.xml.AspectBean"/>
18   
19     <aop:config>
20         <aop:aspect id="afterThrowingAspect" ref="aspectBean">
21             <aop:after-throwing method="healthMonitoring"
22                 pointcut="execution(* com.apress.prospring2.
23                     ch06.services.*.*(..)) &amp;&amp; target(target)"
24                 arg-names="target,ex"
25                 throwing="ex"/>
26         </aop:aspect>
27     </aop:config>
28 </beans>

这个after throwing advice绑定调用的目标和他抛出的异常到healthMonitoring方法的参数。这意味着该方法需要两个参数;为了捕捉任何类型目标的任何异常,参数类型需要被设置为Object和Throwable。不过可以使用参数类型进行过滤,如,

Description: After Throwing Method and Argument Filtering

1 public class AspectBean {
2   
3     public void healthMonitoring(Object target, NullPointerException ex) {
4         System.out.println("Target " + target + " has thrown " + ex);
5     }
6 }

注意高亮部分的参数类型是NullPointerException,所以该advice将只在目标抛出NullPointerException时运行。

After Advice

after或者说finally advice在目标方法执行完成后执行,不管目标方法是否正常结束或者抛出异常。使用<aspect>元素的子元素<after>创建这样的advice;因为他是after advice,所以我们只需要设置method和pointcut或pointcut-ref属性。如果需要参数绑定我们可以使用arg-names属性。因为这类advice的本质,我们不能使用returning属性和throwing属性。例,

Description: After Advice Configuration

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService"
11         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
12   
13     <bean id="stockService"
14         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
15   
16     <bean id="aspectBean" class="com.apress.prospring2.ch06.xml.AspectBean"/>
17   
18     <aop:config>
19         <aop:aspect id="afterAspect" ref="aspectBean">
20             <aop:after-throwing method="after"
21                 pointcut="execution(* com.apress.prospring2.
22                     ch06.services.*.*(..)) &amp;&amp; target(target)"
23                 arg-names="target"/>
24         </aop:aspect>
25     </aop:config>
26 </beans>

通知方法(AspectBean.after)只需要一个参数,并且因为我们只想通知到UserService bean,所以我们声明他为public void after(UserService target)

Around Advice

和名字暗示的一样,这个方法在与之匹配的方法调用周围(前后)执行,所以你可以写出在匹配方法之前执行的代码(这意味着你可以操控覅方法的参数或者甚至是完全的跳过方法执行)。你可以写出在匹配的方法完成执行后执行的代码;你可以自由的使用标准try/catch块捕获任何异常。你可以操控返回值,返回类型是通过与之匹配的方法的返回值决定的(比如说被通知的方法返回Number,你可以在通知中返回Long,但是不能返回Stirng)。使用<aspect>元素的子元素<around>声明around advice,你必须指定method和pointcut或pointcut-ref属性,并且你可以设置arg-names属性。例,

Description: XML Configuration of Around Advice

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService"
11         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
12     <bean id="stockService"
13         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
14     <bean id="aspectBean" class="com.apress.prospring2.ch06.xml.AspectBean"/>
15   
16     <aop:config>
17         <aop:aspect id="aroundAspect" ref="aspectBean">
18             <aop:around method="censorStringArguments"
19                 pointcut="execution(* com.apress.prospring2.ch06.
20                     services.*.*(..)) and args(argument)"
21                 arg-names="pjp, argument"/>
22         </aop:aspect>
23     </aop:config>
24 </beans>

注意我们定义了两个参数,但是并没有在pointcut表达式中使用pjp参数。因为around advice方法必须有至少一个ProceedingJoinPoint参数(否则你将不能调用所匹配的方法)并且必须返回Object。除了ProceedingJoinPoint参数,你可以自由的使用任何你需要的参数数量。例,

Description: Around Advice Method

01 public class AspectBean {
02   
03     public Object censorStringArguments(ProceedingJoinPoint pjp, String argument) throws Throwable {
04         Object[] arguments;
05         if (argument != null) {
06             System.out.println("censored " + argument + "!");
07             arguments = new Object[] { "****" };
08         } else {
09             arguments = new Object[] { null };
10         }
11         return pjp.proceed(arguments);
12     }
13     ...
14 }

这里你可以看到方法的参数匹配arg-names属性值并且第二个参数类型是String,这意味着我们仅仅匹配具有一个String类型参数的方法;在这个例子中就是UserService.setAdministratorUsername,UserService.login和StockService.getStockLevel。

Introductions in the aop Namespace

这部分讨论introductions;introductions提供了一种通过代理被通知的对象向已经存在的对象添加额外方法的办法,可以使代理实现原始接口和任何我们声明的接口。为了使用XML introductions,你需要使用<apsect>元素下的<declare-parents>元素;为了使新声明的parent可用(比如一个新的接口),你需要使用introduced接口创建至少一个advice片段。下面将使用XML introduction重新实现前一个introduction的例子,我们将引入(introduce)CallTracker接口到DefaultUserService和DefaultStockService。下面是配置CallTracker introduction的XML文件,

Description: Introduction Declared in XML

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:aop="http://www.springframework.org/schema/aop"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/aop
09         http://www.springframework.org/schema/aop/spring-aop.xsd">
10     <bean id="userService"
11         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
12   
13     <bean id="stockService"
14         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
15   
16     <bean id="aspectBean" class="com.apress.prospring2.ch06.xml.AspectBean"/>
17   
18     <aop:config>
19         <aop:aspect id="aroundAspect" ref="aspectBean">
20             <aop:declare-parents
21                 types-matching="com.apress.prospring2.ch06.services.*"
22                 implement-interface="com.apress.prospring2.
23                     ch06.introductions.CallTracker"
24                 default-impl="com.apress.prospring2.ch06.
25                 introductions.DefaultCallTracker"/>
26             <aop:after-returning method="normalCall"
27                 arg-names="tracker"
28                 pointcut="execution(* com.apress.prospring2.
29                     ch06.services.*.*(..)) and this(tracker)"/>
30             <aop:after-throwing method="failingCall"
31                 arg-names="tracker"
32                 pointcut="execution(* com.apress.prospring2.
33                     ch06.services.*.*(..)) and this(tracker)"/>
34         </aop:aspect>
35     </aop:config>
36 </beans>

在<declare-parents>元素中,types-matching属性指定那些是我们希望实现通过inplement-interface属性指定的接口的类。完整的<declare-parents>元素需要指定default-imple属性,他表示实现implement-interface指定的接口的类。现在,我们可以下类似下面的代码,

1 CallTracker usct = (CallTracker)ac.getBean("userService");

如果ac是使用上面XML配置文件配置的ApplicationContext实例,上面的转型代码将验证userService bean现在实现了CallTracker接口,虽然这个bean的实际类别是只实现了UserService接口的DefaultUserService。但是现在这个应用程序还不完整,目前DefaultCallTracker中的normalCalls和failingCalls字段的值不会被改变。为了完善这个例子。还需要声明两个advice块:after returning和after throwing,

Description: After Returning and After Throwing Advices

01 ...
02 <aop:config>
03     <aop:aspect id="aroundAspect" ref="aspectBean">
04         <aop:declare-parents ... />
05         <aop:after-returning method="normalCall"
06             arg-names="tracker"
07             pointcut="execution(* com.apress.prospring2.
08                 ch06.services.*.*(..)) and this(tracker)"/>
09         <aop:after-throwing method="failingCall"
10             arg-names="tracker"
11             pointcut="execution(* com.apress.prospring2.
12                 ch06.services.*.*(..)) and this(tracker)"/>
13     </aop:aspect>
14 </aop:config>
15 ...

我们需要在AspectBean中实现normalCall和failingCall方法。这两个advice块都有一个被设置为tracker的arg-names属性,所以这两个方法都需要一个名字为tracker的参数。他们的类型应该是CallTracker;我们将使用这些方法对方法调用进行计数。

Description: Implementation of the After Advice Methods

01 public class AspectBean {
02     ...
03     public void normalCall(CallTracker tracker) {
04         tracker.markNormal();
05     }
06     public void failingCall(CallTracker tracker) {
07         tracker.markFailing();
08     }
09     ...
10 }

上面的方法非常简单:他们使用track参数增加适当的调用计数。下面的清单演示了这个应用程序。这个应用程序调用userService和stockService bean并且通过CallTracker接口显示方法调用统计。

Description: Example Application for the Introductions

01 public class XmlDemo6 {
02   
03     public static void main(String[] args) {
04         ApplicationContext ac = new ClassPathXmlApplicationContext(
05             "/META-INF/spring/xmldemo6-context.xml"
06         );
07         UserService userService = (UserService) ac.getBean("userService");
08         StockService stockService = (StockService) ac.getBean("stockService");
09         userService.login("janm");
10         stockService.getStockLevel("A");
11         stockService.applyDiscounts(new Date(), BigDecimal.ONE);
12         describeTracker(userService);
13         describeTracker(stockService);
14     }
15     private static void describeTracker(Object o) {
16         CallTracker t = (CallTracker)o;
17         System.out.println(t.describe());
18     }
19 }

Which Style Should You Use?

对于如何选择Spring AOP风格你可能会感到困惑。@AspectJ非常容易使用但是需要JDK1.5以后的版本;XML基础的配置文件可以工作于1.5版本之前的JDK。我们推荐你只要可能就使用@AspectJ。以XML为基础配置方式可能对于老的Spring开发者来说更加熟悉,但问题是使用XML基础的配置方式开发者可能不知道他们正在使用方面(aspect)工作,并且XML配置的方式将单一的功能分割进两个不同的文件(XML文件和JAVA源文件)。况且,你还不能像使用@AspectJ一样灵活的合并pointcut。前面说过,你可以为pointcut属性制定一个pointcut表达式或者为pointcut-ref属性制定一个已经在XML配置文件中存在的pointcut。不过你无法将一个已经定义的pointcut表达式和pointcut表达式进行合并。例如你无法写类似下面代码的表达式,

Description: Illegal Combination of pointcut and pointcut-ref

1 <aop:config>
2     <aop:pointcut id="x" expression="..."/>
3     <aop:aspect>
4         <aop:before pointcut="x() and target(y)"/>
5     </aop:aspect>
6 </aop:config>

到两部分的代码是无效的,因为表达式“x() and target()”是无效的。这样的值在pointcut-ref属性中也是无效的,因为没有符合ID x() and target(y)的bean(pointcut)存在。

最坏的情况是同时使用@AspectJ和XML配置这两种方式,这样只会导致混乱并且可能导致重复的advice。

Working with Spring AOP Proxies

Spring AOP支持代理(proxy),下图是代理模式的UML类图

Description: UML class diagram of a proxy pattern
pro-spring-2-5_advanced-aop_uml-class-diagram-of-a-proxy-pattern

上图显示的是JDK动态代理;代理实现了Interface1,Interface2和Interface3。InvocationHandler.invoke()方法实现处理代理实现的所有接口的所有方法的所有调用。在Spring AOP中,InvocationHandler保存一个被通知对象的引用;他处理所有的before,after returning,after throwing,around通知到目标方法。如果目标对象没有实现任何接口或者如果你不希望使用JDK动态代理,你可以使用CGLIB代理,CGLIB是字节码操作库,使用它你可以代理没有实现任何接口的类。下图显示CGLIB的类图,

Description: CGLIB proxy class digram
pro-spring-2-5_advanced-aop_uml-class-diagram-of-the-main-components

本质上,CGLIB代理是目标类的子类;因为你不能重载final方法,所有final方法的advice无法工作。同样,目标类(被通知的类)的构造函数将被调用两次:一次创建被通知的类,第二次创建代理。通常两次构造函数的调用不会造成任何问题;如果你不在构造函数中对参数进行检查并且设置实例字段,你可以随心所欲的多次运行构造函数。不过如果构造函数中有某种商业逻辑,他就可能会导致出现问题。你可以通过设置<aop:config …/>中的proxy-target-class=”true”或者<aop:aspectj-autoproxy …/>控制Spring创建哪种类型的代理。如果在你的配置文件中有多个<aop:config …/>或<aop:aspectj-autoproxy …/>元素并且如果他们中至少有一个的proxy-target-class=”true”,则所有设置proxy-target-class=”true”的配置元素的代理都将使用CGLIB。

Impact of Proxies

现在让我们来看看代理对你的代码可能有的影响。这个例子将使用StockService接口和他的实现-DefaultStockService。为了演示这部分我们将要讨论的一些概念,我们需要对DefaultStockService进行一些小小的修改,如下,

Description: Modified DefaultStockService

01 public class DefaultStockService implements StockService {
02   
03     public long getStockLevel(String sku) {
04         try {
05             Thread.sleep(2000L);
06         } catch (InterruptedException ignored) {
07         }
08         return getPredictedStockLevel (sku) / 2L;
09     }
10   
11     public long getPredictedStockLevel(String sku) {
12         return 6L * sku.hashCode();
13     }
14   
15     public void applyDiscounts(Date cutoffDate, BigDecimal maximumDiscount) {
16         // do some work
17     }
18 }

如果我们创建一个没有被代理的DefaultStockService实例然后调用getStockLevel(“X”)方法,则对于getPredictedStockLevel的调用操作是在同一个实例上的。下面的代码更直观的说明了这种观点。

Description: Calling Unproxied Methods

1 public class ProxyDemo {
2   
3     public static void main(String[] args) {
4         StockService dss = new DefaultStockService();
5         dss.getStockLevel("X");
6     }
7 }

当我们调用dss.getStockLevel(“X”)时,dss直接引用DefaultStockService,所以当DefaultStockService.getStockLevel()调用DefaultStockService.getPredictedStockLevel()方法时,就相当于我们在main()方法中调用((DefaultStockService)dss).getPredictedStockLevel()。

下面考虑一下类似的场景,但是这次StockService将是DefaultStockService的一个代理。我们从JDK动态代理开始,例,

Description: JDK Dynamic Proxy for the StockService

01 public class ProxyDemo2 {
02   
03     private static class DelegatingInvocationHandler implements
04             InvocationHandler {
05         private Object target;
06   
07         private DelegatingInvocationHandler(Object target) {
08             this.target = target;
09         }
10   
11         public Object invoke(Object target, Method method, Object[] args)
12                 throws Throwable {
13             return method.invoke(this.target, args);
14         }
15     }
16   
17     public static void main(String[] args) {
18         DefaultStockService targetReference = new DefaultStockService();
19         StockService proxyReference = (StockService) Proxy.newProxyInstance(
20                 ProxyDemo2.class.getClassLoader(),
21                 new Class<?>[] { StockService.class },
22                 new DelegatingInvocationHandler(targetReference));
23         proxyReference.getStockLevel("X");
24     }
25 }

现在当我们调用的proxyReference.getStockLevel(“X”)时,实际上是调用代理。代理的Invocationhandler委托DefaultStockService实例进行方法调用。因此proxyReference.getStockLevel()方法实际上调用代理的,而getPredictedStockLevel方法是调用DefaultStockService的。

在开始讨论代理的实际影响之前,我们需要研究一下Spring如何创建代理,就像上面例子中你看到的一样,即使是创建最简单的代理也需要做很多的工作。Spring通过提供ProxyFactory类简化这样的操作,它不仅能创建目标对象的代理并且会添加advice。下面显示ProxyFactory的简单用法,

Description: Using the ProxyFactory

01 public class ProxyDemo3 {
02   
03     public static void main(String[] args) {
04         DefaultStockService target = new DefaultStockService();
05         ProxyFactory pf = new ProxyFactory(target);
06         pf.addInterface(StockService.class);
07         StockService stockService = (StockService) pf.getProxy();
08         stockService.getStockLevel("A");
09     }
10   
11 }

我们创建目标对象(DefaultStockService)并且使用ProxyFactory创建实现StockService的代理。下面我们需要研究一下如何使用ProxyFactory的子类创建advised proxies。下面先看一个简单的before advice,

Description: Simple Aspect with Before Advice

1 public class BeforeAspect {
2   
3     @Before("execution(* com.apress.prospring2.ch06.services.*.*(..))")
4     public void simpleLog(JoinPoint jp) {
5         System.out.println("Before " + jp);
6     }
7   
8 }

关于这个aspect没有什么特别的地方;他只是简单的在被通知方法调用前打印消息。现在我们有一个aspect,我们将使用AspectJProxyFactory创建一个被通知的代理。例,

Description: Using AspectJProxyFactory

01 public class ProxyDemo4 {
02   
03     public static void main(String[] args) {
04         DefaultStockService target = new DefaultStockService();
05         AspectJProxyFactory pf = new AspectJProxyFactory(target);
06         pf.addInterface(StockService.class);
07         pf.addAspect(BeforeAspect.class);
08   
09         StockService stockService = (StockService) pf.getProxy();
10         stockService.getStockLevel("A");
11     }
12 }

现在我们运行这个例子是,他将显示与我们使用<aop:aspectj-autoproxy …/>配置的Spring的ApplicationContext和aspect bean相同的信息。

现在我们知道如何利用Spring创建被通知代理,我们需要研究一下代理调用的影响,他非常重要:想象一个被通知的对象和一个around通知一起工作并在任何方法调用期间提交了任何事务。如果你获得被通知的对象(代理)并且调用它的方法,advice将开始工作。不过当你在被通知的对象内部调用被通知的方法时,这样的调用将不会通过代理进行调用,并且advice不会运行。因此,当你使用上面例子中的StockService代理时,我们只看到了getStockLevel方法被通知;其内部对于getPredictedStockLevel方法的调用则没有被通知。

避免这种情况最好的方法是在被通知的类中不要使用方法调用链。在一些情况下,这样的调用实际上会导致一些问题(在11章会研究一些这样的问题)。但是这里有其他的方法可以使你在被通知的对象中的调用通过适合的代理。我们强烈的建议你不要这么做,但是如果你必须这么做,你可以使用AopContext.currentProxy()方法获得代表this的代理。下面的代码通过修改DefaultStockService演示如何进行这样的调用,

Description: Modified DefaultStockService

01 public class DefaultStockService implements StockService {
02   
03     public long getStockLevel(String sku) {
04         try {
05             Thread.sleep(2000L);
06         } catch (InterruptedException ignored) {
07         }
08         return ((StockService) AopContext.currentProxy())
09                 .getPredictedStockLevel(sku) / 2L;
10     }
11   
12     public long getPredictedStockLevel(String sku) {
13         return 6L * sku.hashCode();
14     }
15   
16     public void applyDiscounts(Date cutoffDate, BigDecimal maximumDiscount) {
17         // do some work
18     }
19 }

我们使用AopContext.currentProxy()获得的代理替换原来的this.getPredictedStockLevel(sku)调用。如果我们要运行这个应用程序,他将会失败,我们必须在ProxyFactory配置中设置暴露当前代理,例,

Description: Modified ProxyFactory Configuration

01 public class ProxyDemo4 {
02   
03     public static void main(String[] args) {
04         DefaultStockService target = new DefaultStockService();
05         AspectJProxyFactory pf = new AspectJProxyFactory(target);
06         pf.addInterface(StockService.class);
07         pf.setExposeProxy(true);
08         pf.addAspect(BeforeAspect.class);
09         StockService stockService = (StockService) pf.getProxy();
10         stockService.getStockLevel("A");
11     }
12 }

这是使这个应用程序可以工作的最后步骤。当我们运行他时,before advice将通知到getStockLevel和getPredictedStockLevel:

1 Before org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint: execution(getStockLevel)
2 Before org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint: execution(getPredictedStockLevel)

即使这是解决由被通知方法调用链导致的问题的解决方案,我们也强烈建议你重新思考你的设计以便防止类似的链式调用。只有在没有任何其他选择的情况下你才应该考虑使用AppContext并且改变ProxyFactory的设置。这样做将使你的bean彻底的爱上(依赖上)Spring AOP;Spring设法避免在任何事情上使你的代码依赖框架。

AspectJ Integration

AOP为OOP应用程序中出现的很多常见问题都提供了很强大的解决方案。当使用Spring AOP时,你可以获得精选的AOP功能子集的好处,允许你解决在你的应用程序中出现的大部分问题。不过有些时候,你可能希望使用Spring AOP范围外的AOP功能。这些情况下,你需要研究具有完整功能的AOP实现。这些时候我们偏爱使用AspectJ,并且因为你现在可以使用Spring配置AspectJ的方面,所以AspectJ是Spring AOP的完美补充。

AspectJ是全功能的AOP实现,它使用编译时织入你的代码中引用的aspect。在AspectJ中,使用Java-like的语法建立aspect和pintcut,这将减少Java开发者学习的时间。我们不打算花费很多的时间去研究AspectJ以及他是怎么工作的,我们将呈现一些简单的AAspectJ的例子并且想你显示如何使用Spring配置他们。关于AspectJ更详细的研究你可以阅读《AspectJ in Action》

Creating Your First AspectJ Aspect

让我们从一个简单的例子开始,我们将创建一个aspect并且使用AspectJ编译器进行织入。接着我们吧这个aspect配置为标准的Spring bean。我们可以这么做是因为每一个AspectJ方面都暴露一个方法,aspectOf(),他可以用来访问aspect实例。使用aspectOf()方法和Spring配置能力,你可以获得一个Spring配置的aspect。这样做的好处是你可以利用所有AspectJ提供的强大的AOP功能并且不会丧失Spring卓越的依赖注入(DI)和配置能力。这样做同样意味着你不需要对你的应用程序分别进行配置,你可以为所有Spring管理的bean和你的AspectJ aspect使用相同的Spring ApplicationContext

下面的例子我们使用AspectJ通知所有在com.apress.prospring2.ch06.services包中的所有类的所有方法,并且在方法调用前后打印消息。这些消息是可以用Spring进行配置的。下面是StockServiceAspect aspect的代码

Description: StockServiceAspect Aspect

01 package com.apress.prospring2.ch06.aspectj;
02   
03 public aspect StockServiceAspect {
04   
05     private String suffix;
06     private String prefix;
07   
08     public void setPrefix(String prefix) {
09         this.prefix = prefix;
10     }
11   
12     public void setSuffix(String suffix) {
13         this.suffix = suffix;
14     }
15   
16     pointcut doServiceCall() :
17         execution(* com.apress.prospring2.ch06.services.*.*(..));
18   
19     before() : doServiceCall() {
20         System.out.println(this.prefix);
21     }
22   
23     after() : doServiceCall() {
24         System.out.println(this.suffix);
25     }
26 }

上面的大部分代码看起来都应该非常熟悉。本质上我们常见了一个名为StockServiceAspect的aspect,并且像普通的Java类一样我们为aspect设置了两个属性,suffix和prefix,他们在通知services包中的所有类的所有方法时使用。下面我们定义了一个的命名pointcut,doServiceCall(),这个pointcut在这个例子中是指service method的执行(AspectJ有大量的joinpoints,都没有在这个例子中体现)。最后我们定义了两个advice块:一个在doServiceCall() pointcut之前执行,一个在之后执行。before advice打印包含prefix的消息,after advice打印包含suffix的消息,下面是配置文件,

Description: Configuring an AspectJ Aspect

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xsi:schemaLocation="
05         http://www.springframework.org/schema/beans
06         http://www.springframework.org/schema/beans/spring-beans.xsd">
07   
08     <bean id="userService"
09         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
10   
11     <bean id="stockService"
12         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
13   
14     <bean class="com.apress.prospring2.ch06.aspectj.StockServiceAspect"
15         factory-method="aspectOf">
16         <property name="prefix" value="Before call"/>
17         <property name="suffix" value="After call"/>
18     </bean>
19 </beans>

和你看到的一样,aspect bean的大部分配置信息和标准的bean配置非常类似。唯一不同的地方时使用了<bean>元素的factory-method属性以便使传统意义的工厂类准确的融入Spring。比如说如果你的类Foo只有私有的构造函数以及一个静态工厂方法getInstance(),使用factory-method属性则允许这个类的bean被Spring管理。aspectOf()方法将暴露每一个AspectJ方面以便允许你访问aspect的实例,并且因此而允许Spring设置apsect的属性。不过请注意一下,我们并没有使用aop命名空间;除此之外,StockServiceAspect并不是一个有效的Java类。下面将完成这个例子,

Description: AspectJ Sample Application

01 public class AspectJDemo1 {
02   
03     public static void main(String[] args) {
04         ApplicationContext ac = new ClassPathXmlApplicationContext(
05             "/META-INF/spring/aspectjdemo1-context.xml"
06         );
07         UserService userService = (UserService) ac.getBean("userService");
08         userService.login("janm");
09         StockService stockService = (StockService) ac.getBean("stockService");
10         System.out.println(stockService.getStockLevel("ABC"));
11     }
12 }

如果我们直接在IDE中运行这个程序,他将失败,他的输出将指出这里没有StockServiceAspect类;这是意料之中的,因为StockServiceAspect是一个aspect,而不是Java class。

1 ...
2 Exception in thread "main" org.springframework.beans.factory.CannotLoadBeanClassException:
3     Cannot find class [com.apress.prospring2.ch06.aspectj.StockServiceAspect]
4     for bean with name 'com.apress.prospring2.ch06.aspectj.StockServiceAspect#0'
5     defined in class path resource [META-INF/spring/aspectjdemo1-context.xml];
6     nested exception is java.lang.ClassNotFoundException:
7     com.apress.prospring2.ch06.aspectj.StockServiceAspect
8 ...

为了运行这个例子,我们必须使用AspectJ编译器。AspectJ编译器将转换StockServiceAspect到Java类文件,并且他会将aspect的代码织入被通知的类。换句话说也就是ApsectJ提供的字节码与标准的Java编译器提供的不同,因为他需要织入StockServiceAspect。除了编译时织入,AspectJ编译器将为StockServiceAspect.aj源文件提供一个有效的Java字节码文件。在StockServiceAspect类中最重要的方法或许就是我们在Spring bean定义中使用的public static Aspect aspectOf()方法。

Compiling the Sample Application

如果熟悉AspectJ编程可以跳过这部分,否则,请仔细阅读并了解如何使你的编译器和AspectJ编译器一起工作。首先应该从http://www.eclipse.org/aspectj中获得AspectJ的发行版本。下载安装包(.jar文件)并设置$ASPECTJ_HOME系统变量-比如/usr/share/aspectj-1.5或C:\Program Files\aspectj-1.5。安装AspectJ编译器之后,修改系统变量PATH使其包含$ASPECTJ_HOME/bin。现在你可以在命令行中使用ajc -version命令,你将看到下面的提示,

1 AspectJ Compiler 1.5.4 built on Thursday Dec 20, 2007 at 13:44:10 GMT

因为直接使用AspectJ编译器非常负责,所以我们使用Apache ANT简化这些工作。AspectJ发行版包含一个自定义的Ant tasks;安装自定义task,需要将$ASPECTJ_HOME/lib中的aspectjtools.jar拷贝到$ANTHOME/lib。

Description: The Ant Build File

01 <?xml version="1.0"?>
02 <project name="ch06" default="all" basedir="." xmlns:aspectj="antlib:org.aspectj">
03     <property name="dir.src.main.java" value="./src/main/java"/>
04     <property name="dir.src.main.resources" value="./src/main/resources"/>
05     <property name="dir.module.main.build" value="./target/build-main"/>
06     <property name="dir.lib" value="http://www.cnblogs.com/lib"/>
07     <property name="module.jar" value="ch06.jar"/>
08     <path id="module.classpath">
09         <fileset dir="${dir.lib}" includes="**/*.jar"/>
10         <fileset dir="${dir.lib}" includes="**/*/*.jar"/>
11     </path>
12     <target name="all">
13         <aspectj:iajc
14             outjar="${module.jar}"
15             sourceRootCopyFilter="**/*.java"
16             source="1.5"
17             target="1.5">
18             <classpath refid="module.classpath"/>
19             <sourceroots>
20                 <path location="${dir.src.main.java}
21                     /com/apress/prospring2/ch06/services"/>
22                 <path location="${dir.src.main.java}
23                     /com/apress/prospring2/ch06/aspectj"/>
24                 <path location="${dir.src.main.java}
25                     /com/apress/prospring2/ch06/common"/>
26                 <path location="${dir.src.main.resources}"/>
27             </sourceroots>
28         </aspectj:iajc>
29         <java classname="com.apress.prospring2.ch06.aspectj.AspectJDemo1"
30             fork="yes">
31             <classpath>
32                 <path refid="module.classpath"/>
33                 <pathelement location="${module.jar}"/>
34             </classpath>
35         </java>
36     </target>
37 </project>

当我们使用这个Ant Script时,AspectJ编译器将织入aspect并且创建StockServiceAspect。运行这个例子(使用ant)产生下面的结果,

01 [java] DEBUG [main] CachedIntrospectionResults.<init>(265) |
02     Found bean property 'suffix' of type [java.lang.String]
03 ...
04 [java] DEBUG [main] AbstractBeanFactory.getBean(197) |
05     Returning cached instance of singleton bean 'userService'
06 userService.login("janm")
07 [java] Before call
08 [java] After call
09 [java] DEBUG [main] AbstractBeanFactory.getBean(197) |
10     Returning cached instance of singleton bean 'stockService'
11 stockService.getStockLevel("ABC")
12 [java] Before call
13     stockService.getPredictedStockLevel("ABC")
14     [java] Before call
15     [java] After call
16 [java] After call
17 [java] 193734

这个结果清楚的显示了aspect已经可以正常工作。我们成功的通知到DefaultStockService和DefaultUserService中的方法并设置了aspect的prefix和suffix属性。因为AspectJ编译器完成编译时织入,所以我们不需要担心任何代理的问题:DefaultStockService.getStockLevel中调用的getPredictedStockLevel被正确通知而不需要应用任何AopContext。

AspectJ’s Aspect Scope

默认情况下AspectJ的aspect是单例的-每一个classloader获得单一的一个实例。如果你需要随pointcut使用不同的aspect实例,你需要创建不同的aspect。下面的清单显示了这样的aspect(AspectJ将为每一个相匹配的pointcut创建一个不同的实例)

Description: Stateful (Per This) Aspect

01 package com.apress.prospring2.ch06.aspectj;
02   
03 public aspect ThisCountingAspect perthis(doServiceCall()) {
04   
05     private int count;
06   
07     pointcut doServiceCall() :
08         execution(* com.apress.prospring2.ch06.services.*.*(..));
09   
10     before() : doServiceCall() {
11         this.count++;
12         System.out.println("Before call");
13     }
14   
15     after(Object target) : doServiceCall() && this(target) {
16         System.out.println(target + " executed " + this.count + " times");
17     }
18   
19 }

这样做的唯一问题是你无法在Spring中配置这个aspect:如果我们按照prototype配置他,他将只设置一个spect的实例的属性,但是这个实例并不是在join point中使用的那个。如果我们需要一个scoped AspectJ aspect,我们无法使用Spring配置他的属性。

Load-Time Weaving

Load-time weaving是指当ClassLoader读取被通知的类时织入使用的aspect的处理过程。AspectJ使用Java agent支持load-time weaving;你需要指定agent的JAR文件(参考JVM的-javaagent命令行参数)。不过agents将带来一些限制:最明显的是agents智能工作于1.5以后的JVM,第二个限制是JVM为整个JVM读取并使用agent。这可能在一些应用程序中造成很严重的问题,当你在virtual machine上部署多于一个应用程序时,你可能需要更细致的控制load-time weaving process。

Spring的load-time weaving支持这样做:它允许你对每一个ClassLoader进行load-time weaving控制。这对你在servlet容器或者应用程序服务器中部署web应用程序非常有帮助。你可以为每个web应用程序配置不同的load-time weaving并且不影响容器读取类。此外,如果你使用Spring load-time weaving支持,你可能不需要改变servlet容器和application服务器的配置。

Your First Load-Time Weaving Example

我们从一个简单的aspect开始,他只是跟踪所有服务方法的调用。这个aspect是没有实际意义的,

Descrption: A Load-Time Weaving Demonstration Aspect

01 @Aspect
02 public class AuditAspect {
03   
04     @After(value =
05             "execution(* com.apress.prospring2.ch06.services.*.*(..)) && " +
06             "this(t)",
07         argNames = "jp,t")
08     public void audit(JoinPoint jp, Object t) {
09         System.out.println("After call to " + t + " (" + jp + ")");
10     }
11   
12 }

现在我们有了aspect,我们将使用它去通知我们的userService和stockService bean。我们像使用JVM agen一样使用spring-agent.jar并且使用context namespace开始load-time weaving。除了ApplicationContext XML配置文件,我们还需要创建/META-INF/aop.xml文件。aop.xml文件是AspectJ的标准组件;他在load时告诉AspectJ织入那些class。下面清单时aop.xml文件,

Description: The META-INF/aop.xml file

01 <!DOCTYPE aspectj PUBLIC
02     "-//AspectJ//DTD//EN"
03     "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
04 <aspectj>
05     <weaver>
06         <include within="com.apress.prospring2.ch06.services.*"/>
07     </weaver>
08     <aspects>
09         <aspect name="com.apress.prospring2.ch06.ltw.AuditAspect"/>
10     </aspects>
11 </aspectj>

高亮部分的代码告诉AspectJ将AuditAspect织入到所有在com.apress.prospring2.ch06.service包中的所有类。下面的清单显示了包含<context:load-time-weaver>元素的ApplicationContext XML配置文件。<context:load-time-weaver>元素包含一个属性,aspectj-weaving。你可以使用“on”打开load-time weaving,“off”将关闭load-time weaving,“autodetect”将在有至少一个/META-INF/aop.xml文件时打开load-time weaving。如果你省略了aspectj-weaving属性,Spring假设该值为“autodetect”。

Description: The ApplicationContext XML Configuration File

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04     xmlns:context="http://www.springframework.org/schema/context"
05     xsi:schemaLocation="
06         http://www.springframework.org/schema/beans
07         http://www.springframework.org/schema/beans/spring-beans.xsd
08         http://www.springframework.org/schema/context
09         http://www.springframework.org/schema/context/spring-context.xsd">
10   
11     <bean id="userService"
12         class="com.apress.prospring2.ch06.services.DefaultUserService"/>
13   
14     <bean id="stockService"
15         class="com.apress.prospring2.ch06.services.DefaultStockService"/>
16   
17     <context:load-time-weaver />
18 </beans>

在我们运行这个应用程序之前,无论是否需要load-time weaving进行工作,我们都需要添加-javaagent JVM参数。针对这个例子,-javaagent的参数值应该是../lib/org/springframework/spring/spring-agent.jar。没有agent,应用程序将抛出IllegalStateException。

1 ...
2 Caused by: java.lang.IllegalStateException:
3     ClassLoader [sun.misc.Launcher$AppClassLoader]
4     does NOT provide an 'addTransformer(ClassFileTransformer)' method.
5     Specify a custom LoadTimeWeaver or start your Java virtual machine with
6     Spring's agent: -javaagent:spring-agent.jar

上面的错误消息告诉我们我们需要指定JVM agent为spring-agent.jar。当我们这么做的时候,应用程序工作正常并产生下面的结果,

01 userService.login("janm")
02 After call to com.apress.prospring2.ch06.services.
03     DefaultUserService@4cb44131
04     (execution(void com.apress.prospring2.ch06.services.
05     DefaultUserService.login(String)))
06   
07 stockService.getStockLevel("ABC")
08 After call to com.apress.prospring2.ch06.services.
09     DefaultStockService@197a64f2
10     (execution(long com.apress.prospring2.ch06.services.
11         DefaultStockService.getPredictedStockLevel(String)))
12     DefaultStockService.getPredictedStockLevel("ABC")
13     After call to com.apress.prospring2.ch06.services.
14         DefaultStockService@197a64f2
15         (   execution(long com.apress.prospring2.ch06.services.
16             DefaultStockService.getStockLevel(String)))
17 193734

不仅是应用程序工作于load-time weaving,从DefaultStockService.getStockLevel调用的DefaultStockService.getPredictedStockLevel也被通知了,说明这种情况下的代理没有任何问题-DefaultStockService的字节码和磁盘上保存的字节码不同,AspectJ在应用程序的ClassLoader读取class前完成织入。load-time weaving不仅可以解决代理问题,他还可以消除任何代理带来的性能损失。因为这里并没有代理,运行的代码是在编译时将advice织入匹配的join point的。

LoadTimeWeaver Lookup Strategies

Spring在JVM agent库spring-agent.jar中使用InstrumentationSavingAgent保存当前JVM暴露的Instrumentation实例。DefaultContextLoadTimeWeaver尝试自动发现最为匹配应用程序环境的LoadTimeWeaver实例。下表显示了不同环境下的LoadTimeWeaver实现。

Description: LoadTimeWeaver Implementations

LoadTimeWeaver Environment
InstrumentationLoadTimeWeaver JVM started with Spring InstrumentationSavingAgent(using -javaagent:$LIB/spring-agent.jar)
WebLogicLoadTimeWeaver LoadTimeWeaver implementation in a running BEA WebLogic 10 or later application server
GlassFishLoadTimeWeaver Works in the GlassFish application server V2
OC4JLoadTimeWeaver The implementation of the LoadTimeWeaver that works with Oracle Application Server version 10.1.3.1 or later
ReflectiveLoadTimeWeaver Intended to be used with the TomcatInstrumentableClassLoader to provde load-time weaving in the Tomcat servlet container and the default fallback LoadTimeWeaver implementation
SimpleLoadTimeWeaver A test-only implementation of the LoadTimeWeaver (By “test-only,” we mean that it performs the necessary weaving transformations on a newly created ClassLoader.)

无论你是用那个策略,最重要的是必须了解到load-time weaving需要使用AspectJ,不是@AspectJ。这意味着我们不能使用@AspectJ支持的bean() pointcut。

Practical Uses of AOP

当有人提到AOP时总会首先想起logging,然后是事务管理(transaction management)。不过这些只是AOP的特殊应用。在我们的应用程序开发体验中,我们会使用AOP对应用程序的性能和健康状况进行监视、进行调用统计、缓存和错误恢复。刚高级的AOP应用包括编译时体系标准检查。例如,你可以创建一个aspect在特定类的特定方法调用时执行。不过这些更高级的AOP用法很少会使用Spring AOP,所以我们将演示性能和健康状况的监视。你同样可以参考这本书第22章关于缓存SpringModule的例子,它使用AOP实现声明式缓存支持。

Performance and Health Moitoring

监视应用程序的关键在于他们部署的产品环境。在我们进行log的时候,在log中查找特定的问题通常十分困难。如果我们能够跟踪应用程序的性能状况并快速的找到任何重大的性能损失肯定会更好一些。并且他对记录在应用程序运行期间发生的异常也非常有帮助。除了记录异常类型(和消息)之外,记录引起异常的参数值也非常重要。这样可以帮助我们集中注意力在应用程序出现问题的时候,我们也将按照异常类型和参数值对异常报告进行分组。

在我们继续实现这个aspect之前,我们先看看这个应用程序的其他组件,下图显示了这个应用程序组件的UML类图,

Description: UML class diagram of the main components
pro-spring-2-5_advanced-aop_uml-class-diagram-of-the-main-components

现在我们可以看一下PeformanceAndHealthCollectingAspect的实现。这个aspect只有一个around advice块;这个advice搜集方法执行的时间和异常信息,见下面清单,

Description: PerformanceAndHealthCollectingAspect

01 package com.apress.prospring2.ch06.practical;
02   
03 import org.aspectj.lang.ProceedingJoinPoint;
04 import org.aspectj.lang.annotation.Around;
05 import org.aspectj.lang.annotation.Aspect;
06 import org.aspectj.lang.annotation.Pointcut;
07 import org.springframework.util.StopWatch;
08   
09 /**
10  * @author janm
11  */
12 @Aspect
13 public class PerformanceAndHealthCollectingAspect {
14   
15     private PerformanceLogDao performanceLogDao;
16     private ExceptionLogDao exceptionLogDao;
17     private PerformanceLogExtractor performanceLogExtractor =
18             new PerformanceLogExtractor();
19     private ExceptionLogExtractor exceptionLogExtractor =
20             new ExceptionLogExtractor();
21   
22     @Pointcut("execution(* com.apress.prospring2.ch06.services.*.*(..))")
23     private void serviceCall() { }
24   
25     @Around(value = "serviceCall() && target(target)",
26             argNames = "pjp, target")
27     public Object collectPerformance(ProceedingJoinPoint pjp, Object target)
28             throws Throwable {
29         Throwable exception = null;
30         Object ret = null;
31   
32         StopWatch stopWatch = new StopWatch();
33         stopWatch.start();
34         try {
35             ret = pjp.proceed();
36         } catch (Throwable t) {
37             exception = t;
38         }
39         stopWatch.stop();
40   
41         if (exception == null) {
42             this.performanceLogDao.insert(
43                     this.performanceLogExtractor.extract(pjp, target),
44                     stopWatch.getLastTaskTimeMillis()
45             );
46         } else {
47             this.exceptionLogDao.insert(
48                     this.exceptionLogExtractor.extract(pjp, target),
49                     exception
50             );
51         }
52   
53         if (exception != null) throw exception;
54         return ret;
55     }
56   
57     public void setPerformanceLogDao(PerformanceLogDao performanceLogDao) {
58         this.performanceLogDao = performanceLogDao;
59     }
60   
61     public void setExceptionLogDao(ExceptionLogDao exceptionLogDao) {
62         this.exceptionLogDao = exceptionLogDao;
63     }
64 }

你可以看到我们上面的代码中记录了方法的执行性能和可能出现的异常。我们将在PerformanceMonitoringAspect中使用这些信息。和名字暗示的一样,这个方面用来监视应用程序的性能。这个aspect需要before和after通知;before advice将记录方法调用的开始;after advice将对开始的记录进行匹配和排除。你可能会问为什么不使用around advice进行这么简单的跟踪。理由是我们需要获得指定方法运行的时间而不仅是他仍然在运行。如果我们已经有用于health监视的around advice实现,我们的代码将只需要在方法完成后获得该方法使用了多长时间。下面的清单是aspect实现的要点,

Description: Outline Implemention of the PerformanceMonitoringAspect

01 package com.apress.prospring2.ch06.practical;
02   
03 import org.aspectj.lang.JoinPoint;
04 import org.aspectj.lang.annotation.After;
05 import org.aspectj.lang.annotation.Aspect;
06 import org.aspectj.lang.annotation.Before;
07 import org.aspectj.lang.annotation.Pointcut;
08   
09 /**
10  * @author janm
11  */
12 @Aspect
13 public class PerformanceMonitoringAspect {
14     private PerformanceLogDao performanceLogDao;
15     private CallLogDao callLogDao;
16     private PerformanceLogExtractor performanceLogExtractor =
17             new PerformanceLogExtractor();
18   
19     @Pointcut("execution(* com.apress.prospring2.ch06.services.*.*(..))")
20     private void serviceCall() { }
21   
22     @Before(value = "serviceCall() && target(target)",
23             argNames = "jp, target")
24     public void logStartCall(JoinPoint jp, Object target) {
25     }
26   
27     @After(value = "serviceCall() && target(target)",
28             argNames = "jp, target")
29     public void logEndCall(JoinPoint jp, Object target) {
30   
31     }
32   
33 }

logging必须在方法每次开始调用时插入记录并在方法完成后排除它。最后一个组件是PerformanceMonitor类,他定期的扫描调用记录并找到call log中的所有条目,PerformanceAndHealthMonitoringAspect检查收集到的统计数据。如果PerformanceMonitor发现一个调用比平时花费更多的事件,他将向应用程序管理员进行示警。

Summary

In this chapter, we concluded our discussion on AOP. We looked at the advanced options for pointcutting, as well as how to extend the set of interfaces implemented by an object using introductions. A large part of this chapter focused on using Spring Framework services to configure AOP declaratively, thus avoiding the need to hard-code AOP proxy construction logic into your code. We spent some time looking at how Spring and AspectJ are integrated to allow you to use the added power of AspectJ without losing any of the flexibility of Spring. Next, we looked at how you can use AspectJ with load-time weaving to minimize the overhead Spring AOP’s proxies add to the application. Finally, we looked at how we can use AOP to solve an application-specific problems in real-life applications.

Even though we have discussed how to use AOP in Spring applications, we have barely scratched the surface of all the features AspectJ provides. If you are interested in more details about AspectJ, we recommend two excellent books. The first one is Eclipse AspectJ by Adrian Colyer, Andy Clement, George Harley, and Matthew Webster (Addison-Wesley, 2005), which provides a comprehensive introduction and reference for the AspectJ language; the second book, AspectJ in Action by Ramnivas Laddad (Manning, 2003), focuses on the syntax of AspectJ as well as covering a lot of general AOP topics.

posted on 2010-04-20 23:25  瞌睡虫  阅读(454)  评论(0编辑  收藏  举报