Spring MVC HandlerInterceptor 拦截请求及响应体
使用Spring MVC HandlerInterceptor拦截请求和响应体的实现方案。
通过自定义LoggingInterceptor
在preHandle
、postHandle
和afterCompletion
方法中记录请求信息,并利用RequestBodyCachingFilter
缓存请求体以便多次读取。
具体步骤:
- 使用InterceptorRequest对象存储请求信息;
- 通过Filter实现请求体缓存;
- 使用RequestWrapper处理一次性读取的InputStream问题。
该方案适用于需要记录完整请求/响应信息的应用场景。
- 前期想法
java
@Slf4j
public class LoggingInterceptor implements HandlerInterceptor {
// create a InterceptorRequest object to store request info
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// save request header, request body into InterceptorRequest
// set InterceptorRequest into request attribute, eg "InterceptorRequest"
log.info("preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// get InterceptorRequest from request attribute "InterceptorRequest"
// update response body, response code into InterceptorRequest
// save into persistence system
if (null == ex) {
log.info("afterCompletion");
} else {
log.error("afterCompletion -ex - {}", ex.getMessage());
}
}
}
- 将请求结果一次性塞入持久层
java
@Slf4j
public class LoggingInterceptor implements HandlerInterceptor {
// create a InterceptorRequest object to store request info
// ...
// update code as below
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// get InterceptorRequest from request attribute "InterceptorRequest"
// update response body, response code into InterceptorRequest
// save into persistence system
}
}
- 使用 Filter 拦截获取Request Body,获取请求体
java
@Slf4j
public class HttpRequestContext {
public static String getRequestBody(ServletRequest request) {
StringBuilder builder = new StringBuilder();
try (InputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((null != (line = reader.readLine()))) {
builder.append(line);
}
} catch (IOException e) {
log.warn("Error reading request body", e);
throw new RuntimeException(e);
}
return builder.toString();
}
}
java
/**
* It is a filter class used to cache request bodies, commonly employed in web applications,
* especially when handling POST or PUT requests. Since the input stream (InputStream) of an **HTTP request**
* can only be read once, it is necessary to cache the request body in scenarios where the content
* of the request body needs to be accessed multiple times (such as logging, verification, filtering, etc.).
*/
@Slf4j
public class RequestBodyCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String requestId = request.getAttribute("requestId")
if (null == requestId) {
requestId = UUID.randomUUID().toString().replaceAll("-", "");
}
MDC.put("requestId", requestId);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
// Wrap the request to cache the request body
ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
chain.doFilter(requestWrapper, responseWrapper);
} finally {
responseWrapper.copyBodyToResponse();
MDC.remove("requestId");
}
}
@Getter
static class RequestWrapper extends ContentCachingRequestWrapper {
private final String requestBody;
public RequestWrapper(HttpServletRequest request) {
super(request);
requestBody = HttpRequestContext.getRequestBody(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
// Return a ServletInputStream that reads from the cached request body
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody.getBytes());
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
}
}
需要将自定义的Filter注册到FilterRegistrationBean
中
java
@Bean
public FilterRegistrationBean<RequestBodyCachingFilter> requestBodyCachingFilter() {
FilterRegistrationBean<RequestBodyCachingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestBodyCachingFilter());
registration.addUrlPatterns("/*");
registration.setOrder(1);
return registration;
}
- 统一接口封装响应实体
java
@Getter
@Setter
public class ApiObj<T> {
private String code;
private String message;
private T data;
public ApiObj(String code, String message, T data) {
this.code = code;
this.data = data;
this.message = message;
}
public static <T> ApiObj<T> success(T data) {
return new ApiObj<>("200", "success", data);
}
public static <T> ApiObj<T> failure(String code, String message) {
return new ApiObj<>(code, message, null);
}
}
java
封装异常返回。这样返回结果可以与对外接口提供的数据一致。
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class GlobalException {
static final ObjectMapper objectMapper = new ObjectMapper();
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiObj<String>> handleValidationExceptions(MethodArgumentNotValidException ex) throws JsonProcessingException {
log.error("handleValidationExceptions - {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
String message = objectMapper.writeValueAsString(errors);
return new ResponseEntity<>(ApiObj.failure(String.valueOf(HttpStatus.BAD_REQUEST.value()), message), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(value = Exception.class)
public ResponseEntity<ApiObj<String>> defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
log.error("defaultErrorHandler - {}", e.getMessage());
Map<String, String> map = Map.of("k1", "k2");
String message = objectMapper.writeValueAsString(map);
return new ResponseEntity<>(ApiObj.failure(String.valueOf(HttpStatus.BAD_REQUEST.value()), message), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(value = IOException.class)
public ResponseEntity<ApiObj<String>> defaultIOException(HttpServletRequest req, IOException e) throws Exception {
log.error("defaultIOException - {}", e.getMessage());
Map<String, String> map = Map.of("k3", "k4");
String message = objectMapper.writeValueAsString(map);
return new ResponseEntity<>(ApiObj.failure(String.valueOf(HttpStatus.BAD_REQUEST.value()), message), HttpStatus.BAD_REQUEST);
}
}
= 完整更新 LoggingInterceptor
java
@Slf4j
public class LoggingInterceptor implements HandlerInterceptor {
final static String INTERNAL_REQUEST_BODY = "INTERNAL_REQUEST_BODY";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// save request header, request body into InterceptorRequest
// set InterceptorRequest into request attribute, eg "InterceptorRequest"
log.info("preHandle");
String requestBody = HttpRequestContext.getRequestBody(request);
log.info("Request Body: {}", requestBody);
request.setAttribute(INTERNAL_REQUEST_BODY, requestBody);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// get InterceptorRequest from request attribute "InterceptorRequest"
// update response body, response code into InterceptorRequest
// save into persistence system
log.info("afterCompletion");
String requestBody = (String) request.getAttribute(INTERNAL_REQUEST_BODY);
log.info("Request Body: {}", requestBody);
if (response instanceof ContentCachingResponseWrapper responseWrapper) {
byte[] responseBody = responseWrapper.getContentAsByteArray();
String responseBodyStr = new String(responseBody, response.getCharacterEncoding());
log.info("Response Body: {}", responseBodyStr);
// write response body to response
responseWrapper.copyBodyToResponse();
}
request.removeAttribute(INTERNAL_REQUEST_BODY);
}
}
logback-spring.xml
%X{requestId:-MISSING} 输出MDC requestId
xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %X{requestId:-MISSING} %logger{36} - %msg%n</pattern> </encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>