工作中可能需要记录某些操作的日志,若将涉及到的业务模块,逐个添加日志逻辑,会显得代码非常冗余,也是万万不可取的,故实现了此注解,来解决这一问题。
涉及技术:
AOP,注解,反射,异步
使用案例:
/**
* 申请加入项目
* @param form
* @return
*/
@LogAnnotation(enuLogHandle = EnuLogHandle.JOIN_PROJECT,operateType = EnuOperateType.CREATE)
@SneakyThrows
@Override
public void joinProject(ProjectMemberProcessForm form) {
// 业务逻辑 ...
}
以下是日志组件具体的实现:
核心注解 @LogAnnotation
package com.demo.log;
import java.lang.annotation.*;
/**
* 日志注解
* @Author: liufanghe
* @Date: 2023/2/10 14:15
* @Version 1.0
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
/***
* 是否记录参数
* @return
*/
boolean recodParameters() default true;
/***
* 是否记录执行结果
* @return
*/
boolean recodResult() default true;
/***
* 记录到指定日志表
* @return
*/
// LogTypeEnum logRecordTable();
/***
* 操作人工号(员工工号)
* 对应方法参数中工号属性名
* @return
*/
// String operator() default "operator";
/**
* 具体菜单模块
* @return
*/
EnuLogHandle enuLogHandle();
/**
* 操作类型
* @return
*/
EnuOperateType operateType();
}
注解的实现 LogInfoAspect
package com.demo.log;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import org.aspectj.lang.JoinPoint;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* @Author: liufanghe
* @Date: 2023/2/10 14:15
* @Version 1.0
*/
@Aspect
@Component
public class LogInfoAspect {
private final static Logger logger = LoggerFactory.getLogger(LogInfoAspect.class);
@Autowired
private UserUtils userUtils;
@Autowired
private OperationLogMapper operationLogMapper;
@Pointcut("@annotation(com.demo.log.LogAnnotation)")
public void pointCut() {
}
@Around("pointCut()")
public Object around(JoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
OperationLog log = new OperationLog();
LogAnnotation logAnnotation = null;
EnuLogStatus logStatusEm = EnuLogStatus.SUCCESS;
EnuLogHandle enuLogHandle = null;
List<Object> paramList = new ArrayList<>();
try {
Method method = getMethod(joinPoint.getTarget().getClass(), joinPoint.getSignature().getName());
logAnnotation = method.getAnnotation(LogAnnotation.class);
if (logAnnotation != null) {
enuLogHandle = logAnnotation.enuLogHandle();
if (logAnnotation.recodParameters()) {
// 处理参数
paramHandle(joinPoint, log, logAnnotation, paramList);
}
}
// 执行业务
Object result = ((ProceedingJoinPoint) joinPoint).proceed();
if (result != null && (logAnnotation != null && logAnnotation.recodResult())) {
log.setExecuteResult(JSON.toJSONString(result));
}
return result;
} catch (Throwable throwable) {
logger.error("日志拦截异常:" + throwable.getMessage(), throwable);
logStatusEm = EnuLogStatus.ERROR;
log.setErrorMsg(throwable.getMessage());
throw throwable;
} finally {
logReplace(startTime, log, logStatusEm, enuLogHandle);
// 入库
logger.info("日志记录:{}", JSON.toJSONString(log));
// 异步 避免遇到业务回滚,日志丢失
ThreadPoolServices.getThreadPool().execute(() -> {
if (!CollectionUtils.isEmpty(paramList)) {
List<OperationLog> logCheckComputations = new ArrayList<>();
paramList.forEach(param -> {
OperationLog logCheckComputation = new OperationLog();
BeanUtils.copyProperties(log, logCheckComputation);
logCheckComputation.setParams(JSON.toJSONString(param));
logCheckComputations.add(logCheckComputation);
});
batchInsert(logCheckComputations);
} else {
operationLogMapper.insert(log);
}
});
}
}
/**
* 日志参数补全
*
* @param startTime
* @param log
* @param logStatusEm
* @param enuLogHandle
*/
private void logReplace(long startTime, OperationLog log, EnuLogStatus logStatusEm, EnuLogHandle enuLogHandle) {
Long operatorId = userUtils.getUserInfo().getUserId();
log.setOperator(operatorId);
log.setStatus(logStatusEm.getStatus());
log.setCreateById(operatorId);
log.setCreateAt(new Date());
log.setUpdateById(operatorId);
log.setUpdateAt(new Date());
log.setConsumingTime(System.currentTimeMillis() - startTime);
log.setLogHandleCode(enuLogHandle.getCode());
log.setLogHandleDesc(enuLogHandle.getDesc());
}
private void paramHandle(JoinPoint joinPoint, OperationLog log, LogAnnotation logAnnotation, List<Object> paramList) {
Object[] args = joinPoint.getArgs();
EnuOperateType operateType = logAnnotation.operateType();
String id = null;
if (args != null && args.length > 0) {
StringBuilder sbParameters = new StringBuilder();
for (Object obj : args) {
// 日志记录参数 list每条数据需要独立存储
if (obj instanceof List) {
JSONArray jsonArray = JSON.parseArray(JSON.toJSONString(obj));
paramList.addAll(jsonArray);
} else {
if (operateType.equals(EnuOperateType.MODIFY) && StrUtil.isBlank(id)) {
Object o = getObjectAttribute(obj, "id");
id = (String) o;
}
sbParameters.append(obj.getClass().getName())
.append("=")
.append(JSON.toJSONString(obj))
.append(" | ");
}
}
logger.info("日志业务参数:{}", JSON.toJSONString(sbParameters));
log.setParams(sbParameters.toString());
}
log.setOperateId(id);
log.setReferenceName(joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
log.setOperateType(operateType);
}
private void batchInsert(List<OperationLog> operationLogList) {
ThreadPoolServices.getThreadPool().execute(() -> {
if (operationLogList.size() > 0) {
if (operationLogList.size() > 1000) {
HashMap<Integer, List<OperationLog>> hashMap = splitData(operationLogList, 1000);
for (Map.Entry<Integer, List<OperationLog>> operationLogs : hashMap.entrySet()) {
List<OperationLog> splitEntity = operationLogs.getValue();
operationLogMapper.batchInsert(splitEntity);
}
} else {
operationLogMapper.batchInsert(operationLogList);
}
}
});
}
/**
* 集合切分
*
* @param list
* @param splitCount
* @param <T>
* @return
*/
public <T> HashMap<Integer, List<T>> splitData(List<T> list, int splitCount) {
HashMap<Integer, List<T>> hashMap = new HashMap<>(16);
if (list != null && list.size() > 0) {
int totalSize = list.size();
// 每份splitCount条
for (int i = 0; i < totalSize; i = i + splitCount) {
int index = i + splitCount;
if (totalSize < index) {
index = totalSize;
}
List spliteList = list.subList(i, index);
hashMap.put(i, spliteList);
}
return hashMap;
}
return hashMap;
}
/***
* 获取对象属性值,
* 只对object是对象 或String有效
* @param object
* @param attributeName
* @return
*/
public static Object getObjectAttribute(Object object, String attributeName) {
Field[] fieldList = ReflectUtils.getAllField(object.getClass());
for (Field field : fieldList) {
if (field.getName().equals(attributeName)) {
field.setAccessible(true);
try {
Object fieldObj = field.get(object);
return fieldObj;
} catch (IllegalAccessException e) {
logger.error("读取属性值时出现异常");
}
}
}
return null;
}
/***
* 以方法名获取方法
* @param clazz 对象类
* @param methodName 方法名
* @return Method
*/
public static Method getMethod(Class<?> clazz, String methodName) {
return Arrays.stream(clazz.getMethods()).filter(x -> x.getName().equals(methodName)).findFirst().get();
}
}
业务枚举 EnuLogHandle
package com.demo.log;
/**
* @Author: liufanghe
* @Date: 2023/2/17 8:55
* @Version 1.0
*/
public enum EnuLogHandle {
WORKTIME_ENTRY("100_1_1", "工时填写"),
WORKTIME_PROCESS_DEPART("100_2_1", "部门工时审核"),
WORKTIME_PROCESS_PROJECT("100_3_1", "项目工时审核"),
PROJECT_MANAGE("100_4_1", "项目管理"),
JOIN_PROJECT("100_5_1", "加入项目"),
HOLIDAYS_IN_GENERAL("100_6_1", "节假日管理"),
;
private String code;
private String desc;
EnuLogHandle(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
业务处理状态 EnuLogStatus
package com.demo.log;
public enum EnuLogStatus {
SUCCESS(1,"成功"),
ERROR(-1,"失败"),
;
private int status;
private String name;
EnuLogStatus(int status, String name) {
this.status = status;
this.name = name;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
业务操作类型 EnuOperateType
package com.demo.log;
/**
* @Author: liufanghe
* @Date: 2023/6/19 14:36
* @Version 1.0
*/
public enum EnuOperateType {
CREATE("1", "新增"),
MODIFY("2", "修改"),
IMPORT("3", "导入");
/**
* 编码
*/
private String code;
/**
* 名称
*/
private String cnName;
EnuOperateType(String code, String cnName) {
this.code = code;
this.cnName = cnName;
}
public static EnuOperateType fromString(String code) {
for (EnuOperateType way : EnuOperateType.values()) {
if (way.getCode().equals(code)) {
return way;
}
}
return null;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getCnName() {
return cnName;
}
public void setCnName(String cnName) {
this.cnName = cnName;
}
}
反射工具 ReflectUtils
package com.demo.log;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* @author liufanghe
*/
@Slf4j
public class ReflectUtils {
/***
* 获取类的所有属性
* @param clazz
* @return
*/
public static Field[] getAllField(Class<?> clazz) {
Field[] fields = {};
List<Class> clList = new ArrayList<>();
getSuperClass(clazz, clList);
for (Class clz : clList) {
Field[] f = clz.getDeclaredFields();
fields = ArrayUtils.addAll(fields, f);
}
return fields;
}
/**
* 获取所有类,包括所有,除object父类
*
* @param clazz
* @return
*/
public static void getSuperClass(Class<?> clazz, List<Class> list) {
list.add(clazz);
Class superclass = clazz.getSuperclass();
if (!superclass.getName().equals("java.lang.Object")) {
getSuperClass(superclass, list);
}
}
}
线程池 ThreadPoolServices
package com.demo.log;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
/**
* 线程池
*/
public class ThreadPoolServices {
/**
* 预定义线程池大小
*/
private static int availableProcessors = 8 < Runtime.getRuntime().availableProcessors() ? 5 : Runtime.getRuntime().availableProcessors();
/**
* 创建发送消息用定长线程池
*/
private static ExecutorService threadPoolServices;
static {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
threadPoolServices = new ThreadPoolExecutor(availableProcessors, 200, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy());
}
/**
* 返回发送消息线程池
* @return
*/
public static ExecutorService getThreadPool() {return threadPoolServices;}
}
Q.E.D.