SpringBoot 整合 AOP

一、前言

AOP(Aspect Oriented Programming, 面向切面编程),是 Spring 的核心思想之一,即纵向重复,横向抽取,它在 Spring 中应用广泛,例如 拦截器、日志、事务等等,在 SpringBoot 中使用 AOP 之前,我们先复习下 AOP 的理论知识。

二、AOP理论

2.1 术语解释

为了方便解释,给出一个例子:

1
2
3
4
5
6
public interface UserService {
void save();
void delete();
void update();
void query();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserServiceImpl implements UserService {
@Override
public void save() { System.out.println("save"); }

@Override
public void delete() { System.out.println("delete"); }

@Override
public void update() { System.out.println("update"); }

@Override
public void query() { System.out.println("query"); }
}

若我们想要对这四个方法进行增强,假设增强如下:

1
2
3
4
5
6
7
...
public void delete() {
System.out.println("开启事务");
System.out.println("delete");
System.out.println("提交事务");
}
...

(1)连接点(JoinPoint)

解释:目标对象中,所有可以增强的方法

例子:即save()、delete()、update()、query()这四个方法(因为我们可以使用动态代理技术对这些方法进行增强)。

(2)切入点(PointCut)

解释:目标对象中,已经增强的方法

例子:若 delete() 方法在动态代理中被增强了,则 delete() 方法是切入点。

(3)通知/增强(Advice)

解释:增强的代码

例子:即System.out.println(“开启事务”); 和 System.out.println(“提交事务”);

(4)目标对象(Target)

解释:被代理对象

例子:即 UserServiceImpl 类

(5)织入(Weaving)

解释:将通知应用到切入点的过程

(6)代理(Proxy)

解释:将通知织入到切入点后,形成代理对象

(7)切面(aspect)

解释:切入点和通知合称为切面

2.2 通知类型

通知类型 描述
前置通知 Before advice 在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常
正常返回通知 After returning advice 在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行
异常返回通知 After throwing advice 在连接点抛出异常后执行
返回通知 After (finally) advice 在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容
环绕通知 Around advice 环绕着在切入点选择的连接点处的方法所执行的通知,环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法、替换返回值、抛出异常等等。

2.3 底层实现

AOP 的实现是基于动态代理技术,一般有基于 JDK 的动态代理基于 Cglib 的动态代理两种实现。

这两种实现的区别是:

  • JDK动态代理:针对实现了接口的类进行代理,代理对象和目标对象实现了共同的接口,拦截器必须实现InvocationHanlder接口。

  • Cglib动态代理:针对没有实现接口的类进行代理,底层是字节码增强技术,生成当前类的子类对象。代理对象是目标对象的子类,拦截器必须实现 MethodInterceptor 接口。

关于使用 JDK 动态代理 和 Cglib 动态代理实现上述例子,这里就不再赘述了,可以参考文章:JDK动态代理与Cglib动态代理

三、与 SpringBoot 整合

3.1 依赖配置

导入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

相关配置只有一项:

1
2
# 当值为 true 时,使用 Cglib 动态代理,否则使用 JDK 动态代理,默认值为 true
spring.aop.proxy-target-class=true

3.2 入门程序

源码地址:https://github.com/jitwxs/blog_sample

3.2.1 环境准备

(1)编写 UserService 和 UserServiceImpl:

1
2
3
public interface UserService {
void delete(String name);
}
1
2
3
4
5
6
7
@Service
public class UserServiceImpl implements UserService {
@Override
public void delete(String name) {
System.out.println("delete:" + name);
}
}

(2)编写UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {
@Autowired
private UserService userService;

@GetMapping("/delete")
public String restDelete(String name) {
userService.delete(name);
return "success";
}

@GetMapping("/exception")
public void restException() {
int i = 1 / 0;
}
}

3.2.2 切面表达式

首先复习下 AOP 中的切面表达式,假设 UserController 类位于 jit.wxs.aop.controller 包下,那么如果我们为其 restDelete() 方法 配置切入点,如下:

1
public void jit.wxs.aop.controller.UserController.restDelete()

也就是 restDelete() 方法的 reference 路径加上访问修饰符返回值,一般来说我们不对非 public 方法添加切入点,也不限制其返回值,因此省略掉访问修饰符(默认值即为 public),并将返回值修改为 *(即任何返回值均可):

1
* jit.wxs.aop.controller.UserController.restDelete()

再将切面方法扩展到 UserController 下的所有方法:

1
* jit.wxs.aop.controller.UserController.*()

此时只能对 UserController 下的所有无参 public 方法添加切入点,将其修改为不限制参数:

1
2
// 将 () 修改为 (..) ,不限制参数
* jit.wxs.aop.controller.UserController.*(..)

如果我们想要 controller 包下的所有类添加切入点,修改为:

1
2
// 将 controller包下所有以 ServiceImpl 结尾的类添加切入点
* jit.wxs.aop.controller.*Controller.*(..)

一般这样就可以了,如果你想为 controller 的子包也添加切入点,修改为:

1
2
// 在 controller 后多添加一个 . 包含它的子包
* jit.wxs.aop.controller..*Controller.*(..)

3.2.3 切面类

  1. 使用 @Aspect 注解将一个类标记为切面类,@Component 将该类加入 Spring 容器中。

  2. 使用 @Pointcut 注解定义切面表达式,为 controller 包下所有 Controller 结尾的类的 public 方法添加切入点。

  3. 通过 @Before@AfterReturning@AfterThrowing@After@Around 注解定义通知类型。

  4. doBefore() 方法中,通过 joinPoint 参数获取通知的签名信息,如目标方法名、目标方法参数信息等。、、

  5. doAroundAdvice() 方法中,处理环绕通知。切面中可选择执行方法与否,执行几次方法等。环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Component
@Aspect
public class TestAspect {
private Logger logger = LoggerFactory.getLogger(getClass());

@Pointcut("execution(* jit.wxs.aop.controller.*Controller.*(..))")
public void pointCunt(){}

@Before("pointCunt()")
public void doBefore(JoinPoint joinPoint) {
logger.info("---前置通知---");

// 获取目标方法的参数信息
logger.info("目标对象参数信息: {}", Arrays.toString(joinPoint.getArgs()));

// AOP代理类的信息
logger.info("AOP代理类: {}", joinPoint.getThis());

// 代理的目标对象
logger.info("代理的目标对象: {}", joinPoint.getTarget());

// 通知的签名
Signature signature = joinPoint.getSignature();
// 代理的是哪一个方法
logger.info("代理的方法名: {}", signature.getName());

// AOP代理类的名字
logger.info("AOP代理类的名字: {}", signature.getDeclaringTypeName());

// AOP代理类的类信息
logger.info("AOP代理类的类信息: {}", signature.getDeclaringType());

// RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 获取 HttpServletRequest
// HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

// 获取 Session
// HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
}

@AfterReturning(returning = "ret", pointcut = "pointCunt()")
public void doAfterReturning(JoinPoint joinPoint, Object ret) {
logger.info("---后置返回通知---");
logger.info("代理方法: {}, 返回参数: {} ", joinPoint.getSignature().getName(), ret);
}

@AfterThrowing(pointcut = "pointCunt()", throwing = "e")
public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable e) {
logger.info("---后置异常通知---");
logger.info("代理方法: {}, 异常信息: {} ", joinPoint.getSignature().getName(), e.getMessage());
}

@After("pointCunt()")
public void doAfterAdvice(JoinPoint joinPoint) {
logger.info("---后置最终通知---");
}

@Around("pointCunt()")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
logger.info("---环绕通知开始---");
logger.info("代理方法: {}", proceedingJoinPoint.getSignature().getName());
try {
Object obj = proceedingJoinPoint.proceed();
logger.info("---环绕通知结束---");
return obj;
} catch (Throwable throwable) {
logger.info("---环绕通知异常---");
throwable.printStackTrace();
}
return null;
}
}

3.2.4 执行程序

当我们执行 “/delete” 接口时,访问 /delete?name=jitwxs,运行结果如下:

当我们执行 “/exception” 接口时,访问 /exception,运行结果如下:

3.3 资源同步

使用 ThreadLocal 实现资源同步,例如统计切入点执行时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Aspect
public class TestAspect {
private Logger logger = LoggerFactory.getLogger(getClass());

private ThreadLocal<Long> threadLocal = new ThreadLocal<>();

@Pointcut("execution(* jit.wxs.aop.controller.*Controller.*(..))")
public void pointCunt(){}

@Before("pointCunt()")
public void doBefore(JoinPoint joinPoint) {
logger.info("---前置通知---");
threadLocal.set(System.currentTimeMillis());
}

@AfterReturning(returning = "ret", pointcut = "pointCunt()")
public void doAfterReturning(JoinPoint joinPoint, Object ret) {
logger.info("---后置返回通知---");
logger.info("共执行时间:{} ", System.currentTimeMillis() - threadLocal.get());
}
}

3.4 优先级

当我们对同一个切入点设置了多个切面后,我么你可以定义切面的优先级,来达到预想的执行顺序。

@Order() 注解来标识切面的优先级,i的值越小,优先级越高。加入存在两个切面,定义了 @Order(5) 和 @Order(10),那么:

  • 在 @Before 中优先执行 @Order(5) 的内容,再执行 @Order(10) 的内容

  • 在 @After 和 @AfterReturning 中优先执行 @Order(10) 的内容,再执行 @Order(5) 的内容

所以我们可以这样总结:

  • 在切入点前的操作,按order的值由小到大执行

  • 在切入点后的操作,按order的值由大到小执行

文章作者: Jitwxs
文章链接: https://blog.jitwxs.cn/77bba914.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来自 Jitwxs