深度解析!Spring AOP 操作日志框架:从注解到切面的全流程拆解
今天带大家啃一个实战性极强的 Spring AOP 框架------操作日志自动记录组件。这套代码看似复杂,实则是"配置驱动+分层解耦"的典范,兼容静态/动态/历史系统三种场景,还能保证接口性能不降级。
接下来我会分两步走:
- 先给核心代码加上 超详细注释+关键日志打印,让大家一眼看懂每行代码的作用;
- 再逐段解析"工作原理+设计亮点",拆解背后的编程思想,让你不仅会用,还能举一反三。
话不多说,直接上代码+解析!
一、核心代码(带详细注释+日志打印)
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 中功能最强的通知,能在方法执行前、执行后做操作。这里的流程是:
- 执行
joinPoint.proceed():调用目标接口方法(比如新增客户),先让业务逻辑完成; - 处理日志:获取注解配置、解析操作类型、构建日志实体、异步保存;
- 返回业务结果:无论日志处理成功与否,都返回接口的响应结果,不影响用户体验。
设计亮点
- 业务优先:先执行业务,再记录日志,避免"日志记录失败导致业务回滚"(比如日志保存失败,用户新增的客户数据不会丢);
- 异步解耦:用线程池异步保存日志,接口响应时间不受日志 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 时:
- 先调用
getParamValue获取入参值1; - 解析
valueMap得到1→enable,再通过matchByCode找到OperationTypeEnum.ENABLE; - 直接返回该枚举,后续步骤不执行。
如果用户传入 status=2(未配置在 valueMap 和 operationTypes 中):
valueMap匹配失败;operationTypes匹配失败;- 返回
operationType兜底值(默认NONE,实际项目可配置"未知操作"枚举)。
设计亮点
- 优先级穿透:从最高优先级到最低优先级依次尝试,确保"能匹配则匹配,匹配不到则兜底";
- 日志清晰:每个步骤都打印日志,便于问题排查(比如"为什么匹配到的是默认操作类型",看日志就知道是入参值未配置);
- 兼容强:支持"入参值→自定义映射→枚举"和"入参值→枚举候选集"两种匹配方式,覆盖绝大多数场景。
4. 辅助方法:getParamValue(入参值获取)
工作原理
负责从接口入参中提取 paramKey 对应的值,支持两种入参格式:
- 直接参数名:比如
paramKey="status",接口入参是Integer status,直接返回status的值; - 嵌套属性:比如
paramKey="approveParam.status",接口入参是ApproveParam approveParam,通过反射获取approveParam.getStatus()的值。
设计亮点
- 支持嵌套属性:解决"入参是对象,需要获取对象属性"的场景,不用手动拆解对象;
- 反射增强 :
getDeclaredField递归查找父类字段,避免"属性在父类中无法获取"的问题; - 容错处理:入参为空、参数名不存在、反射失败等场景都有日志提示,便于调试。
5. 日志实体构建:buildOperationLog(审计级日志)
工作原理
组装日志的所有字段,满足"审计、统计、追溯"三大需求:
- 审计:记录操作人、部门、租户,确保"谁操作的"可追溯;
- 统计:记录一级/二级菜单、操作类型,便于按"菜单维度""操作类型维度"统计;
- 追溯:记录接口路径、入参JSON、操作时间,便于排查问题(比如用户说"操作失败",可以通过日志看入参是否正确)。
设计亮点
- 字段完整:覆盖审计所需的所有核心字段,不用后续补充;
- 多租户支持 :
tenantId字段适配多租户系统,避免日志数据混乱; - 容错处理:用户不存在、菜单不存在等场景都有日志提示,且不会导致日志实体构建失败(缺省对应字段)。
三、总结:这套框架的核心价值
- 无侵入式开发:业务代码只需加一个注解,不用写任何日志相关代码,降低开发成本;
- 场景全覆盖:静态/动态/历史系统三种场景都能支持,不用为不同场景写不同的日志逻辑;
- 高性能:异步保存日志+精准拦截,对系统性能影响极小;
- 高可用:多层容错+异常捕获,日志处理失败不影响业务;
- 可维护性:分层解耦(注解配置→切面拦截→逻辑解析→日志保存),每个组件职责单一,便于后续扩展(比如新增"操作结果记录""耗时统计"等功能)。
如果你的项目需要记录操作日志,这套框架可以直接复用,只需根据业务调整 OperationTypeEnum(操作类型枚举)和 OperationLog(日志实体)的字段即可。
如果还有哪个细节没看懂,或者想知道"如何扩展这套框架"(比如新增日志脱敏、自定义保存逻辑),欢迎在评论区留言!