问题描述
Java项目开发中可能存在以下几种情况:
1、你需要在拦截器中统一拦截请求,拿到请求中的参数,来做统一的判断处理或者其他操作。
那问题就来了,由于request输入流的数据只能读取一次,所以你在拦截器中你读取了输入流的数据,当请求进入后边的Controller时,输入流中已经没有数据了,导致获取不到数据。
2、你项目里可能需要搞一个统一的异常处理器,然后想在异常处理器中把发生异常的接口地址,方法名,以及请求的参数记录到日志里或者直接发送给你配置的告警系统,比如发送给钉钉群通知。这种情况下,因为你前边controller已经获取过一次request输入流了,在后边的异常处理器里你还想再从request输入流中拿到请求参数等信息,所以也会出现request流只能读取一次的错误。
以上两种情况是开发中比较常见的,当然除此之外,别的场景下你可能也会遇到request流只能读取一次的错误,所以今天就来讲一下如果遇到这种情况该怎么解决。
产生原因
1、一个InputStream对象在被读取完成后,将无法被再次读取,始终返回-1;
2、InputStream并没有实现reset方法(可以重置首次读取的位置),无法实现重置操作;
解决方法
一、引入依赖
commons-iocommons-io2.7
二、自定义Wrapper类来包装HttpServletRequest
我们需要写一个自定义包装类,并继承HttpServletRequestWrapper
import org.apache.commons.io.IOUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * @描述 包装HttpServletRequest * MyServletRequestWrapper + RequestReplaceFilter 的作用是: * 解决异常处理器中拿post请求的json参数时,报request流只能读一次的错 * 原因是 request.getReader() 和 request.getInputStream() 都是只能调用一次 * 所以我这里要使用 HttpServletRequestWrapper 来实现自定义的 MyServletRequestWrapper包装类 * 把request里的 body 保存在 MyServletRequestWrapper中, 并且重写 getInputStream()方法 * 然后所有的request都在RequestReplaceFilter中被转换成了我自定义的HttpServletRequestWrapper * 然后获取 body时就都是调用 MyServletRequestWrapper中的 getBody()方法了 * @创建人 caoju */ public class MyServletRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public MyServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); body = IOUtils.toByteArray(super.getInputStream()); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { return new RequestBodyCachingInputStream(body); } private class RequestBodyCachingInputStream extends ServletInputStream { private byte[] body; private int lastIndexRetrieved = -1; private ReadListener listener; public RequestBodyCachingInputStream(byte[] body) { this.body = body; } @Override public int read() throws IOException { if (isFinished()) { return -1; } int i = body[lastIndexRetrieved + 1]; lastIndexRetrieved++; if (isFinished() && listener != null) { try { listener.onAllDataRead(); } catch (IOException e) { listener.onError(e); throw e; } } return i; } @Override public boolean isFinished() { return lastIndexRetrieved == body.length - 1; } @Override public boolean isReady() { return isFinished(); } @Override public void setReadListener(ReadListener listener) { if (listener == null) { throw new IllegalArgumentException("listener cann not be null"); } if (this.listener != null) { throw new IllegalArgumentException("listener has been set"); } this.listener = listener; if (!isFinished()) { try { listener.onAllDataRead(); } catch (IOException e) { listener.onError(e); } } else { try { listener.onAllDataRead(); } catch (IOException e) { listener.onError(e); } } } @Override public int available() throws IOException { return body.length - lastIndexRetrieved - 1; } @Override public void close() throws IOException { lastIndexRetrieved = body.length - 1; body = null; } } }
三、创建过滤器,通过过滤器包装原有的request对象
import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @描述 * @创建人 caoju */ @Component public class RequestReplaceFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!(request instanceof MyServletRequestWrapper)) { request = new MyServletRequestWrapper(request); } filterChain.doFilter(request, response); /*//如果有文件上传的业务场景,需要用下面的代码进行处理,不然文件上传的流会有问题 String contentType = request.getContentType(); //如果contentType是空 //或者contentType是多媒体的上传类型则忽略,不进行包装,直接return if (contentType == null) { filterChain.doFilter(request, response); return; }else if(request.getContentType().startsWith("multipart/")){ filterChain.doFilter(request, response); return; }else if (!(request instanceof MyServletRequestWrapper)) { request = new MyServletRequestWrapper(request); } filterChain.doFilter(request, response); */ } }
通过以上几步,我们就实现了把request里的 body 保存在 MyServletRequestWrapper中的效果
就可以在整个请求链路中任何地方去重复的获取request流了
四、使用案例
配置好之后,就可以在整个请求链路中任何地方去重复的获取request流了。
比如,你可以在请求刚进来时,在过滤器或者拦截器里拿到request对象,再拿到request对象的流数据,去做一些事情,或者你也可以在请求即将结束时,在统一的异常处理器中拿到request对象,拿到request对象流数据里请求的json参数;等等等等,还有其他很多你想使用的场景,都可以这么做。
下面是在代码中利用RequestContextHolder获取request对象,拿到request对象后就可以获取请求方式、请求url、以及请求参数这些数据了。如果你在某些地方也有需要打印记录请求方式、请求url、请求参数的这些需求,那可以直接复制粘贴我下边的代码就ok了
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); String mode = ""; String methodUrl = ""; String param = ""; if (requestAttributes != null) { ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = attributes.getRequest(); //请求方式 mode = request.getMethod(); //方法URL methodUrl = request.getRequestURI(); if(mode.equals(HttpMethod.GET.name())){ param = request.getQueryString(); } if(mode.equals(HttpMethod.POST.name())){ param = getJsonRequest(request); } }
/** * 获取Request中的JSON字符串 * @param request * @return * @throws IOException */ public static String getJsonRequest(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); try (BufferedReader reader = request.getReader();) { char[] buff = new char[1024]; int len; while ((len = reader.read(buff)) != -1) { sb.append(buff, 0, len); } } catch (IOException e) { log.error("POST请求参数获取异常", e); } return sb.toString(); }
ok,到这里解决request流只能获取一次的问题就搞定了
希望对你有所帮助
以上就是解决Java项目中request流只能获取一次的问题的详细内容,更多关于Java request流获取一次的资料请关注IT俱乐部其它相关文章!