Spring中面向切面编程-AOP
读Spring框架官方文档记录。
面向切面编程是对面向对象编程(OOP)的补充,在OOP中模块化的关键单元是类,而在AOP中模块化的单元是切面。切面支持对跨多种类型和对象的关注点(如事务管理)进行模块化。(在AOP中,这种关注点通常被称为“横切”关注点。)
Spring的一个关键组件是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着如果不想使用AOP就可以不使用),但AOP补充了Spring IoC,提供了一个功能非常强大的中间件解决方案。
AOP在Spring框架中被用于:
- 提供声明性企业服务。这类服务中最重要的是声明式事务管理。
- 让用户实现自定义切面,用AOP补充他们对OOP的使用。
1. AOP概念
首先介绍一些AOP的概念和术语。这些术语不是特定于Spring的。
- 横切关注点:跨越应用程序多个模块的方法或者功能。即与业务逻辑无关,但是需要我们关注的部分就是横切关注点,如日志、安全、缓存、事务等。
- 切面(Aspect):跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于模式的方法)或使用@Aspect注解(@AspectJ样式)注解的常规类来实现的。即它是一个类。
- 连接点:程序执行期间的点,例如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法执行。
- 通知(Advice):切面在特定连接点上采取的操作。通知的类型包括环绕通知、前通知和后通知。许多AOP框架,包括Spring,将通知建模为拦截器,并维护围绕连接点的拦截器链。可以理解为切面类中的具体方法。
- 切入点(Pointcut):切面通知 执行的 “地点”的定义,即切面对象中的方法应该在目标对象的哪个方法相关联执行。通知与切入点表达式相关联,并在切入点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。
- 引入(Introduction):代表类型声明其他方法或字段。Spring AOP允许向任何被通知的对象引入新接口(和相应的实现)。例如,可以使用引入绍使bean实现IsModified接口,从而简化缓存。
- 目标对象:一个对象被一个或多个方面通知。也称为“被通知对象”。由于Spring AOP是通过使用运行时代理实现的,因此该对象始终是一个代理对象。
- AOP代理:由AOP框架创建的对象,以便实现方面契约(通知方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
- 织入(Weaving):将切面与其他应用程序类型或对象链接起来,以创建被通知的对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行织入。
各个名词的联系如下图(图片来自)所示:
Spring AOP包含五种类型的通知:
- Before Advice:在连接点之前运行的通知,但不能阻止执行流继续到连接点(除非它抛出异常)。
- After returning advice:在连接点正常完成后运行的通知(例如,如果一个方法返回而没有抛出异常)。
- After throwing advice:方法退出时抛出异常后运行的通知。
- After Advice:无论连接点以何种方式退出(正常或异常返回),都要运行的通知。
- Around advice:环绕连接点(如方法调用)的通知。这是最有力的通知。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或抛出异常来缩短被通知的方法执行。
Around advice是最通用的通知。由于Spring AOP(像AspectJ)提供了各种各样的通知类型,所以我们建议使用功能最弱的通知类型来实现所需的行为。例如,如果您只需要用方法的返回值更新缓存,那么最好实现after returning advice,而不是around advice,尽管around advice可以完成相同的任务。使用最具体的通知类型可以提供更简单的编程模型,出错的可能性更小。例如,不需要在用于around advice的连接点上调用proceed()方法,因此,不会产生失败调用的情况。
所有的通知参数都是静态类型的,这样就可以使用适当类型的通知参数(例如,方法执行返回值的类型)。
切入点匹配的连接点的概念是AOP的关键,它区别于只提供拦截的旧技术。切入点使通知能够独立于面向对象的层次结构进行定向。例如,可以将提供声明式事务管理的around advice应用到跨多个对象(例如服务层中的所有业务操作)的一组方法。
2. AOP的功能和目的
- Spring AOP为企业级Java应用程序中的大多数问题提供了优秀的解决方案。Spring AOP是用纯Java实现的。不需要特殊的编译过程且Spring AOP不需要控制类加载器层次结构,因此适合在servlet容器或应用程序服务器中使用。使用Spring AOP有时不能高效地完成一些事,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最佳选择。
- Spring AOP目前只支持方法执行连接点(通知在Spring bean上执行方法)。没有实现字段拦截,尽管可以在不破坏Spring AOP核心api的情况下添加对字段拦截的支持。如果需要通知字段访问和更新连接点,可以考虑使用AspectJ之类的语言。
- Spring AOP处理AOP的方法不同于大多数其他AOP框架。其目的不是提供最完整的AOP实现(尽管Spring AOP非常强大)。相反,其目标是在AOP实现和Spring IoC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。因此,Spring框架的AOP功能通常与Spring IoC容器一起使用。
- 切面是通过使用普通的bean定义语法来配置的(尽管这允许强大的自动代理功能)。这是与其他AOP实现的一个重要区别。
- 基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是相互竞争的。Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,从而在一致的基于Spring的应用程序体系结构中支持AOP的所有使用。这种集成不会影响Spring AOP API或AOP Alliance API。Spring AOP仍然是向后兼容的。
注意:Spring框架的核心原则之一是非侵入性。这就是不应该强迫在业务中引入特定于框架的类和接口的思想。但是,在某些地方,Spring框架确实允许将Spring框架特定的依赖项引入到代码库中。提供这些选项的原因是,在某些情况下,以这种方式阅读或编写某些特定的功能片段可能更容易。
3. AOP代理
Spring AOP默认使用标准的JDK动态代理,需要实现接口来实现代理。Spring AOP还可以使用CGLIB代理,不要求要实现接口。默认情况下,如果业务对象没有实现接口,则使用CGLIB。由于根据接口而不是类编程是一种很好的实践,所以业务类通常实现一个或多个业务接口。需要通知一个没有在接口上声明的方法,或者需要将一个代理对象作为一个具体类型传递给一个方法,可以强制使用CGLIB。
4. 基于@AspectJ的AOP支持
@AspectJ指的是一种将切面声明为用注解注解的常规Java类的风格。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注解。然而,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或weaver。
(1) 启动@AspectJ支持
要在Spring配置中使用@AspectJ切面,需要启用Spring支持,以便基于@AspectJ切面配置Spring AOP,并根据这些切面是否通知自动代理bean。自动代理即如果Spring确定一个bean是由一个或多个切面通知的,它将自动为该bean生成一个代理,以拦截方法调用,并确保通知按需要运行。
可以通过XML或java风格的配置启用@AspectJ支持。在这两种情况下,还需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径中(1.8或更高版本)。该库可在AspectJ发行版的lib目录或Maven*存储库中获得。
1) 使用Java配置方法开启@AspectJ支持
要使用Java @Configuration启用@AspectJ支持,要添加@EnableAspectJAutoProxy注解,如下面的示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
2) 使用XML配置方法开启@AspectJ支持
要使用基于XML的配置启用@AspectJ支持,使用aop:aspectj autoproxy元素,如下面的示例所示:
<aop:aspectj-autoproxy/>
(2) 声明一个切面
启用了@AspectJ支持后,Spring将自动检测在应用程序上下文中定义的任何带有@AspectJ切面(带有@Aspect注释)类的bean,并用于配置Spring AOP。下面两个例子展示了一个切面所需的最小定义。两个示例中的第一个展示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect注解的bean类。两个示例中的第二个显示了NotVeryUsefulAspect类定义,它是用org.aspectj.lang.annotation注解的注解切面。
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
与任何其他类一样,切面(用@Aspect注解的类)可以有方法和字段。它们还可以包含切入点、通知和引入(类型间)声明。
通过组件扫描自动检测切面
可以将切面类注册为Spring XML配置中的常规bean,或者通过类路径扫描自动检测它们——与任何其他Spring管理bean一样。但是,请注意@Aspect注解不足以实现类路径中的自动检测。为了达到这个目的,需要添加一个单独的@Component注解(或者,根据Spring的component scanner的规则,一个自定义的原型注解)。
能不能使用切面通知其它切面?
在Spring AOP中,切面本身不能成为来自其他切面的通知的目标。类上的@Aspect注解将其标记为切面,因此将其排除在自动代理之外。
(3) 声明一个切入点
切入点确定连接点,从而控制通知何时运行。Spring AOP只支持Spring bean的方法执行连接点,因此可以将切入点看作匹配Spring bean上方法的执行。切入点声明有两部分:包含名称和任何参数的签名,以及确定方法执行的切入点表达式。在AOP的@AspectJ注释风格中,切入点签名是由常规方法定义提供的,切入点表达式是通过使用@Pointcut注解表示的(作为切入点签名的方法必须有一个void返回类型)。
一个示例可能有助于明确切入点签名和切入点表达式之间的区别。下面的示例定义了一个名为anyOldTransfer的切入点,它与任何名为transfer的方法的执行相匹配:
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
形成@Pointcut注解值的切入点表达式是一个正则的AspectJ 5切入点表达式。表达式语法可参见AspectJ Programming Guide。
1) 支持的切入点指示符
Spring AOP支持在切入点表达式中使用以下AspectJ切入点指示符(PCD):
- execution:用于匹配方法执行连接点。这是使用Spring AOP时要使用的主要切入点指示符。
- within:限制匹配到特定类型中的连接点(使用Spring AOP时,在匹配类型中声明的方法的执行)。
- this:限制匹配到连接点(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。
- traget:匹配到连接点(使用Spring AOP时方法的执行)的限制,其中目标对象(被代理的应用程序对象)是给定类型的实例。
- args:匹配到连接点(使用Spring AOP时方法的执行)的限制,其中参数是给定类型的实例。
- @target:将匹配限制为连接点(使用Spring AOP时方法的执行),其中目标对象的类具有给定类型的注解。
- @args:匹配到连接点(使用Spring AOP时方法的执行)的限制,其中传递的实际参数的运行时类型具有给定类型的注解。
- @within:将匹配限制为具有给定注解的类型中的连接点(使用Spring AOP时,使用给定注解的类型声明的方法的执行)。
- @annotation:匹配到连接点的限制,连接点的主体(在Spring AOP中运行的方法)具有给定的注解。
注意:由于Spring AOP框架基于代理的特性,根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能拦截对该代理的公共接口方法调用。使用CGLIB,代理上的公共和受保护的方法调用会被拦截(甚至包可见的方法,如果需要的话)。通过代理的公共交互应该始终设计为公共方法。
注意:切入点定义通常与任何拦截方法相匹配。如果切入点严格意义上是公共的,即使在通过代理进行潜在非公共交互的CGLIB代理场景中,也需要相应地定义它。
注意:如果需要拦截目标类中的方法或者构造函数,考虑使用AspectJ织入,而非基于Spring代理的AOP框架。
Spring AOP还支持一个名为bean的附加PCD。此PCD允许将连接点的匹配限制为特定的命名Spring bean或一组命名Spring bean(当使用通配符时)。bean PCD具有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean
可以是任何Spring bean的名称。可以使用*通配符。与其他切入点指示符一样,bean PCD可以与&&(和)、||(或)和!(非)操作符一起使用。
注意:bean PCD仅在Spring AOP中支持,在AspectJ织入中不受支持。它是对AspectJ定义的标准PCD的特定于spring的扩展,因此,对于@Aspect中声明的切面是不可用的。
注意:bean PCD在实例级(基于Spring bean名称概念)操作,而不仅仅是在类型级(基于编织的AOP被限制在这一级别)操作。基于实例的切入点指示符是Spring基于代理的AOP框架的一种特殊功能,它与Spring bean工厂紧密集成,在Spring bean工厂中,通过名称识别特定bean是很自然和直接的。
2) 组合切入点表达式
可以通过使用&&/||/!来祝贺切入点表达式。还可以通过名称引入切入点表达式。下面的例子标识了三个切入点表达式。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} //anyPublicOperation匹配表示任何公共方法的执行的方法执行连接点。
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} //如果方法执行在trading模块中,inTrading匹配。
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //如果一个方法的执行代表了trading模块中的任何公共方法,那么tradingOperation就匹配。
从更小的命名组件构建更复杂的切入点表达式是最佳实践。当通过名称引用切入点时,应用常规的Java可见性规则(可以看到同一类型的私有切入点、层次结构中受保护的切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。
3) 共享通用的切入点定义
在处理企业应用程序时,开发人员通常希望从几个方面引用应用程序的模块和特定的操作集。为此,我们建议定义一个CommonPointcuts切面来捕获通用的切入点表达式。该切面通常如下所示:
package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.myapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
在任何需要切入点表达式的地方,都可以引用在上面切面中定义的切入点。例如,要使服务层成为事务性的,可以编写以下代码:
<aop:config><!--后面会介绍这些标签的作用-->
<aop:advisor
pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
4) 实例
Spring AOP用户可能最常使用execution切入点指示符。execution表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型、名称及参数其它都是可选的。ret-type-pattern确定为了匹配连接点,方法的返回类型必须是什么。*
最常用作返回类型模式。它匹配任何返回类型。只有当方法返回给定类型时,才匹配完全限定类型名。name-pattern与方法名匹配。可以使用*
通配符作为name-pattern的全部或部分。如果指定了declaring-type-pattern,请包含一个.
将其连接到name-pattern组件。param-pattern中()匹配不带参数的方法,(…)匹配任意数量(0个或多个)的参数,(*)模式匹配带任意类型的一个参数的方法,(*
,String)匹配带有两个参数的方法,第一个参数可以是任何类型,而第二个参数必须是字符串。
下面这些例子显示了常用的切入点表达式:
execution(public * *(..)) //匹配任何公共方法
execution(* set*(..))//匹配以set开头的任何方法
execution(* com.xyz.service.AccountService.*(..))//匹配AccountService接口定义的任何方法
execution(* com.xyz.service.*.*(..))//匹配service包中定义的任何方法
execution(* com.xyz.service..*.*(..))//匹配service包或者子包中定义的方法
within(com.xyz.service.*)//匹配service包中的任何连接点(仅在Spring AOP中的执行方法)
within(com.xyz.service..*)//匹配service包及子包中的任何连接点(仅在Spring AOP中的执行方法)
this(com.xyz.service.AccountService)//代理实现AccountService接口的任何连接点(仅在Spring AOP中执行方法)
target(com.xyz.service.AccountService)//任何目标对象实现AccountService接口的连接点(仅在Spring AOP中执行方法)
args(java.io.Serializable)//任何接受单个参数且在运行时传递的参数是可序列化的连接点(仅在Spring AOP中执行方法)
@target(org.springframework.transaction.annotation.Transactional)//任何目标对象带有@Transactional注解的连接点(仅在Spring AOP中执行方法)
@within(org.springframework.transaction.annotation.Transactional)//任何连接点(仅在Spring AOP中执行方法),其中声明的目标对象类型有@Transactional注解
@annotation(org.springframework.transaction.annotation.Transactional)//任何连接点(仅在Spring AOP中执行方法),其中执行方法有@Transactional注解
@args(com.xyz.security.Classified)//任何接受单个参数的连接点(仅在Spring AOP中执行方法),其中传递的参数的运行时类型有@Classified注解
bean(tradeService)//名为tradeService的Spring bean上的任何连接点(仅在Spring AOP中执行方法)
bean(*Service)//具有与通配符表达式*service匹配名称的Spring bean上的任何连接点(仅在Spring AOP中执行方法)
注意:args(java.io.Serializable)与execution(* *(java.io.Serializable))不同,前者要求参数在运行时是Serializable,后者要求方法前面签名中,声明参数类型是Serializable。
5) 编写好的切入点
在编译期间,AspectJ处理切入点,以优化匹配性能。检查代码并确定每个连接点是否(静态或动态地)匹配给定的切入点是一个代价高昂的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中放置一个测试,以确定在代码运行时是否存在实际匹配)。
在第一次遇到切入点声明时,AspectJ将其重写为匹配流程的最佳形式。即:基本上,切入点是用DNF(析取范式)重写的,切入点的组件被排序,以便首先检查那些计算成本较低的组件。这意味着不必担心理解各种切入点指示符的性能,可以在切入点声明中以任何顺序进行声明。
然而,AspectJ只能使用它被告知的内容。为了优化匹配性能,应该考虑实现什么,并在定义中尽可能缩小匹配的搜索空间。现有的指示符分为三类:kinded、scoping和contextual。(没见过的get,set等非Spring AOP中有的)
- kinded指示符选择特定类型的连接点:execution、get、set、call及handler。
- scoping指示符选择一组连接点(可能是多种连接点):within和withincode。
- Contextual指示器基于上下文匹配(也可以绑定):this、target和@annotation。
一个编写良好的切入点至少应该包括前两种类型(类型和范围)。可以包含Contextual指示符来基于连接点上下文进行匹配,或者绑定该上下文以便在通知中使用。仅提供 kinded指示符或仅提供Contextual可以工作,但由于额外的处理和分析,可能会影响织入性能(使用的时间和内存)。scoping指示符匹配起来非常快,使用它们意味着AspectJ可以非常快地消除不应该进一步处理的连接点组。
(4) 声明一个通知
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。切入点表达式可以是对命名的切入点的简单引用,也可以是在适当位置声明的切入点表达式。
1) Before通知
可以通过使用@Before注解在切面中声明before通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果我们使用一个就地的切入点表达式,我们可以将前面的例子重写为下面的例子:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
2) After Returning通知
当匹配的方法正常返回时,通知就会运行。可以使用@afterReturning注解来声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
有时,需要在通知主体中访问返回的实际值。可以使用@afterReturning的形式绑定返回值来获得访问,如下面的示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning属性中使用的名称必须与通知方法中参数的名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。returning子句还将匹配限制为仅匹配返回指定类型值(在本例中是Object,它匹配任何返回值)的方法执行。
3) After Throwing通知
当匹配的方法执行通过抛出异常退出时,通知就会运行。可以使用@AfterThrowing注解来声明它,如下面的示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,希望通知仅在抛出给定类型的异常时运行,而且还经常需要在通知体中访问被抛出的异常。可以使用throwing属性来限制匹配(如果需要的话—否则使用Throwable作为异常类型),并将被抛出的异常绑定到通知参数。如下所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing属性中使用的名称必须与通知方法中参数的名称相对应。当方法执行通过抛出异常退出时,异常作为相应的参数值传递给通知方法。throwing子句还将匹配限制为仅匹配抛出指定类型异常的方法执行(在本例中是DataAccessException)。
注意:@afterThrowing并不表示一个常规的异常处理回调。具体来说,@AfterThrowing通知方法只接收连接点(用户声明的目标方法)本身的异常,而不接收伴随的@After/@AfterReturn方法的异常。
4) After(Finally)通知
After(finally)通知在匹配的方法执行退出时运行。它是通过使用@After注解声明的。After通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似的目的。下面的示例演示如何使用:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
5) Around通知
Around通知围绕一个匹配的方法执行运行。它有机会在方法运行之前和之后进行工作,并确定何时、如何以及方法是否真正运行。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行前后共享状态,则经常使用Around通知。总是使用最不强大的、符合要求的通知(也就是说,如果在Before通知可以使用,不要使用Around通知)。
Around通知是通过使用@Around注解声明的。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知主体内,对ProceedingJoinPoint调用proceed()会导致底层方法运行。proceed方法还可以传入一个Object[]。当方法继续执行时,数组中的值被用作方法执行的参数。
注意:当使用Object[]调用proceed时,它的行为与通过AspectJ编译器编译的通知时proceed的行为略有不同。对于使用传统AspectJ语言编写的around通知,传递给proceed的参数数量必须与传递给around通知的参数数量匹配(非连接点接收的参数的数量),并且在给定参数位置中传递的proceed值会取代值绑定到的实体的连接点上的原始值。如果编译了为Spring编写的@AspectJ切面,并在AspectJ编译器和weaver中使用proceed参数,则需要注意这个区别。有一种方法可以编写这种在Spring AOP和AspectJ之间100%兼容的切面,下面关于通知参数的一节将对此进行讨论。
下面的例子显示了如何使用Around通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
Around通知返回的值是方法的调用者看到的返回值。例如,一个简单的缓存切面可以从缓存返回一个值(如果有的话),如果没有,则调用proceed()。注意,可以在around通知的主体中调用proceed一次、多次,或者根本不调用。所有这些都是合法的。
6) 通知参数
Spring提供完全类型的通知,这意味着需要在通知签名中声明所需的参数(正如我们在前面的返回和抛出示例中看到的那样),而不是一直使用Object[]数组。在本节的后面部分,我们将看到如何使参数和其他上下文值对通知主体可用。首先,我们来看一下如何编写通用通知,以便找出通知当前通知的方法。
a. 访问当前连接点
任何通知方法都可以声明org.aspectj.lang.JoinPoint类型的参数作为它的第一个参数(注意,around通知需要第一个参数为ProceedingJoinPoint类型,它是JoinPoint的子类。)。JoinPoint接口提供了许多有用的方法:
- getArgs():返回方法参数
- getThis():返回代理对象
- getTarget():返回目标对象
- getSignature():返回被通知的方法的描述
- toString():打印被通知的方法的有用描述
b. 把当前参数传递给通知
我们已经看到了如何绑定返回值或异常值(使用after returning和after throwing通知)。要使通知主体可以使用参数值,可以使用args的绑定形式。如果在args表达式中使用参数名代替类型名,则在调用通知时将相应参数的值作为参数值传递。举个例子,假设想要通知以Account对象作为第一个参数的DAO操作的执行,并且需要访问通知主体中的Account,可以这样写:
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入点表达式的args(account,…)部分有两个目的。首先,它限制只匹配接受至少一个参数,并且传递给该参数的参数是Account的实例的方法执行。其次,它通过Account参数使通知可以使用实际的Account对象。
另一种书写方式是声明一个切入点,当它匹配一个连接点时“提供”Account对象值,然后从通知引用该切入点。如下所示:
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理对象(this)、目标对象(target)和注解(@within、@target、@annotation和@args)都可以以类似的方式绑定。接下来的两个示例展示了如何匹配带有@Auditable注解的方法的执行:
//@Auditable注解的定义
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
//@Auditable注解的方法的执行相匹配的通知
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
c. 通知参数泛型
Spring AOP可以处理类声明和方法参数中使用的泛型。假设有一个如下所示的泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
可以将方法类型限制为特定的参数类型,即通过限制通知参数:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
这种方法不适用于泛型集合。因此不能像下面这样定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
要做到这一点,我们必须检查集合的每个元素,这是不合理的,因为我们也不能决定通常如何处理空值。要实现类似的功能,必须将参数输入到Collection<?>并手动检查元素的类型。
d. 获取参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。参数名不能通过Java反射获得,因此Spring AOP使用以下策略来确定参数名:
- 如果用户明确指定了参数名,则使用指定的参数名。通知和切入点注解都有一个可选的argNames属性,可以使用它来指定注解方法的参数名。这些参数名称在运行时可用。下面的例子展示了如何使用argNames属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,可以从argNames属性的值中省略参数的名称。例如,如果修改上述通知以接收连接点对象,则argNames属性不需要包含它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
如果只有一个JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型的参数,则可以省略argNames属性。
- 使用’argNames’属性有点笨拙,所以如果’argNames’属性没有指定,Spring AOP会查看类的调试信息,并尝试从局部变量表中确定参数名。只要类已经用调试信息编译(至少有’-g:vars’),这些信息就会出现。带着这个标志进行编译的结果是:(1)代码更容易理解(逆向工程),(2)类文件的大小非常大(通常是无关重要的),(3)优化删除未使用的局部变量没有被你的编译器应用。
注意:如果AspectJ编译器(ajc)在没有调试信息的情况下编译了@AspectJ切面,则不需要添加argNames属性,因为编译器保留了所需的信息。
- 如果编译代码时没有必要的调试信息,Spring AOP会尝试推导出绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,而通知方法只接受一个参数,这种配对是明显的)。如果给定可用信息,变量的绑定是不明确的,则抛出二义性bindingexception。
- 如果以上所有策略都失败,抛出一个IllegalArgumentException。
e. 进一步讨论
在前面提到过,我们将描述如何编写一个proceed调用,其参数在Spring AOP和AspectJ之间一致工作。解决方案是确保通知签名按顺序绑定每个方法参数。如下所示:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
7) 通知顺序
当同一个切面的多个通知都想在同一个连接点上运行时,会发生什么情况?Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。“进入”的通知,最高优先级的通知先运行(因此,给定两个before通知,具有最高优先级的先运行)。在连接点上,“出去”的通知,优先级最高的通知最后运行(因此,给定两个after通知,优先级最高的将排在第二位)。
当在不同切面中定义的两条通知都需要在同一个连接点上运行时,除非另有指定,否则执行顺序是未定义的。可以通过指定优先级来控制执行的顺序。这是通过实现org.springframework.core.Ordered以正常的Spring方式完成的。在切面类中实现排序接口或使用@Order注解对其进行注解。给定两个切面,从order. getorder()(或注解值)返回较低值的切面具有较高的优先级。
(5) 引入
引入(在AspectJ中称为类型间声明)使切面能够声明被通知的对象实现给定的接口,并代表这些对象提供该接口的实现。
可以使用@DeclareParents注解实现引入功能。该注解用于声明匹配的类型有一个新的父类型(因此有了这个名称)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked的接口实现,下面的切面声明service接口的所有实现者也实现了UsageTracked接口(例如,通过JMX进行统计):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由带注解字段的类型决定。@DeclareParents注解的value属性是一个AspectJ类型模式。任何匹配类型的bean都实现UsageTracked接口。请注意,在前面示例的before通知中,service beans可以直接用作UsageTracked接口的实现。如果以编程方式访问bean,可以如以下代码所示:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
(6) 切面实例化
默认情况下,在应用程序上下文中每个切面都有一个单独的实例。AspectJ称之为单例实例化模型。可以用不同的生命周期来定义切面。Spring支持AspectJ的perthis和pertarget实例化模型;目前不支持perflow、perflowbelow和pertypewithin。
可以通过在@Aspect注解中指定perthis子句来声明perthis切面。如下所示:
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}
在上面的这个例子中,perthis子句的效果是为执行业务service的每个惟一的service对象创建一个切面实例(每个惟一的对象在切入点表达式匹配的连接点上绑定到这个对象)。切面实例是在第一次在service对象上调用方法时创建的。在创建切面实例之前,其中的任何通知都不会运行。一旦创建了切面实例,其中声明的通知就会在匹配的连接点上运行,但只有当service对象是与此切面相关联的对象时才会如此。
pertarget实例化模型的工作方式与perthis完全相同,但是它在匹配的连接点上为每个惟一的目标对象创建一个切面实例。
(7) AOP举例
business services的执行有时会由于并发性问题而失败(例如,死锁失败)。如果重试该操作,下一次尝试很可能会成功。对于适合在这样的条件下重试的business services(幂等操作不需要返回到用户以解决冲突),我们希望透明地重试操作,以避免客户机看到lockingfailureexception。这是一个明显跨越服务层中的多个服务的需求,因此,非常适合通过一个切面来实现。
因为我们想要重试操作,所以需要使用around通知,以便可以多次调用proceed。下面的清单显示了基本的切面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
请注意,切面实现了Ordered接口,以便我们可以设置切面的优先级高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetries和order属性都是由Spring配置的。主要操作发生在围绕通知的doConcurrentOperation中。请注意,目前我们将重试逻辑应用到每个businessService()。我们试着继续,如果我们用一个悲观的lockingfailureexception失败了,我们就会再次尝试,除非我们已经用尽了所有的重试尝试。
对应的Spring配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了改进切面,使它只重试幂等操作,我们可以定义以下幂等注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
然后,我们可以使用该该来注解service操作的实现。对切面的更改以仅重试幂等操作涉及细化切入点表达式,以便只有@Idempotent操作匹配,如下所示:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
5. 基于XML的AOP支持
如果喜欢基于XML的格式,Spring还提供了使用aop名称空间标记定义切面的支持。它支持与使用@AspectJ风格时完全相同的切入点表达式和通知类型。要使用本节中描述的aop名称空间标记,需要导入spring-aop包,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- bean definitions here -->
</beans>
在Spring配置中,所有切面和通知元素都必须放置在<aop:config>
元素中(在应用程序上下文配置中可以有多个<aop:config>
元素)。<aop:config>
元素可以包含切入点、通知和切面元素(注意,这些元素必须按这个顺序声明)。
注意:<aop:config>
风格的配置大量使用了Spring的自动代理机制。如果已经通过使用BeanNameAutoProxyCreator或类似的东西使用了显式的自动代理,这可能会导致问题(例如没有织入通知)。推荐的使用模式是,要么只使用<aop:config>
风格,要么只使用AutoProxyCreator风格,并且永远不要混合使用它们。
(1) 声明一个切面
当您使用XML支持时,切面是在Spring应用程序上下文中定义为bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和通知信息在XML中捕获。可以通过使用<aop:aspect>
元素声明一个切面,并通过使用ref属性引用bean,如下面的示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
可以像任何其他Spring bean一样配置支持切面的bean(在本例中是bean)并注入依赖项。
(2) 声明一个切入点
可以在<aop:config>
元素中声明一个命名的切入点,让切入点定义跨几个切面和通知共享。表示服务层中任何business service执行的切入点可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
注意,切入点表达式本身使用了4(3)中描述的相同的AspectJ切入点表达式语言。如果使用基于XML的声明样式,则可以引用在切入点表达式的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下所示:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.CommonPointcuts.businessService()"/><!--CommonPointcuts切面定义和4(3)3)中定义相同。-->
</aop:config>
在切面中声明切入点与声明*切入点非常相似,如下面的例子所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与@AspectJ切面非常相似的是,使用基于XML的定义样式声明的切入点可以收集连接点上下文。例如,下面的切入点收集this对象作为连接点上下文并将其传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
通知必须声明为接收收集的连接点上下文,方法是包含匹配名称的参数,如下所示:
public void monitor(Object service) {
// ...
}
在组合切入点子表达式时,&&在XML文档中很尴尬,因此可以分别使用and、or和not关键字来代替&&、||和!例如,前面的切入点可以这样写:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
注意,以这种方式定义的切入点是由它们的XML id引用的,不能作为命名的切入点来形成复合切入点。因此,基于XML的定义样式中的命名切入点支持比@AspectJ样式提供的更有限。
(3) 声明一个通知
1) Before通知
Before通知在匹配的方法执行之前运行。它在<aop:aspect>
中声明,使用<aop:before>
进行声明,如下面的示例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在这里,dataAccessOperation是<aop:config>
级别定义的切入点的id。要以内联方式定义切入点,应该使用一个pointcut属性替换pointcut-ref属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method属性标识一个提供通知主体的方法(doAccessCheck)。必须为包含通知的切面元素所引用的bean定义此方法。在执行数据访问操作(切入点表达式匹配的方法执行连接点)之前,将调用切面bean上的doAccessCheck方法。
2) After Returning通知
在返回之后,当匹配的方法执行正常完成时,通知就会运行。在<aop:aspect>
中声明它的方式与Beforre通知相同。下面的示例演示如何声明:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
与@AspectJ样式一样,可以在通知体中获得返回值。为此,使用returning属性指定返回值应该传递给的参数的名称,如下面的示例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck方法必须声明一个名为retVal的参数。这个参数的类型约束匹配的方式与@afterReturning相同。例如,可以如下声明方法签名:
public void doAccessCheck(Object retVal) {...
3) After Throwing通知
当匹配的方法执行通过抛出异常退出时,通知就会运行。它在<aop:aspect>
中通过使用after-throwing元素声明,如下面的示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>
与@AspectJ样式一样,可以在通知体中获得抛出的异常。为此,使用throwing属性指定应向其传递异常的参数的名称,如下面的示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions方法必须声明一个名为dataaccesssex的参数。此参数的类型约束匹配方式与@afterThrowing相同。例如,方法签名可以声明如下:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
4) After(Finally)通知
无论匹配的方法如何执行,After (finally)通知都会运行。可以使用after元素声明它,如下面的示例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
5) Around通知
Around通知“围绕”一个匹配的方法执行运行。它有机会在方法运行之前和之后进行工作,并确定何时、如何以及方法是否真正运行。Around通知通常用于以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态。总是使用最不强大的、符合你要求的通知。如果Before通知能起作用,就不要使用Around通知。
可以通过使用aop:around元素声明Around通知。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知主体内,对ProceedingJoinPoint调用proceed()会导致底层方法运行。proceed方法也可以用Object[]来调用。当方法继续执行时,数组中的值被用作方法执行的参数。有关调用proceed调用Object[]的注意事项,参考4(4)5)。下面的示例展示了如何在XML中声明around通知:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling通知的实现可以与@AspectJ示例中的完全相同(当然,去掉注解),如下面的示例所示:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
6) 通知参数
基于XML的声明样式支持完全类型的通知,与@AspectJ支持所描述的方式相同——通过按名称匹配切入点参数和通知方法参数。如果想显式地指定通知方法参数名称(不依赖于先前描述的检测策略),可以通过使用通知的arg-names属性元素,和argNames相同。如下所示:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
arg-names属性接受以逗号分隔的参数名列表。
下面这个稍微复杂一些的基于xsd方法的示例展示了与一些强类型参数一起使用的一些Around通知:
package x.y.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
接下来是切面。注意,profile(…)方法接受大量强类型参数,其中第一个参数恰好是用于处理方法调用的连接点。此参数的存在表明profile(…)将被用作Around通知,如下面的示例所示:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
最后,下面的示例XML配置将对特定连接点执行前面的通知产生影响:
<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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
7) 通知顺序
当多个通知方法定义在同一个<aop:aspect>
元素中时,且需要运行在相同的连接点时,优先级由通知元素在封闭的<aop:aspect>
元素中声明的顺序决定,从最高优先级到最低优先级。这与@Aspect定义的切面类中的多个通知方法不同。例如,给定一个around通知和一个before通知,它们是在同一个<aop:aspect>
元素中定义的,适用于同一个连接点,为了确保around通知比before通知有更高的优先级,<aop:around>
元素必须在<aop:before>
元素之前声明。
一般情况下,如果发现有多个通知定义在相同的<aop:aspect>
中并适用于相同的连接点,考虑将这样的通知方法分解为每个<aop:aspect>
元素中的每个连接点的一个通知方法,或者将通知重构为单独的<aop:aspect>
元素,可以在切面级别上对这些元素进行排序。
不同切面中的通知排序参考4(4)7)。
(4) 引入
引入(在AspectJ中称为类型间声明)让切面声明被通知的对象实现给定的接口,并代表这些对象提供该接口的实现。
可以在aop:aspect中使用aop:declare-parents元素实现引入功能。可以使用aop:declare-parents元素声明匹配的类型有一个新的父类型(因此得名)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked的接口实现,下面的切面声明service接口的所有实现者也实现了UsageTracked接口:
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
然后,支持usageTracking bean的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
要实现的接口由implements -interface属性决定。类型匹配属性的值是一个AspectJ类型模式。任何匹配类型的bean都实现UsageTracked接口。请注意,在前面示例的before通知中,service bean可以直接用作UsageTracked接口的实现。要以编程方式访问bean,可以编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
(5) 切面实例化
目前只支持单例化模式。
(6) Advisors
“advisors”的概念来自Spring中定义的AOP支持,在AspectJ中没有直接的对等物。advisors就像一个独立的小切面,只有一条通知。通知本身由bean表示,必须实现Spring中的通知类型中描述的通知接口之一。advisors可以利用AspectJ切入点表达式。
Spring通过<aop:advisor>
元素支持advisor概念。它经常与事务通知一起使用,后者在Spring中也有自己的名称空间支持。下面的示例显示了一个advisor工具:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
要定义Advisor的优先级,以便通知可以参与排序,请使用order属性定义Advisor的有序值。
(7) AOP举例
本节展示4(7)AOP示例中的并发锁定失败重试示例在使用XML支持重写的写法。
business services的执行有时会由于并发性问题而失败(例如,死锁失败)。如果重试该操作,下一次尝试很可能会成功。对于适合在这样的条件下重试的业务服务(幂等操作不需要返回到用户以解决冲突),我们希望透明地重试操作,以避免客户机看到lockingfailureexception。这是一个明显跨越服务层中的多个服务的需求,因此,非常适合通过一个切面来实现。
因为我们想要重试操作,所以需要使用around通知,以便可以多次调用proceed。下面的例子显示了基本的切面实现(它是一个常规Java类):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
注意,切面实现了Ordered接口,以便我们可以设置切面的优先级高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetries和order属性都是由Spring配置的。主要操作发生在around通知方法的doConcurrentOperation中。如果失败,出现PessimisticLockingFailureException,会继续尝试,直到尝试的次数用尽。
对应的Spring配置如下:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
请注意,目前我们假设所有business services都是幂等的。如果不是这样,我们可以通过引入幂等注解并使用该注解来注解service操作的实现来改进切面,使其仅重试真正的幂等操作,如下面的示例所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
对切面的更改以仅重试幂等操作涉及细化切入点表达式,以便只有@Idempotent操作匹配,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
6. 选择AOP声明风格
一旦确定了切面是实现给定需求的最佳方法,那么如何决定是使用Spring AOP还是使用AspectJ,是使用切面语言(代码)风格、@AspectJ注解风格还是使用Spring XML风格呢?这些决定受到许多因素的影响,包括应用程序需求、开发工具和团队对AOP的熟悉程度。
(1) Spring AOP vs Full AspectJ
使用最简单的方法。Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/织入器。如果只需要对Spring bean上的操作执行提出通知,那么Spring AOP是正确的选择。如果需要通知不由Spring容器管理的对象(通常是域对象),需要使用AspectJ。如果希望通知除简单方法执行之外的连接点(例如,字段获取或设置连接点等),还需要使用AspectJ。
当使用AspectJ时,可以选择AspectJ语言语法(也称为代码样式)或@AspectJ注解样式。显然,如果不使用Java 5+,那么可以选择使用代码样式。如果切面在设计中扮演着重要的角色,并且能够使用Eclipse的AspectJ开发工具(AJDT)插件,那么AspectJ语言语法是首选选项。它更干净、更简单,因为语言是专门为编写切面而设计的。如果不使用Eclipse,或者只有少数切面在的应用程序中没有发挥主要作用,考虑使用@AspectJ风格,在IDE中坚持常规的Java编译,并在构建脚本中添加一个切面织入阶段。
(2) Spring AOP: @AspectJ vs XML
如果选择使用Spring AOP,则可以选择@AspectJ或XML样式。有各种各样的权衡需要考虑。
现有的Spring用户可能最熟悉XML样式,并且它得到了真正的pojo的支持。当使用AOP作为配置企业服务的工具时,XML是一个不错的选择(一个好的测试是是否将切入点表达式视为可能希望独立更改的配置的一部分)。使用XML样式,从配置来看,系统中存在哪些切面更清楚。
XML样式有两个缺点。首先,它没有在一个地方完全封装需求的实现。DRY原则认为,在一个系统中,任何需求实现都应该有一个单一的、明确的、权威的表示。在使用XML样式时,需求的实现被划分为支持bean类的声明和配置文件中的XML。当使用@AspectJ样式时,此信息被封装在单个模块中:切面。其次,XML风格在表达切面比@AspectJ风格稍受限制:只支持单例切面实例化模型,而且不可能组合在XML中声明的命名切入点。例如,在@AspectJ风格中,可以编写如下内容:
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
在XML风格中,可以声明前两个切入点:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
XML方法的缺点是不能通过组合这些定义来定义accountPropertyAccess切入点。
@AspectJ样式支持额外的实例化模型和更丰富的切入点组合。它的优点是将切面保持为模块化单元。它还有一个优点,就是@AspectJ切面可以被Spring AOP和AspectJ理解(从而消费)。因此,如果决定需要AspectJ的功能来实现额外的需求,可以很容易地迁移到经典的AspectJ设置。总的来说,除了简单的企业服务配置之外,Spring团队更喜欢使用@AspectJ风格来定制切面。
7. 混合切面类型
完全有可能通过使用自动代理支持,XML定义的<aop:aspect>
切面,<aop: advisor>
声明的advisors,甚至在同一配置中其他样式的代理和拦截器实现混合的@AspectJ风格切面。所有这些都是通过使用相同的底层支持机制实现的,因此可以混合。
8. 代理机制
Spring AOP使用JDK动态代理或CGLIB为给定的目标对象创建代理。JDK动态代理内置在JDK中,而CGLIB是一个常见的开源类定义库(重新打包到spring-core中)。
如果要代理的目标对象实现了至少一个接口,则使用JDK动态代理。由目标类型实现的所有接口都是代理的。如果目标对象没有实现任何接口,则创建一个CGLIB代理。
如果想强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),应该考虑以下问题:
- 使用CGLIB, final方法不能被通知,因为final方法不能被运行时生成的子类方法覆盖。
- 从Spring 4.0开始,代理对象的构造函数不再被调用两次,因为CGLIB代理实例是通过Objenesis创建的。只有在JVM不允许绕过构造函数时,才可能看到来自Spring AOP支持的双重调用和相应的调试日志条目。
为了强制使用CGLIB代理,将<aop:config>
元素的proxy-target-class属性的值设置为true,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
要在使用@AspectJ自动代理支持时强制使用CGLIB代理,请将<aop:aspectj-autoproxy>
元素的proxy-target-class属性设置为true,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
注意:多个<aop:config/>
在运行时被分解成一个统一的自动代理创建器,它应用<aop:config/>
部分(通常来自不同的XML bean定义文件)指定的最强的代理设置。这也适用于<tx:annotation-driven/>
和<aop:aspectj-autoproxy/>
元素。明确地说,在<tx:annotation-driven/>
, <aop:aspectj-autoproxy/>
,或<aop:config/>
元素上使用proxy-target-class="true"将强制对所有这三个元素使用CGLIB代理。
(1) 理解AOP代理
Spring AOP是基于代理的。在编写自己的切面或使用Spring框架提供的任何基于Spring AOP的切面之前,掌握Spring AOP是基于代理的实际含义是非常重要的。
首先考虑这样一个场景,有一个普通的、未代理的、没有任何特殊之处的、直接的对象引用,如下面的代码片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
如果在对象引用上调用一个方法,则该方法将直接在该对象引用上调用,如下图和清单所示:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
当客户端代码的引用是代理时,情况会略有变化。请考虑以下图表和代码片段:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
这里需要理解的关键是,Main类的main(…)方法内部的客户端代码引用了代理。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(在本例中是SimplePojo引用),它可能对自身进行的任何方法调用,例如this.bar()或this.foo(),都将针对目标对象引用而不是代理调用。这种自调用会导致与方法调用相关的通知无法执行。(即需要嵌套调用也能对调用代理而非调用目标对象的方法)
如何实现对代理的调用而不是自调用呢?最好的方法(这里不太严格地使用术语“最好”)是重构代码,使自调用不会发生。如上面的代码片段所示,确实需要做一些工作,但这是最好的,最少侵入性的方法。下面的代码片段也可以实现对代理的调用,但是会将类中的逻辑完全绑定到Spring AOP上,如下面的示例所示:
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
完全将代码与Spring AOP结合在一起,并且使类本身意识到它是在AOP上下文中使用的,不推荐使用。在创建代理时,它也还需要一些额外的配置,如下面的示例所示:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
最后,必须注意的是,AspectJ没有这种自调用问题,因为它不是基于代理的AOP框架。
9. 编程创建@AspectJ的代理
除了使用<aop:config>
或者 <aop:aspectj-autoproxy>
,也可以通过编程方式创建目标对象的代理。这里,我们将重点关注通过使用@AspectJ切面自动创建代理的能力。
可以使用org.springframework.aop.aspectj.AspectJProxyFactory类来为一个或多个@AspectJ切面通知的目标对象创建代理。这个类的基本用法非常简单,如下面的示例所示:
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
10. 在Spring应用中使用AspectJ
到目前为止,我们所涉及的都是纯Spring AOP。在本节中,将介绍超出了Spring AOP单独提供的功能,即如何使用AspectJ编译器或weaver来代替Spring AOP或者实现Spring AOP不能实现的功能。Spring附带了一个小的AspectJ切面库,发行版为spring-aspects.jar,可单独使用。需要将其添加到类路径中。
(1) 使用AspectJ实现对域对象的依赖注入
Spring容器实例化并配置在应用程序上下文中定义的bean。给定包含要应用的配置的bean定义的名称,还可以要求bean工厂配置已存在的对象。spring-aspects.jar包含一个注解驱动的切面,它利用这个功能来允许任何对象的依赖项注入。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用new操作符以编程方式创建的,或者是通过ORM工具作为数据库查询的结果创建的。
@Configurable注解将类标记为符合spring驱动配置的条件。在最简单的情况下,可以纯粹地将它用作标记注解,如下面的示例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
当以这种方式作为标记接口使用时,Spring通过使用与完全限定类型名称(com.xyz.myapp.domain.Account)相同的bean定义(通常是原型作用域)来配置注解类型(在本例中是Account)的新实例。由于bean的默认名称是其类型的完全限定名,声明原型定义的一种方便方法是省略id属性,如下面的示例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果想显式指定要使用的原型bean定义的名称,可以直接在注解中完成,如下面的示例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
Spring现在寻找名为account的bean定义,并使用它作为配置新帐户实例的定义。
还可以使用自动装配来避免指定专用bean定义。要让Spring应用自动装配,请使用@Configurable注解的autowire属性。可以指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME)用于分别根据类型或名称自动装配。作为一种替代方案,最好是在字段或方法级别通过@Autowired或@Inject为@@Configurablebean指定显式的、注解驱动的依赖注入。
最后,可以使用dependencyCheck属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))来为新创建和配置的对象引用启用Spring依赖项检查。如果这个属性被设置为true, Spring在配置之后会验证所有的属性(不是原语或集合)已经被设置。
注意,单独使用注解不会产生任何效果。是spring-aspects.jar中的AnnotationBeanConfigurerAspect对注解的存在起作用。本质上,切面在初始化一个用@Configurable注解的类型的新对象返回后,使用Spring根据注解的属性配置新创建的对象。在这个上下文中,初始化指的是新实例化的对象(例如,用new操作符实例化的对象)以及正在进行反序列化的可序列化对象(例如,通过readResolve())。
注意:在大多数情况下,新对象初始化后返回的确切语义是正确的。在这种情况下,初始化后意味着依赖关系是在对象构造完成之后注入的。这意味着依赖项不能在类的构造函数体中使用。如果希望在构造函数体运行之前注入依赖项,从而可以在构造函数体中使用,则需要在@Configurable声明中定义依赖项,如下所示:
@Configurable(preConstruction = true)
要做到这一点,带注解的类型必须使用AspectJ编织器进行织入。可以使用构建时Ant或Maven任务来完成这一任务,也可以使用加载时编织入(见10(4))。AnnotationBeanConfigurerAspect本身需要由Spring进行配置(以便获得用于配置新对象的对bean工厂的引用)。如果使用基于java的配置,可以添加@EnableSpringConfigured到任何@Configuration类,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果喜欢基于XML的配置,那么Spring上下文名称空间定义了一个方便的上下文:Spring -configured元素,可以如下所示使用它:
<context:spring-configured/>
在配置切面之前创建的@Configurable对象的实例会导致向调试日志发出一条消息,并且不会发生对象的配置。一个例子可能是Spring配置中的bean,它在被Spring初始化时创建域对象。在这种情况下,可以使用depends-on bean属性手动指定该bean依赖于配置切面。下面的例子展示了如何使用depends-on属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>