建行网站查询密码是什么东西,手机浏览器下载网页视频,网站运营的发展方向,广西宏泰成建设集团网站事务管理 在学习数据库时#xff0c;讲到#xff1a;事务是一组操作的集合#xff0c;它是一个不可分割的工作单位。事务会把所有的操作作为一个整体#xff0c;一起向数据库提交或者是撤销操作请求#xff0c;要么同时成功#xff0c;要么同时失败。
事务的操作主要有三…事务管理 在学习数据库时讲到事务是一组操作的集合它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向数据库提交或者是撤销操作请求要么同时成功要么同时失败。
事务的操作主要有三步1. 开启事务一组操作开始前开启事务start transaction / begin ;2. 提交事务这组操作全部成功后提交事务commit ;3. 回滚事务中间任何一个操作出现异常回滚事务rollback ;Spring事务管理
解散部门案例 解散部门就是删除部门 需求部门解散不仅要把部门信息删除该部门下的员工数据也要删除。
步骤根据ID删除部门数据根据部门ID删除该部门下的员工先执行根据id删除部门的操作这步执行完毕数据库表 dept 中的数据就已经删除了。在抛出异常抛出异常之后根据部门ID删除该部门下的员工这个操作也不会执行 。此时就出现问题了部门删除了部门下的员工还在业务操作前后数据不一致。而要想保证操作前后数据的一致性就需要在delete删除业务功能中添加事务让解散部门中涉及到的两个业务操作要么全部成功要么全部失败 。 在spring框架当中已经把事务控制的代码都已经封装好了并不需要人为手动实现只需要通过注解Transactional就搞定了。
Transactional注解 Transactional作用在当前这个方法执行开始之前来开启事务方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常就会进行事务的回滚操作。 Transactional注解一般会在业务层当中来控制事务因为在业务层当中一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务就可以将多个数据访问操作控制在一个事务范围内。
Transactional注解书写位置方法当前方法交给spring进行事务管理类当前类中所有的方法都交由spring进行事务管理接口接口下所有的实现类当中所有的方法都交给spring 进行事务管理代码实现
DeptServiceImpl
Override
Transactional //当前方法添加了事务管理
public void deptDeleteByID(Integer id) {//调用deptMapper,进行数据库删除deptMapper.deptDeleteByID(id);//删除部门下的所有员工信息empMapper.deleteByDeptId(id);
}//根据部门id删除部门下所有员工
Delete(delete from empf where dept_id#{deptId})
public int deleteByDeptId(Integer deptId);可以在application.yml配置文件中开启事务管理日志这样就可以在控制看到和事务相关的日志信息了
#spring事务管理日志
logging:level:org.springframework.jdbc.support.JdbcTransactionManager: debugTransactional详解 接下来详细的介绍一下Transactional事务管理注解的使用细节。这里主要介绍Transactional注解当中的两个常见的属性
1. 异常回滚的属性rollbackFor
2. 事务传播行为propagationrollbackFor属性 默认情况下只有出现RuntimeException(运行时异常)才会回滚事务。如果想让所有的异常都回滚需要来配置Transactional注解当中的rollbackFor属性通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
Override
Transactional(rollbackForException.class) //当前方法添加了事务管理
public void deptDeleteByID(Integer id) {//调用deptMapper,进行数据库删除deptMapper.deptDeleteByID(id);//删除部门下的所有员工信息empMapper.deleteByDeptId(i);
}propagation属性 propagation属性是用来配置事务的传播行为的。 事务的传播行为就是当一个事务方法被另一个事务方法调用时这个事务方法应该如何进行事务控制。例如两个事务方法一个A方法一个B方法。在这两个方法上都添加了Transactional注解就代表这两个方法都具有事务而在A方法当中又去调用了B方法。所谓事务的传播行为指的就是在A方法运行的时候首先会开启一个事务在A方法当中又调用了B方法 B方法自身也具有事务那么B方法在运行的时候到底是加入到A方法的事务当中来还是B方法在运行的时候新建一个事务这个就涉及到了事务的传播行为。 想控制事务的传播行为在Transactional注解的后面指定一个属性propagation通过propagation 属性来指定传播行为。常见的事务传播行为
属性值含义REQUIRED【默认值】需要事务有则加入无则创建新事务REQUIRES_NEW需要新事务无论有无总是创建新事务SUPPORTS支持事务有则加入无则在无事务状态中运行NOT_SUPPORTED不支持事务在无事务状态下运行,如果当前存在已有事务,则挂起当前事务MANDATORY必须有事务否则抛异常NEVER必须没事务否则抛异常
对于这些事务传播行为我们只需要关注以下两个就可以了1. REQUIRED默认值2. REQUIRES_NEW接下来通过解散部门案例来演示事务传播行为propagation属性的使用。 需求解散部门时需要记录操作日志。由于解散部门是一个非常重要而且非常危险的操作所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹就是要记录操作日志。而且还要求无论是执行成功还是执行失败都需要留下痕迹。
步骤1. 执行解散部门的业务先删除部门再删除部门下的员工前面已实现2. 记录解散部门的日志到日志表未实现创建数据库表 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 部门操作日志表;引入实体类DeptLog
Data
NoArgsConstructor
AllArgsConstructor
public class DeptLog {private Integer id;private LocalDateTime createTime;private String description;
}引入Mapper接口DeptLogMapper
Mapper
public interface DeptLogMapper {Insert(insert into dept_log(create_time,description) values(#{createTime},#{description}))void insert(DeptLog log);
}引入业务接口DeptLogService
public interface DeptLogService {void insert(DeptLog deptLog);
}业务实现类DeptLogServiceImpl
Service
public class DeptLogServiceImpl implements DeptLogService {Autowiredprivate DeptLogMapper deptLogMapper;Transactional(propagation Propagation.REQUIRES_NEW) //事务传播行为有事务就加入、没有事务就新建事务Overridepublic void insert(DeptLog deptLog) {deptLogMapper.insert(deptLog);}
}业务实现类DeptServiceImpl
Override
Transactional(rollbackForException.class) //当前方法添加了事务管理
public void deptDeleteByID(Integer id) {try {//调用deptMapper,进行数据库删除deptMapper.deptDeleteByID(id);//int i1/0;//删除部门下的所有员工信息empMapper.deleteByDeptId(i);} finally {DeptLog deptLog new DeptLog();deptLog.setCreateTime (LocalDateTime.now());deptLog.setDescription(执行了惟部门的操作北次解散的是id号部门);deptLogService.insert(deptLog);}
}事务的传播行为我们只需要掌握两个REQUIRED、REQUIRES_NEW。
REQUIRED 大部分情况下都是用该传播行为即可。
REQUIRES_NEW 当我们不希望事务之间相互影响时可以使用该传播行为。比如下订单前需要记录日志不论订单保存成功与否都需要保证日志记录能够记录成功。AOP AOP也是spring框架的第二大核心。
AOP基础
AOP概述 AOP英文全称Aspect Oriented Programming面向切面编程、面向方面编程。其实面向切面编程就是面向特定方法编程。 那什么又是面向方法编程呢为什么又需要面向方法编程呢来我们举个例子做一个说明比如一个项目中开发了很多的业务功能。 然而有一些业务功能执行效率比较低执行耗时较长需要针对于这些业务方法进行优化。 首先第一步就需要定位出执行耗时比较长的业务方法再针对于业务方法再来进行优化。此时就需要统计当前项目中每一个业务方法的执行耗时。那么统计每一个业务方法的执行耗时该怎么实现可能多数人首先想到的就是在每一个业务方法运行之前记录这个方法运行的开始时间。在这个方法运行完毕之后再来记录这个方法运行的结束时间。拿结束时间减去开始时间不就是这个方法的执行耗 时吗但是对于一个项目来讲里面会包含很多的业务模块每个业务模块又包含很多增删改查的方法如果要在每一个模块下的业务方法中添加记录开始时间、结束时间、计算执行耗时的代码就会让程序员的工作变得非常繁琐。而AOP面向方法编程就可以做到在不改动这些原始方法的基础上针对特定的方法进行功能的增强。AOP的作用在程序运行期间在不修改源代码的基础上对已有方法进行增强无侵入性: 解耦。要想完成统计各个业务方法执行耗时的需求只需要定义一个模板方法将记录方法执行耗时这一部分公共的逻辑代码定义在模板方法当中在这个方法开始运行之前来记录这个方法运行的开始时间在方法结束运行的时候再来记录方法运行的结束时间中间就来运行原始的业务方法。 而中间运行的原始业务方法可能是其中的一个业务方法也可能是其中的多个业务方法。 那面向这样的指定的一个或多个方法进行编程我们就称之为 面向切面编程。 和动态代理技术是非常类似的。所说的模板方法其实就是代理对象中所定义的方法那代理对象中的方法以及根据对应的业务需要 完成了对应的业务功能当运行原始业务方法时就会运行代理对象中的方法从而实现统计业务方法执行耗时的操作。 其实AOP面向切面编程和OOP面向对象编程一样它们都仅仅是一种编程思想而动态代理技术是这种思想最主流的实现方式。而Spring的AOP是Spring框架的高级技术旨在管理bean对象的过程中底层使用动态代理机制对特定的方法进行编程(功能增强)。
AOP的优势1. 减少重复代码2. 提高开发效率3. 维护方便AOP快速入门案例
需求统计各个业务层方法执行耗时。
实现步骤1. 导入依赖在pom.xml中导入AOP的依赖2. 编写AOP程序针对于特定方法根据业务需要进行编程pom.xml导入AOP的依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId
/dependencyAOP程序TimeAspect Component
Aspect // AOP类
Slf4j
public class TimeAspect {// 指定作用域com.example包下的service业务层的.所有类*.所有方法*(..))Around(execution(* com.example.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;}
}
AOP常见的应用场景如下记录系统的操作日志权限控制 事务管理我们前面所讲解的Spring事务管理底层其实也是通过AOP来实现的只要添加Transactional注解之后AOP程序自动会在原始方法运行前先来开启事务在原始方法运行完毕之后提交或回滚事务AOP面向切面编程的优势代码无侵入没有修改原始的业务方法就已经对原始的业务方法进行了功能的增强或者是功能的改变减少了重复代码提高开发效率维护方便AOP核心概念 下面学习AOP当中涉及到的一些核心概念。
连接点JoinPoint 连接点JoinPoint可以被AOP控制的方法暗含方法执行时的相关信息连接点指的是可以被aop控制的方法。例如入门程序当中所有的业务方法都是可以被aop控制的方法。 通知Advice 通知Advice指重复的逻辑也就是共性功能最终体现为一个方法。在统计各个业务方法的执行耗时时就需要在业务方法运行开始之前记录运行的开始时间和结束时间。在AOP面向切面编程当中只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑也就是共性的功能。 切入点PointCut 切入点PointCut匹配连接点的条件。所定义的共性功能到底要应用在哪些方法上此时就涉及到了切入点pointcut概念通知仅会在切入点方法运行时才会被应用。在aop的开发当中我们通常会通过一个切入点表达式来描述切入点(后面会有详解)。 假如切入点表达式改为DeptServiceImpl.list()此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。 切面Aspect 切面Aspect描述通知与切入点的对应关系通知切入点。当通知和切入点结合在一起就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法在什么时候执行什么样的操作。切面所在的类我们一般称为切面类被Aspect注解标识的类 目标对象Target 目标对象Target通知所应用的对象。目标对象指的就是通知所应用的对象我们就称之为目标对象。 AOP的核心概念介绍完毕之后接下来再来分析一下所定义的通知是如何与目标对象结合在一起对目标对象当中的方法进行功能增强的。 Spring的AOP底层是基于动态代理技术来实现的也就是说在程序运行的时候会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。
AOP进阶
通知类型 在入门程序当中已经使用了一种功能最为强大的通知类型Around环绕通知。只要在通知方法上加上了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;
}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 ...);}
}在使用通知时的注意事项Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行其他通知不需要考虑目标方法执行。Around环绕通知方法的返回值必须指定为Object来接收原始方法的返回值否则原始方法执行完毕是获取不到返回值的。在上面代码中每一个注解里面都指定了切入点表达式而且这些切入点表达式一样。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 ...);}
}需要注意的是当切入点方法使用private修饰时仅能在当前切面类中引用该表达式 当外部其他切面类中也要引用当前类中的切入点表达式就需要把private改为public而在引用的时候具体的语法为全类名.方法名()具体形式如下
Slf4j
Component
Aspect
public class MyAspect2 {//引用MyAspect1切面类中的切入点表达式Before(com.itheima.aspect.MyAspect1.pt())public void before(){log.info(MyAspect2 - before ...);}
}通知顺序 在项目开发当中定义了多个切面类而多个切面类中多个切入点都匹配到了同一个目标方法那多个切面类当中的通知方法都会运行那么多个通知方法的运行顺序是怎样的
在不同切面类中默认按照切面类的类名字母排序目标方法前的通知方法字母排名靠前的先执行目标方法后的通知方法字母排名靠前的后执行控制通知的执行顺序有两种方式1. 修改切面类的类名这种方式非常繁琐、而且不便管理2. 使用Spring提供的Order注解通知的执行顺序主要知道两点即可1. 不同的切面类当中默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的2. 可以在切面类上面加上Order注解来控制不同的切面类通知的执行顺序切入点表达式
切入点表达式描述切入点方法的一种表达式
作用主要用来决定项目中的哪些方法需要加入通知
常见形式1. execution(……)根据方法的签名来匹配2. annotation(……) 根据注解匹配execution execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配语法为
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)其中带 ? 的表示可以省略的部分
访问修饰符可省略比如: public、protected
包名.类名 可省略
throws 异常可省略注意是方法上声明抛出的异常不是实际抛出的异常示例Before(execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)))可以使用通配符描述切入点* 单个独立的任意符号可以通配任意返回值、包名、类名、方法名、任意类型的一个参数也可以通配包、类、方法名的一部分* *多个连续的任意符号可以通配任意层级的包或任意类型、任意个数的参数切入点表达式的语法规则1. 方法的访问修饰符可以省略2. 返回值可以使用 * 号代替任意返回值类型3. 包名可以使用 * 号代替代表任意包一层包使用一个 * 4. 使用 .. 配置包名标识此包以及此包下的所有子包5. 类名可以使用 * 号代替标识任意类6. 方法名可以使用 * 号代替表示任意方法7. 可以使用 * 配置参数一个任意类型的参数8. 可以使用 .. 配置参数任意个任意类型的参数切入点表达式示例
省略方法的修饰符号
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))使用 * 代替返回值类型
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))使用 * 代替包名一层包使用一个 *
execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))使用 .. 省略包名
execution(* com..DeptServiceImpl.delete(java.lang.Integer))使用 * 代替类名
execution(* com..*.delete(java.lang.Integer))使用 * 代替方法名
execution(* com..*.*(java.lang.Integer))使用 * 代替参数
execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))使用 .. 省略参数
execution(* com..*.*(..))注意事项根据业务需要可以使用 且、或||、非! 来组合比较复杂的切入点表达式。execution(* com.itheima.service.DeptService.list(..)) ||execution(* com.itheima.service.DeptService.delete(..))切入点表达式的书写建议 所有业务方法名在命名时尽量规范方便切入点表达式快速匹配。如查询类方法都是 find 开头更新类方法都是update开头 描述切入点方法通常基于接口描述而不是直接描述实现类增强拓展性
execution(* com.itheima.service.DeptService.*(..))在满足业务需要的前提下尽量缩小切入点的匹配范围。如包名匹配尽量不使用 …使用 * 匹配单个包
execution(* com.itheima.*.*.DeptServiceImpl.find*(..))annotation 如果要匹配多个无规则的方法比如list()和 delete()这两个方法。这个时候基于execution这种切入点表达式来描述就不是很方便了。而在之前是将两个切入点表达式组合在了一起完成的需求这个是比较繁琐的。因此可以借助于另一种切入点表达式annotation来描述这一类的切入点从而来简化切入点表达式的书写。
实现步骤1. 编写自定义注解2. 在业务类要做为连接点的方法上添加自定义注解自定义注解MyLog
Target(ElementType.METHOD) // 作用于方法
Retention(RetentionPolicy.RUNTIME) // 运行时生效
public interface MyLog {
}业务类DeptServiceImpl
Slf4j
Service
public class DeptServiceImpl implements DeptService {
Autowired
private DeptMapper deptMapper;OverrideMyLog //自定义注解表示当前方法属于目标方法public ListDept list() {ListDept deptList deptMapper.list();//模拟异常//int num 10/0;return deptList;}OverrideMyLog //自定义注解表示当前方法属于目标方法public void delete(Integer id) {//1. 删除部门deptMapper.delete(id);}Overridepublic void save(Dept dept) {dept.setCreateTime(LocalDateTime.now());dept.setUpdateTime(LocalDateTime.now());deptMapper.save(dept);}Overridepublic Dept getById(Integer id) {return deptMapper.getById(id);}Overridepublic 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 ...);}
}总结
execution切入点表达式根据我们所指定的方法的描述信息来匹配切入点方法这种方式也是最为常用的一种方式如果我们要匹配的切入点方法的方法名不规则或者有一些比较特殊的需求通过execution切入点表达式描述比较繁琐annotation 切入点表达式基于注解的方式来匹配切入点方法。这种方式虽然多一步操作我们需要自定义一个注解但是相对来比较灵活。连接点 连接点可以简单理解为可以被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案例
需求 需求将案例中增、删、改相关接口的操作日志记录到数据库表中。就是当访问部门管理和员工管理当中的增、删、改相关功能接口时需要详细的操作日志并保存在数据表中便于后期数据追踪。 操作日志信息包含操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。所记录的日志信息包括当前接口的操作人是谁操作的什么时间点操作的以及访问的是哪个类当中的哪个方法在访问这个方法的时候传入进来的参数是什么访问这个方法最终拿到的返回值是什么以及整个接口方法的运行时长是多长时间。
分析 由于项目当中增删改相关的方法很多如果针对每一个功能接口方法进行修改在每一个功能接口当中都来记录这些操作日志这种做法比较繁琐。可以使用AOP解决(每一个增删改功能接口中要实现的记录操作日志的逻辑代码是相同)。即可以把这部分记录操作日志的通用的、重复性的逻辑代码抽取出来定义在一个通知方法当中通过AOP面向切面编程的方式在不改动原始功能的基础上来对原始的功能进行增强。 要基于AOP面向切面编程的方式来完成的功能则使用 AOP五种通知类型当中的环绕通知最合适。因为所记录的操作日志当中包括操作人、操作时间访问的是哪个类、哪个方法、方法运行时参数、方法的返回值、方法的运行时长。方法返回值是在原始方法执行后才能获取到的。方法的运行时长需要原始方法运行之前记录开始时间原始方法运行之后记录结束时间。通过计算获得方法的执行耗时。基于以上的分析可知应该使用Around环绕通知。 切入点表达式该怎么写要匹配业务接口当中所有的增删改的方法而增删改方法在命名上没有共同的前缀或后缀。此时如果使用execution切入点表达式也可以但是会比较繁琐。 当遇到增删改的方法名没有规律时就可以使用 annotation切入点表达式使用annotation来描述表达式更合适。
步骤
实现步骤其实就两步
准备工作1. 引入AOP的起步依赖2. 导入数据库表结构并引入对应的实体类编码实现1. 自定义注解Log2. 定义切面类完成记录操作日志的逻辑实现
AOP起步依赖
!--AOP起步依赖--
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-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; //主键IDprivate Integer operateUser; //操作人IDprivate 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);
}自定义注解Log
/**
* 自定义Log注解
*/
Target({ElementType.METHOD})
Documented
Retention(RetentionPolicy.RUNTIME)
public interface Log {
}定义切面类完成记录操作日志的逻辑
Slf4j
Component
Aspect //切面类
public class LogAspect {Autowiredprivate HttpServletRequest request;Autowiredprivate 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;}
}修改Controlle在增删改业务方法上添加Log注解