AOP 面向切面编程

1. AOP简介

  • AOP(Aspect oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
    • OOP(Object oriented Programming)面向对象编程
  • 作用: 在不惊动原始设计的基础上为其进行功能增强
  • spring理念:无入侵式编程
  • SpringAOP的本质:代理模式

2. AOP核心概念

连接点 (JoinPoint ):

  • 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
  • 在SpringAOP中,理解为方法的执行。连接点可以被切入点匹配。

切入点 ( Pointcut ) :

  • 匹配连接点的式子。意思是“根据什么规则去匹配”
  • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法

通知 ( Advice ) :

  • 在切入点处执行的操作,也就是共性功能在SpringAOP中,功能最终以方法的形式呈现
  • 意思是,如果切入点匹配成功后,应该执行什么方法

通知类:

  • 定义通知的类

切面( Aspect ) :

  • 描述通知与切入点的对应关系

织入

  • 执行代理的过程称为织入

通俗的来讲,连接点 就是一个需要被增强的方法,而 切入点 是一套规则,目的是找到对应的连接点。一旦连接点找到以后,就会根据设置执行 通知

3. AOP工作流程

  1. 引入依赖,在配置类中加入启动注解
  2. 读取所有切面配置中的切入点
  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
    • 匹配失败,创建对象
    • 匹配成功,创建原始对象(目标对象)的代理对象
  4. 获取bean执行方法
    • 匹配失败:获取bean,调用方法并执行,完成操作
    • 匹配成功:获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

目标对象(Target ):

​ 原始能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的

代理(Proxy )

​ 目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现


4. 入门案例

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

接口

package link.xiaomo.dao;

public interface BookDao {
    public void update();

     public int save();
}

实现类

package link.xiaomo.dao;

public class BookDaoImpl implements BookDao {
    public void update(){
        System.out.println("book dao update ...");
    }

    public void save(){
        System.out.println("book dao save ...");
    }
}

通知类

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void link.xiaomo.dao.BookDao.save())")
    private void ptx(){}

    @Pointcut("execution(void link.xiaomo.dao.BookDao.update())")
    private void pt(){}

    // @Around表示环绕原方法增强
    @Around("ptx()")
    public Object method(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
         // 表示对原始操作的调用
         Object obj = pjp.proceed();
         System.out.println("around after advice ...");
        return obj;
    }

    // @Before表示在原方法之前增强
    @Before("pt()")
    public void method(){
        System.out.println("around before advice ...");
    }
}

spring里的任意配置类

@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {
}

5. AOP切入表达式

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

5.1. 切入点表达式标准格式

  • 格式:动作关键字 ( 访问修饰符 返回值 全限定名.方法名( 参数 ) 异常名 )
execution (public User link.xiaomo.service.UserService.findById (int ) )
  • 动作关键字: 描述切入点的行为动作。例如,**execution **表示执行到指定切入点
  • 访问修饰符: public,private等,可以省略
  • 返回值
  • 全限定名:可以是类或接口的全限定名,指包名+类/接口名
  • 方法名
  • 参数
  • 异常名: 方法定义中抛出指定异常,可以省略

5.2. 通配符

可以使用通配符描述切入点,快速描述


* :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

execution (public * link.xiaomo.*.UserService.find* (*) )

匹配link.xiaomo包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法


.. :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

execution (public User link..UserService.findById(..))

匹配link包下的任意包中的UserService类或接口中所有名称为findByld的方法


+ :专用于匹配子类类型

execution(**..*Service+.*(..))

匹配任意以Service结尾的类或接口的子类/子接口中的所有方法

5.3. 书写技巧

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类(尽量降低藕合)
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  • 返回值类型对于 增删改 类使用 精准类型 加速匹配,对于 **查询 **类 使用*通配 快速描述(比如查询匹配 getBy*)
  • 包名书写尽量不使用..匹配,效率过低,常用做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用\*匹配,例如UserService书写成*Service,绑定业务层接口名
  • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getByld书写成getBy*,selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则,表达式中一般省略异常部分(因为有全局异常处理)

6. 通知获取数据

在以下代码例子中,我们可以在注释的位置填上适当的代码,实现不同的功能

@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(void link.xiaomo..*Service(..))")
    private void pt(){}    

    @Around("pt()")
    public Object method(ProceedingJoinPoint pjp) throws Throwable {
        // 具体的逻辑
        return obj;
    }
}

6.1. 获取实际连接点的方法名

因为我们的一个切入点可能匹配到多个连接点,如果我们想要区分当前的切入点具体是哪个,可以通过以下的方法获取切入点方法的名字,以及所在类的名字。

需要注意,如果通知方法的形参有ProceedingJoinPoint或者JoinPoint,则它们必须放在形参的第一位,否则会报错。

  • 以下内容填充在上面代码的注释部分
// 通过ProceedingJoinPoint对象获取签名对象
Signature signature  = pjp.getSignature();
// 切入点的类名
String className = signature.getDeclaringTypeName();
// 切入点方法的名字
String methodName = signature.getName();

6.2. 获取切入点数据

我们可以通过ProceedingJoinPoint对象,不仅可以做到上面一个案例中的获取方法和类数据,我们还也可以获取和修改方法的参数、返回值

通过这种方式,我们可以做到在不修改原方法的情况下,实现批量的参数合法性校验,举例而言,比如对以getXXX开头的方法,如果参数是空字符串,就替换成null

获取到的方法,和设置方法的参数类型,都是Obejct数组。


  • 获取参数
Object[] args = pjp.getArgs();
  • 修改原方法的参数(设置一个新的参数传给连接点)
// 自定义一个新参数
Object[] newArgs = new Object[]{1,2,3};
// 把新参数传给切入点
pjp.process(newArgs);
  • 获取返回值
    • 在@AfterReturning中获取返回值,需要使用如下方法
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(JoinPoint jp,String ret) {
    System.out.println("afterReturning advice ..."+ret);
}

6.3. 通知类型

注解 说明
@Before 前置通知,在连接点方法前调用
@Around 环绕通知,它将覆盖原有方法,可以想象成前置+原方法+后置
@After 后置通知,在连接点方法后调用 类比finally
@AfterReturning 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常
@AfterThrowing 异常通知,当连接点方法异常时调用

环绕通知比较常用:

@Component
@Aspect //声明该bean是一个切面bean 找到切入点+添加通知(增强动作)
@Slf4j
@Order(1)
public class RuntimeAroundAspect {

    @Around("link.xiaomo.tlias.aspectj.ZhiFuAspect.pt()")
    public Object around2Time(ProceedingJoinPoint joinPoint) {
        //前置通知
        log.info("前置通知位置:从我这走 买路财,放行");
        //方法执行 放行

        Object result = null;
        try {
            result = joinPoint.proceed();
            //后置通知
            log.info("后置通知位置:放行走完了");
            return result;
        } catch (Throwable e) {
            //异常通知
            log.info("异常通知位置:"+ e.getMessage());
            return Result.error("有异常");
        } finally {
            //最终通知
            log.info("终于走完了.....");
        }

    }

}

image.png

6.4. (单个切面类中)五大通知执行顺序

不同版本的Spring是有一定差异的,使用时候要注意

  • Spring 4
    • 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕返回 ==> 环绕最终 ==> @After ==> @AfterReturning
    • 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕异常 ==> 环绕最终 ==> @After ==> @AfterThrowing
  • Spring 5.28
    • 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterReturning ==> @After ==> 环绕返回 ==> 环绕最终
    • 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterThrowing ==> @After ==> 环绕异常 ==> 环绕最终

6.5. 多个切面类的执行顺序

1)默认情况下:

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行

2)@Order可指定执行顺序

@Component
@Aspect
@Order(1)  //切面类的执行顺序
public class 某切面类 {
}

@Order(1) 注意作用在AOP切面执行顺序上!

  • 注意区分

这些过滤器、拦截器、切面并不是我们在代码中手动调用的,所以需要大家在脑海中强行构建他们的执行顺序。

AOP切面位置: image.png

6.6. 案例:对所有参数去前后空格

@Around("XXXPt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable{
    Object[] args = pjp.getArgs();
    for (int i = @; i < args.length; i++) {
        //判断参数是不是字符串
        if(args[i].getClass().equals(String.class)){
            args[i] = args[i].toString().trim();
        }
    }
    Object ret = pjp.proceed(args);
    return ret;
}
如人饮水,冷暖自知。
最后更新于 2023-08-05