Spring MVC HandlerInterceptor 拦截请求及响应体

Spring MVC HandlerInterceptor 拦截请求及响应体

使用Spring MVC HandlerInterceptor拦截请求和响应体的实现方案。

通过自定义LoggingInterceptor在preHandle、postHandle和afterCompletion方法中记录请求信息,并利用RequestBodyCachingFilter缓存请求体以便多次读取。

具体步骤:

  1. 使用InterceptorRequest对象存储请求信息;
  2. 通过Filter实现请求体缓存;
  3. 使用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>
相关推荐
BLUcoding7 小时前
接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决
java·linux·springboot·aes·securerandom
我最爱吃鱼香茄子17 小时前
【毕业设计优选】人力资源管理系统|Java|SpringBoot|Vue|前后端分离|带详细文档+部署教学视频
java·vue·毕业设计·springboot
做个文艺程序员2 天前
MinIO第05篇:MinIO事件通知机制与Kafka集成——构建SaaS平台的异步文件处理管道
kafka·消息队列·springboot·minio·事件驱动·异步处理
骑着蜗牛撵大象3272 天前
SpringBoot+Vue3 企业智能体侧挂架构:独立服务、独立数据库与主线零侵入落地
数据库·spring boot·架构·vue·springboot·事件驱动·服务拆分
程序猿_极客3 天前
【免费】分享一套优质的基于微信小程序的音乐播放器系统的设计与实现,源码+文档+视频详解(讲解)
微信小程序·小程序·springboot·音乐播放器小程序·音乐管理系统
cpolar技术支持5 天前
Spring Boot 接口本地正常,异地前端却报跨域?用 cpolar 跑通 CORS 预检与白名单
java·springboot·cpolar·前后端分离·cors
弹简特7 天前
【Java项目-企悦抽】17-抽奖模块02-抽奖接口实现
java·开发语言·状态模式·springboot
弹简特7 天前
【Java项目-企悦抽】16-抽奖模块01-获取活动完整信息接口实现
java·开发语言·状态模式·springboot
弹简特9 天前
【Java项目-企悦抽】15-活动管理模块02-活动创建测试与活动列表实现
java·开发语言·springboot
弹简特9 天前
【Java项目-企悦抽】13-奖品管理模块-奖品列表和创建奖品的实现
java·开发语言·网络·springboot