前言
前面写的项目还存在问题,就是删除一个部门的时候,部门下的员工也要一起删除,这个功能没有实现,使用MySQL的物理外键虽然可以解决,但是不够灵活,在某些业务场景要更改表的关联关系比较麻烦,使用逻辑外键的话又会出现代码异常,怎么处理这种情况呢?所以下面使用Springboot的事务管理来解决。最后还要使用SpringAOP实现用户操作日志的记录入数据库。
目录
一、事务回顾
在数据库阶段我们已学习过事务了,我们讲到:
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。
怎么样来控制这组操作,让这组操作同时成功或同时失败呢?此时就要涉及到事务的具体操作了。
事务的操作主要有三步:
开启事务(一组操作开始前,开启事务):start transaction / begin ;
提交事务(这组操作全部成功后,提交事务):commit ;
回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
二、Spring事务管理
1.案例
(1)需求
简单的回顾了事务的概念以及事务的基本操作之后,接下来我们看一个事务管理案例:解散部门 (解散部门就是删除部门)
需求:当部门解散了不仅需要把部门信息删除了,还需要把该部门下的员工数据也删除了。
步骤:
根据ID删除部门数据
根据部门ID删除该部门下的员工
(2)添加逻辑外键
DeptServiceImpl
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; //根据部门id,删除部门信息及部门下的所有员工 @Override public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); } }
DeptMapper
@Mapper public interface DeptMapper { /** * 根据id删除部门信息 * @param id 部门id */ @Delete("delete from dept where id = #{id}") void deleteById(Integer id); }
EmpMapper
@Mapper public interface EmpMapper { //根据部门id删除部门下所有员工 @Delete("delete from emp where dept_id=#{deptId}") public int deleteByDeptId(Integer deptId); }
测试完,删除部门,部门所属员工也跟着删除
(3)删除出现异常
修改DeptServiceImpl类中代码,添加可能出现异常的代码:
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; //根据部门id,删除部门信息及部门下的所有员工 @Override public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 int i = 1/0; //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); } }
以上程序出现的问题:即使程序运行抛出了异常,部门依然删除了,但是部门下的员工却没有删除,造成了数据的不一致。
2.原因分析
原因:
先执行根据id删除部门的操作,这步执行完毕,数据库表 dept 中的数据就已经删除了。
执行 1/0 操作,抛出异常
抛出异常之前,下面所有的代码都不会执行了,根据部门ID删除该部门下的员工,这个操作也不会执行 。
此时就出现问题了,部门删除了,部门下的员工还在,业务操作前后数据不一致。
而要想保证操作前后,数据的一致性,就需要让解散部门中涉及到的两个业务操作,要么全部成功,要么全部失败 。 那我们如何,让这两个操作要么全部成功,要么全部失败呢 ?
那就可以通过事务来实现,因为一个事务中的多个业务操作,要么全部成功,要么全部失败。
3.Transactional注解
@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
方法
当前方法交给spring进行事务管理
类
当前类中所有的方法都交由spring进行事务管理
接口
接口下所有的实现类当中所有的方法都交给spring 进行事务管理
接下来,我们就可以在业务方法delete上加上 @Transactional 来控制事务 。
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; @Override @Transactional //当前方法添加了事务管理 public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 int i = 1/0; //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); } }
添加Spring事务管理后,由于服务端程序引发了异常,所以事务进行回滚。部门并不会删除。
可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
#spring事务管理日志 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
三、事务进阶
前面我们通过spring事务管理注解@Transactional已经控制了业务层方法的事务。接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional注解当中的两个常见的属性:
异常回滚的属性:rollbackFor
事务传播行为:propagation
1.rollbackFor
下面是刚才的代码,没有给@Transactional添加rollbackFor属性是,默认是运行时异常,遇到运行时异常才会回滚。
@Transactional public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 int i = 1/0; //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); }
下面我们在做一个测试,我们修改业务功能代码,在模拟异常的位置上直接抛出Exception异常(编译时异常)
@Transactional public void delete(Integer id) throws Exception { //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 if(true){ throw new Exception("出现异常了~~~"); } //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); }
测试完发现事务没有回滚,只删除了部门。
解决方法,添加 rollbackFor属性指定异常
修改后代码
@Override @Transactional(rollbackFor=Exception.class) public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 if(true){ throw new Exception("出现异常了~~~"); } //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); }
2.propagation
(1)介绍
我们接着继续学习@Transactional注解当中的第二个属性propagation,这个属性是用来配置事务的传播行为的。
什么是事务的传播行为呢?
就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
例如:两个事务方法,一个A方法,一个B方法。在这两个方法上都添加了@Transactional注解,就代表这两个方法都具有事务,而在A方法当中又去调用了B方法。
所谓事务的传播行为,指的就是在A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候新建一个事务?这个就涉及到了事务的传播行为。
我们要想控制事务的传播行为,在@Transactional注解的后面指定一个属性propagation,通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。
属性值 含义 REQUIRED 【默认值】需要事务,有则加入,无则创建新事务 REQUIRES_NEW 需要新事务,无论有无,总是创建新事务 SUPPORTS 支持事务,有则加入,无则在无事务状态中运行 NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 MANDATORY 必须有事务,否则抛异常 NEVER 必须没事务,否则抛异常 … 对于这些事务传播行为,我们只需要关注以下两个就可以了:
REQUIRED(默认值)
REQUIRES_NEW
一般是被引用的那个事务方法需要添加这个属性
(2)使用案例
需求
解散部门时需要记录操作日志
由于解散部门是一个非常重要而且非常危险的操作,所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹,就是要记录操作日志。而且还要求无论是执行成功了还是执行失败了,都需要留下痕迹。
准备工作
1.创建数据库表 dept_log 日志表:
create table dept_log( id int auto_increment comment '主键ID' primary key, create_time datetime null comment '操作时间', description varchar(300) null comment '操作描述' )comment '部门操作日志表';
2.实体类:DeptLog
@Data @NoArgsConstructor @AllArgsConstructor public class DeptLog { private Integer id; private LocalDateTime createTime; private String description; }
3.Mapper接口:DeptLogMapper
@Mapper public interface DeptLogMapper { @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})") void insert(DeptLog log); }
4.业务接口:DeptLogService
public interface DeptLogService { void insert(DeptLog deptLog); }
5.业务实现类:DeptLogServiceImpl
@Service public class DeptLogServiceImpl implements DeptLogService { @Autowired private DeptLogMapper deptLogMapper; @Transactional //事务传播行为:有事务就加入、没有事务就新建事务 @Override public void insert(DeptLog deptLog) { deptLogMapper.insert(deptLog); } }
代码实现
业务实现类:DeptServiceImpl
@Slf4j @Service //@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制 public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; @Autowired private DeptLogService deptLogService; //根据部门id,删除部门信息及部门下的所有员工 @Override @Log @Transactional(rollbackFor = Exception.class) public void delete(Integer id) throws Exception { try { //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常 if(true){ throw new Exception("出现异常了~~~"); } //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); }finally { //不论是否有异常,最终都要执行的代码:记录日志 DeptLog deptLog = new DeptLog(); deptLog.setCreateTime(LocalDateTime.now()); deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门"); //调用其他业务类中的方法 deptLogService.insert(deptLog); } } //省略其他代码... }
在日志业务实现类DeptLogServiceImpl类中insert方法上,添加@Transactional(propagation = Propagation.REQUIRES_NEW)
@Service public class DeptLogServiceImpl implements DeptLogService { @Autowired private DeptLogMapper deptLogMapper; @Transactional(propagation = Propagation.REQUIRES_NEW)//事务传播行为:不论是否有事务,都新建事务 @Override public void insert(DeptLog deptLog) { deptLogMapper.insert(deptLog); } }
四、AOP基础
1.AOP概述
什么是AOP?
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
就是和我们之前所学习的动态代理技术是非常类似的。 我们所说的模板方法,其实就是代理对象中所定义的方法,那代理对象中的方法以及根据对应的业务需要, 完成了对应的业务功能,当运行原始业务方法时,就会运行代理对象中的方法,从而实现统计业务方法执行耗时的操作。
其实,AOP面向切面编程和OOP面向对象编程一样,它们都仅仅是一种编程思想,而动态代理技术是这种思想最主流的实现方式。而Spring的AOP是Spring框架的高级技术,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行编程(功能增强)。
来我们举个例子做一个说明:
前面项目中开发了很多的业务功能。
然而有一些业务功能执行效率比较低,执行耗时较长,我们需要针对于这些业务方法进行优化。 那首先第一步就需要定位出执行耗时比较长的业务方法,再针对于业务方法再来进行优化。
此时我们就需要统计当前这个项目当中每一个业务方法的执行耗时。那么统计每一个业务方法的执行耗时该怎么实现?
可能多数人首先想到的就是在每一个业务方法运行之前,记录这个方法运行的开始时间。在这个方法运行完毕之后,再来记录这个方法运行的结束时间。拿结束时间减去开始时间,不就是这个方法的执行耗时吗?
以上分析的实现方式是可以解决需求问题的。但是对于一个项目来讲,里面会包含很多的业务模块,每个业务模块又包含很多增删改查的方法,如果我们要在每一个模块下的业务方法中,添加记录开始时间、结束时间、计算执行耗时的代码,就会让程序员的工作变得非常繁琐。
而AOP面向方法编程,就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
而中间运行的原始业务方法,可能是其中的一个业务方法,比如:我们只想通过 部门管理的 list 方法的执行耗时,那就只有这一个方法是原始业务方法。 而如果,我们是先想统计所有部门管理的业务方法执行耗时,那此时,所有的部门管理的业务方法都是 原始业务方法。 那面向这样的指定的一个或多个方法进行编程,我们就称之为 面向切面编程。
AOP的优势:
减少重复代码
提高开发效率
维护方便
2.AOP快速入门
需求
统计各个业务层方法执行耗时。
实现步骤
导入依赖:在pom.xml中导入AOP的依赖
编写AOP程序:针对于特定方法根据业务需要进行编程
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
AOP程序:TimeAspect
@Around里面的生效范围后面会讲
@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; } }
测试
3.AOP核心概念
连接点:JoinPoint
可以被AOP控制的方法(暗含方法执行时的相关信息)
连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。
通知:Advice
指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
在入门程序中是需要统计各个业务方法的执行耗时的,此时我们就需要在这些业务方法运行开始之前,先记录这个方法运行的开始时间,在每一个业务方法运行结束的时候,再来记录这个方法运行的结束时间。
但是在AOP面向切面编程当中,我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。
通知有多种类型,由方法上的注解决定,@Around代表环绕通知后面进阶会讲
切入点:PointCut
匹配连接点的条件,通知仅会在切入点方法执行时被应用
在通知当中,我们所定义的共性功能到底要应用在哪些方法上?此时就涉及到了切入点pointcut概念。切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。
在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点(后面会有详解)。
切面:Aspect
描述通知与切入点的对应关系(通知+切入点)
当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
目标对象:Target
通知所应用的对象
目标对象指的就是通知所应用的对象,我们就称之为目标对象。
4. AOP执行流程
Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。CGLIB动态代理
五、AOP进阶
AOP的基础知识学习完之后,下面我们对AOP当中的各个细节进行详细的学习。主要分为4个部分:
通知类型
通知顺序
切入点表达式
连接点
1. 通知类型
Spring中AOP的通知类型:
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
测试代码
@Slf4j @Component @Aspect public class MyAspect1 { //前置通知 @Before("execution(* com.itheima.service.*.*(..))") public void before(JoinPoint joinPoint){ log.info("before ..."); } //环绕通知 @Around("execution(* com.itheima.service.*.*(..))") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before ..."); //调用目标对象的原始方法执行 Object result = proceedingJoinPoint.proceed(); //原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了 log.info("around after ..."); return result; } //后置通知 @After("execution(* com.itheima.service.*.*(..))") public void after(JoinPoint joinPoint){ log.info("after ..."); } //返回后通知(程序在正常执行的情况下,会执行的后置通知) @AfterReturning("execution(* com.itheima.service.*.*(..))") public void afterReturning(JoinPoint joinPoint){ log.info("afterReturning ..."); } //异常通知(程序在出现异常的情况下,执行的后置通知) @AfterThrowing("execution(* com.itheima.service.*.*(..))") public void afterThrowing(JoinPoint joinPoint){ log.info("afterThrowing ..."); } }
测试结果:
1.没有异常情况:
2.有异常情况(目标方法模拟异常):
2.@PointCut
我们发现啊,每一个注解里面都指定了切入点表达式,而且这些切入点表达式都一模一样。此时我们的代码当中就存在了大量的重复性的切入点表达式,假如此时切入点表达式需要变动,就需要将所有的切入点表达式一个一个的来改动,就变得非常繁琐了。
怎么来解决这个切入点表达式重复的问题? 答案就是:抽取
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
@Slf4j @Component @Aspect public class MyAspect1 { //切入点方法(公共的切入点表达式) @Pointcut("execution(* com.itheima.service.*.*(..))") private void pt(){ } //前置通知(引用切入点) @Before("pt()") public void before(JoinPoint joinPoint){ log.info("before ..."); } //环绕通知 @Around("pt()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before ..."); //调用目标对象的原始方法执行 Object result = proceedingJoinPoint.proceed(); //原始方法在执行时:发生异常 //后续代码不在执行 log.info("around after ..."); return result; } //后置通知 @After("pt()") public void after(JoinPoint joinPoint){ log.info("after ..."); } //返回后通知(程序在正常执行的情况下,会执行的后置通知) @AfterReturning("pt()") public void afterReturning(JoinPoint joinPoint){ log.info("afterReturning ..."); } //异常通知(程序在出现异常的情况下,执行的后置通知) @AfterThrowing("pt()") public void afterThrowing(JoinPoint joinPoint){ log.info("afterThrowing ..."); } }
3.通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
执行顺序
1.不同切面类中,默认按照切面类的类名字母排序
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行2.用@Order(数字)加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
4.切入点表达式
(1)execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带
?
的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例:
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))切入点表达式的书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用.,使用*匹配单个包。
(2)@annotation
已经学习了execution切入点表达式的语法。那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。
我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。
实现步骤:
编写自定义注解
在业务类要做为连接点的方法上添加自定义注解
自定义注解:MyLog
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { }
业务类:DeptServiceImpl
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override @MyLog //自定义注解(表示:当前方法属于目标方法) public List<Dept> list() { List<Dept> deptList = deptMapper.list(); //模拟异常 //int num = 10/0; return deptList; } @Override @MyLog //自定义注解(表示:当前方法属于目标方法) public void delete(Integer id) { //1. 删除部门 deptMapper.delete(id); } @Override public void save(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } @Override public Dept getById(Integer id) { return deptMapper.getById(id); } @Override public void update(Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept); } }
切面类
@Slf4j @Component @Aspect public class MyAspect6 { //针对list方法、delete方法进行前置通知和后置通知 //前置通知 @Before("@annotation(com.itheima.anno.MyLog)") public void before(){ log.info("MyAspect6 -> before ..."); } //后置通知 @After("@annotation(com.itheima.anno.MyLog)") public void after(){ log.info("MyAspect6 -> after ..."); } }
5.连接点
前面在讲解AOP核心概念的时候,我们提到过什么是连接点,连接点可以简单理解为可以被AOP控制的方法。
我们目标对象当中所有的方法是不是都是可以被AOP控制的方法。而在SpringAOP当中,连接点又特指方法的执行。
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
实例代码
@Slf4j @Component @Aspect public class MyAspect7 { @Pointcut("@annotation(com.itheima.anno.MyLog)") private void pt(){} //前置通知 @Before("pt()") public void before(JoinPoint joinPoint){ log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ..."); } //后置通知 @Before("pt()") public void after(JoinPoint joinPoint){ log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ..."); } //环绕通知 @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { //获取目标类名 String name = pjp.getTarget().getClass().getName(); log.info("目标类名:{}",name); //目标方法名 String methodName = pjp.getSignature().getName(); log.info("目标方法名:{}",methodName); //获取方法执行时需要的参数 Object[] args = pjp.getArgs(); log.info("目标方法参数:{}", Arrays.toString(args)); //执行原始方法 Object returnValue = pjp.proceed(); return returnValue; } }
运行结果
六、AOP案例
1.需求
需求:将智能辅助系统中增、删、改相关接口的操作日志记录到数据库表中
就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。
操作日志信息包含:
操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
所记录的日志信息包括当前接口的操作人是谁操作的,什么时间点操作的,以及访问的是哪个类当中的哪个方法,在访问这个方法的时候传入进来的参数是什么,访问这个方法最终拿到的返回值是什么,以及整个接口方法的运行时长是多长时间。
2.分析
问题1:项目当中增删改相关的方法是不是有很多?
很多
问题2:我们需要针对每一个功能接口方法进行修改,在每一个功能接口当中都来记录这些操作日志吗?
这种做法比较繁琐
以上两个问题的解决方案:可以使用AOP解决(每一个增删改功能接口中要实现的记录操作日志的逻辑代码是相同)。
可以把这部分记录操作日志的通用的、重复性的逻辑代码抽取出来定义在一个通知方法当中,我们通过AOP面向切面编程的方式,在不改动原始功能的基础上来对原始的功能进行增强。目前我们所增强的功能就是来记录操作日志,所以也可以使用AOP的技术来实现。使用AOP的技术来实现也是最为简单,最为方便的。
问题3:既然要基于AOP面向切面编程的方式来完成的功能,那么我们要使用 AOP五种通知类型当中的哪种通知类型?
答案:环绕通知
问题4:最后一个问题,切入点表达式我们该怎么写?
答案:使用annotation来描述表达式
3.步骤
简单分析了一下大概的实现思路后,接下来我们就要来完成案例了。案例的实现步骤其实就两步:
准备工作
引入AOP的起步依赖
导入资料中准备好的数据库表结构,并引入对应的实体类
编码实现
自定义注解@Log
定义切面类,完成记录操作日志的逻辑
4.准备工作
AOP起步依赖
<!--AOP起步依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
创建操作日志表结构
-- 操作日志表 create table operate_log( id int unsigned primary key auto_increment comment 'ID', operate_user int unsigned comment '操作人', 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 '操作日志表';
创建对应的实体类
//操作日志实体类 @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; //主键ID private Integer operateUser; //操作人ID private LocalDateTime operateTime; //操作时间 private String className; //操作类名 private String methodName; //操作方法名 private String methodParams; //操作方法参数 private String returnValue; //操作方法返回值 private Long costTime; //操作耗时 }
插入操作日志的Mapper接口
@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});") public void insert(OperateLog log); }
5.实现代码
自定义注解@Log
/** * 自定义Log注解 */ @Target({ElementType.METHOD}) @Documented @Retention(RetentionPolicy.RUNTIME) public @interface Log { }
定义切面类,完成记录操作日志的逻辑
@Slf4j @Component @Aspect //切面类 public class LogAspect { @Autowired private HttpServletRequest request; @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(com.itheima.anno.Log)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { //操作人ID - 当前登录员工ID //获取请求头中的jwt令牌, 解析令牌 String jwt = request.getHeader("token"); Claims claims = JwtUtils.parseJWT(jwt); Integer operateUser = (Integer) claims.get("id"); //操作时间 LocalDateTime operateTime = LocalDateTime.now(); //操作类名 String className = joinPoint.getTarget().getClass().getName(); //操作方法名 String methodName = joinPoint.getSignature().getName(); //操作方法参数 Object[] args = joinPoint.getArgs(); String methodParams = Arrays.toString(args); long begin = System.currentTimeMillis(); //调用原始目标方法运行 Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); //方法返回值 String returnValue = JSONObject.toJSONString(result); //操作耗时 Long costTime = end - begin; //记录操作日志 OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime); operateLogMapper.insert(operateLog); log.info("AOP记录操作日志: {}" , operateLog); return result; } }
修改业务实现类,在增删改业务方法上添加@Log注解
@Slf4j @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override @Log public void update(Emp emp) { emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间 empMapper.update(emp); } @Override @Log public void save(Emp emp) { //补全数据 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); //调用添加方法 empMapper.insert(emp); } @Override @Log public void delete(List<Integer> ids) { empMapper.delete(ids); } //省略其他代码... }
以同样的方式,修改EmpServiceImpl业务类