springboot 自定义注解实现操作日志记录
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>
2.自定义注解
import com.micgoo.lezystockmanage.core.enums.OperateBehaviorEnum;
import com.micgoo.lezystockmanage.core.enums.OperateModuleEnum;
import com.micgoo.lezystockmanage.core.enums.ServiceEnum;
import java.lang.annotation.*;
/***
* 操作日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
/**
* 业务名称,例如: "修改商品"
*/
String title() default "";
/***
* 操作类型
*/
OperateBehaviorEnum behavior() default OperateBehaviorEnum.OTHER;
/***
* 模块名称
*/
OperateModuleEnum module() default OperateModuleEnum.OTHER;
/***
* service 名称
*/
ServiceEnum serviceName() default ServiceEnum.OTHER;
}
3.日志类、日志类builder、操作类型枚举、模块类型枚举类、service名枚举类
3.1 日志类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName(value = "operate")
public class OperateEntity {
@TableId(value = "pk_Id",type = IdType.AUTO)
private long pk_Id;
/**
* 操作人userName
*/
private String userName;
/**
* 日志标题
*/
private String title;
/***
* 日志模块
*/
private String module;
/**
* 操作类型
*/
private String behavior;
/**
* 原始数据json
*/
private String oldJSON;
/**
* 新数据json
*/
private String newJSON;
/**
* 操作日期
*/
private String createTime;
}
3.2 日志类builder (使用builder设计模式是为了美化 new 日志对象的操作 当然也可以不用builder 直接 new 对象 set属性)
import cn.hutool.core.bean.BeanUtil;
import com.micgoo.lezystockmanage.entity.db.mysql.OperateEntity;
public class OperateBuilder {
/**
* 操作人userName
*/
private final String userName;
/**
* 日志标题
*/
private final String title;
/***
* 日志模块
*/
private final String module;
/**
* 操作类型
*/
private final String behavior;
/**
* 原始数据json
*/
private final String oldJSON;
/**
* 新数据json
*/
private final String newJSON;
public OperateBuilder(Builder builder) {
this.userName = builder.userName;
this.title = builder.title;
this.module = builder.module;
this.behavior = builder.behavior;
this.oldJSON = builder.oldJSON;
this.newJSON = builder.newJSON;
}
public String getUserName() {
return userName;
}
public String getTitle() {
return title;
}
public String getModule() {
return module;
}
public String getBehavior() {
return behavior;
}
public String getOldJSON() {
return oldJSON;
}
public String getNewJSON() {
return newJSON;
}
/**
*静态内部类 Builder
*/
public static class Builder{
private long pk_Id;
private String userName;
private String title;
private String module;
private String behavior;
private String oldJSON;
private String newJSON;
private String createTime;
//构造方法
public Builder() {
userName = userName;
title = title;
module = module;
behavior = behavior;
oldJSON = oldJSON;
newJSON = newJSON;
}
public Builder setUserName(String userName) {
this.userName = userName;
return this;
}
public Builder setTitle(String title) {
this.title = title;
return this;
}
public Builder setModule(String module) {
this.module = module;
return this;
}
public Builder setBehavior(String behavior) {
this.behavior = behavior;
return this;
}
public Builder setOldJSON(String oldJSON) {
this.oldJSON = oldJSON;
return this;
}
public Builder setNewJSON(String newJSON) {
this.newJSON = newJSON;
return this;
}
//构建一个实体
public OperateEntity build() {
OperateBuilder operateBuilder = new OperateBuilder(this);
OperateEntity operateEntity = BeanUtil.toBean(operateBuilder, OperateEntity.class);
return operateEntity;
}
}
}
3.3 操作类型枚举
import com.micgoo.lezystockmanage.core.enums.abs.MicgooEnumInterface;
/***
* enum
* 操作日志行为类枚举
*/
public enum OperateBehaviorEnum {
ADD("新增"),
DELETE("删除"),
UPDATE("更新"),
EXPORT("导出"),
OTHER("其他")
;
private String behavior;
OperateBehaviorEnum(String behavior) {
this.behavior = behavior;
}
public String getBehavior() {
return behavior;
}
}
3.4 模块类型枚举类
import com.micgoo.lezystockmanage.core.enums.abs.MicgooEnumInterface;
/***
* enum
* 模块枚举类
*/
public enum OperateModuleEnum implements MicgooEnumInterface {
OTHER("other","未知"),
POST("post","岗位"),
SYSUSER("sysUser","系统用户")
;
/**
* code值
*/
private String resultCode;
/**
* 描述
*/
private String resultMsg;
OperateModuleEnum(String resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
@Override
public String getResultCode() {
return resultCode;
}
@Override
public String getResultMsg() {
return resultMsg;
}
}
3.5 service名枚举类
/***
* enum
* service名称枚举类
*/
public enum ServiceEnum{
OTHER(""),
USERPOSTSERVICE("UserPostService"),
POSTSSERVICE("PostsService")
;
private String serviceName;
ServiceEnum(String serviceName) {
this.serviceName = serviceName;
}
ServiceEnum() {
}
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
}
4.aop切面
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.micgoo.lezystockmanage.core.annotion.OperateLog;
import com.micgoo.lezystockmanage.core.enums.MicgooEnum;
import com.micgoo.lezystockmanage.core.enums.OperateBehaviorEnum;
import com.micgoo.lezystockmanage.core.enums.OperateModuleEnum;
import com.micgoo.lezystockmanage.core.enums.ServiceEnum;
import com.micgoo.lezystockmanage.core.exception.MicgooException;
import com.micgoo.lezystockmanage.entity.OperateBuilder;
import com.micgoo.lezystockmanage.entity.db.mysql.OperateEntity;
import com.micgoo.lezystockmanage.service.OperateService;
import lombok.extern.log4j.Log4j2;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/***
* 操作日志切面
*/
@Aspect
@Log4j2
@Component
public class OperateLogAop {
@Autowired
private OperateService operateService;
/***
* 切点
*/
@Pointcut("@annotation(com.micgoo.lezystockmanage.core.annotion.OperateLog)")
public void operateLogPointCut(){
}
/***
* 环绕通知
*/
@Around( value = "operateLogPointCut()")
public void recordOperateLog(ProceedingJoinPoint proceedingJoinPoint){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();//从RequestContextHolder获得requestAttributes
HttpServletRequest request = requestAttributes.getRequest();
HttpSession session = request.getSession();
Object userName = session.getAttribute("userName");//操作人userName
if(Objects.isNull(userName)){
log.error("userName为空! 请求URL为 {} ",request.getServletPath());
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志userName为空!");
}
MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();
Method method = methodSignature.getMethod();
OperateLog annotation = method.getAnnotation(OperateLog.class);
String title = annotation.title();//业务名称
OperateBehaviorEnum behavior = annotation.behavior();//操作类型
OperateModuleEnum module = annotation.module();//操作模块
ServiceEnum serviceName = annotation.serviceName();//serviceName
if(StrUtil.isBlank(title)){
log.debug("业务名称为空! 请求URL为 {} ",request.getServletPath());
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志业务名称为空,请联系管理员修改!");
}
if(behavior==OperateBehaviorEnum.OTHER){
log.debug("操作类型为空! 请求URL为 {}",request.getServletPath());
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志操作类型为空,请联系管理员修改!");
}
if(module==OperateModuleEnum.OTHER){
log.debug("模块类型为空! 请求URL为 {}",request.getServletPath());
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志模块类型为空,请联系管理员修改!");
}
if(serviceName==ServiceEnum.OTHER){
log.debug("service为空! 请求URL为 {}",request.getServletPath());
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志service为空,请联系管理员修改!");
}
//获得参数名和参数
Object[] args = proceedingJoinPoint.getArgs();
String[] parameterNames = methodSignature.getParameterNames();
//定义一个map存放参数
Map<String, Object> argsMap = new HashMap<>();
for (int i = 0; i < args.length; i++) {
argsMap.put(parameterNames[i],args[i]);
}
//参数是否为空
if(CollUtil.isEmpty(argsMap)){
log.debug("请求参数为空!, 请求URL为:{}",request.getServletPath());
}
OperateEntity operateEntity = null;
switch (behavior){
case ADD_OR_UPDATE:
if(Long.parseLong(argsMap.get("id").toString())==0){
operateEntity = new OperateBuilder.Builder()
.setUserName(String.valueOf(userName))
.setBehavior(OperateBehaviorEnum.ADD.getBehavior())
.setModule(module.getResultCode())
.setTitle(title)
.setOldJSON("[]")
.setNewJSON(JSONUtil.toJsonStr(argsMap))
.build();
}else{
operateEntity = new OperateBuilder.Builder()
.setUserName(String.valueOf(userName))
.setBehavior(OperateBehaviorEnum.UPDATE.getBehavior())
.setModule(module.getResultCode())
.setTitle(title)
.setOldJSON(getOldJSON(serviceName.getServiceName(),Long.parseLong(argsMap.get("id").toString())))
.setNewJSON(JSONUtil.toJsonStr(argsMap))
.build();
}
break;
case ADD:
operateEntity = new OperateBuilder.Builder()
.setUserName(String.valueOf(userName))
.setBehavior(behavior.getBehavior())
.setModule(module.getResultCode())
.setTitle(title)
.setOldJSON("[]")
.setNewJSON(JSONUtil.toJsonStr(argsMap))
.build();
break;
case DELETE:
operateEntity = new OperateBuilder.Builder()
.setUserName(String.valueOf(userName))
.setBehavior(behavior.getBehavior())
.setModule(module.getResultCode())
.setTitle(title)
.setOldJSON(getOldJSON( serviceName.getServiceName(), Long.parseLong(argsMap.get("id").toString())))
.setNewJSON("[]")
.build();
break;
case UPDATE:
operateEntity = new OperateBuilder.Builder()
.setUserName(String.valueOf(userName))
.setBehavior(behavior.getBehavior())
.setModule(module.getResultCode())
.setTitle(title)
.setOldJSON(getOldJSON(serviceName.getServiceName(), Long.parseLong(argsMap.get("id").toString())))
.setNewJSON(JSONUtil.toJsonStr(argsMap))
.build();
break;
case EXPORT:
//TODO
break;
default:
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志未知操作类型!");
}
try {
/*
* 执行此方法前,为环绕通知的前置操作
*/
proceedingJoinPoint.proceed();//执行被环绕的方法
/*
* 执行后为环绕通知的后置操作
*/
if(ObjectUtil.isNotNull(operateEntity)){
operateService.save(operateEntity);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
log.debug("操作日志环绕通知执行proceed方法失败! 请求URL为 "+request.getServletPath());
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"操作日志环绕通知执行proceed方法失败!");
}
}
//为了获取要修改数据的修改前数据
private String getOldJSON(String servicelName, long id){
Class<?> clazz = null;
Field service = null;
Method getById = null;
Object obj = null;
try {
clazz = Class.forName("com.micgoo.lezystockmanage.service."+servicelName);
} catch (ClassNotFoundException e) {
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"获取class失败!");
}
try {
getById = clazz.getMethod("getById", Serializable.class);
} catch (NoSuchMethodException e) {
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"获取getById方法失败!");
}
try {
/*
* 注意:
* 这里之所以用 SpringUtil.getBean(clazz) 来获取对象 是因为反射的对象为接口是没有构造方法的,不能被实例化,所以采用从spring容器中获取
* 通过id获取,值得注意的是这里是通过反射获取的,为了达到代码的通用性,这里每个被反射的service都应继承mybatis的ServiceImpl,接口都应继承IService
*/
obj = getById.invoke(SpringUtil.getBean(clazz),id);
} catch (Exception e) {
e.printStackTrace();
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"调用getById方法失败!");
}
if(Objects.isNull(obj)){
throw new MicgooException(MicgooEnum.FAIL.getResultCode(),"调用getById方法获取数据为null!");
}
return JSONUtil.toJsonStr(obj);
}
}
5.使用
@PostMapping("/DeletePosts")
@OperateLog(title = "删除岗位",behavior = OperateBehaviorEnum.DELETE,module = OperateModuleEnum.POSTS,serviceName = ServiceEnum.POSTSSERVICE)
public Object DeletePosts(@RequestParam("id") Long id){
if(postsService.InquirerPosts(id)){
return new ResultMap("Same");
}else{
postsService.DelePosts(id);
return new ResultMap(MicgooEnum.SUCCESS.getResultCode());
}
}
写在最后只得注意的一点是,同事在使用自定义注解的时候发现怎么注解都不生效,还以为是注解没有开启的原因,找了许久才发现,使用注解的方法是private的,这里切面类没有实现接口,故是用的cglib的动态代理,是生成一个子类作为代理对象,子类不能继承父类private的方法和属性。改为public就好了。