首页 / 资讯中心 / 文章详情

Java统一日志切面实战:AspectJ+logback构建可追溯WebLog体系

Java统一日志切面实战:AspectJ+logback构建可追溯WebLog体系 ★ FEATURED ARTICLE
1. 项目概述为什么“统一日志处理切面”不是锦上添花而是系统稳定性的底层基建“统一日志处理切面”这八个字听上去像教科书里的概念名词但在我带过的十几个中大型Java项目里它从来不是写在PPT里的技术亮点而是每次线上告警凌晨三点被叫醒后第一个要翻看、比对、校验的那根“生命线”。它解决的不是“要不要记日志”的问题而是“日志能不能信、能不能用、能不能救火”的问题。核心关键词——统一日志、切面、WebLog、AspectJ、logback——每一个都不是孤立存在统一日志是目标是结果切面Aspect是实现路径是手段WebLog是最典型、最高频的落地场景AspectJ是技术选型的基石logback则是日志输出的最终执行者是整条链路的“发声器官”。我见过太多团队初期靠System.out.println打天下中期用logger.info(xxx)满天飞后期运维一查日志发现同一个用户操作在Controller层、Service层、DAO层、甚至第三方SDK里日志格式五花八门有的带traceId有的没有有的时间戳是毫秒有的是秒有的用中文“开始处理”有的用英文“Processing started”更别提SQL参数全被?代替根本看不出实际执行了什么。这种日志不是资产是噪音是故障排查时的“反向干扰器”。而“统一日志处理切面”就是一把手术刀它不改变业务代码一行逻辑却能从横切面cross-cutting concern上把所有分散的日志入口收束、标准化、结构化。它让日志从“谁写的算谁的”变成“系统说了算”。适合谁不是只给架构师看的而是给每一位每天要和日志打交道的开发、测试、运维同学准备的——当你不再需要在几十个类里逐个改logger.info不再需要猜某个DEBUG日志到底出自哪一层不再需要对着一团乱码般的日志文本抓耳挠腮时你就真正理解了这个切面的价值。它不是炫技是降本增效最实在的体现。2. 整体设计思路与方案选型深度拆解为什么是AspectJ logback而不是Spring AOP或SLF4J绑定2.1 切面技术栈的硬核对比AspectJ为何成为不可替代的“主刀医生”在Java生态里“切面”有两条主流技术路线Spring AOP和AspectJ。很多团队第一反应是“Spring AOP够用了”但我在三个高并发电商系统的日志重构项目中最终都坚定地选择了AspectJ原因非常具体且致命织入时机决定能力上限Spring AOP是运行时代理Runtime Proxy只能拦截Spring容器管理的Bean的public方法调用。这意味着如果你的Service层有个private方法被public方法调用或者你直接new了一个工具类对象去调用Spring AOP就完全失效。而AspectJ支持编译时织入ajc编译器和加载时织入LTW它是在字节码层面做修改能拦截任意方法、任意访问修饰符、任意调用来源。我们曾遇到一个支付回调的异步处理模块核心逻辑在一个Async方法里里面又调用了多个非Spring管理的工具类用Spring AOP根本无法覆盖日志断层严重。切换到AspectJ后所有调用链日志瞬间完整。性能损耗的量化差异我做过压测对比。在QPS 5000的订单创建接口上纯Spring AOP的环绕通知平均增加1.8ms延迟而AspectJ编译时织入的同等功能切面仅增加0.3ms。这0.3ms来自JVM对增强字节码的正常执行开销而1.8ms中的大部分是动态代理对象创建、反射调用、代理链维护的额外成本。对于毫秒级响应的系统这点差异就是SLA服务等级协议的生死线。WebLog场景的特殊性WebLog的核心诉求是“请求-响应全生命周期”的日志捕获包括Controller方法进入、参数解析、业务处理、异常抛出、HTTP响应返回。Spring AOP的Before/After无法精准捕获ExceptionHandler处理后的最终响应状态而AspectJ可以通过aroundadvice在proceed()前后精确控制甚至能在response.getWriter().write()之后再记录日志确保日志与真实HTTP状态码100%一致。提示AspectJ不是银弹它需要引入aspectjweaver.jar和配置aop.xmlLTW模式或使用aspectj-maven-plugin编译时织入。后者更推荐因为构建过程可控无运行时依赖风险。2.2 日志框架的终极抉择logback为何稳坐C位而非log4j2或slf4j-simpleSLF4J只是一个门面Facade真正的日志实现有logback、log4j2、JUL等。选择logback是基于它与Spring Boot的深度集成、极高的性能以及对结构化日志的原生支持性能基准无可争议logback的创始人正是log4j的作者Ceki Gülcü他为了解决log4j 1.x的性能瓶颈和线程安全问题亲自打造了logback。在Log4j2发布前logback是公认的最快日志框架。即使现在log4j2在异步日志上略有优势但logback的同步日志吞吐量依然领先且其AsyncAppender经过十年打磨稳定性远超早期log4j2的AsyncLogger。我们在一个日志峰值每秒2万条的风控系统中logbackAsyncAppenderRollingFileAppender的CPU占用率稳定在3%而同配置log4j2则波动在7%-12%。与Spring Boot的“零配置”默契Spring Boot 2.x默认日志实现就是logback。这意味着你无需额外引入依赖只需一个logback-spring.xml就能激活Spring Profile、变量替换、条件化配置等高级特性。比如springProfile nameprod标签可以让你在生产环境自动启用JSON格式日志在开发环境用彩色控制台日志这种开箱即用的体验是log4j2需要额外写Log4j2Configuration类才能勉强模拟的。结构化日志的基石能力WebLog的核心价值之一是日志可被ELKElasticsearch, Logstash, Kibana或LokiGrafana高效索引和查询。这要求日志必须是结构化的JSON。logback原生支持JsonLayout配合logback-access模块甚至能将Nginx级别的访问日志也纳入同一套体系。而SLF4J本身不提供任何布局Layout能力它只是把日志事件交给底层实现所以选logback就是选定了结构化日志的“高速公路”。注意logback的encoder配置是关键。PatternLayout适合开发调试JsonLayout才是生产标配。但JsonLayout默认会把整个MDCMapped Diagnostic Context内容扁平化输出如果MDC里有嵌套Map会变成字符串失去结构化意义。解决方案是自定义JsonLayout重写toJsonString()方法或使用logstash-logback-encoder这个成熟库它提供了LogstashEncoder能完美处理嵌套结构。2.3 “统一”的本质不是格式统一而是上下文统一与语义统一很多人误解“统一日志”就是让所有日志都长成一个样子比如都用[INFO] [2024-03-15 10:00:00.123] [traceIdabc123] [userId1001] ...。这仅仅是表层的“格式统一”。真正的“统一”是上下文统一和语义统一上下文统一指一次用户请求的所有日志必须共享同一个traceId、spanId、userId、requestId等标识。这靠的是MDCMapped Diagnostic Context。MDC是一个ThreadLocal Map切面在Controller方法入口处将HttpServletRequest中的X-B3-TraceId或自动生成放入MDC在方法退出时清空。这样后续所有logger.info()调用只要在PatternLayout里配置%X{traceId}就能自动带上。但难点在于异步线程——Async或CompletableFuture会丢失MDC。解决方案是AspectJ切面在Async方法入口手动将父线程的MDCcopy到子线程并在子线程结束时clear。这是统一日志最易被忽视的“断点”。语义统一指日志内容表达的业务含义必须一致。例如记录“用户登录成功”不能在Controller层写Login success for user: username在Service层又写User authenticated: userId。切面应该定义一套标准的WebLog事件模型WebLogEvent包含eventTypeLOGIN_SUCCESS, LOGIN_FAIL, ORDER_CREATE、statusSUCCESS, FAILED、durationMs、clientIp、userAgent等字段。切面只负责采集这些字段日志输出由JsonLayout按固定Schema序列化。这样运维在Kibana里搜索eventType: LOGIN_SUCCESS就能得到所有登录成功的记录无需正则匹配不同字符串。3. 核心细节解析与实操要点从WebLog切面到logback配置的每一处魔鬼细节3.1 WebLog切面的黄金三要素切入点Pointcut、通知Advice、织入Weaving一个健壮的WebLog切面绝不是简单地在Controller上加个Around。它必须精准、轻量、可配置。我总结出三个不可妥协的核心要素切入点Pointcut必须细粒度分层不能只写execution(* com.xxx.web..*.*(..))。这会导致所有Controller方法都被拦截包括健康检查/actuator/health、静态资源/static/**它们产生大量无意义日志。正确的做法是分层定义Pointcut(annotation(org.springframework.web.bind.annotation.RequestMapping) || annotation(org.springframework.web.bind.annotation.GetMapping) || annotation(org.springframework.web.bind.annotation.PostMapping))—— 只拦截有明确HTTP映射的方法。Pointcut(execution(* com.xxx.service..*.*(..)) !execution(* com.xxx.service..*.get*(..)) !execution(* com.xxx.service..*.find*(..)))—— 对Service层只记录写操作create/update/delete读操作get/find默认不记录避免日志爆炸。这需要在切面里通过Pointcut组合实现。通知Advice必须分离关注点一个Around方法里塞进所有逻辑记录请求、记录响应、记录异常、计算耗时、清理MDC是灾难。应该拆分为Before只做MDC初始化、startTime记录、请求头提取X-Forwarded-For,User-Agent。AfterReturning只记录响应状态码、响应体大小谨慎大JSON体不要记录、耗时。AfterThrowing只记录异常类型、消息、堆栈throwingex参数并标记statusFAILED。 这样每个通知职责单一易于单元测试也便于未来扩展比如单独为AfterThrowing添加告警通知。织入Weaving必须规避Classloader陷阱在Spring Boot的Fat Jar里aspectjweaver的LTWLoad-Time Weaving经常失败因为javaagent参数和ClassLoader层级冲突。我的经验是强制使用编译时织入CTW。在pom.xml中配置aspectj-maven-plugin并指定complianceLevel1.8/complianceLevel必须与项目Java版本一致。同时sources必须包含所有需要被切面的源码目录否则ajc编译器找不到目标类织入失败。一个常见坑是src/main/java下有com.xxx.web包但src/main/resources下的配置文件也被sources误包含导致编译报错。解决方案是显式指定sourcessourcesrc/main/java/source/sources。3.2 logback-spring.xml的生产级配置从控制台输出SQL到JSON日志的完整链条网络热词里提到“maven项目logback配置文件 查看控制台输出的sql”这恰恰暴露了日志配置的最大误区开发环境看SQL生产环境却不敢看因为日志量太大、格式太乱。一个真正统一的日志配置必须让SQL日志在开发和生产都“可控、可查、可过滤”。!-- logback-spring.xml -- ?xml version1.0 encodingUTF-8? configuration !-- 定义全局变量 -- springProperty scopecontext nameAPP_NAME sourcespring.application.name defaultValueunknown/ springProperty scopecontext namePROFILE sourcespring.profiles.active defaultValuedev/ !-- 控制台输出仅dev, test -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender filter classch.qos.logback.core.filter.EvaluatorFilter evaluator classch.qos.logback.core.boolex.OnMarkerEvaluator markerSQL/marker /evaluator onMatchDENY/onMatch onMismatchNEUTRAL/onMismatch /filter encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- SQL专用控制台仅dev -- appender nameSQL_CONSOLE classch.qos.logback.core.ConsoleAppender filter classch.qos.logback.core.filter.EvaluatorFilter evaluator classch.qos.logback.core.boolex.OnMarkerEvaluator markerSQL/marker /evaluator onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter encoder pattern%d{HH:mm:ss.SSS} [SQL] %msg%n/pattern /encoder /appender !-- JSON文件输出prod -- appender nameJSON_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/${APP_NAME}.json/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/${APP_NAME}.%d{yyyy-MM-dd}.%i.json/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy maxHistory30/maxHistory /rollingPolicy encoder classnet.logstash.logback.encoder.LogstashEncoder/ /appender !-- Root Logger -- root levelINFO appender-ref refCONSOLE/ appender-ref refJSON_FILE/ appender-ref refSQL_CONSOLE/ /root !-- MyBatis SQL日志 -- logger nameorg.apache.ibatis levelDEBUG additivityfalse appender-ref refSQL_CONSOLE/ appender-ref refJSON_FILE/ /logger logger nameorg.apache.ibatis.logging.jdbc.BaseJdbcLogger levelDEBUG additivityfalse appender-ref refSQL_CONSOLE/ appender-ref refJSON_FILE/ /logger /configuration这段配置的关键细节Marker过滤器是灵魂OnMarkerEvaluator允许你用logger.debug(SELECT * FROM user, MarkerFactory.getMarker(SQL))来标记SQL日志。这样CONSOLEappender会拒绝所有SQL日志DENY而SQL_CONSOLE只接受SQL日志ACCEPT。这比用Logger Name过滤更精准因为MyBatis的SQL日志可能分散在多个包名下。JSON日志的Encoder选择LogstashEncoder是业界事实标准它生成的JSON严格遵循Logstash的json_event格式timestamp、version、message、logger_name等字段开箱即用ELK摄入零配置。LogstashEncoder还支持customFields可以注入{app: ${APP_NAME}, env: ${PROFILE}}让日志自带环境上下文。SQL日志的双通道输出开发时SQL只输出到SQL_CONSOLE清晰不干扰生产时SQL_CONSOLE被Spring Profile禁用springProfile nameprod包裹SQL日志只进入JSON_FILE并通过LogstashEncoder的includeContextDatatrue选项将SQL语句作为sql_statement字段结构化存储方便在Kibana里用sql_statement: SELECT * FROM user WHERE id ?精确查询。实操心得LogstashEncoder的stackTraceAsArraytrue必须开启。默认的stack trace是单行字符串Kibana无法解析为数组导致告警规则无法匹配特定异常类。开启后stack trace变成stack_trace: [com.xxx.service.UserService.getUser(UserService.java:45), ...]告警规则可写stack_trace: com.xxx.exception.BusinessException。3.3 MDC上下文传递的终极方案穿透异步线程的“日志DNA”前面提到异步线程会丢失MDC。Async方法内部的logger.info()%X{traceId}会是空。这是统一日志最大的“断点”。网上常见的TaskDecorator方案只适用于ThreadPoolTaskExecutor对CompletableFuture无效。我的生产级方案是双重保障AspectJ切面主动复制MDC为所有Async方法和CompletableFuture.supplyAsync()等创建新线程的地方编写专门的切面。Aspect Component public class AsyncMdcAspect { Around(annotation(org.springframework.scheduling.annotation.Async)) public Object handleAsync(ProceedingJoinPoint joinPoint) throws Throwable { // 获取当前线程的MDC副本 MapString, String parentMdc MDC.getCopyOfContextMap(); try { // 在新线程执行前设置MDC if (parentMdc ! null) { MDC.setContextMap(parentMdc); } return joinPoint.proceed(); } finally { // 清理防止内存泄漏 MDC.clear(); } } // 对CompletableFuture的supplyAsync进行织入 Around(execution(* java.util.concurrent.CompletableFuture.supplyAsync(..))) public Object handleSupplyAsync(ProceedingJoinPoint joinPoint) throws Throwable { // 同上获取parentMdc传入lambda // 注意supplyAsync的第二个参数是Executor需包装其execute方法 return joinPoint.proceed(); } }自定义ExecutorWrapper对于ThreadPoolTaskExecutor在setTaskDecorator时传入一个能复制MDC的TaskDecorator并在execute(Runnable)方法里将Runnable包装为MdcAwareRunnable。public class MdcAwareRunnable implements Runnable { private final Runnable delegate; private final MapString, String mdcContext; public MdcAwareRunnable(Runnable delegate) { this.delegate delegate; this.mdcContext MDC.getCopyOfContextMap(); } Override public void run() { if (mdcContext ! null) { MDC.setContextMap(mdcContext); } try { delegate.run(); } finally { MDC.clear(); } } }踩过的坑MDC.getCopyOfContextMap()返回的是一个HashMap它是浅拷贝。如果MDC里存了可变对象如一个List子线程修改它父线程也会看到。所以永远只存不可变对象String, Long。traceId、userId都是String绝对安全。4. 实操过程与核心环节实现从零搭建一个可立即上线的统一日志切面4.1 Maven依赖与插件配置一步到位的pom.xml骨架一个能跑通的pom.xml是项目成功的50%。以下是经过生产验证的最小可行依赖集properties aspectj.version1.9.21/aspectj.version logstash-logback-encoder.version7.4/logstash-logback-encoder.version /properties dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- AspectJ Runtime -- dependency groupIdorg.aspectj/groupId artifactIdaspectjrt/artifactId version${aspectj.version}/version /dependency dependency groupIdorg.aspectj/groupId artifactIdaspectjweaver/artifactId version${aspectj.version}/version /dependency !-- logback 结构化JSON -- dependency groupIdnet.logstash.logback/groupId artifactIdlogstash-logback-encoder/artifactId version${logstash-logback-encoder.version}/version /dependency /dependencies build plugins !-- AspectJ 编译时织入插件 -- plugin groupIdorg.codehaus.mojo/groupId artifactIdaspectj-maven-plugin/artifactId version1.14.0/version configuration complianceLevel1.8/complianceLevel source1.8/source target1.8/target showWeaveInfotrue/showWeaveInfo verbosetrue/verbose encodingUTF-8/encoding sources sourcesrc/main/java/source /sources weaveDirectories weaveDirectory${project.build.outputDirectory}/weaveDirectory /weaveDirectories /configuration executions execution goals goalcompile/goal goaltest-compile/goal /goals /execution /executions /plugin !-- 确保aspectjweaver在运行时可用 -- plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build关键点说明aspectj-maven-plugin的weaveDirectories必须指向target/classes这是ajc编译器查找已编译class文件的地方。如果漏掉切面不会被织入到任何class运行时毫无效果。logstash-logback-encoder的版本必须与logback-core兼容。7.x系列对应logback 1.3.x如果项目用的是Spring Boot 2.7.xlogback 1.2.x则必须降级到6.6版本否则启动报NoSuchMethodError。aspectjweaver依赖是运行时必需的即使使用CTWaspectjrt也是编译时必需的。两者缺一不可。4.2 WebLog切面的完整代码实现可直接复制粘贴的生产级代码以下是一个经过三个项目验证的WebLogAspect它包含了所有前述要点Aspect Component Slf4j public class WebLogAspect { private static final String TRACE_ID traceId; private static final String SPAN_ID spanId; private static final String USER_ID userId; private static final String REQUEST_ID requestId; // 定义切入点所有被RequestMapping及其派生注解标记的Controller方法 Pointcut(annotation(org.springframework.web.bind.annotation.RequestMapping) || annotation(org.springframework.web.bind.annotation.GetMapping) || annotation(org.springframework.web.bind.annotation.PostMapping) || annotation(org.springframework.web.bind.annotation.PutMapping) || annotation(org.springframework.web.bind.annotation.DeleteMapping)) public void webLogPointcut() {} // Controller方法执行前 Before(webLogPointcut()) public void doBefore(JoinPoint joinPoint) { // 1. 生成/获取traceId String traceId getTraceId(); MDC.put(TRACE_ID, traceId); // 2. 生成spanId简单版实际可用snowflake String spanId UUID.randomUUID().toString().replace(-, ); MDC.put(SPAN_ID, spanId); // 3. 提取userId从JWT Token或Session HttpServletRequest request getCurrentRequest(); String userId extractUserId(request); if (userId ! null) { MDC.put(USER_ID, userId); } // 4. 生成requestId String requestId UUID.randomUUID().toString().replace(-, ); MDC.put(REQUEST_ID, requestId); // 5. 记录请求基本信息 ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes ! null; HttpServletRequest req attributes.getRequest(); String url req.getRequestURL().toString(); String method req.getMethod(); String clientIp getClientIp(req); String userAgent req.getHeader(User-Agent); WebLogEvent event WebLogEvent.builder() .eventType(WEB_REQUEST_START) .url(url) .method(method) .clientIp(clientIp) .userAgent(userAgent) .startTime(System.currentTimeMillis()) .build(); // 使用Marker标记便于日志过滤 log.info(event.toString(), MarkerFactory.getMarker(WEBLOG)); } // Controller方法执行后成功 AfterReturning(pointcut webLogPointcut(), returning retVal) public void doAfterReturning(JoinPoint joinPoint, Object retVal) { long endTime System.currentTimeMillis(); long duration endTime - getStartTimeFromMDC(); ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes ! null; HttpServletResponse response attributes.getResponse(); WebLogEvent event WebLogEvent.builder() .eventType(WEB_REQUEST_END) .status(SUCCESS) .durationMs(duration) .httpStatus(response ! null ? response.getStatus() : 0) .responseSize(getResponseSize(retVal)) .build(); log.info(event.toString(), MarkerFactory.getMarker(WEBLOG)); } // Controller方法抛出异常 AfterThrowing(pointcut webLogPointcut(), throwing ex) public void doAfterThrowing(JoinPoint joinPoint, Throwable ex) { long endTime System.currentTimeMillis(); long duration endTime - getStartTimeFromMDC(); WebLogEvent event WebLogEvent.builder() .eventType(WEB_REQUEST_ERROR) .status(FAILED) .durationMs(duration) .exceptionType(ex.getClass().getSimpleName()) .exceptionMessage(ex.getMessage()) .build(); log.error(event.toString(), ex, MarkerFactory.getMarker(WEBLOG)); } // 方法执行完毕清理MDC After(webLogPointcut()) public void doAfter() { MDC.clear(); } // 辅助方法 private String getTraceId() { HttpServletRequest request getCurrentRequest(); String traceId request ! null ? request.getHeader(X-B3-TraceId) : null; return StringUtils.defaultString(traceId, UUID.randomUUID().toString().replace(-, )); } private String extractUserId(HttpServletRequest request) { // 从JWT Header或Cookie中解析 String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { // 解析JWT获取userId return 1001; // 实际项目中应解析JWT } return null; } private HttpServletRequest getCurrentRequest() { RequestAttributes attributes RequestContextHolder.getRequestAttributes(); return attributes instanceof ServletRequestAttributes ? ((ServletRequestAttributes) attributes).getRequest() : null; } private String getClientIp(HttpServletRequest request) { String ip request.getHeader(X-Forwarded-For); if (ip null || ip.isEmpty() || unknown.equalsIgnoreCase(ip)) { ip request.getHeader(X-Real-IP); } if (ip null || ip.isEmpty() || unknown.equalsIgnoreCase(ip)) { ip request.getRemoteAddr(); } return ip; } private long getStartTimeFromMDC() { // 从MDC中获取startTime需在doBefore中存入 String startTimeStr MDC.get(startTime); return startTimeStr ! null ? Long.parseLong(startTimeStr) : System.currentTimeMillis(); } private int getResponseSize(Object retVal) { // 简单估算实际可根据HttpServletResponse获取 if (retVal null) return 0; return retVal.toString().length(); } }这个切面的亮点WebLogEventBuilder模式确保日志事件字段完整、不可变避免null值污染JSON。MarkerFactory.getMarker(WEBLOG)所有WebLog日志都打上WEBLOG标记可以在logback配置中用filter精准路由到JSON_FILE而其他普通日志走CONSOLE。getStartTimeFromMDC()MDC是ThreadLocaldoBefore和doAfterReturning在同一线程所以可以把startTime存入MDCdoAfterReturning再取出计算耗时。这是跨通知传递数据的最轻量方式。4.3 logback-spring.xml的实战配置与验证如何确认你的日志真的“统一”了配置写完不代表成功。必须有一套验证流程启动应用观察控制台在devprofile下你应该看到两行日志10:00:00.123 [SQL] Preparing: SELECT * FROM user WHERE id ? 10:00:00.124 [SQL] Parameters: 123(Long)同时WEBLOG日志应该以彩色格式显示在控制台且包含traceId、userId等字段。发送一个HTTP请求用curl或Postman调用一个Controller接口然后立刻查看logs/your-app-name.json文件。用tail -f实时观察。你应该看到类似这样的JSON{ timestamp: 2024-03-15T10:00:00.123Z, version: 1, message: {\eventType\:\WEB_REQUEST_START\,\url\:\http://localhost:8080/user/123\,\method\:\GET\,\clientIp\:\127.0.0.1\,\userAgent\:\curl/7.64.1\,\startTime\:1710496800123}, logger_name: com.xxx.aspect.WebLogAspect, level: INFO, traceId: abc123def456, userId: 1001, requestId: xyz789uvw012 }验证结构化字段用jq命令快速验证# 检查是否所有日志都有traceId字段 jq -r .traceId logs/your-app-name.json | head -5 # 检查SQL日志是否被正确结构化 jq -r select(.sql_statement ! null) | .sql_statement logs/your-app-name.json | head -3模拟异步场景写一个Async方法里面调用logger.info(Async task done)然后触发。检查该日志是否也带有traceId。如果没有说明AsyncMdcAspect没生效回到pom.xml检查aspectj-maven-plugin是否正确织入。实操心得logback-spring.xml放在src/main/resources下Spring Boot会自动加载。但如果项目是多模块且web模块依赖service模块那么logback-spring.xml必须放在web模块的resources下否则service模块的日志会走默认配置。这是多模块项目最常见的配置遗漏点。5. 常见问题与排查技巧实录那些只有踩过坑才知道的真相5.1 日志“失踪”问题为什么切面写了日志却没出来这是新手最常问的问题。原因往往不在切面代码而在构建和运行时环境。现象最可能原因排查步骤解决方案本地IDE运行正常打包jar后日志消失aspectj-maven-plugin未生效切面未织入1.java -jar your-app.jar --debug看启动日志是否有[AspectJ]信息2. jar -tf your-app.jargrep .class检查WebLogAspect.class是否存在以及YourController.class的字节码是否被ajc修改文件时间戳应晚于源码切面方法被调用但log.info()没输出logback-spring.xml未被加载或Logger Level设置过高1. 在WebLogAspect的doBefore里加System.out.println(Aspect triggered!)br
阅读完成 · 觉得有帮助?
咨询建站