目录
一、完善解散部门功能
二、spring 事务
(1)@Transactional 事务管理
① rollbackFor 控制异常类型
② propagation 事务传播控制
1、定义解散部门操作日记
三、AOP基础
1、概述
2、快速入门
(1)案例:统计各个业务层方法的执行耗时
① 引入AOP依赖
② 建立AOP类
3、AOP核心概念
(1)AOP的执行流程
四、AOP进阶
1、通知类型
(1)@PointCut 公共切点表达式
2、通知顺序
3、切入点表达式
(1)execution
(2)@annotation
4、连接点
五、AOP案例
(1)引入AOP依赖
(2)在数据库里建操作日记记录表
(3)定义数据库表对应的实体类
(4)定义对应的Mapper接口
(5)定义注解
(6)完成AOP类编写
(7)给需要匹配的增删改方法加上注解
一、完善解散部门功能
删除部门时,应该把部门下相应的员工也一并删除
注意:数据库不推荐物理外键,一般都是逻辑外键
step 1:改写Dept的Service层
@Overridepublic void delete(Integer id) {deptMapper.deleteById(id); //根据部门id删除部门empMapper.deleteByDeptId(id); //根据部门id删除该部门下对应的员工}
step 2:完善Emp的mapper层
//根据部门id删除对应员工@Delete("delete from emp where dept_id = #{deptId}")void deleteByDeptId(Integer deptId);
二、spring 事务
当中间出现异常时,会出现,异常前的语句执行了,但是异常后的语句没有成功执行,会出现bug
因此我们需要进行事务回滚(事务回滚指的是当发生错误或异常时,事务能够自动地撤销已经执行的操作,返回到事务开始之前的状态)
(1)@Transactional 事务管理
① rollbackFor 控制异常类型
- 作用:控制出现何种异常类型,事务回滚
- 默认情况下,只有出现RuntimeException才会回滚异常
@Transactional(rollbackFor = Exception.class)@Overridepublic void delete(Integer id) {deptMapper.deleteById(id); //根据部门id删除部门empMapper.deleteByDeptId(id); //根据部门id删除该部门下对应的员工}
② propagation 事务传播控制
作用:当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
属性值 | 含义 |
REQUIRED(默认值) | 需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无事务,总是创建新事务, 当我们不希望事务相互影响时使用 |
这里我们运用一个案例进行详细说明:
要求解散部门时,无论解散成功or失败,都要记录操作日志
1、定义解散部门操作日记
(1)DeptLog实体类
// 解散部门日志 @Data //@Data注解的主要作用是提高代码的简洁,使用这个注解可以省去实体类中大量的get()、 set()、 toString()等方法 @NoArgsConstructor @AllArgsConstructor public class DeptLog {private Integer id;private LocalDateTime createTime;private String description; }
(2)DeptLogService
public interface DeptLogService {//插入日志void insert(DeptLog deptLog); }
public class DeptLogServiceImpl implements DeptLogService {@Autowiredprivate DeptLogMapper deptLogMapper;@Transactional@Overridepublic void insert(DeptLog deptLog) {deptLogMapper.insert(deptLog);} }
(3)DeptLogMapper
@Mapper public interface DeptLogMapper {@Insert("insert into dept_log(create_time,description) values (#{createTime},#{description})")void insert(DeptLog deptLog); }
- 因为若不指定propagation的值,默认为REQUIRED,即为若需要新事务,则无需再创建,直接加入已有事务,也就是insert方法加入到delete方法的事务中
- 此时若delete事务出现异常,整个事务发生回滚,因此也不会有日志记录
- 因此我们需要在insert上指定propagation的值为REQUIRES_NEW,即若需要新事务,则再开一个新事务,当delete事务出现异常时,只在delete事务中发生回滚,insert事务正常运行,日志正常记录
@Transactional(rollbackFor = Exception.class)@Overridepublic void delete(Integer id) {try{deptMapper.deleteById(id); //根据部门id删除部门empMapper.deleteByDeptId(id); //根据部门id删除该部门下对应的员工}finally {DeptLog deptLog = new DeptLog();deptLog.setCreateTime(LocalDateTime.now());deptLog.setDescription("本次解散的是"+id+"号部门");deptLogService.insert(deptLog);}}
@Transactional(propagation = Propagation.REQUIRES_NEW)@Overridepublic void insert(DeptLog deptLog) {deptLogMapper.insert(deptLog);}
三、AOP基础
1、概述
开发中在多个模块间有某段重复的代码,我们通常是怎么处理的?在传统的面向过程编程中,我们也会将这段代码,抽象成一个方法,然后在需要的地方分别调用这个方法,这样当这段代码需要修改时,我们只需要改变这个方法就可以了。然而需求总是变化的,有一天,新增了一个需求,需要再多出做修改,我们需要再抽象出一个方法,然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们还是得删除掉每一处调用该方法的地方。实际上涉及到多个地方具有相同的修改的问题我们都可以通过 AOP 来解决
AOP 的主要作用就是在不侵入原有程序的基础上实现对原有功能的增强
2、快速入门
(1)案例:统计各个业务层方法的执行耗时
① 引入AOP依赖
<!--AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
② 建立AOP类
@Slf4j @Component @Aspect // AOP类 public class TimeAspect {@Around("execution(* com.itroye.service.*.*(..))") //切入点表达式public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {//记录开始时间long begin = System.currentTimeMillis();//调用原始方法Object result = joinPoint.proceed();//记录结束时间long end = System.currentTimeMillis();log.info(joinPoint.getSignature()+"执行耗时:{}ms",end-begin);return result;} }
3、AOP核心概念
- 连接点 JoinPoint:可以被AOP控制的方法
- 通知 Advice:指重复的逻辑,也就是共性功能
- 切入点 PointCut:匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面 Aspect:通知+切入点
- 目标对象 Target:通知所应用的对象
(1)AOP的执行流程
运行的不是原始的目标对象,而是基于目标对象所生成的代理对象
四、AOP进阶
1、通知类型
- 前置通知(@Before):在目标方法执行前执行的通知。
- 后置通知(@After):在目标方法执行后执行的通知,无论目标方法是否抛出异常都会执行。
- 返回通知(@AfterReturning):在目标方法正常返回后执行的通知。
- 异常通知(@AfterThrowing):在目标方法抛出异常后执行的通知。
- 环绕通知(@Around):在目标方法执行前后都可以执行的通知,可以控制目标方法的执行。
注意:
- @Around 需要调用ProceedingJoinPoint.proceed()让原始方法执行,其他通知不需要目标方法执行
- @Around 的返回值必须是Object,来接收原始方法的返回值
(1)@PointCut 公共切点表达式
@Slf4j
@Component
@Aspect
public class MyAspect1 {@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")public void pt(){}@Before("pt()")public void before(){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(){log.info("after ...");}@AfterReturning("pt()")public void afterReturning(){log.info("afterReturning ...");}@AfterThrowing("pt()")public void afterThrowing(){log.info("afterThrowing ...");}
}
2、通知顺序
3、切入点表达式
(1)execution
(2)@annotation
用于匹配标识有特定注解的方法
新建注解
@Retention(RetentionPolicy.RUNTIME) //运行时机 @Target(ElementType.METHOD) //该注解可以定义在方法上 public @interface MyLog { }
在需要切入的切入点方法上加上该注解
然后在切面处 @annotation(注解全类名),即可匹配拥有该注解的方法
4、连接点
连接点就是可以被AOP控制的方法
- 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
- 对于@Around通知,获取连接点信息只能用ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能用JoinPoint,它是ProceedingJoinPoint的父类型
五、AOP案例
思路分析:
- 需要对所有Service的增删改方法添加统一功能,使用AOP技术 运用@Around环绕通知
- 由于增删改方法名无规律,自定义@Log注解完成目标方法匹配
(1)引入AOP依赖
<!--AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
(2)在数据库里建操作日记记录表
-- 操作日志表
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)定义数据库表对应的实体类
@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; //操作耗时
}
(4)定义对应的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)定义注解
@Retention(RetentionPolicy.RUNTIME) //运行时机
@Target(ElementType.METHOD) //该注解可以定义在方法上
public @interface Log {
}
(6)完成AOP类编写
@Slf4j
@Component
@Aspect //切面类
public class LogAspect {@Autowiredprivate HttpServletRequest request;@Autowiredprivate OperateLogMapper operateLogMapper;@Around("@annotation(com.itroye.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;}}
(7)给需要匹配的增删改方法加上注解
这里是给Controller层加的注释!
保证返回值都是Result