工作中可能需要记录某些操作的日志,若将涉及到的业务模块,逐个添加日志逻辑,会显得代码非常冗余,也是万万不可取的,故实现了此注解,来解决这一问题。

涉及技术:

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.


如人饮水、冷暖自知