优秀网文 / 从零搭建开发脚手架 从零搭建开发脚手架(一) 2021-04-22 # 从零搭建开发脚手架 HttpServletRequest多次读取异常,仅能读取一次 [TOC] ## 文章背景 在过滤器或者Controller中多次调用HttpServletRequest.getReader()或getInputStream()方法,会导致异常。 **给出示例代码如下:** ```java @RequestMapping(value = "/param") private ResponseEntity<String> param(HttpServletRequest request, @RequestBody Map body){ // ... String string = IOUtils.toString(request.getInputStream()); // ... } ``` **Postman请求如下:** ![](/uploads/1/image/public/202104/20210422102924_2u09ga1k7a.png) **错误如下:** ![](/uploads/1/image/public/202104/20210422103036_fdfo2d20o6.png) ## 原因 Json数据是放在Http协议的Body中的,我们需要通过request.getInputStream()或者@RequestBody(本质也是调用request.getInputStream())获取请求体内容。 当我们调用request.getInputStream()时,可以查看其Api,其返回的是ServletInputStream继承于InputStream。 ```java public ServletInputStream getInputStream() throws IOException; public abstract class ServletInputStream extends InputStream { // ... } ``` **知识梳理-流:** InputStream的read方法内部有一个position标志标记当前读取到的位置,当读取到最后会返回-1,表示读取完毕。如果想要重新读取则需要使用mark和reset方法配合使用,把position移动到起始位置,就能从头读取实现多次读取,但是InputStream和ServletInputStream都未重写mark和reset方法。 所以就导致HttpServletRequest.getReader()或getInputStream()方法不能多次读取。 ## 解决办法 使用HttpServletRequestWrapper,此类是HttpServletRequest的包装类,基于装饰器模式实现HttpServletRequest功能扩展。我们可以通过继承包装类HttpServletRequestWrapper来实现自定义扩展功能。 - ①重新定义一个容器(字节数组),把读取到的流数据存储其中供以后多次使用。 - ②重写getReader()和getInputStream()方法,改为每次从自定义容器中获取内容。 - ③配合Filter把原始的HttpServletRequest替换为我们自定义的包装类xxxHttpServletRequestWrapper **代码如下:** - CachedBodyHttpServletRequestWrapper.java ```java public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper { private byte[] cachedBody; public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); InputStream requestInputStream = request.getInputStream(); this.cachedBody = StreamUtils.copyToByteArray(requestInputStream); } @Override public ServletInputStream getInputStream() throws IOException { return new CachedBodyServletInputStream(this.cachedBody); } @Override public BufferedReader getReader() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody); return new BufferedReader(new InputStreamReader(byteArrayInputStream)); } public class CachedBodyServletInputStream extends ServletInputStream { private InputStream cachedBodyInputStream; public CachedBodyServletInputStream(byte[] cachedBody) { this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody); } @Override public int read() throws IOException { return cachedBodyInputStream.read(); } // ... } } ``` - ContentCachingFilter.java ```java @Order(value = Ordered.HIGHEST_PRECEDENCE) @Component @WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*") public class ContentCachingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { // 不能拦截 application/x-www-form-urlencoded 和 multipart/form-data请求,否则会出现参数丢失,因为都是是http body中的 它们2个只能从流读取一次,后面解析的时候会出现问题。 if (StrUtil.contains(httpServletRequest.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { CachedBodyHttpServletRequestWrapper cachedBodyHttpServletRequest = new CachedBodyHttpServletRequestWrapper(httpServletRequest); filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse); } else { filterChain.doFilter(httpServletRequest, httpServletResponse); } } } ``` ## 扩展思考 ### 1.是否存在线程安全问题? 实测结果如下图,非单例,不存在线程安全问题。 ![](/uploads/1/image/public/202104/20210422104119_ulhuo5u8h2.png) ### 2.加载顺序问题? ContentCachingFilter必须在Filter链中的第一个,否则后面使用的是非自定义包装类而是默认的HttpServletRequest,将无法起作用。 ### 3.OncePerRequestFilter和Filter的区别 OncePerRequestFilter 实现了 Filter 接口。 ```java OncePerRequestFilter extends GenericFilterBean implements Filter{ } ``` **在Spring中,Filter默认继承OncePerRequestFilter。**,顾名思义,它能够确保在一次请求中只通过一次filter,这样做的原因是为了**兼容不同的容器(web container)**,也就是说并不是所有的container都入我们期望的只过滤一次,servlet版本不同,执行过程也不同,Spring的javadoc这么说的: ```java * * <p>As of Servlet 3.0, a filter may be invoked as part of a * {@link javax.servlet.DispatcherType#REQUEST REQUEST} or * {@link javax.servlet.DispatcherType#ASYNC ASYNC} dispatches that occur in * separate threads. A filter can be configured in {@code web.xml} whether it * should be involved in async dispatches. However, in some cases servlet * containers assume different default configuration. # 意思:去适配了不同的web容器,以及对异步请求,也只过滤一次的需求。如:servlet2.3与servlet2.4也有一定差异: ``` > - 在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况 > - 在servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤, **因此建议:在Spring环境下使用Filter的话,建议继承OncePerRequestFilter** ![](/uploads/1/image/public/202104/20210422104431_036c0takme.png) ## 扩展阅读 ![](/uploads/1/image/public/202104/20210422105816_fxsjmtchbf.png) ### 前言 > 在Spring中,Filter默认继承OncePerRequestFilter,只执行一次的Filter。 ### 阅读理解 **需要注意的是:**在OncePerRequestFilter的父类中,init方法被final掉了,因此无法被重写,但是我们可以通过重写initFilterBean这个方法实现我们比init方法更强大的一些逻辑。如下: ```java @Component("helloFilter") public class HelloFilter extends OncePerRequestFilter { @Override protected void initFilterBean() throws ServletException { System.out.println("Filter初始化..."); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { FilterConfig filterConfig = super.getFilterConfig(); ServletContext servletContext = super.getServletContext(); Environment environment = super.getEnvironment(); filterChain.doFilter(request, response); } } ``` **注意:initFilterBean方法在初始化的时候会被执行两次(没什么影响),**主要是在GenericFilterBean类: ```java @Override public void afterPropertiesSet() throws ServletException { initFilterBean(); } //还有init方法中: @Override public final void init(FilterConfig filterConfig) throws ServletException { ... // Let subclasses do whatever initialization they like. initFilterBean(); ... } ``` 另外,继承自OncePerRequestFilter的Filter采用@WebFilter以及Spring Bean的方式好使的,若采用@WebFilterinitFilterBean方法就只会被执行一次,但是,但是,但是此时@Autowaire自动注入就不好使了,需要自己去容器里拿:**super.getServletContext();**,所以看自己需求,哪个方便选哪个。(比如我的Filter只需要记录一下请求日志,没必要注入Spring的Bean,那么选@WebFilter就更方便了) ### OncePerRequestFilter源码解读 ```java /** * 过滤器基类,旨在确保每个请求调度在任何servlet容器上执行一次执行。 * 它提供了一个带有HttpServletRequest和HttpServletResponse参数的{@link #doFilterInternal}方法。 */ public abstract class OncePerRequestFilter extends GenericFilterBean { ... } ``` ```java // 已过滤过的过滤器的固定后缀名 public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED"; // 相当于生成已过滤的过滤器的名字(全局唯一的) protected String getAlreadyFilteredAttributeName() { String name = getFilterName(); if (name == null) { name = getClass().getName(); } return name + ALREADY_FILTERED_SUFFIX; } //判断该请求是否是异步请求(Servlet 3.0后有异步请求,Spring MVC3.2开始) protected boolean isAsyncDispatch(HttpServletRequest request) { return WebAsyncUtils.getAsyncManager(request).hasConcurrentResult(); } //是否需要不过滤异步的请求(默认是不多次过滤异步请求的) //javadoc:javax.servlet.DispatcherType.ASYNC的请求方式意味着可能在一个请求里这个过滤器会被多个不同线程调用多次,而这里返回true,就能保证只会被调用一次 protected boolean shouldNotFilterAsyncDispatch() { return true; } //原理基本同上 protected boolean shouldNotFilterErrorDispatch() { return true; } //可以人工直接返回true 那这个请求就肯定不会被过滤了~~~~ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { return false; } //这里很清楚的记录着 需要被跳过的请求们,这种请求直接就放行了 private boolean skipDispatch(HttpServletRequest request) { if (isAsyncDispatch(request) && shouldNotFilterAsyncDispatch()) { return true; } if (request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null && shouldNotFilterErrorDispatch()) { return true; } return false; } ``` **doFilter方法:** ```java @Override public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)throws ServletException, IOException { //只处理http请求 if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { throw new ServletException("OncePerRequestFilter just supports HTTP requests"); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; //判断这个请求是否需要执行过滤 String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null; if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) { // 直接放行,不执行此过滤器的过滤操作 filterChain.doFilter(request, response); } else { // 执行过滤,并且向请求域设置一个值,key就是生成的全局唯一的·alreadyFilteredAttributeName· request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { //由子类自己去实现拦截的逻辑 注意 自己写时,filterChain.doFilter(request, response);这句代码不要忘了 doFilterInternal(httpRequest, httpResponse, filterChain); } finally { // Remove the "already filtered" request attribute for this request. request.removeAttribute(alreadyFilteredAttributeName); } } } ``` 最后有必要父类中提供的一个方法,获取到该Filter的名字: ```java // 如果在Spring应用程序上下文中初始化为bean,那么它将返回到bean工厂中定义的bean名称。 // 需要注意的是,如果是以bean的形式加入了。(比如Boot环境下),此时FilterConfig还未null的,所以有这个判断 @Nullable protected String getFilterName() { return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName); } ``` ### 几个常用的实现了OncePerRequestFilter的Spring内置Filter #### CharacterEncodingFilter 解决body编码(乱码问题的) **web.xml时代时,这样配置:** ```xml <!-- characterEncodingFilter字符编码过滤器 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <!--要使用的字符集,一般我们使用UTF-8(保险起见UTF-8最好)--> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <!--是否强制设置request的编码为encoding,默认false,不建议更改--> <param-name>forceRequestEncoding</param-name> <param-value>false</param-value> </init-param> <init-param> <!--是否强制设置response的编码为encoding,建议设置为true,下面有关于这个参数的解释--> <param-name>forceResponseEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <!--这里不能留空或者直接写 ' / ' ,否者不起作用--> <url-pattern>/*</url-pattern> </filter-mapping> ``` **当前解决方案:** - ①Filter的方式:使用传统的Spring提供的字符编码过滤器 ```java @Configuration public class CharacterConfig { @Bean public FilterRegistrationBean filterRegistrationBean(){ //注意:只有当application.properties配置文件中server.servlet.encoding.enabled=false时,上述编码过滤器才会工作 //创建SpringWeb提供的字符编码过滤器,主要实现字符编码过滤 CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setForceEncoding(true);//强制对请求的编码, filter.setEncoding("GBK");//使用GBK编码 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(filter); //表示将请求路径为“/”的所有请求都进行字符编码的过滤 filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } } ``` - ①配置文件的方式:使用传统的Spring提供的字符编码过滤器(优先级高于Filter方式) - 第一种加入编码的方式。这种方式好像现在的springboot不太推荐 server.servlet.encoding.enabled=true - 第二种方式:推荐的方式,第二种方式需要将上面的false改成true server.servlet.encoding.charset=UTF-8 server.servlet.encoding.force=true #### HiddenHttpMethodFilter 让form表单拥有发标准http请求的能力 浏览器form表单只支持GET与POST请求,而DELETE、PUT等method并不支持,spring3.0添加了这个过滤器,可以让我们的form表达拥有发任何标准的http请求的能力了。 #### HttpPutFormContentFilter 解决tomcat默认只解析POST的表单请求问题 用ajax发送一个put请求给后台的Spring MVC,发现request.getParameter()的时候拿不到值,很是纳闷。 其实,是因为对于表单提交,tomcat默认只解析POST的表单,对于PUT和DELETE的不处理,所以Spring拿不到值,解决办法: - 办法一:修改tomcat的server.xml(极其不推荐) ```xml <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" parseBodyMethods="POST,PUT,DELETE" URIEncoding="UTF-8" /> ``` - 办法二:使用Spring提供的HttpPutFormContentFilter(推荐) ```xml <filter> <filter-name>httpPutFormContentFilter</filter-name> <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class> </filter> <filter-mapping> <filter-name>httpPutFormContentFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ``` 或 ```java @Configuration public class HttpMethodConfig { @Bean public FilterRegistrationBean filterRegistrationBean(){ //注意:只有当application.properties配置文件中server.servlet.encoding.enabled=false时,上述编码过滤器才会工作 //创建SpringWeb提供的字符编码过滤器,主要实现字符编码过滤 HttpPutFormContentFilter filter = new HttpPutFormContentFilter(); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(filter); //表示将请求路径为“/”的所有请求都进行过滤 filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } } ``` #### FormContentFilter:处理PUT请求等的请求参数 Spring 5.1后才推出。该过滤器针对DELETE,PUT和PATCH这三种HTTP method分析其FORM表单参数,将其暴露为Servlet请求参数,缺省情况下,Servlet规范仅针对HTTP POST做这样的要求,因为FormContentFilter依赖的是Spring MVC的消息转换器:FormHttpMessageConverter,所以它支持的MediaType也必须只能是application/x-www-form-urlencoded的,Servlet规范对POST一样对待,也是必须遵循我们博文里说的4大规范的从FormContentFilter的效果也能想到,它肯定会调用request.getInputStream();,所以后续我们不能再使用getInputStream()了。另外它要想getParameter系列方法有效果,所以必须包装一下request。它用的是自己的静态内部类:**private static class FormContentRequestWrapper extends HttpServletRequestWrapper { ... }** #### RequestContextFilter 请求上下文过滤器 作用:让你在一个请求的线程内,任意地方都可以获取到请求参数的相关信息,非常的方便。 这里面有两个Spring里面非常重要的类: org.springframework.context.i18n.LocaleContextHolder org.springframework.web.context.request.RequestContextHolder 这样当前请求随后的处理过程中,就可以在当前线程中获取的当前请求的信息,而无需把请求对象作为参数到处传递 。 这里注意一个概念,缺省情况下,Servlet容器对一个请求的整个处理过程,是由同一个线程完成的,中途不会切换线程。但这个线程在处理完一个请求后,会被放回到线程池用于处理其他请求。 该过滤器由web.xml或者WebMvcAutoConfiguration(Spring Boot内的)注册。 Spring还有两个类,也做了同样的事情,也会达到此种效果: RequestContextListener和DispatcherServlet。因此可以看出,如果我们已经配置了DispatcherServlet是正常的Spring MVC环境,是没必要在配置此Filter的。 DispatcherServlet中往当前线程中设置请求的逻辑已经已经足够了,但是在一个Web应用中,并不是所有的请求都最终会被DispatcherServlet处理,比如匿名用户访问一个登录用户才能访问的资源,此时请求只会被安全过滤器处理,而不会到达DispatcherServlet,在这种情况下,该过滤器RequestContextFilter就起了担当了相应的职责。 Springboot 提供了一个OrderedRequestContextFilter继承自RequestContextFilter应用在基于Springboot的Servlet Web应用中。OrderedRequestContextFilter在RequestContextFilter的功能上仅仅增加了接口OrderedFilter定义的过滤器顺序,并且缺省使用优先级(-105)。在整个Servlet过滤器链中,过滤器的顺序数字越小,表示越先被调用。 这个过滤器的源码比较简单,反倒我觉得核心在LocaleContextHolder和RequestContextHolder这里。这里就不展开了,核心原理还是强大的ThreadLocal #### MultipartFilte 和文件上传有关 当我们需要自定义文件上传解析器的时候,需要用到它来切换。 Spring内置了两个上传处理器 ![](/uploads/1/image/public/202104/20210422121409_ahlo87iq39.png) CommonsMultipartResolver:使用commons Fileupload来处理multipart请求,使用时需导入jar包。 StandardServletMultipartResolver:是基于Servlet3.0来处理multipart请求的,所以不需要引用其他jar包,但是必须使用支持Servlet3.0的容器 #### CorsFilter 跨域相关的过滤器 > 跨域:当一个资源从与该资源本身所在的服务器不同的域或端口不同的域或不同的端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 出于安全考虑,浏览器会限制从脚本内发起的跨域HTTP请求。跨域资源共享机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。浏览器支持在 API 容器中使用 CORS,以降低跨域 HTTP 请求所带来的风险。 针对于JAVA开发而言,为了更好的做业务分层,经常会将前后端代码分离开来,发布在不同的服务器上,此时,便会遇到跨域的问题。 #### AbstractRequestLoggingFilter 记录请求日志的一个过滤器 Spring默认给我们提供两个实现: - CommonsRequestLoggingFilter: 它调用初始化时候设置的GenericFilterBean中的logger进行记录,并且默认记录debug级别日志。 ```java @Override protected boolean shouldLog(HttpServletRequest request) { return logger.isDebugEnabled(); } @Override protected void beforeRequest(HttpServletRequest request, String message) { logger.debug(message); } @Override protected void afterRequest(HttpServletRequest request, String message) { logger.debug(message); } ``` - ServletContextRequestLoggingFilter: 使用ServletContext来记录日志,【不会输出到控制台,ServletContext.log()日志输出tomcat的目录下,具体位置和tomcat的配置有关】 ```java @Override protected void beforeRequest(HttpServletRequest request, String message) { getServletContext().log(message); } @Override protected void afterRequest(HttpServletRequest request, String message) { getServletContext().log(message); } ``` > 内置的两个实现,我们一般都用不着,此处主要是我们自己去实现的意义非常大。 ```java /** * 打印请求日志,默认打印get放,只打印POST、PUT、DELETE方法的请求日志哦~ * 会计算出请求耗时、client请求的ip地址等等 contentType也会记录打印出来 信息比较全 方便查找问题~ * * @author fangshixiang * @description * @date 2019-02-16 22:04 */ @Slf4j @WebFilter(urlPatterns = "/*") public class RequestLogFilter extends AbstractRequestLoggingFilter { //这些配置都可以在init-param中进行设置,但是基于注解的,这里就不要这么麻烦了,统一在初始化的时候设置值吧 //private boolean includeQueryString = false; //private boolean includeClientInfo = false; //private boolean includeHeaders = false; //private boolean includePayload = false; private static final String PROCESS_START_TIME_SUFFIX = ".PROCESS_START_TIME"; @Override protected void initFilterBean() throws ServletException { super.setIncludeQueryString(true); super.setIncludeClientInfo(true); //因为headers里面的信息太多了 所以这里不输出了,否则过于干扰,只手动把content-type等关键信息输出即可 super.setIncludeHeaders(false); super.setIncludePayload(true); super.setMaxPayloadLength(1000); //最大支持到1000个字符 //头信息 super.setBeforeMessagePrefix("请求开始 ["); super.setBeforeMessageSuffix("]"); super.setAfterMessagePrefix("请求结束 ["); super.setAfterMessageSuffix("]"); } @Override protected boolean shouldLog(HttpServletRequest request) { String method = request.getMethod(); return HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.DELETE.matches(method); } @Override protected void beforeRequest(HttpServletRequest request, String message) { logger.info(calcRequestTime(request) .concat(getConfigTypeLog(request)) .concat(getThreadId()) .concat(message)); } @Override protected void afterRequest(HttpServletRequest request, String message) { logger.info(calcRequestTime(request) .concat(getConfigTypeLog(request)) .concat(getThreadId()) .concat(message)); } //拼装contentType private String getConfigTypeLog(HttpServletRequest request) { String contentType = request.getContentType(); String method = request.getMethod(); return "[contentType=" + contentType + "] " + method.toUpperCase() + " "; } //计算请求耗时 private String calcRequestTime(HttpServletRequest request) { long mills = 0; String requestTimeUniqueName = getRequestTimeUniqueName(); Object processStartTime = request.getAttribute(requestTimeUniqueName); if (processStartTime == null) { //首次 放置值 request.setAttribute(requestTimeUniqueName, Instant.now()); } else { //请求结束的处理 开始计算吧 Instant start = (Instant) processStartTime; Instant now = Instant.now(); mills = Duration.between(start, now).toMillis(); request.removeAttribute(requestTimeUniqueName); } return mills == 0 ? "" : ("[耗时:" + mills + "ms] "); } private String getRequestTimeUniqueName() { return this.getClass().getName().concat(PROCESS_START_TIME_SUFFIX); } private String getThreadId() { return "[ThreadId:" + Thread.currentThread().getId() + "] "; } } ``` > 此请求日志Filter一般放在TokenFilter后面执行(如果业务特殊,放在前面执行也可) 拦截器(Interceptor)是基于Java的反射机制,而过滤器(Filter)是基于函数回调 ## 参考: - https://cloud.tencent.com/developer/article/1497822 - https://blog.csdn.net/abu935009066/article/details/113870578