在HandlerInterceptor中获取请求体与响应体
1. 概述
在本教程中,我们将探讨几种在 Spring MVC 中使用 HandlerInterceptor 记录 HTTP 请求和响应体的方式,同时不干扰正常的请求处理。
简而言之,我们将使用 servlet 过滤器,特别是 ContentCaching 包装器,来记录一个简单 REST API 的详细信息。
2. Spring MVC 执行链
首先,我们需要了解请求在 Spring MVC 执行链中的各个阶段。
该链从 servlet 过滤器开始,接着是 DispatcherServlet、处理器执行链,最后是 HTTP 消息转换层。
我们关注的是 HandlerInterceptor,它是处理器执行链的一部分。
2.1. 理解 HandlerInterceptor
在请求到达 DispatcherServlet 之后,Spring 需要解析一个处理器并运行处理器执行链,该链可能包含零个或多个 HandlerInterceptor。该链包含 preHandle(),然后是控制器及其相关逻辑,接着是 postHandle() 和 afterCompletion()。
postHandle() 的行为取决于控制器的返回类型。当控制器返回一个视图时,响应尚未提交,因此 postHandle() 仍然可以修改它。然而,对于 @ResponseBody 或 ResponseEntity,HTTP 消息转换器会在 postHandle() 运行之前写入并提交响应体,因此此时的响应实际上是只读的。
2.2. 请求和响应体是一次性的
请求和响应体的一个特性是它们都表示为流。因此,一旦某个组件消费了该流,除非我们显式地将其保存到某个缓冲区中,否则无法再次读取。
这一点很重要,因为如果过滤器或拦截器在请求到达 Spring 的 HTTP 消息转换层(HttpMessageConverter)之前读取了请求体,那么转换器将收到一个空流,导致 @RequestBody 反序列化失败:
现在我们已经掌握了所有要素,可以理解底层细节以及 Spring 如何处理 HTTP 请求。
3. 示例 REST API
让我们创建一个简单的 REST API,使用 @RequestBody 创建书籍:
现在,我们的目标是记录每次调用中的传入 CreateBookRequest 和 BookCreatedResponse 记录,以及 HTTP 方法、URI 和状态。
4. 过滤器中的 ContentCaching 包装器
使用 Spring,我们可以利用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 将请求和响应内容保存在缓存中,并通过 getContentAsByteArray() 暴露出来。这样,即使我们首次读取了流,后续层也不会丢失内容。
为了实现这一点,我们将实现一个 servlet 过滤器,通过继承 OncePerRequestFilter 将其注册到所有请求:
过滤器在 DispatcherServlet 之前运行,因此我们在到达控制器之前就保存了所有内容。接下来看看如何使用这个缓存值。
值得注意的是,Spring 已经内置了用于请求端日志记录的 CommonsRequestLoggingFilter。
4.1. 在 HandlerInterceptor 中记录缓存内容
为了记录缓存的值,我们实现了 HandlerInterceptor。让我们逐步构建它。
首先,声明类和日志记录器:
这是一个由 Spring 管理的组件,实现了 HandlerInterceptor,因此稍后可以通过 WebMvcConfigurer 注册。
接下来,在 preHandle() 中,我们只记录轻量级元数据(方法和 URI),并有意避免接触请求体流,因为此时 HttpMessageConverter 尚未运行,缓存也未被填充:
记录完整请求和响应的正确位置是 afterCompletion(),因为到那时,控制器和 HttpMessageConverter 都已运行,因此 ContentCaching 包装器已被完全填充:
最后,辅助方法读取缓存的字节并将其转换为字符串。两个提取方法都通过 instanceof 检查进行保护。如果没有注册过滤器或者包装器不存在,它们会安全地返回空字符串。当请求或响应未声明字符集时,编码回退到 UTF-8:
这在请求提取中使用:
同样在响应提取中使用:
值得注意的是,ContentCachingRequestWrapper 不会急切地复制请求体。只有当某个组件实际读取输入流时,它才会填充缓存,这发生在 Spring 的 HttpMessageConverter 反序列化 @RequestBody 时。
4.2. 注册 HandlerInterceptor
现在剩下的最后一件事情是为所有传入的 API 调用注册这个处理器:
此配置将日志记录与控制器代码分离,并保证所有 /api/** 下的请求都经过我们的 HttpLoggingInterceptor。
4.3. 测试实现
为了验证我们的设置,我们可以编写一个使用 MockMvc 和 Spring Boot 的 OutputCaptureExtension 的集成测试,这使得我们可以针对测试期间生成的日志输出进行断言:
5. 总结
在本文中,我们学习了如何在 Spring MVC 应用程序中记录 HTTP 请求和响应体,同时不破坏 @RequestBody 的反序列化。
我们首先回顾了请求在执行链中的流程以及 HTTP 体流的一次性特性。
然后,我们在 OncePerRequestFilter 中使用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 来缓冲请求和响应体,最后在 HandlerInterceptor 的 afterCompletion() 中读取这些缓冲区,此时消息转换器已运行且响应已经完成。
像往常一样,本文的源代码可在 GitHub 上获取。