在微服务和高并发架构下,日志是排查问题的“救命稻草”。本文将讲解如何利用 aop、mdc 等技术,构建一套包含请求追踪、出入参记录、敏感信息脱敏的全局日志系统。

一、 为什么需要全局日志和链路追踪?
想象一下这样的场景:用户反馈下单失败,客服给你一个订单号。你开始查日志…
- 如果不带唯一标识:你需要在一堆杂乱的并发请求中找到属于那个用户的那一次请求,犹如大海捞针。
- 如果接口多:如果涉及多个服务调用,你需要跨机器拼接日志片段。
为了解决这些问题,我们需要:
- 链路追踪 (traceid) :为每个请求生成唯一的 id,贯穿整个处理流程,串联起所有相关日志。
- 全局日志 :自动记录接口入参、出参、耗时,减少手动写
log.info(...)的工作量。
二、 核心组件:mdc (mapped diagnostic context)
mdc 是 slf4j 提供的一个机制,它实际上是一个线程安全的 threadlocal<map>。我们可以往里面放入键值对(如 traceid),然后在 logback 配置中通过 %x{key} 的格式输出。
1. 生成 traceid
我们需要一个工具类来管理 traceid。
public class traceidutil {
private static final string trace_id_key = "traceid";
/**
* 获取当前线程的 traceid
*/
public static string gettraceid() {
string traceid = mdc.get(trace_id_key);
return stringutils.isempty(traceid) ? "" : traceid;
}
/**
* 设置 traceid
*/
public static void settraceid(string traceid) {
mdc.put(trace_id_key, traceid);
}
/**
* 清除 traceid (非常重要,防止线程池复用导致数据污染)
*/
public static void removetraceid() {
mdc.remove(trace_id_key);
}
}
2. 通过 filter 拦截请求并设置 traceid
在请求进入 controller 之前,我们通过一个 filter 来生成并设置 traceid。
@slf4j
@component
@order(ordered.highest_precedence) // 确保最先执行
public class traceidfilter implements filter {
@override
public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception {
httpservletrequest httprequest = (httpservletrequest) request;
try {
// 尝试从请求头获取 traceid(适用于网关传递过来的场景)
string traceid = httprequest.getheader("x-trace-id");
if (stringutils.isempty(traceid)) {
traceid = uuid.randomuuid().tostring().replace("-", "");
}
traceidutil.settraceid(traceid);
chain.dofilter(request, response);
} finally {
// 务必清理,防止内存泄漏或线程池复用导致的脏数据
traceidutil.removetraceid();
}
}
}
3. 配置 logback 输出格式
修改 logback-spring.xml,在 pattern 中加入 %x{traceid}:
<configuration>
<property name="log_pattern" value="%d{yyyy-mm-dd hh:mm:ss.sss} [%thread] [%x{traceid}] %-5level %logger{36} - %msg%n"/>
<appender name="console" class="ch.qos.logback.core.consoleappender">
<encoder>
<pattern>${log_pattern}</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
</root>
</configuration>现在,你的日志输出将类似这样:2023-10-01 12:00:00.001 [http-nio-8080-exec-1] [a1b2c3d4e5f6...] info c.e.controller.usercontroller - user created successfully
三、 aop 实现全局接口日志
虽然 mdc 帮我们串联了日志,但我们还需要知道每个接口的入参、出参和耗时。手动打印太麻烦,使用 aop (aspect oriented programming) 是最好的选择。
1. 引入依赖与启用 aop
确保引入了 spring-boot-starter-aop。
2. 编写日志切面
我们定义一个切面,拦截所有 controller 层的 public 方法。
@slf4j
@aspect
@component
public class controllerlogaspect {
// 定义切点:拦截 controller 包下的所有方法
@pointcut("execution(public * com.example..controller..*.*(..))")
public void controllerlog() {}
@around("controllerlog()")
public object doaround(proceedingjoinpoint joinpoint) throws throwable {
long starttime = system.currenttimemillis();
// 获取请求信息
servletrequestattributes attributes = (servletrequestattributes) requestcontextholder.getrequestattributes();
httpservletrequest request = attributes.getrequest();
// 构建日志对象
loginfo loginfo = new loginfo();
loginfo.settraceid(traceidutil.gettraceid());
loginfo.setmethod(request.getmethod());
loginfo.seturl(request.getrequesturi());
loginfo.setip(request.getremoteaddr());
loginfo.setclassmethod(joinpoint.getsignature().getdeclaringtypename() + "." + joinpoint.getsignature().getname());
// 获取入参
object[] args = joinpoint.getargs();
// 注意:过滤掉 httpservletrequest, httpservletresponse 等无法序列化的参数
object[] logargs = arrays.stream(args)
.filter(arg -> !(arg instanceof httpservletrequest) && !(arg instanceof httpservletresponse))
.toarray();
loginfo.setargs(logargs);
// 打印入参日志
log.info("request info: {}", json.tojsonstring(loginfo));
object result;
try {
result = joinpoint.proceed();
} catch (exception e) {
// 记录异常日志(通常由全局异常处理器处理,这里也可以记录一份用于审计)
log.error("controller exception: ", e);
throw e;
}
// 计算耗时
long costtime = system.currenttimemillis() - starttime;
loginfo.setcosttime(costtime);
loginfo.setresult(result);
// 打印出参日志
log.info("response info ({}ms): {}", costtime, json.tojsonstring(loginfo));
return result;
}
@data
public static class loginfo {
private string traceid;
private string method;
private string url;
private string ip;
private string classmethod;
private object[] args;
private object result;
private long costtime;
}
}
四、 敏感信息脱敏
在实际生产环境中,日志里绝对不能出现明文密码、身份证号、手机号等敏感信息。我们可以在序列化(如 jackson 或 fastjson)输出日志前进行处理。
自定义脱敏序列化器 (以 jackson 为例)
public class sensitivedataserializer extends jsonserializer<string> {
@override
public void serialize(string value, jsongenerator gen, serializerprovider serializers) throws ioexception {
if (stringutils.isempty(value)) {
gen.writenull();
return;
}
// 简单的脱敏逻辑:保留前 3 后 4,中间星号
if (value.length() > 8) {
gen.writestring(value.substring(0, 3) + "****" + value.substring(value.length() - 4));
} else {
gen.writestring("****");
}
}
}
使用方式:在 dto 的敏感字段上添加注解
public class userdto {
@jsonserialize(using = sensitivedataserializer.class)
private string phone;
// ...
}
或者在 aop 中通过正则替换整个 json 字符串后再打印。
五、 最佳实践
- 异步日志 (asyncappender) :生产环境日志量巨大,同步打印日志会阻塞业务线程。务必在 logback 配置中使用
asyncappender包装console和fileappender。 - 线程池透传 mdc :如果你的代码使用了
@async或自定义线程池,mdc 的值不会自动传递到子线程。你需要实现taskdecorator或threadpooltaskexecutor的包装类,在提交任务时拷贝 mdc 上下文,执行完毕后恢复。 - 避免打印大对象 :如果入参包含大文件流(multipartfile)或巨大的列表,不要直接打印 json,会撑爆日志文件。在 aop 中判断参数类型进行过滤。
- 日志级别控制 :不要把所有日志都设为 info。开发环境可以是 debug,生产环境建议 info 或 warn,error 必须报警。
六、 总结
通过 mdc + filter + aop 的组合拳,我们实现了:
- 全链路追踪:每个请求都有唯一的 traceid。
- 自动审计 :无需修改业务代码,自动记录接口出入参和耗时。
- 数据安全 :敏感信息脱敏,避免合规风险。
这套方案是构建生产级 springboot 应用的标配,掌握它将为你的系统可观测性打下坚实基础。
到此这篇关于springboot中全局日志记录与请求链路追踪实战教学的文章就介绍到这了,更多相关springboot全局日志系统内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论