SpringBoot之AOP的使用

spring核心思想分三大类:控制反转(IOC),依赖注入(DI)和面向切面(AOP)

1.什么是面向切面编程

  • AOP简介

    AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。

  • 为什么使用AOP编程范式?

    分离功能性需求和非功能性需求

    集中处理某一关注点

    侵入性少,增强代码可读性及可维护性

  • AOP应用场景

    权限控制、缓存控制、事务控制、分布式追踪、异常处理等

    总之,在程序运行时,动态地将代码切入到类的指定方法或位置上的思想,就是面向切面编程。

2.AOP常用术语

Spring的AOP中有几个重要概念搞清楚就行

  • 执行点(Executepoint) - 类初始化,方法调用。
  • 连接点(Joinpoint) - 执行点+方位的组合,可确定Joinpoint,比如类开始初始化前,类初始化后,方法调用前,方法调用后。
  • 切点(Pointcut) - 在众多执行点中,定位感兴趣的执行点。Executepoint相当于数据库表中的记录,而Pointcut相当于查询条件。
  • 增强(Advice) - 织入到目标类连接点上的一段程序代码。除了一段程序代码外,还拥有执行点的方位信息。
  • 目标对象(Target) - 增强逻辑的织入目标类
  • 引介(Introduction) - 一种特殊的增强(advice),它为类添加一些额外的属性和方法,动态为业务类添加其他接口的实现逻辑,让业务类成为这个接口的实现类。
  • 代理(Proxy) - 一个类被AOP织入后,产生一个结果类,它便是融合了原类和增强逻辑的代理类。
  • 切面(Aspect) - 切面由切点(Pointcut)和增强(Advice/Introduction)组成,既包括横切逻辑定义,也包括连接点定义。

AOP工作重点:

  • 如何通过切点(Pointcut)和增强(Advice)定位到连接点(Jointpoint)上;
  • 如何在增强(Advice)中编写切面的代码。

3.Advice-五种增强方式

例如在执行某个特定方法的时候,我们可以选择不同的增强方式(如前置通知/增强,在方法运行前执行),达到我们织入后的不同效果。

  • 前置通知:在我们执行目标方法之前运行(@Before)
1
2
3
4
5
6
7
@Pointcut("within(com.example.demo.Service.*)")
public void matchType(){}

@Before("matchType()") //可在此加入JoinPoint打印切点信息
public void before(JoinPoint joinPoint){
System.out.println("------【前置通知】------" + joinPoint);
}
  • 后置通知:在我们目标方法运行结束之后 ,不管有没有异常(@After)
1
2
3
4
@After(value="execution(* com.example.aspectJ.demo1.ProductDao.findAll(..))")
public void after(){
System.out.println("最终通知==================");
}
  • 返回通知:在我们的目标方法正常返回值后运行(@AfterReturning)
1
2
3
4
@AfterReturning(value="execution(* com.example.aspectJ.demo1.ProductDao.update(..))" ,returning = "result")
public void afterReturning(Object result){ //通过returning属性,定义方法返回值作为参数
System.out.println("后置通知========="+result);
}

sVsXeU.md.png

  • 异常通知:在我们的目标方法出现异常后运行(@AfterThrowing)
1
2
3
4
5
//通过设置throwing属性,可以设置发生异常对象参数
@AfterThrowing(value = "execution(* com.example.aspectJ.demo1.ProductDao.findOne(..))",throwing = "e")
public void afterThrowing(Throwable e){
System.out.println("抛出异常通知"+e.getMessage());
}
  • 环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知(@Around)
1
2
3
4
5
6
7
8
@Around(value = "execution(* com.example.aspectJ.demo1.ProductDao.delete(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

System.out.println("环绕前通知");
Object obj = joinPoint.proceed(); //执行目标方法
System.out.println("环绕后通知");
return obj;
}

sVsrid.md.png

4.SpringAOP使用详解

切面表达式

sVcsJJ.jpg

excution表达式

  • execution(
  • 修饰符pattern

  • 返回值pattern

  • 描述包名

  • 方法名(参数)

  • 方法抛出异常pattern

    )

1
2
3
4
5
6
7
@Pointcut("execution(public * com.example.controller.*Controller.*(..))")
public void match(){}

@Before("match()")
public void before(){
//前置通知...
}

within表达式

1
2
3
4
5
6
7
//匹配StudentService类里所有方法
@Pointcut("within(com.example.service.StudentService)")
public void matchType(){}

//匹配com.example包及子包下所有类方法
@Pointcut("within(com.example..*)")
public void matchPackage(){}

对象匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
/*public class serviceImpl implements service*/

//匹配AOP对象的目标对象为指定类型方法,即serviceImpl的aop代理对象方法
@Pointcut("this(com.example.serviceImpl)")
public void thisDemo(){}

//匹配实现service接口的目标对象(非aop代理后的对象)方法,这里指的就是serviceImpl的方法
@Pointcut("target(com.example.service)")
public void targetDemo(){}

//匹配所有以Service结尾的bean中方法
@Pointcut("bean(*Service)")
public void beanDemo(){}

参数匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//匹配任何以find开头且只有一个Long参数的方法
@Pointcut("execution(* *..find*(Long))")
public void argsDemo1(){}

//匹配任何只有一个Long参数的方法
@Pointcut("args(Long)")
public void argsDemo2(){}

//匹配任何以find开头且第一个参数为Long的方法
@Pointcut("execution(* *..find*(Long,..))")
public void argsDemo3(){}

//匹配第一个参数为Long的方法
@Pointcut("args(Long,..)")
public void argsDemo4(){}

注解匹配(自定义注解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//匹配方法注解有@AdminOnly的方法
@Pointcut("@annotation(com.example.security.AdminOnly)")
public void annoDemo(){}

//匹配注解有@Test1的类下所有方法,要求注解的RetentionPolicy级别为CLASS
@Pointcut("@within(com.example.annotation.Test1)")
public void annoWithinDemo(){}

//匹配注解有@Test2类下所有方法,要求注解的RetentionPolicy级别为RUNTIME
@Pointcut("@target(com.example.repository.Test2)")
public void annoTargetDemo(){}

//匹配传入参数类具有@Test3的注解的方法(例如student实体类有注解@Test3,只要方法传入student类就会被拦截)
@Pointcut("@args(org.example.repository.Test3)")
public void annoArgsDemo(){}

两个aop切面类都工作了,顺序呢就是下面的:

sVrzxP.png

spring aop就是一个同心圆,要执行的方法为圆心,最外层的order最小。从最外层按照AOP1、AOP2的顺序依次执行doAround方法,doBefore方法。然后执行method方法,最后按照AOP2、AOP1的顺序依次执行doAfter、doAfterReturn方法。也就是说对多个AOP来说,先before的,一定后after。

对于上面的例子就是,先外层的就是对所有controller的切面,内层就是自定义注解的。 那不同的切面,顺序怎么决定呢,尤其是同格式的切面处理,譬如两个execution的情况,那spring就是随机决定哪个在外哪个在内了。

所以大部分情况下,我们需要指定顺序,最简单的方式就是在Aspect切面类上加上@Order(1)注解即可,order越小最先执行,也就是位于最外层。像一些全局处理的就可以把order设小一点,具体到某个细节的就设大一点。

5.参考文档

SpringAOP-什么是面向切面编程?

SpringBoot系列 - 使用AOP

6.GitHub源码

springboot-aop