AOP - 面向切面编程

Parker

1. 基础

1.1 概述

AOP:Aspect Oriented Programming (面向切面编程),即面向特定方法编程。

场景

  • 统计业务方法的执行耗时
  • 记录操作日志
  • 权限管理
  • 事务管理
  • ……

AOP的作用: 在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

AOP的优势

  • 代码无侵入
  • 减少重复代码
  • 提高开发效率
  • 维护方便

1.2 SpringAOP开发步骤

需求:统计各个业务层方法执行耗时

实现步骤

  1. 导入依赖:在项目pom.xml中导入AOP的依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写AOP程序:针对于特定方法根据业务需要进行编程
1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Component
@Aspect // AOP类
public class TimeAspect {
@Around("execution(* com.parker.service.*.*(..))") // 切入点表达式:作用于com.parker.service包下所有接口/类的所有方法,形参和返回值均为任意值
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis(); // 记录开始时间
Object result = proceedingJoinPoint.proceed(); // 调用原始方法运行
long end = System.currentTimeMillis(); // 记录结束时间
log.info(proceedingJoinPoint.getSignature() + "执行耗时:{}ms", end - begin); // 写入日志
return result; // 返回结果
}
}

1.3 核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
  • 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象

1.4 执行流程

Spring的AOP底层是基于动态代理技术来实现的。也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

2. 进阶

2.1 通知类型

Spring中AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行

在使用通知时的注意事项:

  • @Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的

当代码中存在大量重复的切入点表达式时,Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可,具体形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.parker.service.*.*(..))")
private void pt(){}

//环绕通知(引用切入点)
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
Object result = proceedingJoinPoint.proceed();
log.info("around after ...");
return result;
}
……
}

需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:全类名.方法名(),具体形式如下:

1
2
3
4
5
6
7
8
9
10
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.parker.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
}

2.2 通知顺序

当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。

在不同切面类中,默认按照切面类的类名字母排序:

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

如果我们想控制通知的执行顺序有两种方式:

  • 修改切面类的类名(这种方式非常繁琐、而且不便管理)

  • 使用Spring提供的@Order注解,形如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Slf4j
    @Component
    @Aspect
    @Order(2) // 切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
    public class MyAspect2 {
    @Before("execution(* com.parker.service.*.*(..))")
    public void before(){
    log.info("MyAspect2 -> before ...");
    }
    ……
    }

2.3 切入点表达式

切入点表达式:

  • 描述切入点方法的一种表达式

  • 作用:主要用来决定项目中的哪些方法需要加入通知

  • 常见形式:

    1. execution(……):根据方法的签名来匹配

    2. @annotation(……) :根据注解匹配

2.3.1 execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

1
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)
  • 包名.类名: 可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

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

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

注意事项:

  • 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。形如:

    1
    execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))

切入点表达式的书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 …,使用 * 匹配单个包

2.3.2 @annotation

如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。此时,我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。

实现步骤:

  1. 编写自定义注解

  2. 在业务类要作为连接点的方法上添加自定义注解

自定义注解:MyLog

1
2
3
4
@Target(ElementType.METHOD)	// 作用域
@Retention(RetentionPolicy.RUNTIME) // 作用时间
public @interface MyLog {
}

业务类:DeptServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Override
@MyLog // 自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
……
}

切面类

1
2
3
4
5
6
7
8
9
10
@Slf4j
@Component
@Aspect
public class MyAspect {
@Before("@annotation(com.itheima.anno.MyLog)") // annotation切入点表达式
public void before(){
log.info("MyAspect -> before ...");
}
……
}

2.3.3 总结

  • execution切入点表达式
    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
  • annotation切入点表达式
    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

2.4 连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

1
2
3
4
5
6
7
@Before("execution(* com.itheima.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名
Signature signature = joinPoint.getSignature(); // 获取目标方法签名
String methodName =joinPoint.getSignature().getName(); // 获取目标方法名
Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数
}
评论