Spring Aop 基于注解的实现

1、用到的技术

  • 动态代理(InvocationHandler):JDK 原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口。
  • cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
  • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver 就是织入器。Spring 只是借用了 AspectJ 中的注解。

2、AOP 术语

① 横切关注点

从每个方法中抽取出来的同一类非核心业务。

在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

② 通知

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知(@Before):在被代理的目标方法前执行
  • 返回通知(@AfterReturning):在被代理的目标方法成功结束后执行
  • 异常通知(@AfterThrowing):在被代理的目标方法异常结束后执行
  • 后置通知(@After):在被代理的目标方法最终结束后执行
  • 环绕通知(@Around):使用 try…catch…finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

③ 切面

封装通知方法的类。

④ 目标

被代理的目标对象。

⑤ 代理

向目标对象应用通知之后创建的代理对象

⑥ 连接点

把方法排成一排,每一个横切位置看成 x 轴方向,把方法从上到下执行的顺序看成 y 轴,x 轴和 y 轴的交叉点就是连接点。

⑦ 切入点

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP 可以通过切入点定位到特定的连接点。

切入点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

3、基于注解的 Spring AOP

① 导入 jar 包

com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
commons-logging-1.1.3.jar
hamcrest-core-1.3.jar
junit-4.12.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar

② 创建被代理类

由于我们加入了 cglib 的支持,所以允许被代理类不实现任何接口。但是这个类必须加入 IOC 容器,否则 Spring 没法控制和操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class CalculatorImpl {
public int add(int a, int b) {
System.out.println("(a+b) = " + (a + b));
return a + b;
}

public int sub(int a, int b) {
System.out.println("(a-b) = " + (a - b));
return a - b;
}

public int mul(int a, int b) {
System.out.println("(a×b) = " + (a * b));
return a * b;
}

public int div(int a, int b) {
System.out.println("(a÷b) = " + (a / b));
return a / b;
}
}

③ 创建切面类

要想把一个类变成切面类,只需 3 步:

[1] 在类上使用 @Aspect 注解使之成为切面类

[2] 切面类需要交由 Spring 容器管理,所以类上还需要有 @Service、@Repository、@Controller、@Component 等注解

[3] 在切面类中自定义方法接收通知

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
/**
* 切面注解 Aspect 使用入门
* 1、@Aspect:声明本类为切面类
* 2、@Component:标记这个被扫描包扫描到时需要加入IOC容器,将本类交由 Spring 容器管理
* 3、@Order:控制切面优先级,数值越小,优先级越高,优先级高的切面套在外面,默认为 Integer.MAX_VALUE
*/

@Aspect
@Component
public class LogAspect {

/**
* 前置通知:目标方法执行之前执行以下方法体的内容。
* value:绑定通知的切入点表达式,通俗来说就是这个通知往谁身上套。可以关联切入点声明,也可以直接设置切入点表达式
*
* 切入点表达式常用格式举例如下:
* (* com.atguigu.aop.target.CalculatorImpl.*(..)):表示 com.atguigu.aop.target.CalculatorImpl 类中的任意方法)
* (* com.atguigu.aop.target.*.*(..)):表示 com.atguigu.aop.target 包(不含子包)下任意类中的任意方法)
* (* com.atguigu.aop.target..*.*(..)):表示 com.atguigu.aop.target 包及其子包下任意类中的任意方法)
*/

@Before(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))")
public void doBeforeLog() {
System.out.println("[aop log]method begin");
}

@AfterReturning(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))")
public void doSuccessLog() {
System.out.println("[aop log]method successfully end");
}

@AfterThrowing(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))")
public void doExceptionLog() {
System.out.println("[aop log]method ended with exception");
}

@After(value="execution(* com.atguigu.aop.target.CalculatorImpl.*(..))")
public void doAfterLog() {
System.out.println("[aop log]method finally end");
}
}

④ 配置 Spring 配置文件(applicationContext.xml)

1
2
3
4
5
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.atguigu.aop"/>

<!-- 启用AspectJ注解 -->
<aop:aspectj-autoproxy/>

⑤junit 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 读取Spring配置文件,创建IOC容器对象
* 指定的路径是相对于类路径的根目录
*/
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");

@Test
public void testAOP() {
CalculatorImpl bean = iocContainer.getBean(CalculatorImpl.class);
bean.div(5, 1);

System.out.println("bean.getClass().getName() = " + bean.getClass().getName());
}

4、通知执行的顺序

  • Spring 版本 5.3.x 以前:
    • 前置通知
    • 目标操作
    • 后置通知
    • 返回通知或异常通知
  • Spring 版本 5.3.x 以后:
    • 前置通知
    • 目标操作
    • 返回通知或异常通知
    • 后置通知

5、各个通知获取细节信息

① JoinPoint 接口

org.aspectj.lang.JoinPoint

JoinPoint 对象封装了 Spring Aop 中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的 JoinPoint 对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用@Before注解将当前方法定义为前置通知
// value属性:指定切入点表达式,将前置通知“套”在div()方法上
@Before(value = "execution(public void com.atguigu.aop.target.CalculatorImpl.div(int,int))")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {

// 1.通过JoinPoint对象获取目标方法的签名
// 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
Signature signature = joinPoint.getSignature();

// 2.通过方法签名对象可以获取方法名
String methodName = signature.getName();

// 3.通过JoinPoint对象获取目标方法被调用时传入的参数
Object[] args = joinPoint.getArgs();

// 4.为了方便展示参数数据,把参数从数组类型转换为List集合
List<Object> argList = Arrays.asList(args);

System.out.println("[前置]"+ methodName + " 方法开始执行,参数列表是" + argList);
}

需要获取方法签名、传入的实参等信息时,可以在通知方法声明 JoinPoint 类型的形参。

② 方法返回值

在返回通知中,通过@AfterReturning 注解的 returning 属性获取目标方法的返回值

1
2
3
4
5
6
7
8
9
10
11
// 使用@AfterReturning注解将当前方法定义为返回通知
@AfterReturning(
pointcut = "execution(public void com.atguigu.aop.target.CalculatorImpl.div(int,int))",

// 使用returning属性指定一个名称,Spring会自动将目标方法的返回值传入到同名的参数位置
// 注意:目标方法必须确定有返回值,如果目标方法返回值是void,那么Spring给参数位置传入null
returning = "targetMethodReturnValue"
)
public void printLogAfterCoreOperationSuccess(Object targetMethodReturnValue){
System.out.println("[返回]method success,return value=" + targetMethodReturnValue);
}

③ 目标方法抛出的异常

在异常通知中,通过@AfterThrowing 注解的 throwing 属性获取目标方法抛出的异常对象

1
2
3
4
5
6
7
8
9
10
// 使用@AfterThrowing注解将当前方法定义为异常通知
@AfterThrowing(
value = "execution(public void com.atguigu.aop.target.CalculatorImpl.div(int,int))",

// 使用throwing属性指定一个形参名称,Spring调用当前方法时,会把目标方法抛出的异常对象从这里传入
throwing = "ep"
)
public void printLogAfterCoreOperationFailed(Throwable ep) {
System.out.println("[异常]method failed,exception name=" + ep.getClass().getName());
}

6、切入点表达式语法

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
  • 在包名的部分,使用***..**表示包名任意、包的层次深度任意
  • 在类名的部分,使用*****号表示类名任意
  • 在类名的部分,可以使用*****号代替类名的一部分
1
*Service

上面例子表示匹配所有类名、接口名以 Service 结尾的类或接口

  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
1
*Operation

上面例子表示匹配所有方法名以 Operation 结尾的方法

  • 在方法参数列表部分,使用(..)表示参数列表任意
  • 在方法参数列表部分,使用(int,..)表示参数列表以一个 int 类型的参数开头
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
1
execution(public int *..*Service.*(.., int))

上面例子是对的,下面例子是错的:

1
execution(* int *..*Service.*(.., int))
  • 对于 execution()表达式整体可以使用三个逻辑运算符号
    • execution() || execution()表示满足两个 execution()中的任何一个即可
    • execution() && execution()表示两个 execution()表达式必须都满足
    • !execution()表示不满足表达式的其他方法

7、环绕通知

环绕通知对应整个 try…catch…finally 结构,包括前面四种通知的所有功能。

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
@Aspect
@Component
public class MessageAspect {

// 使用表示当前方法是环绕通知,同时需要在LogAspect类中声明declarePointCut()
@Around(value = "com.atguigu.aop.component.LogAspect.declarePointCut()")
// 环绕通知的通知方法一定要设定有返回值,通常是需要将目标方法执行后的返回值在这里返回
public Object roundAdvice(ProceedingJoinPoint joinPoint) {

// 获取目标方法名
String methodName = joinPoint.getSignature().getName();

// 声明一个变量,用来接收目标方法的返回值
Object targetMethodReturnValue = null;

// 获取外界调用目标方法时传入的实参
Object[] args = joinPoint.getArgs();

try {

// 调用目标方法之前的位置相当于前置通知
System.out.println("[环绕] message before target method " + methodName);

// 调用ProceedingJoinPoint对象的proceed(Object[] var1)调用目标方法
// 将目标方法的返回值赋值给targetMethodReturnValue变量
targetMethodReturnValue = joinPoint.proceed(args);

// 调用目标方法成功返回之后的位置相当于返回通知
System.out.println("[环绕]message after target method " + methodName + " success,return value="+targetMethodReturnValue);

} catch (Throwable throwable) {
throwable.printStackTrace();

// 调用目标方法抛出异常之后的位置相当于异常通知
System.out.println("[环绕]message after target method " + methodName + " failed " + throwable.getClass().getName());

} finally {
// 调用目标方法最终结束之后的位置相当于后置通知
System.out.println("[环绕]message after target method " + methodName + " finally end");
}

// 将目标方法的返回值返回
// 这里如果环绕通知没有把目标方法的返回值返回,外界将无法获取这个返回值数据
return targetMethodReturnValue;
}
}

8、重用切入点表达式

在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。

声明方式如下:

1
2
3
// 使用@Pointcut注解重用切入点表达式
@Pointcut("execution(* *..*.div(..))")
public void declarPointCut() {}

同一个类内部引用时:

1
2
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {

在不同类中引用:

1
2
@Around(value = "com.atguigu.aop.component.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {

举个例子,上面 5.2 章节的代码可以修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @AfterReturning注解将当前方法标记为返回通知
@AfterReturning(
value = "declarePointCut()",

// 使用returning指定一个形参名,Spring会在调用当前方法时,把目标方法的返回值从这个位置传入
returning = "returnValue"
)

public void printLogAfterCoreOperationReturn(JoinPoint joinPoint, Object returnValue) {

String methodName = joinPoint.getSignature().getName();

System.out.println("[返回通知]" + methodName + "方法成功结束,返回值是:" + returnValue);

}

通过将 value 的值改为”declarePointCut()”,我们实现了代码的重用,易于维护,一处修改,处处生效。

9、切面的优先级

① 概念

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order 注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

② 实际意义

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

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