1 Spring AOP
1.1 AOP 概念、好处、应用场景
有两种编程方式,OOP 和 AOP。
OOP 是面向对象编程,Object-Oriented Programming。这种编程方式容易造成大量代码的重复,不利于各个模块的重用。
AOP 是面向切面编程,Aspect-Oriented Programming。这种编程方式将业务无关但是对多个对象产生影响的公共行为和逻辑抽取出来,并封装为一个可重用的模块,这个模块被命名为“切面”。这种编程方式可以减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
- 每个功能内实现输出日志、执行事务的功能。
缺点:重复性高,主业务与交叉业务逻辑深度耦合。 - 创建工具类,工具类中提供输出日志、执行事务的功能。
缺点:主业务与交叉业务逻辑深度耦合。 - 动态代理。
优点:在不修改主业务逻辑的前提下,扩展和增强其功能。
常见的切面功能有日志,事务,统计信息,参数检查,权限验证。切面的特点: 一般都是非业务方法,独立使用的。
1.2 AOP 的两种实现方式
AOP 实现的关键在于代理模式,主要分为静态代理和动态代理。静态代理的代表为 AspectJ;动态代理则以 Spring AOP 为代表。
(1)AspectJ 是静态代理的增强,所谓静态代理,就是 AOP 框架会在编译阶段生成 AOP 代理类,因此也称为编译时增强,他会在编译阶段将 AspectJ (切面)织入到 Java 字节码中,运行的时候就是增强之后的 AOP 对象。官方网站:The AspectJ Project | The Eclipse Foundation
(2)Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
静态代理与动态代理区别在于生成 AOP 代理对象的时机不同,相对来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译器进行处理,而 Spring AOP 则无需特定的编译器处理。
1.3 JDK 动态代理和 CGLIB 动态代理的区别
Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理:
JDK 动态代理只提供接口的代理,不支持类的代理。核心 InvocationHandler 接口和 Proxy 类,InvocationHandler 通过 invoke ()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy 利用 InvocationHandler 动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
如果代理类没有实现 InvocationHandler 接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现 AOP。CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。
1.4 动态代理
动态代理实现方式
Jdk 动态代理,使用 jdk 中的 Proxy,Method,InvocaitonHanderl 创建代理对象。JDK 在创建动态代理对象时,默认继承了 Proxy 类。但是 Java 不支持多继承,所以 JDK 要求通过接口去实现动态代理。需要被代理的目标类必须实现相应接口。利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理。
Cglib 动态代理:第三方的工具库,创建代理对象,原理是继承。通过继承目标类,创建子类。子类就是代理对象。要求目标类不能是 final 的,方法也不能是 final 的。通过继承被代理的目标类实现代理,所以不需要目标类实现接口。利用 ASM(开源的 Java 字节码编辑库,操作字节码)开源包,将代理对象类的 class 文件加载进来,通过修改其字节码生成子类来处理。
动态代理的作用
1)在目标类源代码不改变的情况下,增加功能。
2)减少代码的重复
3)专注业务逻辑代码
4)解耦合,让你的业务功能和日志,事务非业务功能分离。
public class MyIncationHandler implements InvocationHandler {
//目标对象
private Object target; //SomeServiceImpl类
public MyIncationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//通过代理对象执行方法时,会调用执行这个invoke()
System.out.println("执行MyIncationHandler中的invoke()");
System.out.println("method名称:"+method.getName());
String methodName = method.getName();
Object res = null;
if("doSome".equals(methodName)){ //JoinPoint Pointcut
ServiceTools.doLog(); //在目标方法之前,输出时间
//执行目标类的方法,通过Method类实现
res = method.invoke(target,args); //SomeServiceImpl.doSome()
ServiceTools.doTrans(); //在目标方法执行之后,提交事务
} else {
res = method.invoke(target,args); //SomeServiceImpl.doOther()
}
//目标方法的执行结果
return res;
}
}//创建目标对象
SomeService target = new SomeServiceImpl();
//创建InvocationHandler对象
InvocationHandler handler = new MyIncationHandler(target);
//使用Proxy创建代理
SomeService proxy = (SomeService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),handler);
//com.sun.proxy.$Proxy0
System.out.println("proxy======"+proxy.getClass().getName());
//通过代理执行方法,会调用handler中的invoke()
proxy.doSome();
proxy.doOther();1.5 AOP 涉及的专业术语
切面(Aspect)
切面泛指交叉业务逻辑。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。
连接点(JoinPoint)
连接点指可以被切面植入的具体方法。通常业务接口中的方法均为连接点。上面动态代理中,dosome 方法是连接业务方法和切面方法的。
切入点(Pointcut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。
被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
目标对象(Target)
目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。上例中的 StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。
通知(Advice)
通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
2 基于 AspectJ 的 AOP 实现
2.1 AspectJ 的通知类型
AspectJ 中常用的通知有五种类型:
(1)前置通知 @Before 方法调用前通知
(2)最终通知 @After 方法完成后通知
(3)后置通知 @AfterReturning 方法成功执行后通知
(4)异常通知 @AfterThrowing 方法抛出异常后通知
(5)环绕通知 @Around 包裹方法
无异常:around before method around after afterReturning
有异常:around before method around after afterThrowing
Spring 版本 5.3. X 以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
Spring 版本 5.3. X 以后: - 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
try{
// 前置通知
// 目标方法
// 返回通知
} catch (Exception e){
// 异常通知
} finally{
// 后置通知
}2.2 AspectJ 测试举例
课程代码:ch 06-aop-aspectj
为了方便理解上面的通知类型,下面结合 try-catch-finally 模型举例:
-
新建 maven 项目 & 加入依赖
Spring 依赖
Aspectj 依赖
Junit 单元测试<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!--spring依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!--aspectj依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency> -
创建目标类:接口和他的实现类
接口:SomeServrice. Java
实现类:SomeServriceImpl. Java
要做的是给类中的方法增加功能。 -
创建切面类:普通类
创建 MyAspect. Java 类,类的上面加入@Aspect。
在类中定义方法,方法就是切面要执行的功能代码。
在方法的上面加入 AspectJ 中的通知注解,例如@Before。
有需要指定切入点表达式 execution ()。
方法类型为 public,方法无返回值 void,方法名称自定义,方法可以无参数,方法有参数不是自定义的。
-
创建 spring 的配置文件:声明对象,把对象交给容器统一管理
声明对象你可以使用注解或者 xml 配置文件<bean> 1)声明目标对象 2)声明切面类对象 3)声明 aspectj 框架中的自动代理生成器标签。
自动代理生成器:用来完成代理对象的自动创建功能的。<!--把对象交给spring容器,由spring容器统一创建,管理对象--> <!--声明目标对象--> <bean id="someService" class="com.bjpowernode.ba02.SomeServiceImpl" /> <!--声明切面类对象--> <bean id="myAspect" class="com.bjpowernode.ba02.MyAspect" /> <!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。 创建代理对象是在内存中实现的, 修改目标对象的内存中的结构。 创建为代理对象 所以目标对象就是被修改后的代理对象. aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象。 --> <aop:aspectj-autoproxy /> -
创建测试类,从 spring 容器中获取目标对象(实际就是代理对象)。
通过代理执行方法,实现 aop 的功能增强。String config="applicationContext.xml"; ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //从容器中获取目标对象 SomeService proxy = (SomeService) ctx.getBean("someService"); System.out.println("proxy:"+proxy.getClass().getName()); //通过代理的对象执行方法,实现目标方法执行时,增强了功能 proxy.doThird();
2.3 编写切点
Spring AOP 所支持的 AspectJ 切点指示器:
| AspectJ 指示器 | 描述 |
|---|---|
| arg () | 限制连接点匹配参数为指定类型的执行方法 |
| @args () | 限制连接点匹配参数由指定注解标注的执行方法 |
| execution () | 用于匹配是连接点的执行方法 |
| this () | 限制连接点匹配 AOP 代理的 bean 引用为指定类型的类 |
| target | 限制连接点匹配目标对象为指定类型的类 |
| @target () | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
| within () | 限制连接点匹配指定的类型 |
| @within () | 限制连接点匹配指定注解所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里) |
| @annotation | 限定匹配带有指定注解的连接点 |
在 Spring 中尝试使用 AspectJ 其他指示器时,将会抛出 IllegalArgumentException 异常。
只有 execution 指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。
execution 表达式
package concert;
public interface Performance {
public void perform();
}
Execution 表达式的编写规则:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern:访问权限类型
- ret-type-pattern:返回值类型
- declaring-type-pattern:包名. 类名
- name-pattern (param-pattern):方法名 (参数类型和参数个数)
- throws-pattern:抛出异常类型
- 问号表示可以不写
- 全部总结为 execution (访问权限方法返回值方法声明 (参数) 异常类型)
假设当执行 perform 方法时触发:


| 符号 | 意义 |
|---|---|
* | 0 至多个任意字符 |
.. | 用在方法参数中,表示任意多个参数; 用在包名后,表示当前包及其子包路径 |
+ | 用在类名后,表示当前类及其子类; 用在接口后,表示当前接口及其实现类 |
上面相关函数的详细使用可以参考:spring aop 中 pointcut 表达式完整版
切点中选择 bean
Spring 还引入了一个新的 bean () 指示器,它允许我们在切点表达式中使用 bean 的 ID 来标识 bean。Bean () 使用 bean ID 或 bean 名称作为参数来限制切点只匹配特定的 bean。
例如,在执行 Performance 的 perform () 方法时应用通知,但限定 bean 的 ID 为 woodstock;又或者非操作来排除:
execution(* concert.Performance.perform()) and bean('woodstock')
execution(* concert.Performance.perform()) and !bean('woodstock')2.4 编写切面(注解)
这里定义一个 Audience 切面:在类上添加 @Aspect 注解,即可将当前类定义为切面。
package concert;
import org.aspect.lang.annotation.AfterReturning;
import org.aspect.lang.annotation.AfterThrowing;
import org.aspect.lang.annotation.Aspect;
import org.aspect.lang.annotation.Before;
// 定义一个切面
@Aspect
public class Audience {
// 演出前行为,关闭手机声音
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
// 演出前行为,入座
@Before("execution(** concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}
// 演出结束后行为,鼓掌
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
// 演出发生异常后行为,退钱
@AfterThrowing ("execution (** concert.Performance.Perform (..))")
Public void demandRefund () {
System.Out.Println ("Demanding a refund");
}
}2.5 编写连接点
上面切面对应的切点存在重复,因此使用 @Pointcut 注解来创建连接点来进行简化。
操作方法为,创建一个自定义方法,添加 @Pointcut 注解,在其他通知的切入点表达式中,直接使用该自定义方法的方法名。
@Aspect
Public class Audience {
// 定义一个连接点
@Pointcut ("execution (** concert.Performance.Perform (..))")
Public void performce () { }
// 使用连接点
@Before("performce ()")
Public void silenceCellPhones () {... }
@Before ("performce ()")
Public void takeSeats () {... }
@AfterReturning ("performce ()")
Public void applause () {... }
@AfterThrowing ("performce ()")
Public void demandRefund () {... }
}连接点也可以在不同切面中使用,如下所示:
@Before("com.atguigu.aop.CommonPointCut.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}2.6 编写通知
@Before 前置通知-方法可有 JoinPoint 参数
在使用 @Before 前置通知时,可以引入 JoinPoint 参数。基于 JoinPoint 参数来获取方法执行时的信息,如前面、名称、参数等。
如果引入 JoinPoint 参数,那么需要将该参数放到方法的第一个参数位置。
@Before (value = "execution (void *..SomeServiceImpl.DoSome (String, Integer))")
Public void myBefore (JoinPoint jp){
System.Out.Println ("方法的签名(定义)="+jp.GetSignature ());
System.Out.Println ("方法的名称="+jp.GetSignature (). GetName ());
Object args [] = jp. GetArgs (); //获取方法的实参
}@AfterReturning 后置通知-注解有 returning 属性、方法可有 Object 参数
后置通知的方法,必须 public、方法可有 Object 参数和 JoinPoint 参数。
后置通知的注解,value-切入点表达式,returning-自定义变量、表示目标方法的返回值、与 Object 入参的变量名相同。
后置通知的入参 Object 对象对应着目标方法的返回值。在后置通知中处理该对象,可能会影响目标对象的返回结果。影响程度与 Java 传引用的效果一样,改变引用无影响,改变引用属性有影响。
@AfterReturning (value = "execution (* *.. SomeServiceImpl. DoOther 2 (..))", returning = "res")
Public void myAfterReturing 2 (Object res){
// Object res: 是目标方法执行后的返回值,根据返回值做你的切面的功能处理
System. Out. Println ("后置通知:在目标方法之后执行的,获取的返回值是:"+res);
// 处理一:改变引用,不会影响目标方法返回值
Res = ((String) res). ToUpperCase ();
// 处理二:改变引用内属性值,会影响目标方法返回值
Res = (Student) res;
((Student) res). SetAge (3333);
}@AfterThrowing 异常通知-注解中有 throwing 属性、方法可有 Exception 参数
异常通知的方法,必须 public、没有返回值、方法可有 Exception 参数和 JoinPoint 参数。
异常通知的注解,value-切入点表达式,throwing-自定义变量、表示目标方法抛出的异常对象、与 Exception 入参的变量名相同。
@AfterThrowing (value = "execution (* *.. SomeServiceImpl. DoSecond (..))",
Throwing = "ex")
Public void myAfterThrowing (Exception ex) {
System. Out. Println ("异常通知:方法发生异常时,执行:"+ex. GetMessage ());
//发送邮件,短信,通知开发人员
}@After 最终通知
最终通知的方法,必须 public、没有返回值、方法无参或 JoinPoint。
无论目标方法是否抛出异常,@After 均会被执行。
@Around 环绕通知-方法有 ProceedingJoinPoint 参数
环绕通知的方法,必须 public、必须有返回值、必须有 ProceedingJoinPoint 参数。
ProceedingJoinPoint 就等同于 Method,用于执行目标方法,等同于 jdk 动态代理的 InvocationHandler 接口。
环绕通知可以更改目标方法的返回值,环绕通知方法的返回值就是最终目标方法的返回值。
环绕通知一般用于事务处理。
@Around (value = "execution (* *.. SomeServiceImpl. DoFirst (..))")
Public Object myAround (ProceedingJoinPoint pjp) throws Throwable {
//0. 在目标方法的前或者后加入功能
Object result = null;
System. Out. Println ("环绕通知:在目标方法之前,输出时间:"+ new Date ());
//1. 目标方法调用
Result = pjp. Proceed ();
//2. 在目标方法的前或者后加入功能
System. Out. Println ("环绕通知:在目标方法之后,提交事务");
//修改目标方法的执行结果,影响方法最后的调用结果
If (result != null)
Result = "Hello AspectJ AOP";
//返回目标方法的执行结果
Return result;
}2.7 启用切面、开启自动代理
使用 @EnableAspectJAutoProxy 注解开启自动代理功能。同时,使用 @Component + @Bean 将切面注入到 Spring 容器中。
@Configuration
@EnableAspectJAutoProxy
@Component
Public class ConcertConfig {
@Bean
Public Audience audience () { return new Audience (); }
}如果使用 XML 来装配 bean,则需要如下配置:
<?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:context=" http://www.springframework.org/schema/context"
xmlns:aop=" http://www.springframework.org/schema/aop"
xsi:schemaLocation=" http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
基于注解的 AOP 的实现:
1、将目标对象和切面交给 IOC 容器管理(注解+扫描)
2、开启 AspectJ 的自动代理,为目标对象自动生成代理
3、将切面类通过注解@Aspect 标识
-->
<context:component-scan base-package="com.atguigu.aop.annotation"/>
<aop:aspectj-autoproxy />
<bean class="concert.Audience" />
<!-- 上面这一个也可以通过 @Component 等方式注入到容器中 -->
</beans>
不管你是使用 JavaConfig 还是 XML,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为 Concert bean 创建一个代理,Audience 类中的通知方法将会在 perform () 调用前后执行。
我们需要记住的是,Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是 Spring 基于代理的切面。这一点非常重要,因为这意味着尽管使用的是 @AspectJ 注解,但我们仍然限于代理方法的调用。如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖 Spring 来创建基于代理的切面。
到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前置通知和后置通知。但是表 4.2 还提到了另外的一种通知:环绕通知 (around advice)。环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。
2.8 处理通知的参数
情景:假设 playTrack (int) 方法是播放音乐列表中第 x 首音乐。现在想生成一个前置通知,记录每首音乐播放的次数。
基于这个情景,需要获得 playTrack 方法的参数并进行处理,具体方法如下:
@Aspect
Public class TrackCounter {
private Map<Integer, Integer> cnt= new HashMap<>();
@Pointcut ("execution (* soundsystem. CompactDisc. PlayTrack (int) " +
"&& args (trackNumber)")
Public void trackPlayed (int trackNumber) { }
@Before ("trackPlayed (trackNumber)")
Public void countTrack (int trackNumber) {
Cnt. Put (trackNumber, cnt. GetOrDefault (trackNumber, 0)+ 1);
}
}
Args () 限定符功能 - 限制参数类型、获取目标参数。此处用的是获取目标参数。
通过 args (trackNumber) 将 playTrack () 方法入参传递给 trackNumber 对象,其中 trackNumber 对象必须是通知方法的入参。这种方式来获取目标参数,并在通知方法中进行处理。
Args () 还有限制参数类型的功能,使用时,填写类型的全限定路径,如 “com. Example. Type. Ball” 的形式。
考虑后续将 AOP 的几个限定符功能进行整理. TODO
2.9 利用 AOP 为 Bean 增加方法
假设,要为某个 bean 的代理对象增加 Encoreable 接口的 performEncore 方法。
Public interface Encoreable {
Void performEncore ();
}
@Aspect
Public class EncodeableIntroducer {
@DeclareParents (value="concert. Performce+",
DefaultImpl=DefaultEncoreable. Class)
Public static Encoreable encoreable;
}@DeclareParents 注解由三部分组成:
- value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
- defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
- @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。
和其他的切面一样,我们需要在 Spring 应用中将 EncoreableIntroducer 声明为一个 bean:
<bean class="concert.EncoreableIntroducer" />2.10 切面的优先级
概念:相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order 注解可以控制切面的优先级:
- @Order (较小的数):优先级高
- @Order (较大的数):优先级低

实际意义:实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。

此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。

3 在 XML 中声明切面
| AOP 配置元素 | 用途 |
|---|---|
| aop:advisor | 定义 AOP 通知器 |
| aop:after | 定义 AOP 后置通知(不管被通知的方法是否执行成功) |
| aop:after-returning | 定义 AOP 返回通知 |
| aop:after-throwing | 定义 AOP 异常通知 |
| aop:around | 定义 AOP 环绕通知 |
| aop:aspect | 定义一个切面 |
| aop:aspectj-autoproxy | 启用 @AspectJ 注解驱动的切面 |
| aop:before | 定义一个 AOP 前置通知 |
| aop:config | 顶层的 AOP 配置元素。大多数的元素必须包含在元素内 |
| aop:declare-parents | 以透明的方式为被通知的对象引入额外的接口 |
| aop:pointcut | 定义一个切点 |
3.1 编写通知
在 aop:config 元素内,可以声明一个或多个通知器、切面或者切点。使用 aop:aspect 元素声明了一个简单的切面。Ref 元素引用了一个 POJO bean,该 bean 实现了切面的功能 —— 在这里就是 audience。Ref 元素所引用的 bean 提供了在切面中通知所调用的方法。
如果想让定义的切点能够在多个切面使用,我们可以把 aop:pointcut 元素放在 aop:config 元素的范围内。
<aop:config>
<aop:aspect ref="audience">
<aop:before pointcut="execution (** concert. Performance. Perform (..))"
method="silenceCellPhones" />
<aop:before pointcut="execution (** concert. Performance. Perform (..))"
method="takeSeats" />
</aop:aspect>
</aop:config>
<!-- 使用 Pointcut -->
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expressions="execution (** concert. Performance. Perform (..))" />
<aop:before pointcut-ref="performance" method="silenceCellPhones" />
<aop:around pointcut-ref="performance" method="watchPerformance" />
</aop:aspect>
</aop:config>3.2 获取参数
配置中声明了 TrackCounter bean 和 BlankDisc bean,并将 TrackCounter 转化为切面。
其中,TrackCounter 方法在中进行了定义,不过 XML 配置切面时,不需要配置其中的注解。
<?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>"
xmlns:util="<http://www.springframework.org/schema/util>"
Xsi:schemaLocation="
<http://www.springframework.org/schema/aop>
<http://www.springframework.org/schema/aop/spring-aop.xsd>
<http://www.springframework.org/schema/beans>
<http://www.springframework.org/schema/beans/spring-beans.xsd>" >
<bean id="trackCounter" class="soundsystem.TrackCounter" />
<bean id="cd" class="soundsystem.BlackDisc" >
<property name="title" value="Sgt. Pepper's Lonelt Hearts Club Band" />
<property name="artist" value="The Beatles" />
<property name="tracks" >
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<value>Fixing a Hole</value>
<!-- ...other tracks omitted for brevity... -->
</list>
</property>
</bean>
<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut
Id="trackPlayed"
Expression="execution (* soundsystem. CompactDisc. PlayTrack (int)) and args (trackNumber)" />
<aop:before pointcut-ref="trackPlayed" method="countTrack" />
</aop:aspect>
</aop:config>
</beans>3.3 增加方法
这里的声明与上述通过注解的声明,效果相同。
<aop:aspect>
<aop: delate-parents
Types-matching="concert. Performance+"
Implement-interface="concert. Encoreable"
Default-impl="concert. DefaultEncoreable" />
</aop:aspect>这里有两种方式标识所引入接口的实现。在本例中,我们使用 default-impl 属性用全限定类名来显式指定 Encoreable 的实现。或者,我们还可以使用 delegate-ref 属性来标识。
<aop:aspect>
<aop: delate-parents
Types-matching="concert. Performance+"
Implement-interface="concert. Encoreable"
Delegate-ref="encoreableDelegate" />
</aop:aspect>Delegate-ref 属性引用了一个 Spring bean 作为引入的委托。这需要在 Spring 上下文中存在一个 ID 为 encoreableDelegate 的 bean。
<bean id="encoreableDelegate" class="concert.DefaultEncoreable" />使用 default-impl 来直接标识委托和间接使用 delegate-ref 的区别在于后者是 Spring bean,它本身可以被注入、通知或使用其他的 Spring 配置。
4 注入 AspectJ 切面
虽然 Spring AOP 能够满足许多应用的切面需求,但是与 AspectJ 相比,Spring AOP 是一个功能比较弱的 AOP 解决方案。AspectJ 提供了 Spring AOP 所不能支持的许多类型的切点。
例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语言中的构造器,Java 构造器不同于其他的正常方法。这使得 Spring 基于代理的 AOP 无法把通知应用于对象的创建过程。
对于大部分功能来讲,AspectJ 切面与 Spring 是相互独立的。虽然它们可以织入到任意的 Java 应用中,这也包括了 Spring 应用,但是在应用 AspectJ 切面时几乎不会涉及到 Spring。
但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助 Spring 的依赖注入把 bean 装配进 AspectJ 切面中。
为了演示,我们为上面的演出创建一个新切面。具体来讲,我们以切面的方式创建一个评论员的角色,他会观看演出并且会在演出之后提供一些批评意见。下面的 CriticAspect 就是一个这样的切面。
Package concert;
Public aspect CriticAspect {
Public CriticApect () { }
Pointcut performance () : execution (* perform (..));
AfterReturning () : performance () {
System. Out. Println (criticismEngine. GetCriticism ());
}
Private CriticismEngine criticismEngine;
Public void setCriticismEngine (CritisicismEngine criticismEngine) {
This. CriticismEngine = criticismEngine;
}
}CriticAspect 的主要职责是在表演结束后为表演发表评论。上述程序中的 performance () 切点匹配 perform () 方法。当它与 afterReturning () 通知一起配合使用时,我们可以让该切面在表演结束时起作用。
上述程序有趣的地方在于并不是评论员自己发表评论,实际上,CriticAspect 与一个 CriticismEngine 对象相协作,在表演结束时,调用该对象的 getCriticism () 方法来发表一个苛刻的评论。为了避免 CriticAspect 和 CriticismEngine 之间产生不必要的耦合,我们通过 Setter 依赖注入为 CriticAspect 设置 CriticismEngine。图 4.9 展示了此关系。

CriticismEngine 自身是声明了一个简单 getCriticism () 方法的接口。程序清单 4.16 为 CriticismEngine 的实现。
Package com. Springinaction. Springidol;
Public class CriticiamEngineImpl implements CriticismEngine {
Public CriticismEngine () { }
Public String getCriticism () {
Int i = (int) (Math. Random () * criticismPool. Length);
Return criticismPool[i];
}
// injected
Private String[] criticismPool;
Public void setCriticismPool (String[] criticismPool) {
This. CriticismPool = criticismPool;
}
}CriticismEngineImpl 实现了 CriticismEngine 接口,通过从注入的评论池中随机选择一个苛刻的评论。这个类可以使用如下的 XML 声明为一个 Spring bean。
<bean id="criticismEngine"
Class="com. Springination. Springidol. CriticismEngineImpl" >
<property name="criticisms" >
<list>
<value>Worst performance ever!</value>
<value>I laughed, I cried, then I realized I was at the wrong show.</value>
<value>A must see show!</value>
</list>
</property>
</bean>到目前为止,一切顺利。我们现在有了一个要赋予 CriticAspect 的 CriticismEngine 实现。剩下的就是为 CriticAspect 装配 CriticismEngineImpl。在展示如何实现注入之前,我们必须清楚 AspectJ 切面根本不需要 Spring 就可以织入到我们的应用中。如果想使用 Spring 的依赖注入为 AspectJ 切面注入协作者,那我们就需要在 Spring 配置中把切面声明为一个 Spring 配置中的 <bean>。如下的 <bean> 声明会把 criticismEngine bean 注入到 CriticAspect 中:
<bean class="com. Springinaction. Springidol. CriticAspect"
Factory-method="aspectOf" >
<property name="criticismEngine" ref="criticismEngine" />
</bean>很大程度上,<bean> 的声明与我们在 Spring 中所看到的其他 <bean> 配置并没有太多的区别,但是最大的不同在于使用了 factory-method 属性。通常情况下,Spring bean 由 Spring 容器初始化,但是 AspectJ 切面是由 AspectJ 在运行期创建的。等到 Spring 有机会为 CriticAspect 注入 CriticismEngine 时,CriticAspect 已经被实例化了。
因为 Spring 不能负责创建 CriticAspect,那就不能在 Spring 中简单地把 CriticAspect 声明为一个 bean。相反,我们需要一种方式为 Spring 获得已经由 AspectJ 创建的 CriticAspect 实例的句柄,从而可以注入 CriticismEngine。幸好,所有的 AspectJ 切面都提供了一个静态的 aspectOf () 方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用 factory-method 来调用 asepctOf () 方法而不是调用 CriticAspect 的构造器方法。
简而言之,Spring 不能像之前那样使用 <bean> 声明来创建一个 CriticAspect 实例 —— 它已经在运行时由 AspectJ 创建完成了。Spring 需要通过 aspectOf () 工厂方法获得切面的引用,然后像 <bean> 元素规定的那样在该对象上执行依赖注入。
5 AOP 其他
5.1 cglib 代理
目标类没有接口,spring 框架会自动应用 cglib 动态代理。
目标类有接口,使用 cglib 动态代理,spring 配置设置为如下方式。
<!--
如果你期望目标类有接口,使用 cglib 代理
Proxy-target-class="true": 告诉框架,要使用 cglib 动态代理
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>