Spring Boot中统一记录请求、响应及异常日志的方法
技术背景
在开发基于Spring Boot的REST API时,为了便于调试、监控和问题排查,需要对所有请求和响应进行详细的日志记录,包括请求的输入参数、请求路径、查询字符串、对应的类方法,以及响应结果(包括成功和错误情况)。
实现步骤
1. 使用Spring Boot Actuator
Actuator是Spring Boot的一个模块,它提供了HTTP请求日志记录功能。
- 添加依赖:在
pom.xml
中添加spring-boot-starter-actuator
依赖。
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
|
- 配置端点:在
application.properties
中配置要暴露的端点。
1
| management.endpoints.web.exposure.include=*
|
- 查看日志:启动应用后,可以通过访问
/actuator/httptrace
查看最近的100个HTTP请求。
2. 使用CommonsRequestLoggingFilter
Spring提供了CommonsRequestLoggingFilter
来记录请求日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.CommonsRequestLoggingFilter;
@Configuration public class LoggingConfig {
@Bean public CommonsRequestLoggingFilter requestLoggingFilter() { CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter(); loggingFilter.setIncludeClientInfo(true); loggingFilter.setIncludeQueryString(true); loggingFilter.setIncludePayload(true); loggingFilter.setMaxPayloadLength(64000); return loggingFilter; } }
|
- 设置日志级别:在
application.properties
中设置过滤器的日志级别为DEBUG
。
1
| logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
|
3. 自定义DispatcherServlet
通过自定义DispatcherServlet
可以同时记录请求和响应日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
public class LoggableDispatcherServlet extends DispatcherServlet {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { if (!(request instanceof ContentCachingRequestWrapper)) { request = new ContentCachingRequestWrapper(request); } if (!(response instanceof ContentCachingResponseWrapper)) { response = new ContentCachingResponseWrapper(response); } try { super.doDispatch(request, response); } finally { log(request, response); updateResponse(response); } }
private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache) { int status = responseToCache.getStatus(); StringBuilder logMessage = new StringBuilder(); logMessage.append("HttpStatus: ").append(status) .append(", path: ").append(requestToCache.getRequestURI()) .append(", method: ").append(requestToCache.getMethod()) .append(", clientIp: ").append(requestToCache.getRemoteAddr()); logger.info(logMessage.toString()); }
private void updateResponse(HttpServletResponse response) throws IOException { ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response; responseWrapper.copyBodyToResponse(); } }
|
- 注册DispatcherServlet:在配置类中注册自定义的
DispatcherServlet
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.DispatcherServlet;
@Configuration public class ServletConfig {
@Bean public ServletRegistrationBean<DispatcherServlet> dispatcherRegistration() { return new ServletRegistrationBean<>(new LoggableDispatcherServlet(), "/"); } }
|
4. 使用Logbook库
Logbook是一个专门用于记录HTTP请求和响应的库。
- 添加依赖:在
pom.xml
中添加logbook-spring-boot-starter
依赖。
1 2 3 4 5
| <dependency> <groupId>org.zalando</groupId> <artifactId>logbook-spring-boot-starter</artifactId> <version>2.4.1</version> </dependency>
|
- 设置日志级别:在
application.properties
中设置org.zalando.logbook
的日志级别为TRACE
。
1
| logging.level.org.zalando.logbook=TRACE
|
5. 使用Spring AOP
通过Spring AOP可以在方法执行前后记录请求和响应信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect @Component public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Before("execution(* com.example.demo.controller.*.*(..))") public void before(JoinPoint joinPoint) { logger.info("Before method: {}", joinPoint.getSignature().toShortString()); logger.info("Arguments: {}", Arrays.toString(joinPoint.getArgs())); }
@AfterReturning(pointcut = "execution(* com.example.demo.controller.*.*(..))", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result) { logger.info("After method: {}", joinPoint.getSignature().toShortString()); logger.info("Result: {}", result); }
@AfterThrowing(pointcut = "execution(* com.example.demo.controller.*.*(..))", throwing = "ex") public void afterThrowing(JoinPoint joinPoint, Exception ex) { logger.error("Exception in method: {}", joinPoint.getSignature().toShortString(), ex); } }
|
核心代码
以下是使用CommonsRequestLoggingFilter
的完整代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.CommonsRequestLoggingFilter;
@Configuration public class LoggingConfig {
@Bean public CommonsRequestLoggingFilter requestLoggingFilter() { CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter(); loggingFilter.setIncludeClientInfo(true); loggingFilter.setIncludeQueryString(true); loggingFilter.setIncludePayload(true); loggingFilter.setMaxPayloadLength(64000); return loggingFilter; } }
|
在application.properties
中设置日志级别:
1
| logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
|
最佳实践
- 选择合适的方法:根据项目的具体需求和复杂度,选择合适的日志记录方法。如果只是简单的请求日志记录,可以使用
CommonsRequestLoggingFilter
;如果需要更详细的信息,可以考虑自定义DispatcherServlet
或使用Logbook库。 - 控制日志级别:在生产环境中,应适当控制日志级别,避免产生过多的日志信息影响性能。可以将日志级别设置为
INFO
或WARN
,只记录关键信息。 - 日志存储和分析:将日志存储在合适的地方,如文件系统或日志管理系统(如ELK Stack),并进行定期分析,以便及时发现和解决问题。
常见问题
1. 日志中没有响应信息
- 原因:某些方法(如
CommonsRequestLoggingFilter
)只记录请求信息,不记录响应信息。 - 解决方法:可以使用自定义
DispatcherServlet
或其他方法来记录响应信息。
2. 日志记录不完整
- 原因:可能是由于日志级别设置不正确或过滤器配置不当导致的。
- 解决方法:检查日志级别设置,确保过滤器的配置正确,如设置
setMaxPayloadLength
来记录完整的请求和响应体。
3. 性能问题
- 原因:过多的日志记录可能会影响应用的性能。
- 解决方法:在生产环境中,适当控制日志级别,避免记录不必要的信息。可以使用异步日志记录来减少对应用性能的影响。