一、AOP的术语和流程
1.1 术语
- 连接点(join point):具体被拦截的对象。在spring中支持的是方法,拦截的对象是方法。譬如在做web开发的时候,我们相对每个controller的方法都进行拦截,打印一下日志。这时候就可以用到AOP进行拦截,这些controller里面的方法就是连接点。
- 切点(point cut):在上面说到,有时候我们要拦截的不仅是单个方法,可能是多个类中的不同方法,这时候我们就需要通过像正则边大师和指示器的规则去定义,去适配连接点。这就是切点。
- 通知(advice):就是安装约定的流程方法,分为前置通知(before advice)、后置通知(after advice)、环绕通知(around advicd)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),根据约定织入到流程中。
- 目标对象(target):被代理的对象,例如有一个HelloController类,通过AOP拦截了这个类里面的一个或多个方法。那么这个HelloController类就是目标对象,它被代理了。
- 引入(introduction):是指引入新的类和其方法,增强现有的Bean的功能。
- 织入(weaving):通过动态代理,为目标对象生成一个代理对象,然后把切入点定义匹配到的连接点进行拦截,并把各种通知织入到这些连接点。
- 切面(asopect):是一个可以定义切点、通知和引入的一个类。Spring AOP将通过该类的信息来增强Bean的功能。
Spring AOP流程:
二、AOP开发入门
SpringBoot中通过注解的方式来声明切面,在开发上会简单很多。
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
|
2.1 编写一个Controller
HelloController
1
2
3
4
5
6
7
8
9
10
|
@RestController
@RequestMapping("hello")
public class HelloController {
@GetMapping("test1/{id}")
public Object test1(@PathVariable("id")Integer id) {
System.out.println(id);
return "success";
}
}
|
2.2 开发切面
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
|
@Aspect
@Component
public class HelloAspect {
/**
* 定义一个切入点
*/
@Pointcut("execution(* com.msr.better.aop.controller.HelloController.test1(..))")
public void pointCut(){
}
/**
* 前置通知
*
* @param joinPoint
*/
@Before("pointCut()")
public void before() {
System.out.println("======================= Before ======================");
}
/**
* 后置通知
*/
@After("pointCut()")
public void after() {
System.out.println("======================= After ======================");
}
/**
* 返回通知
*/
@AfterReturning("pointCut()")
public void afterReturn() {
System.out.println("======================= afterReturn ======================");
}
/**
* 异常通知
*/
@AfterThrowing("pointCut()")
public void afterThrow() {
System.out.println("======================= afterThrow ======================");
}
/**
* 环绕通知
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取拦截的方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("参数:" + args[i]);
}
// 被拦截的方法所在的类
System.out.println(joinPoint.getTarget().getClass().getName());
Object proceed = joinPoint.proceed();
System.out.println("返回结果:" + proceed);
return proceed;
}
}
|
2.3 启动类
1
2
3
4
5
6
7
|
@SpringBootApplication
public class AopApplication {
public static void main(String[] args) {
SpringApplication.run(AopApplication.class, args);
}
}
|
2.4 测试
通过IDEA带的HTTP Client插件,然后查看程序的输出
1
|
GET http://localhost:8088/hello/test1/123
|
输出结果:
1
2
3
4
5
6
7
|
参数:123
com.msr.better.aop.controller.HelloController
======================= Before ======================
controller:123
======================= afterReturn ======================
======================= After ======================
返回结果:success
|
在没有发生异常的时候,切面是以这样的顺序执行的
@Around中执行joinPoint.proceed() 前面的代码被执行
@Before 前置通知被执行
Object proceed = joinPoint.proceed(); 放行
执行连接点方法 ,例如上面例子中的com.msr.better.aop.controller.HelloController.test1方法
@AfterReturn 返回通知被执行
@After 后置通知被执行
@Around中执行joinPoint.proceed() 后面的代码被执行
代码中 return proceed 把结果返回
注意,ProceedingJoinPoint joinPoint 参数只能在环绕通知中引用,在其他通知当作参数引用时会爆一下的错误。可见Spring中切面的放行是在环绕通知中做的。
1
|
ProceedingJoinPoint is only supported for around advice
|
ProceedingJoinPoint该类是一个接口,在环绕通知中使用的实现类是MethodInvocationProceedingJoinPoint,其中比较常用的一些方法:
- getTarget() 获取被代理的对象,即连接点所在的类的实例
- getSignature() 封装了签名信息的对象。通过Signature对象可以进一步拿到一些信息:
- 可以拿到当前连接点的方法名(getName方法)
- 可以拿到连接点所在类的全类名(getDeclaringTypeName方法)
- 连接点的详细信息(toLongString方法)。例如上面的例子调用的话结果是:
public java.lang.Object com.msr.better.aop.controller.HelloController.test1(java.lang.Integer)
- 与toLongString方法相反的toShortString方法则会得到:
HelloController.test1(..)
- getArgs():获取连接点的所有参数
- getThis():也是得到被代理的对象,getTarget方法就是进一步调用getThis方法得到代理对象的
三、多切面执行顺序
如果定义了多个切面,而且Spring是支持这些切面都拦截了同样的连接点。因此我们有必要知道这些切面的运行顺序。
3.1 定义多个切面
切面1
1
2
3
4
5
6
7
8
9
10
11
|
@Aspect
public class MyAspect1 {
@Pointcut("execution(public * com.msr.better.aop.controller.HelloController.test2(..))")
public void pointCut1() {
}
@Before("pointCut1()")
public void b1() {
System.out.println("MyAspect1 before");
}
}
|
切面2
1
2
3
4
5
6
7
8
9
10
11
|
@Aspect
public class MyAspect2 {
@Pointcut("execution(public * com.msr.better.aop.controller.HelloController.test2(..))")
public void pointCut2() {
}
@Before("pointCut2()")
public void b2() {
System.out.println("MyAspect2 before");
}
}
|
切面3
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Aspect
public class MyAspect3 {
@Pointcut("execution(public * com.msr.better.aop.controller.HelloController.test2(..))")
public void pointCut3() {
}
@Before("pointCut3()")
public void b3() {
System.out.println("MyAspect3 before");
}
}
|
在启动类里面注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Bean
public MyAspect1 myAspect1() {
return new MyAspect1();
}
@Bean
public MyAspect2 myAspect2() {
return new MyAspect2();
}
@Bean
public MyAspect3 myAspect3() {
return new MyAspect3();
}
|
test2方法
1
2
3
4
|
@GetMapping("test2")
public Object test2() {
return "success";
}
|
测试,利用IDEA自带的功能
GET http://localhost:8088/hello/test2
输出:
MyAspect1 before
MyAspect2 before
MyAspect3 before
如果改变这三个Bean的注入顺序,列如
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Bean
public MyAspect3 myAspect3() {
return new MyAspect3();
}
@Bean
public MyAspect1 myAspect1() {
return new MyAspect1();
}
@Bean
public MyAspect2 myAspect2() {
return new MyAspect2();
}
|
再次测试,结果顺序也会变。
MyAspect3 before
MyAspect1 before
MyAspect2 before
很明显这来控制切面的执行顺序很不可控,一般我们在切面的的类型上添加注解@Order,例如:
@Aspect
@Order(1)
public class MyAspect1
@Aspect
@Order(2)
public class MyAspect2
@Aspect
@Order(3)
public class MyAspect3
或者切面实现Orderd接口,并且实现getOrder()方法,例如
1
2
3
4
5
6
7
8
|
@Aspect
public class MyAspect3 implements Orderd{
@Override
public int getOrder(){
// 指定顺序
return 3;
}
}
|
四、总结
关于SpringBoot中使用AOP就介绍到这里,或许跟使用纯Spring Framework编写相比,可能也就大同小异吧。使用AOP增强业务方法在现实开发中也是很常见的,例如:通过自定义注解+AOP+多数据,实现数据源的动态切换跟读写分离。