作者:不想打工的码农 原创声明:本文基于笔者在金融项目中的真实实践,所有代码经生产环境验证,拒绝纸上谈兵
一、痛点直击:你是否也这样写日志?
@PostMapping("/user")
public Result createUser(@RequestBody User user) {
log.info("【创建用户】参数: {}", JSON.toJSONString(user)); // 1
try {
userService.save(user);
log.info("【创建用户】成功, 用户ID: {}", user.getId()); // 2
return Result.ok();
} catch (Exception e) {
log.error("【创建用户】失败, 原因: {}", e.getMessage(), e); // 3
return Result.fail("操作失败");
}
}
灵魂三问: ❌ 业务代码被日志“腌入味”? ❌ 修改日志格式要改上百个方法? ❌ 敏感字段(密码/手机号)裸奔记录?
别急!今天手把手带你用 AOP+自定义注解 破局,亲测在日均千万级请求系统中稳定运行2年+。
二、核心设计思路(拒绝理论堆砌)
表格
| 侵入性 | 每个方法硬编码 | 仅需@OptLog("创建用户") |
| 维护成本 | 改一处需全局搜 | 改切面类一处生效 |
| 敏感信息 | 手动脱敏易遗漏 | 切面统一拦截处理 |
| 性能影响 | 同步阻塞 | 异步+线程池优化 |
为什么选自定义注解而非直接切Controller?
笔者踩坑实录:曾直接切execution(* com.xxx.controller..*.*(..)),结果Swagger接口、健康检查全被记录,日志量暴涨300%!精准控制才是生产环境王道。
三、实战四步走(附关键细节)
1️⃣ 定义灵魂注解(带操作类型枚举)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptLog {
String value() default ""; // 操作描述
OptType type() default OptType.OTHER; // 操作类型
enum OptType {
QUERY("查询"), SAVE("新增"), UPDATE("修改"), DELETE("删除"), EXPORT("导出"), OTHER("其他");
private final String desc;
OptType(String desc) { this.desc = desc; }
public String getDesc() { return desc; }
}
}
✨ 设计巧思:枚举类型让日志可被ELK按操作类型聚合分析,运维排查效率提升50%
2️⃣ 编写切面核心(重点处理异常与耗时)
@Aspect
@Component
@Slf4j
public class LogAspect {
// 异步线程池(避免阻塞业务)
private final ExecutorService logExecutor = new ThreadPoolExecutor(
2, 4, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
r -> new Thread(r, "async-log-thread"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Around("@annotation(optLog)")
public Object around(ProceedingJoinPoint pjp, OptLog optLog) throws Throwable {
long start = System.currentTimeMillis();
String methodName = pjp.getSignature().toShortString();
Object result = null;
Exception exception = null;
try {
result = pjp.proceed(); // 执行目标方法
return result;
} catch (Exception e) {
exception = e;
throw e; // 保证异常正常抛出
} finally {
// 异步记录日志(关键!)
logExecutor.execute(() -> buildAndSaveLog(pjp, optLog, result, exception,
System.currentTimeMillis() – start, methodName));
}
}
private void buildAndSaveLog(…) {
// 1. 参数脱敏(重点!)
String argsStr = JSON.toJSONString(pjp.getArgs(),
SerializerFeature.WriteMapNullValue,
// 自定义过滤器:密码/手机号脱敏
(o, fieldName, fieldType, features) -> {
if ("password".equals(fieldName) || "phone".equals(fieldName)) {
return "******";
}
return SerializerFeature.EMPTY;
});
// 2. 构建日志对象(含操作人、IP、耗时等)
SysLog log = SysLog.builder()
.optModule(getModuleName(pjp)) // 从包路径提取模块名
.optType(optLog.type().getDesc())
.optDesc(optLog.value())
.requestParam(argsStr)
.responseResult(exception == null ? JSON.toJSONString(result) : "异常:" + exception.getMessage())
.costTime(costTime)
.createTime(LocalDateTime.now())
.build();
// 3. 持久化(根据环境选择:开发控制台/生产存DB)
if (env.equals("prod")) {
sysLogService.saveAsync(log); // 异步存库
} else {
log.info("【操作日志】{}", JSON.toJSONString(log));
}
}
}
3️⃣ 业务代码清爽示例
@OptLog(value = "重置用户密码", type = OptLog.OptType.UPDATE)
@PostMapping("/resetPwd")
public Result resetPassword(@Valid @RequestBody ResetPwdDTO dto) {
// 业务逻辑干净得像刚洗过的代码
userService.resetPassword(dto.getUserId(), dto.getNewPassword());
return Result.ok("密码重置成功");
}
// 控制台输出:【操作日志】{"optModule":"用户管理","optType":"修改","optDesc":"重置用户密码",…,"requestParam":"{\\"userId\\":1001,\\"newPassword\\":\\"******\\"}"}
4️⃣ 生产环境加固(血泪经验)
- 防日志风暴:在切面开头加if (log.isInfoEnabled())判断
- 大对象处理:对MultipartFile等参数跳过序列化
- 线程安全:ThreadLocal存储操作人信息(配合拦截器)
- 监控告警:日志异常时推送企业微信(示例代码略,可私信索取)
四、效果对比 & 价值升华
表格
| 单接口代码量 | 15+行日志 | 0行侵入 |
| 新增日志需求 | 全局搜索修改 | 仅调整切面 |
| 敏感信息风险 | 高(依赖人工) | 低(统一拦截) |
| 排查效率 | 翻找业务日志 | 按操作类型精准筛选 |
不止于日志:此模式可复用于 ✅ 接口耗时监控(对接Prometheus) ✅ 操作审计留痕(满足等保要求) ✅ 灰度发布流量标记
五、写在最后
技术没有银弹,但解耦思维是永恒的利器。AOP不是炫技,而是让代码回归业务本质的工程实践。笔者在重构某银行核心系统时,仅用3天将200+接口日志统一治理,后续运维反馈“查问题像开了天眼”。
网硕互联帮助中心





评论前必须登录!
注册