1. 简介
在本快速教程中,我们将演示使用 Spring 的日志记录过滤器记录传入请求的基础知识。如果我们刚刚开始使用日志记录,我们可以查看此日志记录介绍文章以及SLF4J 文章。
2. Maven 依赖项
日志记录依赖项将与介绍文章中的依赖项相同;我们在这里简单地添加 Spring:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
Copy
最新版本可以在这里找到弹簧芯。
3. 基本网页控制器
首先,我们将定义一个要在示例中使用的控制器:
@RestController
public class TaxiFareController {
@GetMapping("/taxifare/get/")
public RateCard getTaxiFare() {
return new RateCard();
}
@PostMapping("/taxifare/calculate/")
public String calculateTaxiFare(
@RequestBody @Valid TaxiRide taxiRide) {
// return the calculated fare
}
}
Copy
4. 自定义请求日志记录
Spring 提供了一种机制,用于配置用户定义的拦截器以在 Web 请求之前和之后执行操作。
在 Spring 请求拦截器中,其中一个值得注意的接口是HandlerInterceptor,我们可以通过实现以下方法来记录传入的请求:
- preHandle() – 我们在实际控制器服务方法之前执行此方法
- afterCompletion() – 我们在控制器准备好发送响应后执行此方法
此外,Spring 以 HandlerInterceptorAdaptor 类的形式提供了HandlerInterceptor接口的默认实现,用户可以扩展该类。
让我们通过将HandlerInterceptorAdaptor扩展为来创建我们自己的拦截器:
@Component
public class TaxiFareRequestInterceptor
extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
//
}
}
Copy
最后,我们将在 MVC 生命周期中配置TaxiRideRequestInterceptor,以捕获映射到TaxiFareController类中定义的路径/taxifare的控制器方法调用的预处理和后处理:
@Configuration
public class TaxiFareMVCConfig implements WebMvcConfigurer {
@Autowired
private TaxiFareRequestInterceptor taxiFareRequestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(taxiFareRequestInterceptor)
.addPathPatterns("/taxifare/*/");
}
}
Copy
总之,WebMvcConfigurer通过调用addInterceptors() 方法在 Spring MVC 生命周期中添加了TaxiFareRequestInterceptor。
最大的挑战是获取请求和响应有效负载的副本以进行日志记录,并且仍然将请求的有效负载留给 servlet 来处理它:
读取请求的主要问题是,一旦首次读取输入流,它就会被标记为已使用,无法再次读取。
应用程序将在读取请求流后引发异常:
{
"timestamp": 1500645243383,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.http.converter
.HttpMessageNotReadableException",
"message": "Could not read document: Stream closed;
nested exception is java.io.IOException: Stream closed",
"path": "/rest-log/taxifare/calculate/"
}
Copy
为了克服这个问题,我们可以利用缓存来存储请求流并将其用于日志记录。
Spring 提供了一些有用的类,例如 ContentCachingRequestWrapper 和ContentCachingResponseWrapper,它们可用于缓存请求数据以进行日志记录。
让我们调整TaxiRideRequestInterceptor类的preHandle(),以使用ContentCachingRequestWrapper类缓存请求对象:
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
HttpServletRequest requestCacheWrapperObject
= new ContentCachingRequestWrapper(request);
requestCacheWrapperObject.getParameterMap();
// Read inputStream from requestCacheWrapperObject and log it
return true;
}
Copy
如我们所见,我们使用ContentCachingRequestWrapper类缓存请求对象,我们可以使用它来读取有效负载数据以进行日志记录,而不会干扰实际的请求对象:
requestCacheWrapperObject.getContentAsByteArray();
Copy
限度
- 类仅支持以下内容:
Content-Type:application/x-www-form-urlencoded
Method-Type:POST
Copy
- 我们必须调用以下方法,以确保请求数据在使用之前缓存在ContentCachingRequestWrapper中:
requestCacheWrapperObject.getParameterMap();
Copy
5. 弹簧内置请求记录
Spring 提供了一个内置的解决方案来记录有效负载。我们可以通过使用配置插入 Spring 应用程序来使用现成的过滤器。
AbstractRequestLoggingFilter是一个提供日志记录基本功能的过滤器。子类应覆盖 beforeRequest() 和afterRequest() 方法,以围绕请求执行实际日志记录。
Spring 框架提供了三个具体的实现类,我们可以用来记录传入的请求。这三个类是:
- CommonsRequestLoggingFilter
- Log4jNestedDiagnosticContextFilter(已弃用)
- ServletContextRequestLoggingFilter
现在让我们转到CommonsRequestLoggingFilter,并将其配置为捕获用于日志记录的传入请求。
5.1. 配置 Spring 引导应用程序
我们可以通过添加 Bean 定义来配置 Spring 引导应用程序以启用请求日志记录:
@Configuration
public class RequestLoggingFilterConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter
= new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(10000);
filter.setIncludeHeaders(false);
filter.setAfterMessagePrefix("REQUEST DATA : ");
return filter;
}
}
Copy
此日志记录筛选器还要求我们将日志级别设置为 DEBUG。我们可以通过在logback中添加以下元素来启用DEBUG模式.xml:
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter">
<level value="DEBUG" />
</logger>
Copy
启用 DEBUG 级别日志的另一种方法是在application.properties 中添加以下内容:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=
DEBUG
Copy
5.2. 配置传统 Web 应用程序
在标准的Spring Web应用程序中,我们可以通过XML配置或Java配置来设置过滤器。因此,让我们使用传统的基于Java的配置来设置CommonsRequestLoggingFilter。
众所周知,默认情况下,CommonsRequestLoggingFilter的includePayload属性设置为 false。我们需要一个自定义类来覆盖属性的值,以便在使用 Java 配置注入容器之前启用includePayload:
public class CustomeRequestLoggingFilter
extends CommonsRequestLoggingFilter {
public CustomeRequestLoggingFilter() {
super.setIncludeQueryString(true);
super.setIncludePayload(true);
super.setMaxPayloadLength(10000);
}
}
Copy
然后我们需要使用基于 Java 的 Web 初始值设定项注入CustomeRequestLoggingFilter:
public class CustomWebAppInitializer implements
WebApplicationInitializer {
public void onStartup(ServletContext container) {
AnnotationConfigWebApplicationContext context
= new AnnotationConfigWebApplicationContext();
context.setConfigLocation("com.baeldung");
container.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic dispatcher
= container.addServlet("dispatcher",
new DispatcherServlet(context));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
container.addFilter("customRequestLoggingFilter",
CustomeRequestLoggingFilter.class)
.addMappingForServletNames(null, false, "dispatcher");
}
}
Copy
6. 行动中的例子
最后,我们可以将 Spring Boot 与上下文连接起来,以查看传入请求的日志记录是否按预期工作:
@Test
public void givenRequest_whenFetchTaxiFareRateCard_thanOK() {
TestRestTemplate testRestTemplate = new TestRestTemplate();
TaxiRide taxiRide = new TaxiRide(true, 10l);
String fare = testRestTemplate.postForObject(
URL + "calculate/",
taxiRide, String.class);
assertThat(fare, equalTo("200"));
}
Copy
7. 结论
在本文中,我们学习了如何使用拦截器实现基本的 Web 请求日志记录。我们还探讨了此解决方案的局限性和挑战。
然后,我们讨论了内置筛选器类,它提供了现成的简单日志记录机制。
与往常一样,示例和代码片段的实现可在GitHub 上找到。