16、SpringMVC进阶:使用HttpServletRequestWrapper解决流只能读取一次的问题

文章目录

  • 问题场景
  • 原因分析
  • 解决方案
    • 方案1 自定义实现HttpServletRequestWrapper
      1. HttpServletRequestWrapper 简介
      1. 自定义实现类
      1. 添加过滤器
      1. 测试
  • 方案2 使用官方包装类
      1. 自定义过滤器
      1. 注册过滤器

问题场景

在使用@Aspect进行切面配置打印请求日志时,获取了请求参数,然后在访问接口中,又调用了工具类去获取请求参数,发生异常。

错误信息如下:

// 在使用getInputStream方法时,发现InputStream已经被读取了。
java.lang.IllegalStateException: getInputStream() has already been called for this request
	at org.apache.catalina.connector.Request.getReader(Request.java:1222)
	at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504)
	at javax.servlet.ServletRequestWrapper.getReader(ServletRequestWrapper.java:230)
	at javax.servlet.ServletRequestWrapper.getReader(ServletRequestWrapper.java:230)
	at javax.servlet.ServletRequestWrapper.getReader(ServletRequestWrapper.java:230)
	at com.hoodev.toolkit.util.HttpUtil.readRequestBody(HttpUtil.java:406)
	at com.hoodev.toolkit.util.HttpUtil.readParameter(HttpUtil.java:383)
	at org.prime.core.boot.log.aspect.RequestLogAspect.requestAspect(RequestLogAspect.java:64)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
	at com.hnmqet.example.consume.controller.AreaMainController$$EnhancerBySpringCGLIB$$ac12e51d.list(<generated>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:652)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)

原因分析

首先看下通过Request 获取流进入的方法,该方法进入的是Tomcat 中的Request类。

    public ServletInputStream getInputStream() throws IOException {
   
     
    	// 1. 判断usingReader 属性,为真则抛出异常IllegalStateException
        if (this.usingReader) {
   
     
            throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
        } else {
   
     
        	// 2. 设置usingInputStream 属性为True
            this.usingInputStream = true;
            if (this.inputStream == null) {
   
     
            	// 3. 获取流
                this.inputStream = new CoyoteInputStream(this.inputBuffer);
            }
            return this.inputStream;
        }
    }

然后再看下getReader 方法。

    public BufferedReader getReader() throws IOException {
   
     
    	// 1. 判断usingInputStream 属性,为真则抛出异常IllegalStateException
        if (this.usingInputStream) {
   
     
            throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
        } else {
   
     
            if (this.coyoteRequest.getCharacterEncoding() == null) {
   
     
                Context context = this.getContext();
                if (context != null) {
   
     
                    String enc = context.getRequestCharacterEncoding();
                    if (enc != null) {
   
     
                        this.setCharacterEncoding(enc);
                    }
                }
            }
			// 设置usingReader 为真
            this.usingReader = true;
            this.inputBuffer.checkConverter();
            if (this.reader == null) {
   
     
                this.reader = new CoyoteReader(this.inputBuffer);
            }

            return this.reader;
        }
    }

通过以上源码,可以了解到,在获取流的过程时,会设置usingReader 、usingInputStream为真,表示该流已经被读取过了,再次获取时,则就会直接抛出IllegalStateException异常。

本质原因:因为流在读取的时候,比如read(),文件存放在硬盘上,JVM只能通过操作系统OS读取文件,read()每次读取时,都会进行标记记录当前读取的位置,下次读取时,从标记位置开始,一直到结束时返回-1。结束后再次读取时,则直接返回-1了,所以IO流是无法重复读取的,只能读取一次。

解决方案

方案1 自定义实现HttpServletRequestWrapper

1. HttpServletRequestWrapper 简介

HttpServletRequestWrapper 是tomcat 提供的基于HTTP 的Servlet 请求包装类,继承自ServletRequestWrapper,并实现了HttpServletRequest,所以它本质上也是一个HttpServletRequest。

public class HttpServletRequestWrapper extends ServletRequestWrapper implements
        HttpServletRequest {
   
     
}

官方注释如下:提供了一个HttpServletRequest接口实现,此类实现包装器或装饰器模式,所有方法默认为调用包装器的方法,开发人员可以实现此类,来自定义请求对象。

可以看到该类的构造函数,调用了父类的构造,然后所有的执行方法,都会先调用_getHttpServletReques获取到父类的HttpServletRequest,再通过HttpServletRequest获取请求中的信息。
*
简单来说,HttpServletRequestWrapper 就是一个请求包装类,我们可以通过该类对请求进行装饰,比如对参数、编码方式等等进行重新设置。

2. 自定义实现类

通过以上,可以了解到,我们可以继承HttpServletRequestWrapper,然后定义一个缓存,第一次获取时对缓存进行赋值,每次获取流时,都将缓存中的流再new 一次,重新创建一个流对象,这样就能实现多次读取流了。

public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
   
     

    /**
     * 缓存流
     */
    private byte[] cacheBody;

    /**
     * 构造方法
     *
     * @param request
     * @throws IOException
     */
    public MyHttpServletRequestWrapper(HttpServletRequest request) {
   
     
        super(request);
    }

    /**
     * 获取缓冲流
     *
     * @return
     */
    @Override
    public BufferedReader getReader() throws IOException {
   
     
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
    }

    /**
     * 获取流
     *
     * @return
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
   
     
        // 1. 初始化缓存
        if (cacheBody == null) {
   
     
            cacheBody = StreamUtils.copyToByteArray(super.getInputStream());
        }
        // 3. 从缓存中返回流
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cacheBody);
        return new ServletInputStream() {
   
     
            @Override
            public boolean isFinished() {
   
     
                return false;
            }

            @Override
            public boolean isReady() {
   
     
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
   
     

            }

            @Override
            public int read() throws IOException {
   
     
                return byteArrayInputStream.read();
            }
        };
    }
}

3. 添加过滤器

然后还需要定义一个过滤器,将包装类设置到Request 中,传递给下游使用。

@Component
public class MyHttpServletRequestWrapperFilter implements Filter {
   
     

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
   
     

    }

    /**
     * 对 request 进行包装
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
   
     
        MyHttpServletRequestWrapper requestWrapper = new MyHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy() {
   
     

    }

} 

4. 测试

测试,发现,无论获取多少次,都没有问题了。

方案2 使用官方包装类

官方也提供了HttpServletRequestWrapper的子类ContentCachingRequestWrapper,针对响应包装也提供了ContentCachingResponseWrapper,从名字上可以看出,是对请求响应内容进行缓存。
*
ContentCachingRequestWrapper源码注释如下:将请求内容进行缓存,然后可以通过getContentAsByteArray()方法从缓存中获取内容,org.springframework.web.filter.AbstractRequestLoggingFilter(请求日志记录过滤器)就使用到了该包装类。

那么ContentCachingRequestWrapper能否实现多次读流呢?可以看到其getInputStream方法,只是就流进行了包装,如果获取流的话,仍然是返回之前的流对象,所以并不能实现多次读流。
*
他的主要作用是将请求参数内容进行缓存,然后多次读取,实际多次读流,也是读取参数,可以将读流操作改为读取参数,这样也可以解决问题。
*
所以:如果可以修改读取流为读取请求内容,我们就可以使用官方这个包装类实现。

1. 自定义过滤器

参考AbstractRequestLoggingFilter我们使用该包装类进行请求包装。

首先我们定义过滤器进行请求包装,该过滤器继承OncePerRequestFilter,该类实际上是一个实现了Filter接口的抽象类。spring对Filter进行了一些封装处理。在一次外部请求中只过滤一次。对于服务器内部之间的forward等请求,不会再次执行过滤方法。

public class MyFilter extends OncePerRequestFilter {
   
     
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
   
     
        HttpServletRequest requestToUse = request;
        if (!(request instanceof ContentCachingRequestWrapper)) {
   
     
            requestToUse = new ContentCachingRequestWrapper(request);
        }
        filterChain.doFilter(requestToUse, response);
    }
}

2. 注册过滤器

然后使用FilterRegistrationBean来注册过滤器,就不直接在过滤器上添加注解了,这样做的好处是,我们可以将注册过滤器写在自动配置类中,然后通过Spring Boot的自动配置来实现注册组件。不然在开发公用组件的时候,别的包引入该功能时,还得写一个包扫描,显得不高级。。。

@Configuration
public class MyAutoConfig {
   
     

    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
   
     
        FilterRegistrationBean<MyFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new MyFilter());
        filterFilterRegistrationBean.setOrder(-1);
        filterFilterRegistrationBean.setName("myFilter");
        return filterFilterRegistrationBean;
    }
}

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: