SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 一个系统上线,肯定会或多或少的存在异常情况。为了更快更好的排雷,记录请求参数和响应结果是非常必要的。所以,Nginx 和 Tomcat 之类的 web 服务器,都提供了访问日志,可以帮助我们记录一些请求信息。

image.png

你好,我是看山。


一个系统上线,肯定会或多或少的存在异常情况。为了更快更好的排雷,记录请求参数和响应结果是非常必要的。所以,Nginx 和 Tomcat 之类的 web 服务器,都提供了访问日志,可以帮助我们记录一些请求信息。


本文是在我们的应用中,定义一个Filter来实现记录请求参数和响应结果的功能。


有一定经验的都知道,如果我们在Filter中读取了HttpServletRequest或者HttpServletResponse的流,就没有办法再次读取了,这样就会造成请求异常。所以,我们需要借助 Spring 提供的ContentCachingRequestWrapper和ContentCachingRequestWrapper实现数据流的重复读取。


定义 Filter

通常来说,我们自定义的Filter是实现Filter接口,然后写一些逻辑,但是既然是在 Spring 中,那就借助 Spring 的一些特性。在我们的实现中,要继承OncePerRequestFilter实现我们的自定义实现。


从类名上推断,OncePerRequestFilter是每次请求只执行一次,但是,难道Filter在一次请求中还会执行多次吗?Spring 官方也是给出定义这个类的原因:


Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) method with HttpServletRequest and HttpServletResponse arguments.


As of Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore sub-classes can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types.


Subclasses may use isAsyncDispatch(HttpServletRequest) to determine when a filter is invoked as part of an async dispatch, and use isAsyncStarted(HttpServletRequest) to determine when the request has been placed in async mode and therefore the current dispatch won’t be the last one for the given request.


Yet another dispatch type that also occurs in its own thread is ERROR. Subclasses can override shouldNotFilterErrorDispatch() if they wish to declare statically if they should be invoked once during error dispatches.


也就是说,Spring 是为了兼容不同的 Web 容器,所以定义了只会执行一次的OncePerRequestFilter。


接下来开始定义我们的Filter类:


public class AccessLogFilter extends OncePerRequestFilter {
    //... 这里有一些必要的属性
    @Override
    protected void doFilterInternal(final HttpServletRequest request,
                                    final HttpServletResponse response,
                                    final FilterChain filterChain)
            throws ServletException, IOException {
        // 如果是被排除的 uri,不记录 access_log
        if (matchExclude(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }
        final String requestMethod = request.getMethod();
        final boolean shouldWrapMethod = StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.PUT.name())
                || StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.POST.name());
        final boolean isFirstRequest = !isAsyncDispatch(request);
        final boolean shouldWrapRequest = isFirstRequest && !(request instanceof ContentCachingRequestWrapper) && shouldWrapMethod;
        final HttpServletRequest requestToUse = shouldWrapRequest ? new ContentCachingRequestWrapper(request) : request;
        final boolean shouldWrapResponse = !(response instanceof ContentCachingResponseWrapper) && shouldWrapMethod;
        final HttpServletResponse responseToUse = shouldWrapResponse ? new ContentCachingResponseWrapper(response) : response;
        final long startTime = System.currentTimeMillis();
        Throwable t = null;
        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } catch (Exception e) {
            t = e;
            throw e;
        } finally {
            doSaveAccessLog(requestToUse, responseToUse, System.currentTimeMillis() - startTime, t);
        }
    }
    // ... 这里是一些必要的方法

这段代码就是整个逻辑的核心所在,其他的内容从源码中找到。


分析

这个代码中,整体的逻辑没有特别复杂的地方,只需要注意几个关键点就可以了。


默认的HttpServletRequest和HttpServletResponse中的流被读取一次之后,再次读取会失败,所以要使用ContentCachingRequestWrapper和ContentCachingResponseWrapper进行包装,实现重复读取。

既然我们可以自定义Filter,那我们依赖的组件中也可能会自定义Filter,更有可能已经对请求和响应对象进行过封装,所以,一定要先进行一步判断。也就是request instanceof ContentCachingRequestWrapper和response instanceof ContentCachingResponseWrapper。

只要注意了这两点,剩下的都是这个逻辑的细化实现。


运行

接下来我们就运行一遍,看看结果。先定义几种不同的请求:普通 get 请求、普通 post 请求、上传文件、下载文件,这四个接口几乎可以覆盖绝大部分场景。(因为都是比较简单的写法,源码就不赘述了,可以从文末的源码中找到)


先启动项目,然后借助 IDEA 的 http 请求工具:


###普通 get 请求
GET http://localhost:8080/index/get?name=howard
###普通 post 请求
POST http://localhost:8080/index/post
Content-Type: application/json
{"name":"howard"}
###上传文件
POST http://localhost:8080/index/upload
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="history.txt"
Content-Type: multipart/form-data
</Users/liuxh/history.txt
--WebAppBoundary--
###下载文件
GET http://localhost:8080/index/download

再看看打印的日志:


2021-04-29 19:44:57.495  INFO 83448 --- [nio-8080-exec-1] c.h.d.s.filter.AccessLogFilter           : time=44ms,ip=127.0.0.1,uri=/index/get,headers=[host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=null,responseContentType=text/plain;charset=UTF-8,params=name=howard,request=,response=
2021-04-29 19:44:57.551  INFO 83448 --- [nio-8080-exec-2] c.h.d.s.filter.AccessLogFilter           : time=36ms,ip=127.0.0.1,uri=/index/post,headers=[content-type:application/json,content-length:17,host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=application/json,responseContentType=application/json,params=,request={"name":"howard"},response={"name":"howard","timestamp":"1619696697540"}
2021-04-29 19:44:57.585  INFO 83448 --- [nio-8080-exec-3] c.h.d.s.filter.AccessLogFilter           : time=20ms,ip=127.0.0.1,uri=/index/upload,headers=[content-type:multipart/form-data; boundary=WebAppBoundary,content-length:232,host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=multipart/form-data; boundary=WebAppBoundary,responseContentType=application/json,params=,request=,response={"contentLength":"0","contentType":"multipart/form-data"}
2021-04-29 19:44:57.626  INFO 83448 --- [nio-8080-exec-4] c.h.d.s.filter.AccessLogFilter           : time=27ms,ip=127.0.0.1,uri=/index/download,headers=[host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=null,responseContentType=application/octet-stream;charset=utf-8,params=,request=,response=

文末总结

自定义Filter是比较简单的,只要能够注意几个关键点就可以了。不过后续还有扩展的空间,比如:


定义排除的请求 uri,可以借助AntPathMatcher实现 ant 风格的定义

将请求日志单独存放,可以借助 logback 或者 log4j2 等框架的的日志配置实现,这样能更加方便的查找日志

与调用链技术结合,在请求日志中增加调用链的 TraceId 等,可以快速定位待查询的请求日志

源码

附上源码:https://github.com/howardliu-cn/effective-spring/tree/main/spring-filter


推荐阅读

SpringBoot 实战:一招实现结果的优雅响应

SpringBoot 实战:如何优雅的处理异常

SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器

SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果


相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
目录
相关文章
|
17天前
|
消息中间件 Ubuntu Java
SpringBoot整合MQTT实战:基于EMQX实现双向设备通信
本教程指导在Ubuntu上部署EMQX 5.9.0并集成Spring Boot实现MQTT双向通信,涵盖服务器搭建、客户端配置及生产实践,助您快速构建企业级物联网消息系统。
175 1
|
7月前
|
缓存 NoSQL Java
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
539 79
|
5月前
|
监控 Java 调度
SpringBoot中@Scheduled和Quartz的区别是什么?分布式定时任务框架选型实战
本文对比分析了SpringBoot中的`@Scheduled`与Quartz定时任务框架。`@Scheduled`轻量易用,适合单机简单场景,但存在多实例重复执行、无持久化等缺陷;Quartz功能强大,支持分布式调度、任务持久化、动态调整和失败重试,适用于复杂企业级需求。文章通过特性对比、代码示例及常见问题解答,帮助开发者理解两者差异,合理选择方案。记住口诀:单机简单用注解,多节点上Quartz;若是任务要可靠,持久化配置不能少。
495 4
|
6月前
|
缓存 安全 Java
深入解析HTTP请求方法:Spring Boot实战与最佳实践
这篇博客结合了HTTP规范、Spring Boot实现和实际工程经验,通过代码示例、对比表格和架构图等方式,系统性地讲解了不同HTTP方法的应用场景和最佳实践。
555 5
|
8月前
|
Java Spring
SpringBoot 实战 不同参数调用不同实现
本文介绍了如何在实际工作中根据不同的入参调用不同的实现,采用`map+enum`的方式实现优雅且严谨的解决方案。通过Spring Boot框架中的工厂模式或策略模式,避免了使用冗长的`if...else...`语句。文中详细展示了定义接口、实现类、枚举类以及控制器调用的代码示例,确保用户输入的合法性并简化了代码逻辑。
206 1
SpringBoot 实战 不同参数调用不同实现
|
8月前
|
Java Maven 开发者
编写SpringBoot的自定义starter包
通过本文的介绍,我们详细讲解了如何创建一个Spring Boot自定义Starter包,包括自动配置类、配置属性类、`spring.factories`文件的创建和配置。通过自定义Starter,可以有效地复用公共配置和组件,提高开发效率。希望本文能帮助您更好地理解和应用Spring Boot自定义Starter,在实际项目中灵活使用这一强大的功能。
540 17
|
9月前
|
Java 应用服务中间件 Spring
SpringBoot 响应请求是串行还是并行?
Spring Boot 在默认情况下通过 Servlet 容器的线程池实现并行处理 HTTP 请求。通过适当的线程池配置,可以进一步优化并发性能。此外,Spring Boot 提供了异步处理机制(如使用 `@Async` 注解)和反应式编程模型(Spring WebFlux),使得应用能够处理更高的并发负载。在具体项目中,可以根据需求选择合适的处理模型,以充分利用 Spring Boot 的并发处理能力。
248 21
|
8月前
|
JavaScript 前端开发 Java
Jeesite5:Star24k,Spring Boot 3.3+Vue3实战开源项目,架构深度拆解!让企业级项目开发效率提升300%的秘密武器
Jeesite5 是一个基于 Spring Boot 3.3 和 Vue3 的企业级快速开发平台,集成了众多优秀开源项目,如 MyBatis Plus、Bootstrap、JQuery 等。它提供了模块化设计、权限管理、多数据库支持、代码生成器和国际化等功能,极大地提高了企业级项目的开发效率。Jeesite5 广泛应用于企业管理系统、电商平台、客户关系管理和知识管理等领域。通过其强大的功能和灵活性,Jeesite5 成为了企业级开发的首选框架之一。访问 [Gitee 页面](https://gitee.com/thinkgem/jeesite5) 获取更多信息。
336 0
Jeesite5:Star24k,Spring Boot 3.3+Vue3实战开源项目,架构深度拆解!让企业级项目开发效率提升300%的秘密武器
|
7月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
289 0
|
12月前
|
Java API Spring
在 Spring 配置文件中配置 Filter 的步骤
【10月更文挑战第21天】在 Spring 配置文件中配置 Filter 是实现请求过滤的重要手段。通过合理的配置,可以灵活地对请求进行处理,满足各种应用需求。还可以根据具体的项目要求和实际情况,进一步深入研究和优化 Filter 的配置,以提高应用的性能和安全性。