【笔记】SpringBoot实现AOP

前言

在SpringBoot项目中基于AspectJ框架实现的AOP学习笔记

AOP介绍

  • 面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。(维基百科

代理方式与配置

  • 在SpringBoot项目中有两种代理对象可以实现AOP,一个是JDK代理,一个是CGLIB代理

  • CGLIB代理为SpringAOP的默认代理对象,它的原理是为目标方法所在的类实现一个子类,通过继承关系实现AOP

  • JDK代理的原理是为目标方法所在的类实现一个兄弟类(也就是说目标方法所在的类必须发生了继承关系,且为子类),JDK代理会实现与目标方法所在的类对应父类的继承关系,从而实现AOP,但JDK代理需要修改SpringBoot配置文件,以yml为例

  • 配置的值默认为true表示CGLIB代理,改为false表示JDK代理

1
spring.aop.proxy-target-class=false
1
2
3
spring:
aop:
proxy-target-class: false

实现AOP

定义一个切面

  • 通过@Aspect定义一个切面类
1
2
3
4
5
@Aspect
@Component
public class SysLogAspect {
...
}

定义切面的优先级

@Order():用于描述切面类,数字越小,优先级越高,可以是负数

1
2
3
4
5
6
@Aspect
@Component
@Order(1)
public class SysLogAspect {
...
}

在切面中定义切入点

通过注解的方式定义切入点

  • 通过匹配有指定自定义注解描述的方法,实现细粒度的切入点表达式定义

  • 首先定义一个自定义注解

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String value() default "";
}

<package>.<required>:注解的全局限定名
doLog():该方法仅仅是注解的一个载体,方法体内不需要写任何内容

1
2
@Pointcut("@annotation(<package>.<required>)")
public void doLog(){}
  • 为切入点方法标注自定义注解

通过指定bean的方式定义切入点

  • 通过bean表达式为Spring中的一种粗粒度的切入点表达式,不能精确到具体的方法

<bean>:Spring容器中一个bean对象的名字

1
2
@Pointcut("bean(<bean>)")
public void doLog(){}
匹配更多的bean对象
1
2
@Pointcut("bean(*<bean>)")
public void doLog(){}

通过指定类名的方式定义切入点

  • 通过指定类名的方式定义切入点,实现粗粒度的切入点表达式定义

<package>.<class>:类的全局限定名

1
2
@Pointcut("within(<package>.<class>)")
public void doLog(){}
指定当前目录下的所有类的所有方法
1
2
@Pointcut("within(<package>.*)")
public void doLog(){}
指定当前目录以及子目录中类的所有方法
1
2
@Pointcut("within(<package>..*)")
public void doLog(){}

通过指定方法的方式定义切入点

  • 通过指定方法的方式定义切入点,实现细粒度的切入点表达式定义
匹配无参方法

void:方法返回值类型
<package>.<class>.<method>():方法全局限定名

1
2
@Pointcut("execution(void <package>.<class>.<method>())")
public void doLog(){}
匹配有参方法

String:参数类型

1
2
@Pointcut("execution(void <package>.<class>.<method>(String))")
public void doLog(){}

为切入点定义通知方法

通知的类型

@Around:通过该注解描述的方法可以在目标方法执行之前和之后做功能扩展,在@Around注解描述的方法内部,可以手动调用目标方法

对于@Around注解描述的方法其规范要求为

返回值类型为:Object
参数类型为:ProceedingJoinPoint
抛出的异常为:Throwable

@Before:在目标方法执行之前执行
@After:在目标方法结束之后执行
@AfterReturning:在目标方法正常结束之前执行
@AfterThrowing@AfterThrowing(value="",throwing="e"):在目标方法异常结束之前执行

通知类型的顺序

正常执行顺序@Around.before->@Before->@Around.after->@After->@AfterReturning
异常执行顺序@Around.before->@Before->@Around.error->@After->@AfterThrowing

定义通知方法

直接在advice内部定义切入点表达式
1
2
3
4
5
@Around("@annotation(com.RequiredLog)")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
proceedingJoinPoint.proceed();
...
}
在advice内部通过方法引用定义切入点表达式
1
2
3
4
5
@Around("doLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
proceedingJoinPoint.proceed();
...
}

测试Demo

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
@Component
@Aspect
public class SysTimeAspect {
@Pointcut("bean(sysUserServiceImpl)")
public void doTime(){}

@Before("doTime()")
public void doBefore(JoinPoint jp){
System.out.println("@Before");
}
@After("doTime()")
public void doAfter(){
System.out.println("@After");
}
@AfterReturning("doTime()")
public void doAfterReturning(){
System.out.println("@AfterReturning");
}
@AfterThrowing("doTime()")
public void doAfterThrowing(){
System.out.println("@AfterThrowing");
}
@Around("doTime()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
try {
System.out.println("@Around.before");
Object obj=jp.proceed();
System.out.println("@Around.after");
return obj;
} catch(Throwable e) {
System.out.println("@Around.exception");
throw e;
} finally {
System.out.println("@Around.finally");
}

}
}

完成