当部门解散了不仅需要把部门信息删除了,还需要把该部门下的员工数据也删除了。
记录解散部门的日志到日志表
考虑到删除过程中可能会出现异常,使用Spring框架的事务管理功能实现解散部门功能
无论解散部门是否成功都应该记录日志,故记录日志应开启新事务
1.根据id删除部门
2.根据dept_id删除员工
3.为解散部门方法添加事务
4.记录解散部门日志到日志表
package com.itheima.service.impl;
import com.itheima.anno.MyLog;
import com.itheima.mapper.DeptMapper;
import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Dept;
import com.itheima.pojo.DeptLog;
import com.itheima.service.DeptLogService;
import com.itheima.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@Slf4j
//@Transactional//添加在类上表示为该类的所有方法添加事务,
// 添加在接口上表示为该接口的所有实现类的所有方法添加事务
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
@Override
//在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
@Transactional(rollbackFor = Exception.class)//为该方法开启事务,指定任何异常都回滚事务
public void deleteById(Integer id) throws Exception {
try {
//调用mapper层的deleteById方法
deptMapper.deleteById(id);
// int i = 1/0;//模拟出现异常
//模拟非运行时异常抛出,若不使用rollbackFor属性指定则事务不会因为此异常回滚
/* if (true){
throw new Exception("出错了");
}*/
//删除该部门员工
empMapper.deleteByDeptId(id);
} finally {
//无论是否成功都要记录部门日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是:"+id+"号部门");
//开启了新的事务
deptLogService.insert(deptLog);
}
}
}
package com.itheima.service;
import com.itheima.pojo.DeptLog;
public interface DeptLogService {
/**
* 添加部门操作日志
* @param deptLog
*/
void insert(DeptLog deptLog);
}
package com.itheima.service.impl;
import com.itheima.mapper.DeptLogMapper;
import com.itheima.pojo.DeptLog;
import com.itheima.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* 部门日志记录
* propagation属性默认为REQUIRED:即需要事务,有则加入,无则创建新的事务
* REQUIRES_NEW:需要新的事务,无论有无,总是创建事务
* 当本方法被其他事务方法调用时,挂起其他事务,为本方法创建新的事务并执行完毕后,再执行其他事务
*/
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Override
//配置propagation属性,设置为当前方法开启新的事务,避免加入其它事务中一起回滚
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
@Mapper
public interface DeptMapper {
/**
* 根据id删除部门信息
* @param id 部门id
*/
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
}
@Mapper
public interface EmpMapper {
//根据部门id删除部门下所有员工
@Delete("delete from emp where dept_id=#{deptId}")
public int deleteByDeptId(Integer deptId);
}
package com.itheima.mapper;
import com.itheima.pojo.DeptLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeptLogMapper {
/**
* 添加解散部门日志
* @param deptLog
*/
@Insert("insert into dept_log(create_time, description) values (#{createTime},#{description})")
void insert(DeptLog deptLog);
}
create table dept_log(
id int auto_increment comment '主键ID' primary key,
create_time datetime null comment '操作时间',
description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
统计各个业务层方法执行耗时。
1.导入AOP依赖
2.编写AOP程序,统计各个业务层方法耗时
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect//切面注解,表示该类为切面类
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")//切入点表达式,指定目标对象为service包下所有方法
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录开始时间
long start = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录结束时间,计算方法耗时
long end = System.currentTimeMillis();
log.info(pjp.getSignature()+"执行耗时:{}ms",end-start);
return result;
}
}
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* Spring中的AOP通知Advice
* 通知指的是为目标方法所编写的重复代码,这些代码被抽取成了一个方法
* 通知类型如下:
* @Before ,@Around ,@After ,@AfterReturning ,@AfterThrowing
*/
@Slf4j
@Component
@Order(2)
//@Aspect//切面=通知+切入点
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
private 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");
}
}
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* Spring中的AOP通知顺序
* 1. 不同的切面类当中,默认情况下通知的执行顺序是根据切面类的类名字母顺序
* 2. 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序
*/
@Slf4j
@Component
//@Aspect注释掉此注解,类中通知将不再生效
@Order(1)//切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
private 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");
}
}
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* Spring中的AOP连接点JoinPoint
* 连接点指的是可以被AOP控制的方法
*/
@Slf4j
@Component
//@Aspect
public class MyAspect3 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
private void pt(){}
//环绕通知,在切入方法执行前后都执行
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before...");
/*
仅在环绕通知中,获取连接点信息使用的是ProceedingJoinPoint类型,
在其他通知类型中,获取连接信息的连接点类型为:JoinPoint
*/
//获取目标类名
final String className = proceedingJoinPoint.getTarget().getClass().getName();
log.info("目标类名:{}"+className);
//获取目标方法签名
final Signature methodSignature = proceedingJoinPoint.getSignature();
log.info("目标方法签名:{}"+methodSignature);
//获取目标方法名
final String methodName = proceedingJoinPoint.getSignature().getName();
log.info("目标方法名:{}"+methodName);
//获取方法执行时需要的参数
final Object[] args = proceedingJoinPoint.getArgs();
log.info("方法参数:{}"+ Arrays.toString(args));
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("目标方法的返回值:{}"+result);
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after");
return result;
}
//前置通知,在切入方法执行前执行
@Before("pt()")//引用切入点
public void before(JoinPoint joinPoint){
//获取目标类名
final String className = joinPoint.getTarget().getClass().getName();
log.info("目标类名:{}"+className);
//其他API与环绕通知相同
log.info("before...");
}
}
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* Spring中的AOP切入点表达式execution
* 切入点指的是用于匹配连接点的条件,通知只会在目标方法执行时被应用
* 语法:execution(访问修饰符? 返回值类型 包名.类名?方法名(方法参数) throws 异常?)
* 其中访问修饰符可省略,包名.类名亦可省略但不建议省略(可能导致无法精确匹配目标对象)
* 若方法声明中定义了抛出异常则需要加上抛出异常
* 通配符:
* 1."*",单个独立的任意符号,可通配单个返回值,包名,类名,方法名,方法参数
* 也可以通配包名,类名,方法名的一部分
* 2."..",多个连续的任意符号,可以通配任意层级的包,或者任意类型,任意个数的参数
* 根据业务需要,可以使用&&,||,!来组合较为复杂的切入点表达式
* execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
*/
@Slf4j
@Component
//@Aspect
public class MyAspect4 {
/*省略访问修饰符,方法参数应使用全类名
execution(void com.itheima.service.impl.DeptServiceImpl.deleteById(java.lang.Integer))
使用*通配返回值类型,表示返回任意类型
excution(* com.itheima.service.impl.DeptServiceImpl.deleteById(java.lang.Integer))
使用*通配包名
excution(* com.*.service.impl.DeptServiceImpl.deleteById(java.lang.Integer))
使用*通配类名
excution(* com.*.service.impl.*.deleteById(java.lang.Integer))
使用..省略包名
excution(* com..*.deleteById(java.lang.Integer))
使用*通配方法名
excution(* com..*.*(java.lang.Integer))
使用*通配方法名的一部分
excution(* com..*.delete*(java.lang.Integer))
使用*通配方法参数,一个*通配一个参数
excution(* com..*.deleteById(*))
使用..通配方法参数
excution(* com..*.deleteById(..))
*/
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*())")
private void pt(){}
//环绕通知,在切入方法执行前后都执行
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("目标方法的返回值:{}"+result);
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after");
return result;
}
}
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Spring中的AOP切入点表达式@annotation
* 定义自定义注解
* 在连接点方法上添加自定义的注解
*/
@Slf4j
@Component
//@Aspect
public class MyAspect5 {
//切入点方法(公共的切入点表达式)
@Pointcut("@annotation(com.itheima.aop.MyLog)")
private void pt(){}
//环绕通知,在切入方法执行前后都执行
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("目标方法的返回值:{}"+result);
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after");
return result;
}
}
package com.itheima.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
package com.itheima.service.impl;
import com.itheima.aop.MyLog;
import com.itheima.mapper.DeptMapper;
import com.itheima.pojo.Dept;
import com.itheima.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
@MyLog//自定义注解,表示当前方法为目标方法
@Override
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
@MyLog
@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);
}
@MyLog
@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}
}
将案例中增、删、改相关接口的操作日志记录到数据库表中
-- 操作日志表
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 '操作日志表';
package com.itheima.aop;
import com.alibaba.fastjson.JSONObject;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
@Aspect
@Slf4j
@Component
public class OperateAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.MyLog)")
public Object recordLog(ProceedingJoinPoint pjp) throws Throwable {
//获取请求头中的jwt令牌
String token = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(token);
//获取操作人id
Integer operateUser = (Integer) claims.get("id");
//操作时间
LocalDateTime operateTime = LocalDateTime.now();
//获取目标类名
String className = pjp.getTarget().getClass().getName();
//获取方法名
String methodName = pjp.getSignature().getName();
//获取方法参数
String methodParams = Arrays.toString(pjp.getArgs());
//记录开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录结束时间
long end = System.currentTimeMillis();
/*
将对象转换为字符串,使用的是阿里巴巴的json和对象转换工具
导入依赖即可使用
com.alibaba
fastjson
1.2.76
*/
String resultValue = JSONObject.toJSONString(result);
//计算消耗时间
long costTime = end - begin;
//构建日志对象
OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className,
methodName, methodParams, resultValue, costTime);
//记录日志
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志: {}" , operateLog);
return result;
}
}
package com.itheima.mapper;
import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.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});")
void insert(OperateLog log);
}
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@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; //操作耗时
}
package com.itheima.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
package com.itheima.service.impl;
import com.itheima.anno.MyLog;
import com.itheima.mapper.DeptMapper;
import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Dept;
import com.itheima.pojo.DeptLog;
import com.itheima.service.DeptLogService;
import com.itheima.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@Slf4j
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
@Override
public List<Dept> list() {
//调用mapper层的list方法
List<Dept> deptList = deptMapper.list();
return deptList;
}
@MyLog
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteById(Integer id) throws Exception {
try {
//调用mapper层的deleteById方法
deptMapper.deleteById(id);
//删除该部门员工
empMapper.deleteByDeptId(id);
} finally {
//无论是否成功都要记录部门日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是:"+id+"号部门");
//开启了新的事务
deptLogService.insert(deptLog);
}
}
@MyLog
@Override
public void insert(Dept dept) {
//设置创建时间与更新时间
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
//调用mapper层的insert方法
deptMapper.insert(dept);
}
@Override
public Dept selectById(long id) {
//调用mapper层的selectById方法
Dept dept = deptMapper.selectById(id);
return dept;
}
@MyLog
@Override
public void update(Dept dept) {
//设置更新时间
dept.setUpdateTime(LocalDateTime.now());
//调用mapper层的update方法
deptMapper.update(dept);
}
}