如何优雅的使用aop记录带参数的复杂web接口日志

1、新建一个自定义注解

package com.ninestar.userFile.aop.annotation;


import com.ninestar.userFile.constant.Constants;

import java.lang.annotation.*;

/**
 * 系统日志注解
 * 
 * @Author Tring
 * @Date 2024年5月7日14:31:18
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLog {

    /**
     * 操作日志类型
     * Constants.SAVE
     * 如果是保存操作,则prefix和field值填一个值,fiel是对应保存表的主键,如:prefix = {"员工信息保存成功"},field = {"employeeId"}
     * @return (1增、2删、3查、4该、5导入、6导出,7保存)
     */
    int slOperateType();

    /**
     * 操作日志类型
     * Constants.MODULE_ALL_FILE
     * @return 全部文件、信息查询、待归档文件、操作日志
     */
    String slModule();

    /**
     * 记录日志时,拼接的前缀(默认后面加":"),当记录多个参数的字段时,前缀一般需要和字段(field)一一对应,
     * 当字段(field)数量大于前缀数量时,默认取最后一个前缀,作为超出的字段的记录前缀。
     */
    String[] prefix() default {};

    /**
     * 日志中记录的方法参数索引,默认记录第0个参数。如果字段(field)为空数组,则记录该参数所有信息。<br>
     * 如果该参数是集合(Collection),则遍历记录每一个元素。
     * @return 方法参数索引
     */
    int argsIndex() default 0;

    /**
     * 方法参数中需要记录的属性字段名,可设置多个需要记录日志的字段
     */
    String[] field() default {};

    /**
     * 对应的操作的mapper(一般只有修改的时候才填)
     */
    String mapperCode() default Constants.MAPPER_EMPLOYEEINFO_DAO;
}

2、新增切面(这里只提供部门代码)

@Around("execution(* *..*Controller.*(..)) && @annotation(com.ninestar.userFile.aop.annotation.AutoLog)")
    public Object sysLogs(ProceedingJoinPoint joinPoint) throws Throwable{
        Class<?> targetClass = joinPoint.getTarget().getClass();
        String methodName = joinPoint.getSignature().getName();
        
        Method[] methods = targetClass.getMethods();
        // 当前切中的方法
        Method method = null;
        for (int i = 0; i < methods.length; i++){
            if (methods[i].getName() == methodName){
                method = methods[i];
                break;
            }
        }

        // 执行方法前
        Object proceed = null;
        //访问目标方法的参数:
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = HttpUtil.getRequest();

        // 获取自定义注解中的参数 add by Tring 2024/5/9
        AutoLog autoLog = method.getAnnotation(AutoLog.class);
        // 日志的内容,下面进行拼接
        StringBuilder logContents = new StringBuilder();
        // 方法参数,需要记录的信息
        Integer argsIndex = autoLog.argsIndex();
        String[] prefixs = autoLog.prefix();
        String[] fields = autoLog.field();
        String opeName = autoLog.slModule();
        Integer operateType = autoLog.slOperateType();
        String mapperCode = autoLog.mapperCode();

        switch (operateType) {
            case Constants.ADD:
            case Constants.DEL:
            case Constants.SELECT:
            case Constants.EXPORT:
                // 需要记录日志的参数对象,如果参数是个集合,则遍历每一个元素进行记录
                Object arg = args[argsIndex];
                if (arg instanceof Collection) {
                    Collection as = (Collection) arg;
                    for (Object a : as) {
                        if (logContents.length() > 0) {
                            logContents.append(";");
                        }
                        logContents.append(spliceLogContents(a, fields, prefixs));
                    }
                } else {
                    logContents.append(spliceLogContents(arg, fields, prefixs));
                }
                break;
            case Constants.IMPORT:
                // 获取导入文件名 add by Tring 2024/5/9
                List<String> fileNames = new ArrayList<>();
                for (Object arg1 : args) {
                    if (arg1 instanceof MultipartFile) {
                        MultipartFile file = (MultipartFile) arg1;
                        fileNames.add(file.getOriginalFilename());
                    }
                }
                logContents.append(prefixs[0]).append(":").append(fileNames.toString());
                break;
            case Constants.SAVE:
                arg = args[argsIndex];
                logContents.append(spliceLogContentsOfUpd(arg, fields, prefixs, mapperCode));
                break;
            // ... 其他文件类型
            default:
        }

        try {
            if(BeanUtils.isNotEmpty(request)) {
                request.setAttribute("enterController", true);
            }
            proceed = joinPoint.proceed();
        } catch (Throwable e) {
            logger.error("调用接口异常参数: " + getMethodParamContent(args));
            throw e;
        }finally{
            //保存海纳云用户登陆日志时不记录
            if(BeanUtils.isNotEmpty(request) && !NINESTAR_LOGIN_URL.equals(request.getRequestURI())) {
//                    && !SEARCH_API.equals(request.getRequestURI())
                this.saveLog(request.getRequestURI(), opeName, logContents.toString());
            }
        }
        return proceed;
    }
    
    public void saveLog(String reqUrl,String opeName, String opeContent){
        try {
            String executor = "系统[无用户登录系统]";
            if(StringUtil.isNotEmpty(AuthenticationUtil.getCurrentUserFullname())){
                executor = String.format("%s[%s]",AuthenticationUtil.getCurrentUserFullname(),AuthenticationUtil.getCurrentUsername());  
            }
            String logType = "操作日志";

            SysLogSl log = new SysLogSl(opeName, LocalDateTime.now(), executor,
                    WebUtil.getIpAddr(HttpUtil.getRequest()), logType, reqUrl, opeContent);
            log.setSlId(UniqueIdUtil.getSuid());
            sysLogManager.reader(log);
            
        } catch (Exception e) {
            logger.error("保存操作日志失败。" + ExceptionUtil.getFullStackTrace(e));
        }
    }
    
    /**
     * 通过切面参数获取内容
     * @param args
     * @return
     */
    private String getMethodParamContent(Object[] args) {
        StringBuffer sb = new StringBuffer();
        if (BeanUtils.isNotEmpty(args)) {
            for (Object object : args) {
                if (object instanceof ServletRequest || object instanceof ServletResponse) {
                    continue;
                }
                try {
                    sb.append(JsonUtil.toJson(object));
                    sb.append(" ");
                } catch (Exception e) {
                    sb.append(object.toString());
                }
            }
        }
        return sb.toString();
    }


    /**
     * 利用反射,从对象中,获取属性字段的值,拼接前缀。
     *
     * @param obj     对象
     * @param fields  字段名称集合
     * @param prefixs 前缀集合
     * @return 拼接内容
     * @throws NoSuchFieldException   找不字段异常
     * @throws IllegalAccessException 字段访问异常
     * @Author Tring
     * @Date 2024年5月10日10:50:06
     */
    private String spliceLogContents(Object obj, String[] fields, String[] prefixs) throws IllegalAccessException {
        // 如果没有定义属性,则直接将对象toString后记录,如果定义了前缀,则拼接上前缀后记录
        if (fields == null || fields.length == 0) {
            if (prefixs != null && prefixs.length > 0) {
                return prefixs[0] + ":" + obj.toString();
            }
            return obj.toString();
        }

        StringBuilder sb = new StringBuilder();

        boolean hasPre = prefixs.length > 0;
        int prefixMaxIndex = prefixs.length - 1;
        int prefixIndex = 0;

        Class<?> aClass = obj.getClass();

        // 如果该对象中找不到属性,则向上父类查找
        Map<String, Field> fieldMap = new HashMap<>();
        for (; aClass != Object.class; aClass = aClass.getSuperclass()) {
            for (Field f : aClass.getDeclaredFields()) {
                fieldMap.putIfAbsent(f.getName(), f);
            }
        }

        Field field = null;
        Object fieldValue = null;
        for (int i = 0, len = fields.length; i < len; i++) {
            field = fieldMap.get(fields[i]);
            if (field == null) {
                continue;
            }
            field.setAccessible(true);
            fieldValue = field.get(obj);

            if (sb.length() > 0) {
                sb.append(",");
            }
            if (hasPre) {
                prefixIndex = Math.min(i, prefixMaxIndex);
                sb.append(prefixs[prefixIndex]);
                if (!prefixs[prefixIndex].endsWith(":")) {
                    sb.append(":");
                }
            }
            sb.append(fieldValue == null ? "" : fieldValue);

        }
        return sb.toString();
    }

    /**
     * 利用反射,从对象中,获取属性字段的值,拼接前缀。(用于记录update日志)
     * @param obj     对象
     * @param fields  字段名称集合
     * @param prefixs 前缀集合
     * @param mapperCode mapper字符串
     * @return 拼接内容
     * @throws NoSuchFieldException   找不字段异常
     * @throws IllegalAccessException 字段访问异常
     * @Author Tring
     * @Date 2024年5月10日10:50:06
     */
    public String spliceLogContentsOfUpd(Object obj, String[] fields, String[] prefixs, String mapperCode) throws IllegalAccessException {
        //获取DTO类
        Class<?> aClass = obj.getClass();

        // 如果该对象中找不到属性,则向上父类查找
        Map<String, Field> fieldMap = new HashMap<>();
        for (; aClass != Object.class; aClass = aClass.getSuperclass()) {
            for (Field f : aClass.getDeclaredFields()) {
                fieldMap.putIfAbsent(f.getName(), f);
            }
        }

        //获取主键的值
        Field field = fieldMap.get(fields[0]);
        if (field == null) {
            return "";
        }
        field.setAccessible(true);
        Object fieldValue = field.get(obj);
        if(ObjectUtils.isEmpty(fieldValue)){
            //如果fieldValue为空,则证明是新增操作,把“:”去掉
            String content = spliceLogContents(obj,fields,prefixs);
            return content.length()>0 ? content.substring(0,content.length()-1) : "";
        }

        //获取原数据
        BaseMapper mapper = MapperFactory.getMappers(mapperCode);
        Object oldObj = null;
        if(!ObjectUtils.isEmpty(mapper)){
            oldObj = mapper.selectById(fieldValue.toString());
        }
        return prefixs[0]+ObjUtils.compareObjects(obj,oldObj);
    }

3、创建mapper 工厂

package com.ninestar.userFile.config;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ninestar.userFile.constant.Constants;
import com.ninestar.userFile.persistence.dao.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: Tring
 * @since: 2024/5/10 16:34
 * @description:mapper工厂
 */
@Configuration
public class MapperFactory {

    @Autowired
    private EmployeeInfoDao employeeInfoDao;
    @Autowired
    private FileInfoDao fileInfoDao;
    @Autowired
    private SecretInfoDao secretInfoDao;
    @Autowired
    private SysLogSlDao sysLogSlDao;

    protected static Map<String, BaseMapper> mappers = new ConcurrentHashMap<>();

    @PostConstruct
    public void init(){
        mappers.put(Constants.MAPPER_EMPLOYEEINFO_DAO, employeeInfoDao);
        mappers.put(Constants.MAPPER_FILEINFO_DAO, fileInfoDao);
        mappers.put(Constants.MAPPER_SECRETINFO_DAO, secretInfoDao);
        mappers.put(Constants.MAPPER_SYSLOGSL_DAO, sysLogSlDao);
    }

    public static BaseMapper getMappers(String code) {
        return mappers.get(code);
    }
}

4、写工具类,用于对比修改前后的日志内容显示

package com.ninestar.userFile.utils;

import cn.hutool.core.util.ObjectUtil;

import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author: Tring
 * @since: 2024/5/10 9:57
 * @description:对象工具类
 */
public class ObjUtils {

    /**
     * 获取目标对象跟原对象之间属性的变化
     * @param obj1 目标对象
     * @param obj2 原对象
     * @return 结果:属性名字:原值-->修改后的值,如account: null --> 456,fullName: 333 --> 789
     * @throws IllegalAccessException
     */
    public static String compareObjects(Object obj1, Object obj2) throws IllegalAccessException {
        Class<?> clazz1 = obj1.getClass();
        Class<?> clazz2 = obj2.getClass();

        StringBuilder str = new StringBuilder();

        Field[] fields1 = clazz1.getDeclaredFields();
        Field[] fields2 = clazz2.getDeclaredFields();

        for (Field field1 : fields1) {
            field1.setAccessible(true);
            Object value1 = field1.get(obj1);
            for (Field field2 : fields2) {
                field2.setAccessible(true);
                Object value2 = field2.get(obj2);
                if (value1 != null && ObjectUtil.equal(field1.getName(),field2.getName()) && !ObjectUtil.equal(value1,value2)) {
                    if(value2 instanceof Date){
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                        str.append(field1.getName() + ": " + sdf.format(value2) + " --> " + sdf.format(value1) +",");
                    }else{
                        str.append(field1.getName() + ": " + value2 + " --> " + value1 +",");
                    }

                }
            }
        }
        return str.length()>0 ?str.substring(0,str.length()-1):str.toString();
    }
}

5、最后在需要打印日志的Controller方法中加上一开始创建的自定义注解即可

@AutoLog(slModule= Constants.MODULE_INFO_SEL,slOperateType = Constants.SAVE,prefix = {"保存档案基础信息"},field = {"employeeId"},mapperCode = Constants.MAPPER_EMPLOYEEINFO_DAO)

 

posted @ 2024-05-14 16:49  有缘无分的朋友  阅读(14)  评论(0编辑  收藏  举报