Spring – 记录传入请求

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,我们可以通过实现以下方法来记录传入的请求:

  1. preHandle() – 我们在实际控制器服务方法之前执行此方法
  2. 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:POSTCopy
  • 我们必须调用以下方法,以确保请求数据在使用之前缓存在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=
  DEBUGCopy

5.2. 配置传统 Web 应用程序

在标准的Spring Web应用程序中,我们可以通过XML配置或Java配置来设置过滤器。因此,让我们使用传统的基于Java的配置来设置CommonsRequestLoggingFilter

众所周知,默认情况下,CommonsRequestLoggingFilterincludePayload属性设置为 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 上找到。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值