Spring AOP 操作日志框架(CV可用)

深度解析!Spring AOP 操作日志框架:从注解到切面的全流程拆解

今天带大家啃一个实战性极强的 Spring AOP 框架------操作日志自动记录组件。这套代码看似复杂,实则是"配置驱动+分层解耦"的典范,兼容静态/动态/历史系统三种场景,还能保证接口性能不降级。

接下来我会分两步走:

  1. 先给核心代码加上 超详细注释+关键日志打印,让大家一眼看懂每行代码的作用;
  2. 再逐段解析"工作原理+设计亮点",拆解背后的编程思想,让你不仅会用,还能举一反三。

话不多说,直接上代码+解析!

一、核心代码(带详细注释+日志打印)

1. 注解类:@OperationApiLog(配置规则定义)

java 复制代码
package org.springblade.business.aspect.annotation;

import org.springblade.business.enums.OperationTypeEnum;
import java.lang.annotation.*;

/**
 * 操作日志注解:用于标记需要记录日志的接口方法
 * 核心能力:支持 静态单类型/动态多类型/自定义值映射 三种场景
 * 优先级规则:valueMap(自定义映射)> operationTypes+paramKey(动态多类型)> operationType(静态单类型)
 */
@Target(ElementType.METHOD)  // 仅作用于方法(接口方法才需要记录日志)
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留,允许AOP反射获取注解参数
@Documented  // 生成JavaDoc时包含该注解,便于团队协作
public @interface OperationApiLog {

    /**
     * 静态单操作类型(兼容简单场景:一个接口对应一个操作)
     * 示例:新增用户接口 → operationType = OperationTypeEnum.ADD
     */
    OperationTypeEnum operationType() default OperationTypeEnum.NONE;

    /**
     * 动态多操作类型候选集(同一接口对应多个操作,比如审核接口:通过/拒绝)
     * 示例:审核接口 → operationTypes = {OperationTypeEnum.PASS, OperationTypeEnum.REJECT}
     */
    OperationTypeEnum[] operationTypes() default {};

    /**
     * 区分操作类型的入参名(配合 operationTypes/valueMap 使用)
     * 支持两种格式:
     * 1. 基本类型参数名:如 "status"(接口入参是 Integer status)
     * 2. 实体类嵌套属性:如 "approveParam.status"(接口入参是 ApproveParam 对象,含 status 字段)
     */
    String paramKey() default "";

    /**
     * 入参值与枚举的匹配方式(仅用于 operationTypes 候选集)
     * CODE:匹配枚举的 code(如 "enable" 匹配 OperationTypeEnum.ENABLE)
     * DESC:匹配枚举的 desc(如 "启用" 匹配 OperationTypeEnum.ENABLE)
     */
    MatchType matchType() default MatchType.CODE;

    /**
     * 自定义值映射(兼容历史系统:入参是非枚举值,如 1=启用、0=禁用)
     * 格式要求:{"自定义值1=枚举code1", "自定义值2=枚举code2"}
     * 示例:{"1=enable", "0=disable"} → 入参1 → ENABLE 枚举
     */
    String[] valueMap() default {};

    /**
     * 匹配方式枚举(内部枚举,限定匹配规则,避免乱配置)
     */
    enum MatchType {
        CODE, DESC
    }
}

2. 切面类:OperationLogAspect(核心执行逻辑)

java 复制代码
package org.springblade.business.aspect;

import cn.hutool.json.JSONUtil;
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.aspectj.lang.reflect.MethodSignature;
import org.springblade.business.aspect.annotation.OperationApiLog;
import org.springblade.business.enums.OperationTypeEnum;
import org.springblade.business.entity.Menu;
import org.springblade.business.entity.OperationLog;
import org.springblade.business.entity.User;
import org.springblade.business.service.OperationLogService;
import org.springblade.business.service.IMenuService;
import org.springblade.business.service.IUserService;
import org.springblade.core.tool.utils.StringUtils;
import org.springblade.core.tool.utils.Objects;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ThreadPoolTaskExecutor;

/**
 * 操作日志切面:AOP核心组件,拦截带@OperationApiLog注解的方法,自动记录日志
 * 核心流程:拦截方法 → 执行业务 → 解析操作类型 → 构建日志实体 → 异步保存
 */
@Aspect  // 标记为AOP切面类
@Component  // 交给Spring容器管理
@Slf4j  // 日志打印(Lombok注解)
public class OperationLogAspect {

    // 依赖注入:日志保存服务(业务层,负责将日志写入数据库)
    @Resource
    private OperationLogService operationLogService;

    // 依赖注入:菜单服务(查询操作对应的菜单信息,用于日志分类)
    @Resource
    private IMenuService menuService;

    // 依赖注入:用户服务(查询当前登录用户信息,记录操作人)
    @Resource
    private IUserService userService;

    // 依赖注入:线程池(异步保存日志,避免阻塞接口)
    @Resource(name = "myTaskExecutor")
    private ThreadPoolTaskExecutor taskExecutor;

    /**
     * 切点定义:明确拦截哪些方法
     * 语法解析:
     * execution(* org.springblade.business.controller.*.*(..)) → 拦截 controller 包下所有类的所有方法
     * && @annotation(...) → 叠加注解匹配:只拦截带 @OperationApiLog 注解的方法
     */
    @Pointcut("execution(* org.springblade.business.controller.*.*(..)) && @annotation(org.springblade.business.aspect.annotation.OperationApiLog)")
    public void operationLogPointcut() {
        log.debug("===== 操作日志切点初始化完成 =====");
    }

    /**
     * 环绕通知:方法执行前后的核心逻辑(AOP的核心)
     * 作用:先执行业务方法,再记录日志(确保业务优先,日志不影响业务)
     */
    @Around("operationLogPointcut()")  // 绑定切点:只对 operationLogPointcut 拦截的方法生效
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.debug("===== 进入操作日志环绕通知 =====");
        
        // 第一步:执行目标业务方法(核心!先让接口业务执行,再处理日志)
        log.debug("开始执行业务方法:{}", joinPoint.getSignature().getName());
        Object result = joinPoint.proceed();  // 调用接口方法(比如新增用户、审核订单)
        log.debug("业务方法执行完成,开始处理日志");

        try {
            // 第二步:获取注解配置和HTTP请求上下文(日志需要的基础信息)
            // 1. 获取方法签名和注解参数(拿到 @OperationApiLog 的配置)
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();  // 当前执行的接口方法
            OperationApiLog logAnno = method.getAnnotation(OperationApiLog.class);  // 注解配置
            log.debug("获取注解配置:operationType={}, paramKey={}, valueMap={}",
                    logAnno.operationType().getDesc(), logAnno.paramKey(), Arrays.toString(logAnno.valueMap()));

            // 2. 获取HTTP请求信息(接口路径、请求头、入参等)
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes == null) {
                log.warn("日志记录失败:无法获取HTTP请求上下文(非Web环境?)");
                return result;  // 没有请求上下文,直接返回业务结果,不记录日志
            }
            HttpServletRequest request = attributes.getRequest();
            log.debug("获取请求信息:接口路径={}, 请求IP={}", request.getRequestURI(), request.getRemoteAddr());

            // 第三步:核心逻辑 → 解析操作类型(按优先级匹配:valueMap > operationTypes > operationType)
            OperationTypeEnum operationTypeEnum = resolveOperationType(joinPoint, logAnno);
            if (operationTypeEnum == null || operationTypeEnum == OperationTypeEnum.NONE) {
                log.error("日志记录失败:未匹配到有效操作类型,方法名:{}", method.getName());
                return result;
            }
            log.debug("解析到操作类型:code={}, desc={}", operationTypeEnum.getCode(), operationTypeEnum.getDesc());

            // 第四步:构建操作日志实体(组装日志的所有字段:操作人、时间、接口、入参等)
            OperationLog operationLog = buildOperationLog(request, operationTypeEnum, joinPoint);
            if (operationLog != null) {
                log.debug("日志实体构建完成:{}", JSONUtil.toJsonStr(operationLog));
                
                // 第五步:异步保存日志(关键!用线程池异步执行,不阻塞接口响应)
                taskExecutor.execute(() -> {
                    try {
                        operationLogService.save(operationLog);  // 保存到数据库
                        log.info("操作日志记录成功:菜单={},操作={},用户={}",
                                operationLog.getFirstMenuName(), operationLog.getFunctionDesc(), operationLog.getCreateUserName());
                    } catch (Exception e) {
                        log.error("异步保存日志失败,日志内容:{}", JSONUtil.toJsonStr(operationLog), e);
                    }
                });
            } else {
                log.error("日志记录失败:日志实体构建失败");
            }
        } catch (Exception e) {
            // 异常捕获:日志处理失败绝不影响业务方法的返回结果
            log.error("操作日志处理异常(不影响业务):", e);
        }

        log.debug("===== 操作日志环绕通知结束 =====");
        return result;  // 返回业务方法的结果(比如接口的JSON响应)
    }

    /**
     * 核心方法:解析操作类型(按优先级穿透,确保匹配成功率)
     * 优先级:1. 自定义值映射(valueMap)→ 2. 枚举候选集(operationTypes)→ 3. 静态单类型(operationType)
     */
    private OperationTypeEnum resolveOperationType(ProceedingJoinPoint joinPoint, OperationApiLog logAnno) {
        String paramKey = logAnno.paramKey();
        log.debug("开始解析操作类型:paramKey={}", paramKey);

        // 场景1:未配置 paramKey → 直接返回静态操作类型(简单场景)
        if (StringUtils.isBlank(paramKey)) {
            log.debug("未配置paramKey,使用静态操作类型:{}", logAnno.operationType().getDesc());
            return logAnno.operationType();
        }

        // 步骤1:获取入参中 paramKey 对应的值(比如 paramKey=status,就拿 status 的值)
        Object paramValue = getParamValue(joinPoint, paramKey);
        if (paramValue == null) {
            log.warn("未获取到paramKey[{}]的入参值,降级使用静态操作类型:{}", paramKey, logAnno.operationType().getDesc());
            return logAnno.operationType();
        }
        String valueStr = paramValue.toString().trim();
        log.debug("获取到paramKey[{}]的入参值:{}", paramKey, valueStr);

        // 步骤2:优先使用自定义值映射(兼容历史系统,最高优先级)
        String[] valueMap = logAnno.valueMap();
        if (valueMap.length > 0) {
            log.debug("开始解析自定义值映射:{}", Arrays.toString(valueMap));
            Map<String, String> map = parseValueMap(valueMap);  // 解析成 key→枚举code 的Map
            if (map.containsKey(valueStr)) {
                String enumCode = map.get(valueStr);
                OperationTypeEnum enumType = OperationTypeEnum.matchByCode(enumCode);  // 映射为枚举
                if (enumType != null) {
                    log.debug("自定义映射匹配成功:入参值{}→枚举code{}→操作类型{}", valueStr, enumCode, enumType.getDesc());
                    return enumType;
                }
                log.warn("自定义映射的枚举code[{}]不存在,入参值:{}", enumCode, valueStr);
            } else {
                log.warn("入参值[{}]未配置在valueMap中,映射规则:{}", valueStr, Arrays.toString(valueMap));
            }
        }

        // 步骤3:使用枚举候选集匹配(动态多类型场景,中优先级)
        OperationTypeEnum[] candidateTypes = logAnno.operationTypes();
        if (candidateTypes.length > 0) {
            log.debug("开始匹配枚举候选集:{},匹配方式:{}", Arrays.toString(candidateTypes), logAnno.matchType());
            for (OperationTypeEnum type : candidateTypes) {
                // 按 matchType 匹配:CODE 匹配枚举code,DESC 匹配枚举描述
                if (logAnno.matchType() == OperationApiLog.MatchType.CODE && type.getCode().equals(valueStr)) {
                    log.debug("枚举候选集匹配成功:入参值{}→枚举code{}→操作类型{}", valueStr, type.getCode(), type.getDesc());
                    return type;
                } else if (logAnno.matchType() == OperationApiLog.MatchType.DESC && type.getDesc().equals(valueStr)) {
                    log.debug("枚举候选集匹配成功:入参值{}→枚举desc{}→操作类型{}", valueStr, type.getDesc(), type.getDesc());
                    return type;
                }
            }
            log.warn("入参值[{}]未匹配到候选枚举,候选类型:{}", valueStr, Arrays.toString(candidateTypes));
        }

        // 步骤4:静态类型兜底(最低优先级,确保不会返回null)
        log.debug("前序匹配失败,使用静态操作类型兜底:{}", logAnno.operationType().getDesc());
        return logAnno.operationType();
    }

    /**
     * 辅助方法:解析 valueMap 配置(将字符串数组转为 Map)
     * 输入:{"1=enable", "0=disable"} → 输出:{1→"enable", 0→"disable"}
     */
    private Map<String, String> parseValueMap(String[] valueMap) {
        Map<String, String> result = new HashMap<>();
        for (String entry : valueMap) {
            // 校验配置格式:必须包含 "="
            if (entry == null || !entry.contains("=")) {
                log.warn("无效的valueMap配置项:{}(正确格式:key=value)", entry);
                continue;
            }
            // 分割key和value:split("=", 2) 表示只分割一次,支持value含"="(如 "key=123=enable")
            String[] keyValue = entry.split("=", 2);
            String key = keyValue[0].trim();  // 入参值(比如 "1")
            String value = keyValue[1].trim(); // 枚举code(比如 "enable")
            // 校验key和value不为空
            if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
                result.put(key, value);
                log.debug("解析valueMap配置项:key={}→value={}", key, value);
            } else {
                log.warn("无效的valueMap配置项:{}(key或value不能为空)", entry);
            }
        }
        return result;
    }

    /**
     * 核心辅助方法:获取入参中 paramKey 对应的值(支持基本类型+实体类嵌套属性)
     */
    private Object getParamValue(ProceedingJoinPoint joinPoint, String paramKey) {
        log.debug("开始获取入参值:paramKey={}", paramKey);
        
        // 获取方法的参数名数组和参数值数组(比如参数名:["status"],参数值:[1])
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = signature.getParameterNames();
        Object[] paramValues = joinPoint.getArgs();

        // 校验:方法无入参,直接返回null
        if (paramNames == null || paramNames.length == 0 || paramValues == null || paramValues.length == 0) {
            log.warn("方法无入参,无法获取paramKey[{}]的值", paramKey);
            return null;
        }

        // 场景1:paramKey是嵌套属性(如 "approveParam.status")
        if (paramKey.contains(".")) {
            log.debug("paramKey是嵌套属性,开始解析:{}", paramKey);
            String[] keyParts = paramKey.split("\\.", 2);  // 分割为 ["approveParam", "status"]
            String objParamName = keyParts[0];  // 实体类参数名(如 "approveParam")
            String fieldName = keyParts[1];     // 实体类属性名(如 "status")

            // 遍历方法参数,找到实体类参数(比如找到参数名是 "approveParam" 的参数)
            for (int i = 0; i < paramNames.length; i++) {
                if (objParamName.equals(paramNames[i])) {
                    Object objParam = paramValues[i];  // 实体类对象(如 ApproveParam 实例)
                    if (objParam == null) {
                        log.warn("实体类参数[{}]为空,无法获取属性[{}]的值", objParamName, fieldName);
                        return null;
                    }
                    // 反射获取实体类的属性值(支持父类字段)
                    try {
                        Field field = getDeclaredField(objParam.getClass(), fieldName);  // 找到属性
                        if (field == null) {
                            log.warn("实体类[{}]不存在属性[{}]", objParam.getClass().getName(), fieldName);
                            return null;
                        }
                        field.setAccessible(true);  // 突破访问权限(私有属性也能获取)
                        Object fieldValue = field.get(objParam);  // 获取属性值
                        log.debug("嵌套属性获取成功:{}→{}={}", paramKey, fieldName, fieldValue);
                        return fieldValue;
                    } catch (IllegalAccessException e) {
                        log.error("反射获取属性值失败:paramKey={}", paramKey, e);
                        return null;
                    }
                }
            }
            log.warn("未找到实体类参数[{}],无法获取属性[{}]的值", objParamName, fieldName);
            return null;
        }

        // 场景2:paramKey是直接参数名(如 "status")
        log.debug("paramKey是直接参数名,开始匹配:{}", paramKey);
        for (int i = 0; i < paramNames.length; i++) {
            if (paramKey.equals(paramNames[i])) {
                Object value = paramValues[i];
                log.debug("直接参数获取成功:{}={}", paramKey, value);
                return value;
            }
        }

        log.warn("未找到参数名[{}]对应的入参", paramKey);
        return null;
    }

    /**
     * 辅助方法:递归获取类的字段(支持父类字段)
     * 比如:ApproveParam 继承 BaseParam,status 在 BaseParam 中,也能获取到
     */
    private Field getDeclaredField(Class<?> clazz, String fieldName) {
        while (clazz != null) {  // 递归向上查找父类,直到 Object 类
            try {
                return clazz.getDeclaredField(fieldName);  // 获取当前类的声明字段(包括私有)
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();  // 没找到,向上查找父类
            }
        }
        return null;  // 所有类都没找到,返回null
    }

    /**
     * 核心方法:构建操作日志实体(组装日志的所有字段,满足审计需求)
     */
    private OperationLog buildOperationLog(HttpServletRequest request, OperationTypeEnum operationType, ProceedingJoinPoint joinPoint) {
        try {
            log.debug("开始构建日志实体:操作类型={}", operationType.getDesc());
            
            // 1. 获取当前登录用户信息(日志必须记录"谁操作的")
            Long userId = AuthUtil.getUserId();  // 从上下文获取用户ID(比如Token解析)
            if (userId == null) {
                log.error("日志构建失败:未获取到当前登录用户ID");
                return null;
            }
            User user = userService.getById(userId);  // 查询用户详情
            if (user == null) {
                log.error("日志构建失败:用户不存在,用户ID:{}", userId);
                return null;
            }
            log.debug("获取当前用户信息:用户ID={},用户名={}", userId, user.getRealName());

            // 2. 初始化日志实体
            OperationLog operationLog = new OperationLog();
            Date now = new Date();  // 操作时间(统一用当前时间)

            // 3. 接口相关字段
            operationLog.setApiPath(request.getRequestURI());  // 接口路径(如 /customer/add)
            operationLog.setRequestParamJson(JSONUtil.toJsonStr(joinPoint.getArgs()));  // 入参JSON(便于问题追溯)

            // 4. 菜单相关字段(从请求头获取menuId,查询一级/二级菜单)
            setMenuInfo(operationLog, request);

            // 5. 操作类型相关字段(解析后的枚举值)
            operationLog.setFunctionDesc(operationType.getDesc());  // 操作描述(如 "新增客户")
            operationLog.setFunctionCode(operationType.getCode());  // 操作编码(如 "add")

            // 6. 公共字段(创建人、时间、租户ID等,满足多租户和数据权限需求)
            operationLog.setCreateUser(userId);  // 创建人ID
            operationLog.setCreateUserName(user.getRealName());  // 创建人姓名
            operationLog.setCreateTime(now);  // 创建时间
            operationLog.setUpdateUser(userId);  // 更新人ID
            operationLog.setUpdateTime(now);  // 更新时间
            operationLog.setTenantId(user.getTenantId());  // 租户ID(多租户系统必需)
            // 部门ID(容错处理:用户可能没有部门)
            operationLog.setCreateDept(Objects.nonNull(user.getDeptId()) && StringUtils.isNotBlank(user.getDeptId())
                    ? Long.parseLong(user.getDeptId())
                    : null);

            log.debug("日志实体构建成功:{}", JSONUtil.toJsonStr(operationLog));
            return operationLog;
        } catch (Exception e) {
            log.error("构建日志实体失败,入参:{}", JSONUtil.toJsonStr(joinPoint.getArgs()), e);
            return null;
        }
    }

    /**
     * 辅助方法:设置菜单信息(日志分类用,比如"客户管理→新增客户")
     */
    private void setMenuInfo(OperationLog operationLog, HttpServletRequest request) {
        // 从请求头获取menuId(前端传入,标识当前操作所属菜单)
        String menuId = request.getHeader("menuId");
        log.debug("从请求头获取menuId:{}", menuId);
        
        // 校验menuId不为空
        if (StringUtils.isBlank(menuId)) {
            log.warn("菜单信息获取失败:请求头无menuId");
            return;
        }

        // 设置菜单ID(字符串转Long,容错处理)
        operationLog.setMenuId(Long.parseLong(menuId));
        // 查询当前菜单(二级菜单,比如"新增客户")
        Menu currentMenu = menuService.getById(menuId);
        if (Objects.nonNull(currentMenu)) {
            operationLog.setSecondMenuCode(currentMenu.getCode());  // 二级菜单编码
            operationLog.setSecondMenuName(currentMenu.getName());  // 二级菜单名称

            // 查询一级菜单(比如"客户管理",通过父ID关联)
            if (Objects.nonNull(currentMenu.getParentId())) {
                Menu parentMenu = menuService.getById(currentMenu.getParentId());
                if (Objects.nonNull(parentMenu)) {
                    operationLog.setFirstMenuCode(parentMenu.getCode());  // 一级菜单编码
                    operationLog.setFirstMenuName(parentMenu.getName());  // 一级菜单名称
                }
            }
            log.debug("菜单信息设置成功:一级菜单={},二级菜单={}", operationLog.getFirstMenuName(), operationLog.getSecondMenuName());
        } else {
            log.warn("菜单信息获取失败:menuId[{}]未查询到菜单", menuId);
        }
    }
}

二、逐段解析:工作原理+设计亮点

1. 注解类 @OperationApiLog:配置驱动的核心

工作原理

注解本质是"带参数的标记",作用是告诉 AOP 切面:"这个方法需要记录日志,并且按我配置的规则来记录"。

比如你给"新增客户"接口加注解:

java 复制代码
@PostMapping("/add")
@OperationApiLog(operationType = OperationTypeEnum.ADD)
public Result addCustomer(CustomerParam param) {
    // 业务逻辑
}

切面拦截到这个方法后,会通过反射获取 operationType=ADD,就知道要记录"新增客户"的日志。

设计亮点
  • 场景全覆盖 :用 5 个参数覆盖三种核心场景,不用写重复代码:
    • 静态场景:operationType 直接指定操作类型,适合简单接口;
    • 动态场景:operationTypes+paramKey 支持同一接口多操作,适合复杂接口(如审核);
    • 兼容场景:valueMap 解决历史系统非枚举入参问题,不用修改老代码。
  • 优先级明确:从高到低的优先级规则,确保"特殊场景覆盖通用场景",同时有兜底逻辑。
  • 配置极简:按需配置参数,简单场景只需要 1 个参数,复杂场景才需要多参数组合。

2. 切面类核心组件:切点 + 环绕通知

(1)切点:精准拦截,不浪费性能
工作原理

@Pointcut 注解定义了"拦截规则",只拦截 controller 包下带 @OperationApiLog 注解的方法。

为什么这么设计?因为只有接口方法需要记录操作日志,Service 层、Dao 层的方法不需要,精准拦截能减少 AOP 对系统性能的影响。

设计亮点
  • 语法精准 :用 execution 表达式限定包路径,用 @annotation 叠加注解匹配,避免"误拦截";
  • 解耦 :切点单独定义,后续通知(如 @Around)可以直接引用,便于维护。
(2)环绕通知:先业务,后日志
工作原理

@Around 是 AOP 中功能最强的通知,能在方法执行前、执行后做操作。这里的流程是:

  1. 执行 joinPoint.proceed():调用目标接口方法(比如新增客户),先让业务逻辑完成;
  2. 处理日志:获取注解配置、解析操作类型、构建日志实体、异步保存;
  3. 返回业务结果:无论日志处理成功与否,都返回接口的响应结果,不影响用户体验。
设计亮点
  • 业务优先:先执行业务,再记录日志,避免"日志记录失败导致业务回滚"(比如日志保存失败,用户新增的客户数据不会丢);
  • 异步解耦:用线程池异步保存日志,接口响应时间不受日志 IO 操作影响(比如接口执行 10ms,日志保存 50ms,用户只需等 10ms);
  • 容错性极强 :多层 if (null) 校验 + try-catch 捕获异常,日志处理失败绝不影响业务,也不会导致接口报错。

3. 核心方法:resolveOperationType(操作类型解析)

工作原理

这是整个框架的"大脑",负责按优先级匹配操作类型,我们用一个实际场景拆解:

假设接口注解配置:

java 复制代码
@OperationApiLog(
    paramKey = "status",
    operationTypes = {OperationTypeEnum.ENABLE, OperationTypeEnum.DISABLE},
    valueMap = {"1=enable", "0=disable"}
)
public Result updateStatus(Integer status) { ... }

当用户传入 status=1 时:

  1. 先调用 getParamValue 获取入参值 1
  2. 解析 valueMap 得到 1→enable,再通过 matchByCode 找到 OperationTypeEnum.ENABLE
  3. 直接返回该枚举,后续步骤不执行。

如果用户传入 status=2(未配置在 valueMapoperationTypes 中):

  1. valueMap 匹配失败;
  2. operationTypes 匹配失败;
  3. 返回 operationType 兜底值(默认 NONE,实际项目可配置"未知操作"枚举)。
设计亮点
  • 优先级穿透:从最高优先级到最低优先级依次尝试,确保"能匹配则匹配,匹配不到则兜底";
  • 日志清晰:每个步骤都打印日志,便于问题排查(比如"为什么匹配到的是默认操作类型",看日志就知道是入参值未配置);
  • 兼容强:支持"入参值→自定义映射→枚举"和"入参值→枚举候选集"两种匹配方式,覆盖绝大多数场景。

4. 辅助方法:getParamValue(入参值获取)

工作原理

负责从接口入参中提取 paramKey 对应的值,支持两种入参格式:

  • 直接参数名:比如 paramKey="status",接口入参是 Integer status,直接返回 status 的值;
  • 嵌套属性:比如 paramKey="approveParam.status",接口入参是 ApproveParam approveParam,通过反射获取 approveParam.getStatus() 的值。
设计亮点
  • 支持嵌套属性:解决"入参是对象,需要获取对象属性"的场景,不用手动拆解对象;
  • 反射增强getDeclaredField 递归查找父类字段,避免"属性在父类中无法获取"的问题;
  • 容错处理:入参为空、参数名不存在、反射失败等场景都有日志提示,便于调试。

5. 日志实体构建:buildOperationLog(审计级日志)

工作原理

组装日志的所有字段,满足"审计、统计、追溯"三大需求:

  • 审计:记录操作人、部门、租户,确保"谁操作的"可追溯;
  • 统计:记录一级/二级菜单、操作类型,便于按"菜单维度""操作类型维度"统计;
  • 追溯:记录接口路径、入参JSON、操作时间,便于排查问题(比如用户说"操作失败",可以通过日志看入参是否正确)。
设计亮点
  • 字段完整:覆盖审计所需的所有核心字段,不用后续补充;
  • 多租户支持tenantId 字段适配多租户系统,避免日志数据混乱;
  • 容错处理:用户不存在、菜单不存在等场景都有日志提示,且不会导致日志实体构建失败(缺省对应字段)。

三、总结:这套框架的核心价值

  1. 无侵入式开发:业务代码只需加一个注解,不用写任何日志相关代码,降低开发成本;
  2. 场景全覆盖:静态/动态/历史系统三种场景都能支持,不用为不同场景写不同的日志逻辑;
  3. 高性能:异步保存日志+精准拦截,对系统性能影响极小;
  4. 高可用:多层容错+异常捕获,日志处理失败不影响业务;
  5. 可维护性:分层解耦(注解配置→切面拦截→逻辑解析→日志保存),每个组件职责单一,便于后续扩展(比如新增"操作结果记录""耗时统计"等功能)。

如果你的项目需要记录操作日志,这套框架可以直接复用,只需根据业务调整 OperationTypeEnum(操作类型枚举)和 OperationLog(日志实体)的字段即可。

如果还有哪个细节没看懂,或者想知道"如何扩展这套框架"(比如新增日志脱敏、自定义保存逻辑),欢迎在评论区留言!

相关推荐
r***11331 小时前
【Java EE】Spring请求如何传递参数详解
spring·java-ee·lua
Linux运维技术栈1 小时前
生产环境资源占用过高排查实战:从Heap Dump到全链路优化
java·服务器·网络·数据库·程序
带刺的坐椅1 小时前
Solon v3.7 黑科技: 消灭空指针异常!
java·ai·solon·jspecify
VX:Fegn08951 小时前
计算机毕业设计|基于springboot+vue的健康饮食管理系统
java·vue.js·spring boot·后端·课程设计
l***46681 小时前
Spring之DataSource配置
java·后端·spring
Hubert-hui1 小时前
技术文章推荐
java·开发语言
C++业余爱好者1 小时前
Java Stream API介绍
java·windows·python
家人的拥抱1 小时前
【JAVA】经典的生产者-消费者
java·开发语言
SamRol1 小时前
ThreadLocal、Sychronized和ReentrantLock
java