文章目录
- 问题场景
- 原因分析
- 解决方案
-
- 方案1 自定义实现HttpServletRequestWrapper
-
-
- HttpServletRequestWrapper 简介
-
- 自定义实现类
-
- 添加过滤器
-
- 测试
-
- 方案2 使用官方包装类
-
-
- 自定义过滤器
-
- 注册过滤器
-
问题场景
在使用@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;
}
}
版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: