Featured image of post SpringBoot中使用约定编程AOP

SpringBoot中使用约定编程AOP

一、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+多数据,实现数据源的动态切换跟读写分离。