当前位置: 首页 > news >正文

annotation-logging-guide

基于注解的日志记录实现指南(Java / Spring Boot + AOP)

本文档提供一套“注解 + AOP”的实现方案,包含设计要点、依赖、核心代码、配置、使用方式、扩展与常见问题。复制本页即可在你的项目中落地;文末附完整示例结构建议。


完整代码参考gitee地址: https://gitee.com/xizhyu66/log-annotation

1. 目标与设计原则

目标

  • 通过 @Loggable 注解,精确控制哪些方法/类需要记录日志。
  • 统一记录:入参(可脱敏)、出参、耗时、异常、traceId。
  • 低侵入:对业务代码改动最小;可逐步接入。
  • 易扩展:支持自定义脱敏策略、JSON/控制台输出、接入链路追踪。

设计原则

  • AOP @Around 环绕通知收集上下文。
  • 使用 SLF4J + Logback 输出;MDC 放置 traceId
  • 对大对象/敏感数据做安全处理,避免日志爆量或泄露。

2. 依赖(Maven)

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

建议 Spring Boot 3.x,JDK 17+。


3. 定义注解 @Loggable

package com.example.logging;import java.lang.annotation.*;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {/** 是否记录入参 */boolean logArgs() default true;/** 是否记录返回值 */boolean logResult() default true;/** 业务标签,便于筛选 */String tag() default "";
}
  • 可作用于方法;方法优先级高于类。

4. 生成 traceId(可选但强烈推荐)

package com.example.logging;import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;
import java.util.UUID;@Component
public class TraceIdFilter extends OncePerRequestFilter {public static final String TRACE_ID = "traceId";@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {String traceId = UUID.randomUUID().toString().replace("-", "");MDC.put(TRACE_ID, traceId);try {response.setHeader("X-Trace-Id", traceId);chain.doFilter(request, response);} finally {MDC.remove(TRACE_ID);}}
}
  • 每次 HTTP 请求产生独立 traceId,通过 X-Trace-Id 返回到客户端。非 Web 场景可在调用入口(如 MQ/定时任务)手动放入/清理 MDC

5. 日志切面 LoggingAspect

package com.example.logging;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;@Aspect
@Component
public class LoggingAspect {private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);private static final String[] SENSITIVE_KEYS = {"password", "secret", "token", "accessToken", "refreshToken","authorization", "auth", "passwd", "pwd", "creditCard"};@Around("@annotation(com.example.logging.Loggable) || @within(com.example.logging.Loggable)")public Object around(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();MethodSignature sig = (MethodSignature) pjp.getSignature();Method method = sig.getMethod();Loggable ann = method.getAnnotation(Loggable.class);if (ann == null) ann = pjp.getTarget().getClass().getAnnotation(Loggable.class);String methodName = sig.getDeclaringType().getSimpleName() + "." + method.getName();String tag = ann != null ? ann.tag() : "";boolean logArgs = ann == null || ann.logArgs();boolean logResult = ann == null || ann.logResult();Map<String, Object> argMap = buildArgsMap(sig.getParameterNames(), pjp.getArgs(), logArgs);String traceId = MDC.get("traceId");log.info("➡️ Enter {} tag={} traceId={} args={}", methodName, tag, traceId, argMap);Object result = null;Throwable error = null;try {result = pjp.proceed();return result;} catch (Throwable t) {error = t;throw t;} finally {long cost = System.currentTimeMillis() - start;if (error == null) {if (logResult) {log.info("✅ Exit {} traceId={} cost={}ms result={}", methodName, traceId, cost, safeToString(result));} else {log.info("✅ Exit {} traceId={} cost={}ms", methodName, traceId, cost);}} else {log.error("❌ Error {} traceId={} cost={}ms ex={} msg={}", methodName, traceId, cost,error.getClass().getSimpleName(), error.getMessage(), error);}}}private Map<String, Object> buildArgsMap(String[] names, Object[] values, boolean logArgs) {Map<String, Object> map = new HashMap<>();if (!logArgs) return map;if (names == null || values == null) return map;IntStream.range(0, Math.min(names.length, values.length)).forEach(i -> {String name = names[i];Object value = values[i];if (value == null) { map.put(name, null); return; }// 避免打印大对象/不安全类型if (value instanceof org.springframework.web.multipart.MultipartFile) { map.put(name, "[MultipartFile]"); return; }if (value instanceof jakarta.servlet.http.HttpServletRequest) { map.put(name, "[HttpServletRequest]"); return; }if (value instanceof jakarta.servlet.http.HttpServletResponse) { map.put(name, "[HttpServletResponse]"); return; }// 按名称启发式脱敏if (isSensitiveKey(name)) { map.put(name, "******"); return; }// 常见 Header 容器脱敏if (value instanceof HttpHeaders headers) {HttpHeaders masked = new HttpHeaders();headers.forEach((k, v) -> masked.put(k, isSensitiveKey(k) ? Arrays.asList("******") : v));map.put(name, masked);return;}map.put(name, safeToString(value));});return map;}private boolean isSensitiveKey(String key) {String k = key == null ? "" : key.toLowerCase();for (String s : SENSITIVE_KEYS) if (k.contains(s.toLowerCase())) return true;return false;}private String safeToString(Object obj) {if (obj == null) return "null";try { return String.valueOf(obj); }catch (Throwable t) { return obj.getClass().getName() + "@(toString-error)"; }}
}

6. 应用与示例

控制器(类级别开启日志)

package com.example.demo.web;import com.example.logging.Loggable;
import com.example.demo.service.DemoService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@Loggable(tag = "controller")
public class DemoController {private final DemoService demoService;public DemoController(DemoService demoService) { this.demoService = demoService; }@GetMapping("/api/echo")public String echo(@RequestParam String text,@RequestParam(required = false, defaultValue = "secret") String password) {return demoService.echo(text, password);}
}

服务(方法级别详细控制)

package com.example.demo.service;import com.example.logging.Loggable;
import org.springframework.stereotype.Service;@Service
public class DemoService {@Loggable(tag = "biz:echo", logArgs = true, logResult = true)public String echo(String text, String password) {return "echo:" + text;}
}

7. 日志输出配置

application.yml

logging:level:root: INFOcom.example: INFOpattern:console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - traceId=%X{traceId} - %msg%n"

若接入 ELK/可观测平台,建议改用 JSON 编码(见 §9 扩展)。

示例输出

➡️ Enter DemoService.echo tag=biz:echo traceId=7d2... args={text=hello, password=******}
✅ Exit DemoService.echo traceId=7d2... cost=2ms result=echo:hello

8. 最佳实践

  • 只打必要的日志:对高频方法进行抽样或关闭出参日志以减少 I/O。
  • 限制对象深度:大对象建议输出关键字段或 ID;避免序列化巨型集合。
  • 明确敏感键:根据安全要求扩展 SENSITIVE_KEYS;如需字段级更精确控制,可引入 @Masked 注解。
  • 链路一致性:跨线程/异步时手动透传 MDC(使用 TaskDecorator 或自定义包装器)。
  • 错误场景:异常堆栈务必打出(log.error(..., e)),避免只输出 message。

9. 扩展方向

  • JSON 日志:使用 logstash-logback-encoder 输出 JSON,便于 ELK / Loki 检索与聚合。
  • 动态开关:配合配置中心动态调整某些包或方法的日志级别。
  • 注解参数:可追加 sampleRate()maxArgLength() 等属性,灵活控制产出量。
  • 统一出参包装:结合响应包装器/拦截器统一补充 traceId

10. 常见问题(FAQ)

Q: 为什么我的参数名是 arg0/arg1
A: 需要在编译器开启参数名保留(Maven maven-compiler-plugin 增加 <parameters>true</parameters>),或通过 @RequestParam 显式命名。

Q: 非 HTTP 场景如何拿到 traceId?
A: 在任务入口(定时任务、MQ 监听)生成并放入 MDC,执行完成后记得清理。

Q: 大文件/流如何处理?
A: 切面中直接用占位符 [MultipartFile][InputStream],避免读流。


11. 参考的 pom.xml 片段(含编译参数)

<properties><java.version>17</java.version>
</properties><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><configuration><source>${java.version}</source><target>${java.version}</target><release>${java.version}</release><parameters>true</parameters></configuration></plugin></plugins>
</build>

12. 目录建议(落地到你的项目)

src/main/java
└─ com/example├─ logging│  ├─ Loggable.java            # 注解│  ├─ LoggingAspect.java       # 切面│  └─ TraceIdFilter.java       # 请求级 traceId(可选)└─ demo├─ web/DemoController.java└─ service/DemoService.java
src/main/resources
└─ application.yml                 # 日志级别与输出格式


http://www.dtcms.com/a/540611.html

相关文章:

  • 没有公司做网站重庆网站推广入口
  • 管理k8s的资源类型(PV/PVC)的脚本
  • 【记录】飞书多维表格|做自动显示当前填写情况(包括填写人数、未填写情况、最高分和平均分)的收集表
  • 清除入侵痕迹(winLinuxweb)
  • 找设计师的网站淘宝客 网站建设
  • 第六部分:VTK进阶(第175章 并行 IO管线与检查点)
  • 河南海绵城市建设网站wordpress中文版书籍
  • Opencv(三): 二值化
  • GitHub使用技巧——上传本地项目
  • 网站建设用途一个旅游网站建设需求分析
  • 甜品网站网页设计代码网上免费推广
  • 渗透测试工具 windows上搭建vmware kali-linux
  • ecstore等产品开启缓存-后台及前台不能登录原因-setcookie+session问题
  • 哨兵原理、Redis分片、Redis数据结构、内存回收、缓存问题以及分布式事务相关内容(CAP、BASE、AT脏写及其解决、TCC、最大努力通知)
  • Windows图标修复--缓存重建教程
  • 服务器上用Slurm 管理训练bash 脚本任务,申明使用GPU
  • 上海小程序网站开发公司wordpress国外主题下载地址
  • 新城镇建设官方网站kali建设网站
  • 便携式el检测仪:确保光伏组件的质量与性能稳定
  • 英一2015年真题学习笔记
  • Docker 部署银河麒麟(Kylin Linux)全流程教程
  • GPT、DeepSeek等大语言模型应用
  • 大语言模型基础LLM:Transformer和大模型
  • 本地用docker开发的php 程序如何部署到阿里云的ecs上
  • html css js网页制作成品——一念关山HTML+CSS网页设计(5页)附源码
  • BuildingAI二开 Coze套餐管理页面PRD
  • 加强学院网站的建设与管理一个完整的外贸流程
  • 大屏开发,在线歌词舆情分析系统demo,基于python,flask,web,echart,nlp,自然语言数据库mysql。
  • 网站后台上传缩略图高端网站开发哪家强
  • SAP GUI 800进行品牌化设置