一、AOP入门简介

AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式,可以知道开发者如何组织程序结构

作用:在不惊动原始设计的基础上为其进行功能增强。(无侵入式编程)

AOP的核心概念:

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。
    • 在SpringAOP中可以理解为方法的执行。
  • 切入点(PointCut):匹配连接点的式子
    • 在SpringAOP中,一个切入点只可以描述 一个具体的方法,也可以匹配多个方法。
      • 一个具体方法:com.huawei.dao包下的BookDao接口中的无形参无返回值的save方法。
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法。
  • 通知(Advice):在切入点处执行的操作,也就是共性功能。
    • 在SpringAOP中,功能最终会以 方法的形式呈现出来。
  • 通知类:定义通知的类。
  • 切面(Aspect):描述通知与切入点的对应关系。

连接点代表所有的方法,切入点代表要追加功能的方法,在范围上连接点是包含切入点的。

二、AOP工作流程

1、Spring容器启动

2、读取所有切面配置中的切入点

3、初始化bean,判断bean对应的类中的方法是否匹配到任意切入点

  • 匹配失败、创建对象
  • 匹配成功,创建原始对象(目标对象)的代理对象

4、获取bea执行方法

  • 获取bean,调用方法并执行,完成操作
  • 获取到的bean是代理对象的时候,根据代理对象的运行模式运行原始方法与增强的内容来完成操作

AOP的核心概念

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的;
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。

三、AOP切入点表达式

切入点:要增强的方法

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

描述方式一:执行com.huawei.dao包下的BookDao接口中的无参数update方法

execution(void com.huawei.dao.BookDao.update())

描述方式二:执行com.huawei.dao.impl包下的BookDaoImpl类中的无参数update方法

execution(void com.huawei.dao.impl.BookDaoImpl.update())

切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

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

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.huawei.*.UserService.find*(*))
  • ..:多个连续的任意符号,可以独立出现,常用于简化包名和参数的书写
execution(public User com..UserService.findById(..))
  • +:专用于匹配子类的类型
execution(* *..*Service+.*(..))

书写技巧

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

四、AOP通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码的时候需要将其加入到合理的位置。

AOP通知可以分为5种类型:

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知(了解)
  • 抛出异常后通知(了解)

4.1 前置通知

@Before("pt()")
public void before() {
  System.out.println("before advice...");
}

4.2 后置通知

@After("pt()")
public void after() {
  System.out.println("after advice...");
}

4.3 环绕通知(重点)

@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
  System.out.println("around before advice...");
  
  // 表示对原始程序的调用
  pjp.proceed();
  
  System.out.println("around after advice...");
}

如果我们采取环绕通知的原函数是有返回值的,那么我们也需要采取对应的写法完善这部分内容:

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
  System.out.println("around before advice...");
  
  // 表示对原始程序的调用
  Object ret = pjp.proceed();
  
  System.out.println("around after advice...");
  return ret;
}

@Around注意事项:

1、环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知。

2、通知中如果未使用ProceedingJoinPoing对原始方法进行调用的话就会直接跳过原始方法的执行。

3、对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果需要接收返回值的话,必须设置成Object类型。

4、原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object。

5、由于无法预知原始方法运行之后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象。

4.4 返回后通知

与正常的后置通知的区别是,返回后通知只有在源程序没有抛出异常,正常结束的情况下才会使用。(了解即可,实际并不常用)

4.5 抛出异常后通知

只有在源程序抛出异常的情况下才会被调用,如果源程序正常结束而没有抛出任何异常的话,则不会被调用。

五、AOP通知获取数据

  • 获取切入点方法的参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedJointPoint:适用于环绕通知
  • 获取切入点方法的返回值
    • 返回后通知
    • 环绕通知
  • 获取切入点方法的运行异常信息
    • 抛出异常后通知
    • 环绕通知

5.1 对于参数的获取

前置后置方法获取函数参数的demo:

@Before("pt()")
public void before(JoinPoint pj) {
  Object[] args = pj.getArgs();
  system.out.println(Arrays.toString(args));
  
  System.out.println("before dao ...");
}

环绕通知方法可以拿到参数之后进行一系列处理再传入,具体实现如下:

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
  Object[] args = pjp.getArgs();
  System.out.println(Arrays.toString(args));
  
  // 可以在此处对数据进行清洗或者进一步操作
  args[0] == 666;
  
  
  Object ret = pjp.proceed(args);			// 此时就是使用我们新传入的参数进行相关的业务操作了
  return ret;
}

5.2 对于返回值的获取

采用环绕通知获取返回值的方法上面已经展示了,因此此处不再赘述。

返回后通知demo:

@AfterReturning(value="pt()", returning="ret")		// 此处returning属性就是代表接收返回值的
public void afterReturning(Object ret) {
	System.out.println("afterReturning result is : " + ret);
}

需要注意的是,如果afterReturning方法中需要连接点的话,参数位置必须是第一个,也可以直接省略不写,public void afterReturning(JoinPoint jp, Object ret)

5.3 对于异常的获取

环绕通知中只需要使用try-catch块包围即可,之后我们可以在catch中对捕获到的异常做出对应的操作。

抛出异常后通知demo:

@AfterThrowing(value="pt()", throwing="t")
public void afterThrowing(Throwable t) {
  System.out.println("afterThrowing dao ...");
}

六、案例及总结

6.1 案例

需求:对百度网盘分享链接输入密码时尾部多输入的空格进行兼容性处理

分析

  • 在业务方法执行之前对所有的输入参数进行格式处理
  • 适用处理后的参数再去调用原始方法 —— 环绕通知中存在对原始方法的调用

核心实现代码:

@Around("servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throw Throwable {
  Object[] args = pjp.getArgs();
  
  // 循环判断是否需要去除空格
  for (int i = 0; i < args.length; i++) {
    // 如果判定当前参数是字符串的话,则执行去空格操作
    if (args[i].getClass.equals(String.getClass())) {
      args[i] = args[i].toSrting().trim();
    }
  }
  
  Object ret = pjp.proceed(args);
  return ret;
}

6.2 AOP学习总结

在AOP的实际实现中,我们比较常用的是环绕通知:

  • 环绕通知依赖形参ProceedingJoinPoint才能够实现对原始方法的调用
  • 环绕通知可以隔离原始方法的调用执行
  • 环绕通知的返回值设置为Object类型
  • 环绕通知可以对原始方法调用过程中出现的异常进行处理。