SpringBoot核心框架之AOP详解
一、AOP基础
1.1 AOP概述
- AOP:Aspect Oriented Programming(面向切面编程,面向方面编程),其实就是面向特定方法编程。
- 场景:项目部分功能运行较慢,定位执行耗时较长的业务方法,此时就需要统计每一个业务的执行耗时。
- 思路:给每个方法在开始前写一个开始计时的逻辑,在方法结束后写一个计时结束的逻辑,然后相减得到运行时间。
思路是没问题的,但是有个问题,一个项目是有很多方法的,如果挨个增加逻辑代码,会相当繁琐,造成代码的臃肿,所以可以使用AOP编程,将计时提出成一个这样的模板:
- 获取方法运行开始时间
- 运行原始方法
- 获取方法运行结束时间,计算执行耗时
原始方法就是我们需要计算时间的方法,并且可以对原始方法进行增强,其实这个技术就是用到了我们在Java基础部分学习的动态代理技术。
实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要是通过底层的动态代理机制,对特点的方法进行编程。
1.2 AOP快速入门
统计各个业务层方法执行耗时
- 导入依赖:在pom.xml中导入AOP的依赖。
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
- 编写AOP程序:针对于特定方法根据业务需要进行编程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Slf4j @Component @Aspect public class TimeAspect {
@Around("execution(* com.example.service.*.*(..))") public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature()+"方法执行耗时:{}ms",end - start);
return result; } }
|
- 查看结果

这样我们就完成了,一个AOP的小例子,但是AOP的功能远不能这些,他还有更多的实用的功能。比如:记录操作日志:可以记录谁什么时间操作了什么方法,传了什么参数,返回值是什么都可以很方便的实现。还有比如权限控制,事务管理等等。
我们来总结一下AOP的优势
- 代码无侵入
- 减少重复代码
- 提高开发效率
- 维护方便
1.3. AOP核心概念
连接点:JoinPoint,可以被连接点控制的方法(暗含方法执行时的信息)。 在此例中就是需要被计算耗时的业务方法。
通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)。在此例中就是计算耗时的逻辑代码。
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。在此例中就是com.example.service 包下所有类的所有方法。
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)。在此例中就是TimeAspect方法。
目标对象:Target,通知所应用的对象。在此例中就是通知com.example.service 包下所有类的所有方法。
1.4. AOP的执行流程
因为SpringAOP
是基于动态代理实现的,所有在方法运行时就会先为目标对象基于动态代理生成一个代理对象,为什么说AOP可以增强方法,就是因为有一个代理方法,然后在AOP执行时,Spring就会将通知添加到代理对象的方法前面,也就是记录开始时间的那个逻辑代码,然后调用原始方法,也就是需要计时的那个方法,此时代理对象已经把原始方法添加到代理对象里面了,然后执行调用原始方法下面的代码,在此例中就是计算耗时的那部分,AOP会把这部分代码添加到代理对象的执行方法的下面,这样代理对象就完成了对目标方法的增强,也就是添加了计时功能,最后在程序运行时自动注入的也就不是原来的对象,而是代理对象了,不过这些都是AOP自动完成,我们只需要编写AOP代码即可。
二、AOP进阶
2.1. AOP支持的通知类型
通知类型:
- 环绕通知(Around Advice)
重点
!!!:
- 使用
@Around
注解来定义。
- 包围目标方法的执行,可以在方法执行前后执行自定义逻辑,并且可以控制目标方法的执行。
- 通过
ProceedingJoinPoint
参数的 proceed()
方法来决定是否执行目标方法。
- 前置通知(Before Advice):
- 使用
@Before
注解来定义。
- 在目标方法执行之前执行,无论方法是否抛出异常,都会执行。
- 不能阻止目标方法的执行。
- 后置通知(After Advice) 也叫最终通知:
- 使用
@After
注解来定义。
- 在目标方法执行之后执行,无论方法是否抛出异常,都会执行。
- 通常用于资源清理工作
- 返回通知(After Returning Advice)
了解
:
- 使用
@AfterReturning
注解来定义。
- 在目标方法成功执行之后执行,即没有抛出异常时执行。
- 可以获取方法的返回值。
- 异常通知(After Advice)
了解
:
- 使用
@AfterThrowing
注解来定义。
- 在目标方法抛出异常后执行。
- 可以获取抛出的异常对象。
注意事项:
- 环绕通知需要自己调用
joinPoint.proceed()
来让原始方法执行,其他通知则不需要。
- 环绕通知的返回值必须是
Object
,来接受原始方法的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Slf4j @Component @Aspect public class MyAspect {
@Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void pt(){}
@Before("pt()") public void Before(){ log.info("before ..."); }
@Around("pt()") public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("around after ..."); Object proceed = joinPoint.proceed(); log.info("around after ..."); return proceed; }
@After("pt()") public void After(){ log.info("after ..."); }
@AfterReturning("pt()") public void Returning(){ log.info("returning ..."); }
@AfterThrowing("pt()") public void Throwing(){ log.info("throwing ..."); } }
|
2.2. 多个通知之间的执行顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会执行。那么顺序是怎么的呢?
我们先创建三个AOP程序,分别给他们创建一个前置通知和后置通知,然后启动程序观察他们的输出情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @Slf4j @Component @Aspect public class MyAspect2 {
@Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void befor(){ log.info("befor2 ..."); }
@After("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void after(){ log.info("after2 ..."); } }
@Slf4j @Component @Aspect public class MyAspect3 {
@Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void befor(){ log.info("befor3 ..."); }
@After("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void after(){ log.info("after3 ..."); } }
@Slf4j @Component @Aspect public class MyAspect4 {
@Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void befor(){ log.info("befor4 ..."); }
@After("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void after(){ log.info("after4 ..."); } }
com.example.aop.MyAspect2 : befor2 ... com.example.aop.MyAspect3 : befor3 ... com.example.aop.MyAspect4 : befor4 ...
com.example.aop.MyAspect4 : after4 ... com.example.aop.MyAspect3 : after3 ... com.example.aop.MyAspect2 : after2 ...
com.example.aop.MyAspect3 : befor3 ... com.example.aop.MyAspect4 : befor4 ... com.example.aop.MyAspect5 : befor2 ...
com.example.aop.MyAspect5 : after2 ... com.example.aop.MyAspect4 : after4 ... com.example.aop.MyAspect3 : after3 ...
|
2.2.1 默认情况:
执行顺序是和类名有关系的,对于目标方法前的通知字母越靠前的越先执行,目标方法后的通知则相反,字母越靠前的越晚执行,这和Filter拦截器的规则是一样的。
2.2.2 也可以使用注解的方式指定顺序。使用@Order(数字)
加在切面类上来控制顺序。
目标方法前的通知:数字小的先执行。
目标方法后的通知:数字小的后执行。
1 2 3 4 5 6 7 8 9
| @Slf4j @Component @Aspect
@Order(10)
public class MyAspect3 { ... }
|
2.3. 切入点表达式
切入点表达式:描述切入点方法的一种表达式。
作用:主要决定项目中哪些方法需要加入通知。
常见形式:
- execution(…):根据方法的签名来匹配。
- @annotation:根据注解匹配。
2.3.1 execution(…)
execution主要是通过方法的返回值,类名,包名,方法名,方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常)
其中带 ?
的表示可以省略的部分
- 访问修饰符:可省略(比如:public private …)
- 包名.类名:可省略 但不推荐
- throws 异常:可省略 (注意是方法上声明可抛出的异常,不是实际抛出的异常)
1 2 3 4 5
| @Before("execution(public void com.example.service.impl.DeptServiceImpl.add(java.lang.Integer))") public void befor(){ ... }
|
可以使用通配符描述切入点
- 单个独立的任意符号,可以通配任意返回值,包括包名,类名,方法名,任意一个参数,也可以通配包,类,方法名的一部分。
1
| @After("execution(* com.*.service.*.add*(*))")
|
- 多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数。
1
| @After("execution(* com.example..DeptService.*(..))")
|
- 根据业务的需要,也可以使用 且(&&),或(||),非(!)来组合切入点表达式。
1
| @After("execution(* com.example..DeptService.*(..)) || execution(* com.example.service.DeptService.*(..))")
|
2.3.2 @annotation:用于匹配标识有特定注解的方法
语法:@annotation(注解的全类名)
先新建一个注解:
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLog { }
|
在目标方法上添加注解
1 2 3 4 5 6 7 8 9 10 11 12
| @MyLog @Override public void delete(Integer id) { deptMapper.delect(id); } @MyLog @Override public void add(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.add(dept); }
|
在切入点表达式以注解的方式进行
1 2 3 4
| @After("@annotation(com.example.aop.MyLog)") public void after(){ ... }
|
3.3. 连接点
在Spring中使用JoinPoint抽象了连接点,用它可以获取方法执行时的相关信息,如目标类目,方法名,方法参数等。
- 对于环绕通知(@around),获取连接点信息只能使用
ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用
JoinPoint
,他是ProceedingJoinPoint的父类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Component @Aspect @Slf4j public class MyAspect5 {
@Pointcut("@annotation(com.example.aop.MyLog)") public void pt(){}
@Before("pt()") public void before(JoinPoint joinPoint){ log.info("before ..."); } @Around("pt()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("around ... before"); log.info("目标对象的类名:"+joinPoint.getTarget().getClass().getName()); log.info("目标方法的方法名"+joinPoint.getSignature().getName()); log.info("目标方法运行时传入的参数"+ Arrays.toString(joinPoint.getArgs())); Object object = joinPoint.proceed(); log.info("目标方法的返回值"+ object);
log.info("around ... after"); return object; } }
com.example.aop.MyAspect5 : around ... before com.example.aop.MyAspect5 : 目标对象的类名:com.example.service.impl.DeptServiceImpl com.example.aop.MyAspect5 : 目标方法的方法名select com.example.aop.MyAspect5 : 目标方法运行时传入的参数[1] com.example.aop.MyAspect5 : before ... com.example.aop.MyAspect5 : 目标方法的返回值[Dept(id=1, name=学工部, createTime=2023-11-30T13:55:55, updateTime=2023-11-30T13:55:55)] com.example.aop.MyAspect5 : around ... after
|
三、AOP案例
3.1. 分析
需求:将项目中的增、删、改、相关接口的操作日志记录到数据库表中
- 操作日志包含:操作人,操作时间,执行方法的全类名,执行方法名,方法运行时的参数,返回值,方法运行时长。
思路分析:
- 需要对方法添加统一的功能,使用AOP最方便,并且需要计算运行时长,所以使用 环绕通知
- 因为增删改的方法名没有规则,所以使用注解的方式写切入表达式
步骤:
- 准备:
- 案例中引入AOP的起步依赖
- 设计数据表结构,并且引入对应的实体类
- 编码:
- 自定义注解:@Log
- 定义切面类,完成记录操作日志的逻辑代码
3.2. 开始干活
3.2.1. 创建数据库:
1 2 3 4 5 6 7 8 9 10 11
| create table operate_log ( id int unsigned primary key auto_increment comment 'ID', operate_user int unsigned comment '操作人ID', operate_time datetime comment '操作时间', class_name varchar(100) comment '操作的类名', method_name varchar(100) comment '操作的方法名', method_params varchar(1000) comment '方法参数', return_value varchar(2000) comment '返回值', cost_time bigint comment '方法执行耗时, 单位:ms' ) comment '操作日志表';
|
3.2.2. 引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.7</version> </dependency>
|
3.2.3. 新建实体类
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; private Integer operateUser; private LocalDateTime operateTime; private String className; private String methodName; private String methodParams; private String returnValue; private Long costTime; }
|
3.2.4. 新建Mapper层
1 2 3 4 5 6 7
| @Mapper public interface OperateLogMapper { @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " + "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});") void insert(OperateLog log); }
|
3.2.5. 新建注解
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log { }
|
3.2.6. 定义切面类,完成记录操作日志的逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Component @Aspect @Slf4j public class LogAspect {
@Autowired private HttpServletRequest request;
@Autowired private OperateLogMapper operateLogMapper;
@Around("@annotation(com.example.anno.Log)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
Integer user = (Integer) JwtUtils.parseJWT(request.getHeader("token")).get("id"); LocalDateTime optionTime = LocalDateTime.now(); String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); String returnValue = JSONObject.toJSONString(result); long costTime = end - start; OperateLog operateLog = new OperateLog(null, user, optionTime, className, methodName, args, returnValue, costTime); operateLogMapper.insert(operateLog); log.info("AOP记录操作日志:{}", operateLog); return result; } }
|
3.2.7. 给需要记录的方法上面添加自定义的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Log @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id){ log.info("根据id删除部门:{}",id); deptService.delete(id); return Result.success(); }
@Log @PostMapping public Result add(@RequestBody Dept dept){ log.info("添加部门{}",dept); deptService.add(dept); return Result.success(); }
|
3.3. 查看结果
刚刚进行了部门的增删改以及员工的增删改操作,我们查看数据库,看有没有被记录。
1 2 3 4 5
| 1,1,2024-10-27 20:20:23,com.example.controller.DeptController,delete,[15],"{""code"":1,""msg"":""success""}",40 2,1,2024-10-27 20:20:45,com.example.controller.DeptController,add,"[Dept(id=null, name=测试部, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",5 3,1,2024-10-27 20:21:00,com.example.controller.EmpController,sava,"[Emp(id=null, username=测试, password=null, name=测试, gender=1, image=, job=1, entrydate=2024-10-20, deptId=16, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",6 4,1,2024-10-27 20:23:01,com.example.controller.DeptController,add,"[Dept(id=null, name=1, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",8 5,1,2024-10-27 20:23:18,com.example.controller.DeptController,delete,[17],"{""code"":1,""msg"":""success""}",12
|

完全符合要求!!!!!!## SpringBoot核心框架之AOP详解
一、AOP基础
1.1 AOP概述
- AOP:Aspect Oriented Programming(面向切面编程,面向方面编程),其实就是面向特定方法编程。
- 场景:项目部分功能运行较慢,定位执行耗时较长的业务方法,此时就需要统计每一个业务的执行耗时。
- 思路:给每个方法在开始前写一个开始计时的逻辑,在方法结束后写一个计时结束的逻辑,然后相减得到运行时间。
思路是没问题的,但是有个问题,一个项目是有很多方法的,如果挨个增加逻辑代码,会相当繁琐,造成代码的臃肿,所以可以使用AOP编程,将计时提出成一个这样的模板:
- 获取方法运行开始时间
- 运行原始方法
- 获取方法运行结束时间,计算执行耗时
原始方法就是我们需要计算时间的方法,并且可以对原始方法进行增强,其实这个技术就是用到了我们在Java基础部分学习的动态代理技术。
实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要是通过底层的动态代理机制,对特点的方法进行编程。
1.2 AOP快速入门
统计各个业务层方法执行耗时
导入依赖:在pom.xml中导入AOP的依赖。
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
编写AOP程序:针对于特定方法根据业务需要进行编程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Slf4j @Component @Aspect public class TimeAspect {
@Around("execution(* com.example.service.*.*(..))") public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature()+"方法执行耗时:{}ms",end - start);
return result; } }
|
- 查看结果

这样我们就完成了,一个AOP的小例子,但是AOP的功能远不能这些,他还有更多的实用的功能。比如:记录操作日志:可以记录谁什么时间操作了什么方法,传了什么参数,返回值是什么都可以很方便的实现。还有比如权限控制,事务管理等等。
我们来总结一下AOP的优势
- 代码无侵入
- 减少重复代码
- 提高开发效率
- 维护方便
1.3. AOP核心概念
连接点:JoinPoint,可以被连接点控制的方法(暗含方法执行时的信息)。 在此例中就是需要被计算耗时的业务方法。
通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)。在此例中就是计算耗时的逻辑代码。
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。在此例中就是com.example.service 包下所有类的所有方法。
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)。在此例中就是TimeAspect方法。
目标对象:Target,通知所应用的对象。在此例中就是通知com.example.service 包下所有类的所有方法。
1.4. AOP的执行流程
因为SpringAOP
是基于动态代理实现的,所有在方法运行时就会先为目标对象基于动态代理生成一个代理对象,为什么说AOP可以增强方法,就是因为有一个代理方法,然后在AOP执行时,Spring就会将通知添加到代理对象的方法前面,也就是记录开始时间的那个逻辑代码,然后调用原始方法,也就是需要计时的那个方法,此时代理对象已经把原始方法添加到代理对象里面了,然后执行调用原始方法下面的代码,在此例中就是计算耗时的那部分,AOP会把这部分代码添加到代理对象的执行方法的下面,这样代理对象就完成了对目标方法的增强,也就是添加了计时功能,最后在程序运行时自动注入的也就不是原来的对象,而是代理对象了,不过这些都是AOP自动完成,我们只需要编写AOP代码即可。
二、AOP进阶
2.1. AOP支持的通知类型
通知类型:
- 环绕通知(Around Advice)
重点
!!!:
- 使用
@Around
注解来定义。
- 包围目标方法的执行,可以在方法执行前后执行自定义逻辑,并且可以控制目标方法的执行。
- 通过
ProceedingJoinPoint
参数的 proceed()
方法来决定是否执行目标方法。
- 前置通知(Before Advice):
- 使用
@Before
注解来定义。
- 在目标方法执行之前执行,无论方法是否抛出异常,都会执行。
- 不能阻止目标方法的执行。
- 后置通知(After Advice) 也叫最终通知:
- 使用
@After
注解来定义。
- 在目标方法执行之后执行,无论方法是否抛出异常,都会执行。
- 通常用于资源清理工作
- 返回通知(After Returning Advice)
了解
:
- 使用
@AfterReturning
注解来定义。
- 在目标方法成功执行之后执行,即没有抛出异常时执行。
- 可以获取方法的返回值。
- 异常通知(After Advice)
了解
:
- 使用
@AfterThrowing
注解来定义。
- 在目标方法抛出异常后执行。
- 可以获取抛出的异常对象。
注意事项:
- 环绕通知需要自己调用
joinPoint.proceed()
来让原始方法执行,其他通知则不需要。
- 环绕通知的返回值必须是
Object
,来接受原始方法的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Slf4j @Component @Aspect public class MyAspect {
@Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void pt(){}
@Before("pt()") public void Before(){ log.info("before ..."); }
@Around("pt()") public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("around after ..."); Object proceed = joinPoint.proceed(); log.info("around after ..."); return proceed; }
@After("pt()") public void After(){ log.info("after ..."); }
@AfterReturning("pt()") public void Returning(){ log.info("returning ..."); }
@AfterThrowing("pt()") public void Throwing(){ log.info("throwing ..."); } }
|
2.2. 多个通知之间的执行顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会执行。那么顺序是怎么的呢?
我们先创建三个AOP程序,分别给他们创建一个前置通知和后置通知,然后启动程序观察他们的输出情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @Slf4j @Component @Aspect public class MyAspect2 {
@Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void befor(){ log.info("befor2 ..."); }
@After("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void after(){ log.info("after2 ..."); } }
@Slf4j @Component @Aspect public class MyAspect3 {
@Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void befor(){ log.info("befor3 ..."); }
@After("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void after(){ log.info("after3 ..."); } }
@Slf4j @Component @Aspect public class MyAspect4 {
@Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void befor(){ log.info("befor4 ..."); }
@After("execution(* com.example.service.impl.DeptServiceImpl.*(..))") public void after(){ log.info("after4 ..."); } }
com.example.aop.MyAspect2 : befor2 ... com.example.aop.MyAspect3 : befor3 ... com.example.aop.MyAspect4 : befor4 ...
com.example.aop.MyAspect4 : after4 ... com.example.aop.MyAspect3 : after3 ... com.example.aop.MyAspect2 : after2 ...
com.example.aop.MyAspect3 : befor3 ... com.example.aop.MyAspect4 : befor4 ... com.example.aop.MyAspect5 : befor2 ...
com.example.aop.MyAspect5 : after2 ... com.example.aop.MyAspect4 : after4 ... com.example.aop.MyAspect3 : after3 ...
|
2.2.1 默认情况:
执行顺序是和类名有关系的,对于目标方法前的通知字母越靠前的越先执行,目标方法后的通知则相反,字母越靠前的越晚执行,这和Filter拦截器的规则是一样的。
2.2.2 也可以使用注解的方式指定顺序。使用@Order(数字)
加在切面类上来控制顺序。
目标方法前的通知:数字小的先执行。
目标方法后的通知:数字小的后执行。
1 2 3 4 5 6 7 8 9
| @Slf4j @Component @Aspect
@Order(10)
public class MyAspect3 { ... }
|
2.3. 切入点表达式
切入点表达式:描述切入点方法的一种表达式。
作用:主要决定项目中哪些方法需要加入通知。
常见形式:
- execution(…):根据方法的签名来匹配。
- @annotation:根据注解匹配。
2.3.1 execution(…)
execution主要是通过方法的返回值,类名,包名,方法名,方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常)
其中带 ?
的表示可以省略的部分
- 访问修饰符:可省略(比如:public private …)
- 包名.类名:可省略 但不推荐
- throws 异常:可省略 (注意是方法上声明可抛出的异常,不是实际抛出的异常)
1 2 3 4 5
| @Before("execution(public void com.example.service.impl.DeptServiceImpl.add(java.lang.Integer))") public void befor(){ ... }
|
可以使用通配符描述切入点
- 单个独立的任意符号,可以通配任意返回值,包括包名,类名,方法名,任意一个参数,也可以通配包,类,方法名的一部分。
1
| @After("execution(* com.*.service.*.add*(*))")
|
- 多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数。
1
| @After("execution(* com.example..DeptService.*(..))")
|
- 根据业务的需要,也可以使用 且(&&),或(||),非(!)来组合切入点表达式。
1
| @After("execution(* com.example..DeptService.*(..)) || execution(* com.example.service.DeptService.*(..))")
|
2.3.2 @annotation:用于匹配标识有特定注解的方法
语法:@annotation(注解的全类名)
先新建一个注解:
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLog { }
|
在目标方法上添加注解
1 2 3 4 5 6 7 8 9 10 11 12
| @MyLog @Override public void delete(Integer id) { deptMapper.delect(id); } @MyLog @Override public void add(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.add(dept); }
|
在切入点表达式以注解的方式进行
1 2 3 4
| @After("@annotation(com.example.aop.MyLog)") public void after(){ ... }
|
3.3. 连接点
在Spring中使用JoinPoint抽象了连接点,用它可以获取方法执行时的相关信息,如目标类目,方法名,方法参数等。
- 对于环绕通知(@around),获取连接点信息只能使用
ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用
JoinPoint
,他是ProceedingJoinPoint的父类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Component @Aspect @Slf4j public class MyAspect5 {
@Pointcut("@annotation(com.example.aop.MyLog)") public void pt(){}
@Before("pt()") public void before(JoinPoint joinPoint){ log.info("before ..."); } @Around("pt()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("around ... before"); log.info("目标对象的类名:"+joinPoint.getTarget().getClass().getName()); log.info("目标方法的方法名"+joinPoint.getSignature().getName()); log.info("目标方法运行时传入的参数"+ Arrays.toString(joinPoint.getArgs())); Object object = joinPoint.proceed(); log.info("目标方法的返回值"+ object);
log.info("around ... after"); return object; } }
com.example.aop.MyAspect5 : around ... before com.example.aop.MyAspect5 : 目标对象的类名:com.example.service.impl.DeptServiceImpl com.example.aop.MyAspect5 : 目标方法的方法名select com.example.aop.MyAspect5 : 目标方法运行时传入的参数[1] com.example.aop.MyAspect5 : before ... com.example.aop.MyAspect5 : 目标方法的返回值[Dept(id=1, name=学工部, createTime=2023-11-30T13:55:55, updateTime=2023-11-30T13:55:55)] com.example.aop.MyAspect5 : around ... after
|
三、AOP案例
3.1. 分析
需求:将项目中的增、删、改、相关接口的操作日志记录到数据库表中
- 操作日志包含:操作人,操作时间,执行方法的全类名,执行方法名,方法运行时的参数,返回值,方法运行时长。
思路分析:
- 需要对方法添加统一的功能,使用AOP最方便,并且需要计算运行时长,所以使用 环绕通知
- 因为增删改的方法名没有规则,所以使用注解的方式写切入表达式
步骤:
- 准备:
- 案例中引入AOP的起步依赖
- 设计数据表结构,并且引入对应的实体类
- 编码:
- 自定义注解:@Log
- 定义切面类,完成记录操作日志的逻辑代码
3.2. 开始干活
3.2.1. 创建数据库:
1 2 3 4 5 6 7 8 9 10 11
| create table operate_log ( id int unsigned primary key auto_increment comment 'ID', operate_user int unsigned comment '操作人ID', operate_time datetime comment '操作时间', class_name varchar(100) comment '操作的类名', method_name varchar(100) comment '操作的方法名', method_params varchar(1000) comment '方法参数', return_value varchar(2000) comment '返回值', cost_time bigint comment '方法执行耗时, 单位:ms' ) comment '操作日志表';
|
3.2.2. 引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.7</version> </dependency>
|
3.2.3. 新建实体类
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; private Integer operateUser; private LocalDateTime operateTime; private String className; private String methodName; private String methodParams; private String returnValue; private Long costTime; }
|
3.2.4. 新建Mapper层
1 2 3 4 5 6 7
| @Mapper public interface OperateLogMapper { @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " + "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});") void insert(OperateLog log); }
|
3.2.5. 新建注解
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log { }
|
3.2.6. 定义切面类,完成记录操作日志的逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Component @Aspect @Slf4j public class LogAspect {
@Autowired private HttpServletRequest request;
@Autowired private OperateLogMapper operateLogMapper;
@Around("@annotation(com.example.anno.Log)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
Integer user = (Integer) JwtUtils.parseJWT(request.getHeader("token")).get("id"); LocalDateTime optionTime = LocalDateTime.now(); String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); String returnValue = JSONObject.toJSONString(result); long costTime = end - start; OperateLog operateLog = new OperateLog(null, user, optionTime, className, methodName, args, returnValue, costTime); operateLogMapper.insert(operateLog); log.info("AOP记录操作日志:{}", operateLog); return result; } }
|
3.2.7. 给需要记录的方法上面添加自定义的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Log @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id){ log.info("根据id删除部门:{}",id); deptService.delete(id); return Result.success(); }
@Log @PostMapping public Result add(@RequestBody Dept dept){ log.info("添加部门{}",dept); deptService.add(dept); return Result.success(); }
|
3.3. 查看结果
刚刚进行了部门的增删改以及员工的增删改操作,我们查看数据库,看有没有被记录。
1 2 3 4 5
| 1,1,2024-10-27 20:20:23,com.example.controller.DeptController,delete,[15],"{""code"":1,""msg"":""success""}",40 2,1,2024-10-27 20:20:45,com.example.controller.DeptController,add,"[Dept(id=null, name=测试部, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",5 3,1,2024-10-27 20:21:00,com.example.controller.EmpController,sava,"[Emp(id=null, username=测试, password=null, name=测试, gender=1, image=, job=1, entrydate=2024-10-20, deptId=16, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",6 4,1,2024-10-27 20:23:01,com.example.controller.DeptController,add,"[Dept(id=null, name=1, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",8 5,1,2024-10-27 20:23:18,com.example.controller.DeptController,delete,[17],"{""code"":1,""msg"":""success""}",12
|

完全符合要求!!!!!!