2024-03-13
分类:开发笔记
阅读(293) 评论(0)
完整的请求日志对于 **「故障排查」** 和 **「审计」** 来说极其重要。通过查看日志,可以检查数据的准确性、参数的传递方式以及服务器返回的数据。
由于 Socket 流不能重读,所以需要一种实现来把读取和写入的数据缓存起来,并且可以多次重复读取缓存的内容。
Spring 提供 2 个可重复读取请求、响应的 Wrapper 工具类:
* `ContentCachingRequestWrapper`
* `ContentCachingResponseWrapper`
通过类名不难看出,这是典型的装饰者设计模式。它俩的作用就是把读取到的 **「请求体」** 和写出的 **「响应体」** 都缓存起来,并且提供了访问缓存数据的 API。
创建 RequestLogFilter
-------------------
创建 `RequestLogFilter` 继承 `HttpFilter`,以记录完整的请求和响应日志。
```
package cn.springdoc.demo.web.filter;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 记录请求日志
*/
public class RequestLogFilter extends HttpFilter {
static final Logger log = LoggerFactory.getLogger(RequestLogFilter.class);
/**
*
*/
private static final long serialVersionUID = 8991118181953196532L;
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// Wrapper 封装 Request 和 Response
ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response);
// 继续执行请求链
chain.doFilter(cachingRequest, cachingResponse);
/**
* 在请求完成后记录请求、响应日志
*/
// 请求方法
String method = request.getMethod();
// URI
String uri = request.getRequestURI();
// 缓存的请求体
byte[] requestContent = cachingRequest.getContentAsByteArray();
log.info("Request => {} {} {}", method, uri, new String(requestContent));
// 响应状态
int status = response.getStatus();
// 缓存的响应体
byte[] responseContent = cachingResponse.getContentAsByteArray();
log.info("Response <= {} {}", status, new String(responseContent));
/**
* 把缓存的响应数据,响应给客户端
*/
cachingResponse.copyBodyToResponse();
}
}
```
如上,在 `doFilter` 方法中使用装饰者设计模式,把 `HttpServletRequest` 和 `HttpServletResponse` 分别封装为 `ContentCachingRequestWrapper` 和 `ContentCachingResponseWrapper`。
继续执行请求链,当从 Request 从读取数据或者是把数据写入到 Response 中时,都会在对应的 Wrapper 中缓存数据。在请求完成时,就可以通过 `getContentAsByteArray()` 方法从 Wrapper 中读取到缓存的数据,以实现了 "流重读"。
注意,最后调用的 `copyBodyToResponse();` 方法很关键,由于往 Response 写入数据时都缓存在 Wrapper 中,所以在最后必须调用此方法把缓存的数据的响应给客户端。
通过这种方式,就可以完完整整地记录到整个请求体和响应体,包括请求头和响应头。
配置 Filter
---------
在 Spring Boot 中有多种方式配置 Filter,本例使用 `FilterRegistrationBean` Bean 进行注册。
```
package cn.springdoc.demo.configuration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import cn.springdoc.demo.web.filter.RequestLogFilter;
@Configuration
public class WebFilterConfiguration {
@Bean
public FilterRegistrationBean requestLogFilter (){
FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new RequestLogFilter());
// 拦截 "/api" 开头的请求
registrationBean.addUrlPatterns("/api/*");
// 执行顺序最靠前
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
}
```
首先,指定了仅拦截 `/api` 开头的请求,然后指定 `Filter` 的执行顺序为最先执行。
建议只拦截业务 API 接口。对于静态文件、文件上传、文件下载等请求来说,把体积较大的请求、响应缓存在内存中可能导致内存溢出,且记录文件内容没有太大意义。
当然,你也可以使用 `/*` 拦截所有请求,然后在 `RequestLogFilter` 中进行灵活地判断:
```
private boolean shouldSkip (HttpServletRequest request) {
// 跳过文件上传接口
if (request.getRequestURI().contains("/upload")) {
return true;
}
// 跳过文件下载接口
if (request.getRequestURI().contains("/download")) {
return true;
}
// 跳过静态资源
if (request.getRequestURI().contains("/static")) {
return true;
}
return false;
}
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if(shouldSkip(request)) {
// 不需要记录请求体和响应体
chain.doFilter(request, response);
} else {
// Wrapper 封装 Request 和 Response
ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response);
// TODO ...
}
}
```
DemoController
--------------
创建一个 `DemoController` 用于测试:
```
package cn.springdoc.demo.web.controller;
import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/demo")
public class DemoController {
static final Logger log = LoggerFactory.getLogger(DemoController.class);
@PostMapping
public ResponseEntity
标签:
众生皆苦,唯有自渡!