文章目录
- 前言
- 1. AOP基础
- 1.1 AOP概述: 什么是AOP?
- 1.2 AOP快速入门
- 1.3 Spring AOP核心中的相关术语(面试)
- 2. AOP进阶
- 2.1 通知类型
- 2.1.1 @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(通知的代码在业务方法之前和之后都有)
- 2.1.2 @Before:前置通知,此注解标注的通知方法在目标方法前被执行(通知的代码只放在业务方法之前)
- 2.1.3 @AfterReturning : 后置(返回)通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行(通知的代码只放在业务方法之后)
- 2.1.4 @After :后置最终通知,此注解标注的通知方法无论目标方法如何结束(正常返回或抛出异常),都会执行。(通知代码在 finally 的代码块之中)
- 2.1.5 @AfterThrowing : 后置异常通知,此注解标注的通知方法在目标方法抛出异常后执行。(通知代码在捕获异常 catch(){}的代码块之中)
- 2.2 通知顺序(如果多个切片类都操作一个目标方法会发生什么呢?)
- 2.2.1 默认顺序规则
- 2.2.2 使用Spring提供的@Order注解指定顺序规则
- 2.3 切入点表达式
- 2.3.1 execution
- 2.3.2 @annotation(更灵活简单)
- 2.3.3 @PointCut注解 切入点表达式的抽取 (用于标识切入点方法(公共的切入点表达式))
- 2.4 连接点(连接点对象)
- 2.4.1 JoinPoint连接点对象:对于除环绕通知外的其他所有通知,获取连接点信息只能使用JoinPoint
- 2.4.2 ProceedingJoinPoint连接点对象:对于环绕通知,获取连接点信息只能使用ProceedingJoinPoint类型
- 3. AOP案例:操作日志(重要!!!)
- 3.1 需求:将案例中增、删、改相关接口的操作日志记录到数据库表中
- 3.2 分析
- 3.3 步骤
- 3.4 实现
- 3.4.1 准备工作
- 3.4.2 编码实现
前言
1. AOP基础
在AOP基础这个阶段,我们首先介绍一下什么是AOP,再通过一个快速入门程序,让大家快速体验AOP程序的开发。最后再介绍AOP当中所涉及到的一些核心的概念。
1.1 AOP概述: 什么是AOP?
什么是AOP?
- AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
那什么又是面向方法编程呢,为什么又需要面向方法编程呢?来我们举个例子做一个说明:
比如,我们这里有一个项目,项目中开发了很多的业务功能。
然而有一些业务功能执行效率比较低,执行耗时较长,我们需要针对于这些业务方法进行优化。 那首先第一步就需要定位出执行耗时比较长的业务方法,再针对于业务方法再来进行优化。
此时我们就需要统计当前这个项目当中每一个业务方法的执行耗时。那么统计每一个业务方法的执行耗时该怎么实现?
可能多数人首先想到的就是在每一个业务方法运行之前,记录这个方法运行的开始时间。在这个方法运行完毕之后,再来记录这个方法运行的结束时间。拿结束时间减去开始时间,不就是这个方法的执行耗时吗?
以上分析的实现方式是可以解决需求问题的。但是对于一个项目来讲,里面会包含很多的业务模块,每个业务模块又包含很多增删改查的方法,如果我们要在每一个模块下的业务方法中,添加记录开始时间、结束时间、计算执行耗时的代码,就会让程序员的工作变得非常繁琐。
而AOP面向方法编程,就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
我们要想完成统计各个业务方法执行耗时的需求,我们只需要定义一个模板方法,将记录方法执行耗时这一部分公共的逻辑代码,定义在模板方法当中,在这个方法开始运行之前,来记录这个方法运行的开始时间,在方法结束运行的时候,再来记录方法运行的结束时间,中间就来运行原始的业务方法。
而中间运行的原始业务方法,可能是其中的一个业务方法,比如:我们只想通过 部门管理的 list 方法的执行耗时,那就只有这一个方法是原始业务方法。 而如果,我们是先想统计所有部门管理的业务方法执行耗时,那此时,所有的部门管理的业务方法都是 原始业务方法。 那面向这样的指定的一个或多个方法进行编程,我们就称之为 面向切面编程。
简单了解了一下,相信已经发现了,这个AOP面向切面编程和我们在JavaSE里面学的动态代理技术很像,没错,这个技术的底层就是用动态代理实现的。
那此时,当我们再调用部门管理的 list 业务方法时啊,并不会直接执行 list 方法的逻辑,而是会执行我们所定义的 模板方法 , 然后再模板方法中:
- 记录方法运行开始时间
- 运行原始的业务方法(那此时原始的业务方法,就是 list 方法)
- 记录方法运行结束时间,计算方法执行耗时
不论,我们运行的是那个业务方法,最后其实运行的就是我们定义的模板方法,而在模板方法中,就完成了原始方法执行耗时的统计操作 。(那这样呢,我们就通过一个模板方法就完成了指定的一个或多个业务方法执行耗时的统计)
而大家会发现,这个流程,我们是不是似曾相识啊?
对了,就是和我们之前所学习的动态代理技术是非常类似的。 我们所说的模板方法,其实就是代理对象中所定义的方法,那代理对象中的方法以及根据对应的业务需要, 完成了对应的业务功能,当运行原始业务方法时,就会运行代理对象中的方法,从而实现统计业务方法执行耗时的操作。
其实,AOP面向切面编程和OOP面向对象编程一样,它们都仅仅是一种编程思想,而动态代理技术是这种思想最主流的实现方式。而Spring的AOP是Spring框架的高级技术,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行编程(功能增强)。
AOP的优势:
- 减少重复代码
- 提高开发效率
- 维护方便
1.2 AOP快速入门
我们一般会建一个aop包来存放所有的模版方法:
在了解了什么是AOP后,我们下面通过一个快速入门程序,体验下AOP的开发,并掌握Spring中AOP的开发步骤。
需求:统计各个业务层方法执行耗时。
实现步骤:
- 导入依赖:在pom.xml中导入AOP的依赖
- 编写AOP程序:针对于特定方法根据业务需要进行编程
为演示方便,可以自建新项目或导入提供的
springboot-aop-quickstart
项目工程
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
AOP程序:TimeAspect
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {@Around("execution(* com.itheima.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//记录方法执行开始时间long begin = System.currentTimeMillis();//执行原始方法Object result = pjp.proceed();//记录方法执行结束时间long end = System.currentTimeMillis();//计算方法执行耗时log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);return result;}
}
重新启动SpringBoot服务测试程序(先把过滤器和拦截器都关了):
- 根据id查询员工
我们也可以下断点看看:
我们通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下:
- 记录系统的操作日志
- 权限控制
- 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务
这些都是AOP应用的典型场景。
通过入门程序,我们也应该感受到了AOP面向切面编程的一些优势:
-
代码无侵入:没有修改原始的业务方法,就已经对原始的业务方法进行了功能的增强或者是功能的改变
-
减少了重复代码
-
提高开发效率
-
维护方便
1.3 Spring AOP核心中的相关术语(面试)
这部分建议写学完所有最后再来看才会清晰,不然压根不知道在讲什么
通过SpringAOP的快速入门,感受了一下AOP面向切面编程的开发方式。下面我们再来学习AOP当中涉及到的一些相关术语。
- (1) 切面(Aspect):简单理解为一个包含了通知(advice)和切点(poincut)的类
其实就是定义了一个Java 类,里面包含了通知(advice)和切点(poincut)定义了在何处以及何时执行通知,将切面的一些东西模块化了,即定义横切关注点的模块,封装了不同模块共享的功能.
执行后或抛出异常时运行。 - (2) 通知(Advice): 切面中的实际逻辑,即在连接点上执行的操作。通知可以在方法执行前(就是切面类里面使用了通知注解的哪些方法,所以通知就是方法)
- 前置通知(Before advice): 在目标方法执行前执行。
- 后置通知(After returning advice):在目标方法成功执行后执行。
- 后置异常通知(After throwing advice):在目标方法抛出异常后执行。
- 后置最终通知(After (finally) advice):无论目标方法如何结束(正常返回或抛出异常),都会执行。
- 环绕通知(Around advice):在目标方法执行前后都执行,并且可以控制目标方法的执行过程,环绕通知可以用作日志打印或者权限校验。
- (3) 切点(Pointcut): 切点是一个表达式,用于定义在哪些连接点上执行通知,简单理解就是通过这个表达式可以找到想要织入的哪些方法。
- (4) 连接点(Join point): 连接点是程序执行过程中可以应用切面的点,例如方法的调用、方去的执行、异常的抛出等,可以拿到切入方法名等诸多属性。
其实就是JoinPoint和ProceedingJoinPoint这两个连接点对象,我们可以从这个连接点对象中获取到目标方法的各种信息,目标方法的返回值、方法名、参数列表等(所以我们也可以简单理解连接点就是可以被aop控制的哪些目标方法) - (5) 目标对象(Target Object): 被切面增强的对象,也就是原本的业务类
就是目标方法所在的哪些类对象,就称为目标对象 - (6) 代理(Proxy): Spring AOP 通过生成代理对象来增强目标对象的方法。代理对象包含目对象的原始方法和增强逻辑。
Spring AOP底层是动态代理,当然会有代理对象 - (7) 织入(Weaving): 将切面应用到目标对象上的过程。Spring AOP 是在运行时进行织入的
把通知应用到目标对象上的过程就称为织入
2. AOP进阶
AOP的基础知识学习完之后,下面我们对AOP当中的各个细节进行详细的学习。主要分为4个部分:
- 通知类型
- 通知顺序
- 切入点表达式
- 连接点
我们先来学习第一部分通知类型。
2.1 通知类型
2.1.1 @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(通知的代码在业务方法之前和之后都有)
2.1.2 @Before:前置通知,此注解标注的通知方法在目标方法前被执行(通知的代码只放在业务方法之前)
2.1.3 @AfterReturning : 后置(返回)通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行(通知的代码只放在业务方法之后)
2.1.4 @After :后置最终通知,此注解标注的通知方法无论目标方法如何结束(正常返回或抛出异常),都会执行。(通知代码在 finally 的代码块之中)
2.1.5 @AfterThrowing : 后置异常通知,此注解标注的通知方法在目标方法抛出异常后执行。(通知代码在捕获异常 catch(){}的代码块之中)
在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//记录方法执行开始时间long begin = System.currentTimeMillis();//执行原始方法Object result = pjp.proceed();//记录方法执行结束时间long end = System.currentTimeMillis();//计算方法执行耗时log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);return result;
}
只要我们在通知方法上加上了@Around注解,就代表当前通知是一个环绕通知。
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(通知的代码在业务方法之前和之后都有)
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行(通知的代码只放在业务方法之前)
- @AfterReturning : 后置(返回)通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行(通知的代码只放在业务方法之后)
- @After :(后置)最终通知,此注解标注的通知方法无论目标方法如何结束(正常返回或抛出异常),都会执行。(通知代码在 finally 的代码块之中)
- @AfterThrowing : (后置)异常通知,此注解标注的通知方法在目标方法抛出异常后执行。(通知代码在捕获异常 catch(){}的代码块之中)
下面我们通过代码演示,来加深对于不同通知类型的理解:
@Slf4j
@Component
@Aspect
public class MyAspect1 {//前置通知@Before("execution(* com.itheima.service.*.*(..))")public void before(JoinPoint joinPoint){log.info("前置通知中代码执行了 ...");}//环绕通知@Around("execution(* com.itheima.service.*.*(..))")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {log.info("环绕通知中before代码执行了 ...");//调用目标对象的原始方法执行Object result = proceedingJoinPoint.proceed();//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了log.info("环绕通知中after代码执行了 ...");return result;}//后置最终通知@After("execution(* com.itheima.service.*.*(..))")public void after(JoinPoint joinPoint){log.info("最终通知中代码执行了 ...");}// 后置通知(程序在正常执行的情况下,会执行的后置通知)@AfterReturning("execution(* com.itheima.service.*.*(..))")public void afterReturning(JoinPoint joinPoint){log.info("后置通知中代码执行了 ...");}//异常通知(程序在出现异常的情况下,执行的后置通知)@AfterThrowing("execution(* com.itheima.service.*.*(..))")public void afterThrowing(JoinPoint joinPoint){log.info("异常通知执行了 ...");}
}
重新启动SpringBoot服务,进行测试:
1. 没有异常情况下:
- 使用postman测试查询所有部门数据
- 查看idea中控制台日志输出
程序没有发生异常的情况下,@AfterThrowing标识的异常通知方法不会执行。
2. 出现异常情况下:
修改DeptServiceImpl业务实现类中的代码: 添加异常
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper