Spring 的 AOP 的支持

面向切面编程 (Aspect-oriented Programming, AOP) 通过提供另一种思考程序结构的方法来补充面向对象编程(Object-oriented Programming, OOP)。OOP中模块化的关键单元是类,而 AOP 中模块化的单元是切面。切面支持跨多个类型和对象的关注点(例如事务管理)的模块化

AOP 的概念

AOP 术语

在使用 AOP 之前,先熟悉一下 AOP 概念和术语。这些术语并不特定于 Spring,而是与 AOP 有关的

描述
Aspect(切面) 跨越多个类的关注点的模块化,切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。事务处理和日志处理可以理解为切面
Join point(连接点) 程序执行过程中的一个点,如方法的执行或异常的处理
Advice(通知) 切面在特定连接点上采取的动作
Pointcut(切点) 匹配连接点的断言。通知与切入点表达式相关联,并在切入点匹配的任何连接点上运行(例如,具有特定名称的方法的执行)。切入点表达式匹配的连接点概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言
Introduction(引用) 为类型声明其他方法或字段。Spring AOP 允许您向任何建议的对象引入新的接口(和相应的实现)。例如,您可以使用介绍使 bean 实现 IsModified 接口,以简化缓存
Target object(目标) 由一个或多个切面通知的对象。也称为“通知对象”。由于 Spring AOP 是通过使用运行时代理实现的,所以这个对象始终是代理对象
AOP proxy(代理) AOP 框架为实现切面契约(通知方法执行等)而创建的对象。在 Spring 框架中,AOP 代理是 JDK 动态代理或 CGLIB 代理
Weaving(织入) 织入是将通知添加对目标类具体连接点上的过程,可以在编译时(例如使用AspectJ编译器)、加载时或运行时完成

Spring 切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知
  • 后置通知(After):在目标方法完成之后调用通知(无论是正常还是异常退出)
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

其中环绕通知包括前置、后置、返回、异常通知,这个在后面的例子体现

使用 XML 配置的AOP支持

xml配置中需要aop命名空间标记,需要先导入 spring-aop 模式

1
2
3
4
5
6
7
8
9
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- bean definitions here -->
</beans>

也是需要 AspectJ 的 aspectjweaver.jar 包,spring-aspects 模块是依赖该jar包的

spring-aspects 模块集成自 AspectJ 框架, 主要是为 Spring AOP 提供多种 AOP 实现方法

声明一个切面

使用 <aop:aspect> 标签声明切面,使用 ref 属性去引用支持的 bean

1
2
3
4
5
6
7
8
<aop:config>
<aop:aspect id="myAspect" ref="aspectBean">
...
</aop:aspect>
</aop:config>
<bean id="aspectBean" class="com.demo.aspect.AspectBean">
...
</bean>

切面可以具有与任何其他类相同的方法和字段。它们还可以包含切入点、通知和引入(内部类型)声明。

声明切入点

声明切入点是确定通知感兴趣的或是将要织入的连接点(即目标方法)

1
2
3
4
5
6
7
8
9
10
<aop:config>
<aop:aspect id="myAspect" ref="aspectBean">
<aop:pointcut id="businessService"
expression="execution(* com.demo.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>

<bean id="aspectBean" class="com.demo.aspect.AspectBean">
</bean>

表达式 expression 是对连接点的筛选,上面的例子是 com.demo.service 包下所有类的所有方法

详细的表达式语法见后面详说
也还可以通过名称匹配切入点参数与建议方法参数

声明通知

使用 <aop:{ADVICE-NAME}> 标签声明五个建议中的任意一个,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<aop:config>
<aop:aspect id="myAspect" ref="aspectBean">
<aop:pointcut id="businessService"
expression="execution(* com.demo.service.*.*(..))" />

<!-- 前置通知定义 -->
<aop:before pointcut-ref="businessService"
method="doBeforeTask" />
<!-- 后置通知定义 -->
<aop:after pointcut-ref="businessService"
method="doAfterTask" />
<!-- 返回通知定义 -->
<!-- doReturnTask方法必须要有一个名字与“returning”值一致(如retVal)的参数,这个是目标方法的返回值 -->
<aop:after-returning
pointcut-ref="businessService" returning="retVal"
method="doReturnTask" />
<!-- 异常通知定义 -->
<!-- doRequiredTask方法必须要有一个名字与“throwing”值一致(如ex)的参数,这个是目标方法的抛出的异常-->
<aop:after-throwing pointcut-ref="businessService"
throwing="ex" method="doThrowTask" />

<!-- 环绕通知定义(环绕通知包含了其他的通知) -->
<!-- <aop:around pointcut-ref="businessService"
method="doAroundTask" /> -->

</aop:aspect>
</aop:config>

<bean id="aspectBean" class="com.demo.aspect.AspectBean">
</bean>

其中环绕通知包括前置、后置、返回、异常通知,在待会的例子中可以看出

这里 pointcut-ref="businessService" 是在 <aop:config> 里定义的切入点。也可以改为内联切入点,使用 pointcut 属性替换 pointcut-ref 属性

1
2
3
4
5
6
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.demo.service.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>

例子

我们使用上面的例子,定义的切入点为 com.demo.service 下的所有的类的所有方法,该切面支持的 bean 为 com.demo.aspect 下的 AspectBean。我们的切入点也是需要在IoC容器中被管理的 bean,可以用注解也可以xml配置

1
<bean id="targetService" class="com.demo.service.TargetService"/>

TargetService,目标对象,这个operation方法便是我们的切入点

1
2
3
4
5
6
7
8
public class TargetService {
public String operation(String msg) {
System.out.println("执行目标方法,方法参数[msg:" + msg + "]");
// 测试异常通知
// throw new RuntimeException("我是个异常");
return msg;
}
}

同时我也写一下 AspectBean 里面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class AspectBean {
private static int step = 0;

public void doBeforeTask(String msg) {
System.out.println(++step + " 前置通知,参数为[msg:" + msg + "]");
}

public void doAfterTask() {
System.out.println(++step + " 后置通知");
}

public void doReturnTask(Object retVal) {
System.out.println(++step + " 返回通知,返回值为:" + retVal.toString());
}

public void doThrowTask(Exception ex) {
System.out.println(++step + " 异常通知,异常信息为:" + ex.getMessage());
}

/**
* 环绕通知需要携带ProceedingJoinPoint类型的参数
* 环绕通知类似于动态代理的全过程ProceedingJoinPoint类型的参数可以决定是否执行目标方法
* 且环绕通知必须有返回值,返回值即目标方法的返回值
*/
public Object doAroundTask(ProceedingJoinPoint pjp) {
String methodname = pjp.getSignature().getName();
Object result = null;
try {
// 前置通知
System.out.println("目标方法" + methodname + "开始,参数为" + Arrays.asList(pjp.getArgs()));
// 执行目标方法
result = pjp.proceed();
// 返回通知
System.out.println("目标方法" + methodname + "执行成功,返回" + result);
} catch (Throwable e) {
// 异常通知
System.out.println("目标方法" + methodname + "抛出异常: " + e.getMessage());
}
// 后置通知
System.out.println("目标方法" + methodname + "结束");
return result;
}
}

我们先注释掉xml中环绕通知的配置,运行一下看下结果如何

1
2
3
4
5
6
7
public class MainApp {
public static void main( String[] args ){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
TargetService targetService = (TargetService) applicationContext.getBean("targetService");
targetService.operation("zou");
}
}

结果为

1
2
3
4
1 前置通知
执行目标方法,方法参数[msg:zou]
2 后置通知
3 返回通知,返回值为:zou

然后我们注释掉其他其他通知,测试一下环绕通知,并且我们在目标方法中抛出一个异常,再运行一下,结果为

1
2
3
4
目标方法operation开始,参数为[zou]
执行目标方法,方法参数[msg:zou]
目标方法operation抛出异常: 我是个异常
目标方法operation结束

使用 @AspectJ 的AOP支持

启用@AspectJ支持

要在 Spring 配置中使用 @AspectJ 切面,需要启用 Spring 支持

使用 XML 配置启用 @AspectJ 支持

1
<aop:aspectj-autoproxy/>

这里也需要 AspectJ 的 aspectjweaver.jar 包,spring-aspects 模块是依赖该 jar 包的

声明一个切面

Aspects 类和其他任何正常的 bean 一样,除了它们将会用 @AspectJ 注释之外,它和其他类一样可能有方法和字段

1
2
3
4
5
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AnnotationAspect {

}

也可以在 xml 中配置,和其他 bean 一样

1
2
3
<bean id="myAspect" class="org.xyz.AnnotationAspect">
<!-- configure properties of the aspect here -->
</bean>

切面(用 @Aspect 标注的类)可以具有与任何其他类相同的方法和字段。它们还可以包含切入点、通知和引入(内部类型)声明。

声明切入点

切入点声明有两部分:包含名称和任何参数的签名,以及准确确定我们感兴趣的方法执行的切入点表达式

在 AOP 的 @AspectJ 注释样式中,切入点签名是由一个常规方法定义提供的,切入点表达式是通过使用 @Pointcut 注释表示的(作为切入点签名的方法必须为 void 返回类型)

如下代码,定义了一个名为 anyOldTransfer 的切入点,该切入点与任何名为 transfer 的方法的执行匹配

1
2
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

声明通知

你可以使用 @{advice-name} 注释声明五个建议中的任意一个,如下所示。这假设你已经定义了一个切入点标签方法 operation()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Aspect
public class AnnotationAspect {
private static int step = 0;

@Pointcut("execution(* transfer(..))") // the pointcut expression
private void operation() {}

@Before("operation()")
public void doBeforeTask() {
System.out.println(++step + " 前置通知");
}

@After("operation()")
public void doAfterTask() {
System.out.println(++step + " 后置通知");
}

@AfterReturning(pointcut = "operation()", returning = "retVal")
public void doAfterReturnningTask(Object retVal) {
System.out.println(++step + " 返回通知,返回值为:" + retVal.toString());
}

@AfterThrowing(pointcut = "operation()", throwing = "ex")
public void doAfterThrowingTask(Exception ex) {
System.out.println(++step + " 异常通知,异常信息为:" + ex.getMessage());
}

/**
* 环绕通知需要携带ProceedingJoinPoint类型的参数
* 环绕通知类似于动态代理的全过程ProceedingJoinPoint类型的参数可以决定是否执行目标方法
* 且环绕通知必须有返回值,返回值即目标方法的返回值
*/
//@Around("operation()")
public Object doAroundTask(ProceedingJoinPoint pjp) {
String methodname = pjp.getSignature().getName();
Object result = null;
try {
// 前置通知
System.out.println("目标方法" + methodname + "开始,参数为" + Arrays.asList(pjp.getArgs()));
// 执行目标方法
result = pjp.proceed();
// 返回通知
System.out.println("目标方法" + methodname + "执行成功,返回" + result);
} catch (Throwable e) {
// 异常通知
System.out.println("目标方法" + methodname + "抛出异常: " + e.getMessage());
}
// 后置通知
System.out.println("目标方法" + methodname + "结束");
return result;
}
}

也可以为任意一个通知直接写入内联切入点

1
2
3
4
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}

其中环绕通知包括前置、后置、返回、异常通知
运行的结果这里不给了,可以参考上面xml配置的例子