技术背景
在开发基于Spring Boot的REST API时,为了便于调试、监控和问题排查,需要对所有请求和响应进行详细的日志记录,包括请求的输入参数、请求路径、查询字符串、对应的类方法,以及响应结果(包括成功和错误情况)。
实现步骤
1. 使用Spring Boot Actuator
Actuator是Spring Boot的一个模块,它提供了HTTP请求日志记录功能。
- 添加依赖:在pom.xml中添加spring-boot-starter-actuator依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 配置端点:在application.properties中配置要暴露的端点。
management.endpoints.web.exposure.include=*
- 查看日志:启动应用后,可以通过访问/actuator/httptrace查看最近的100个HTTP请求。
2. 使用CommonsRequestLoggingFilter
Spring提供了
CommonsRequestLoggingFilter来记录请求日志。
- 配置过滤器:在配置类中添加过滤器Bean。
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。
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
3. 自定义DispatcherServlet
通过自定义DispatcherServlet可以同时记录请求和响应日志。
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。
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依赖。
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>2.4.1</version>
</dependency>
- 设置日志级别:在application.properties中设置org.zalando.logbook的日志级别为TRACE。
logging.level.org.zalando.logbook=TRACE
5. 使用Spring AOP
通过Spring AOP可以在方法执行前后记录请求和响应信息。
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的完整代码示例:
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中设置日志级别:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
最佳实践
- 选择合适的方法:根据项目的具体需求和复杂度,选择合适的日志记录方法。如果只是简单的请求日志记录,可以使用CommonsRequestLoggingFilter;如果需要更详细的信息,可以考虑自定义DispatcherServlet或使用Logbook库。
- 控制日志级别:在生产环境中,应适当控制日志级别,避免产生过多的日志信息影响性能。可以将日志级别设置为INFO或WARN,只记录关键信息。
- 日志存储和分析:将日志存储在合适的地方,如文件系统或日志管理系统(如ELK Stack),并进行定期分析,以便及时发现和解决问题。
常见问题
1. 日志中没有响应信息
- 原因:某些方法(如CommonsRequestLoggingFilter)只记录请求信息,不记录响应信息。
- 解决方法:可以使用自定义DispatcherServlet或其他方法来记录响应信息。
2. 日志记录不完整
- 原因:可能是由于日志级别设置不正确或过滤器配置不当导致的。
- 解决方法:检查日志级别设置,确保过滤器的配置正确,如设置setMaxPayloadLength来记录完整的请求和响应体。
3. 性能问题
- 原因:过多的日志记录可能会影响应用的性能。
- 解决方法:在生产环境中,适当控制日志级别,避免记录不必要的信息。可以使用异步日志记录来减少对应用性能的影响。