• Java项目里解决request流只能获取一次的问题


    问题描述

    Java项目开发中可能存在以下几种情况:

    1、你需要在拦截器中统一拦截请求,拿到请求中的参数,来做统一的判断处理或者其他操作。

    那问题就来了,由于request输入流的数据只能读取一次,所以你在拦截器中你读取了输入流的数据,当请求进入后边的Controller时,输入流中已经没有数据了,导致获取不到数据。

    2、你项目里可能需要搞一个统一的异常处理器,然后想在异常处理器中把发生异常的接口地址,方法名,以及请求的参数记录到日志里或者直接发送给你配置的告警系统,比如发送给钉钉群通知。这种情况下,因为你前边controller已经获取过一次request输入流了,在后边的异常处理器里你还想再从request输入流中拿到请求参数等信息,所以也会出现request流只能读取一次的错误。

    以上两种情况是开发中比较常见的,当然除此之外,别的场景下你可能也会遇到request流只能读取一次的错误,所以今天就来讲一下如果遇到这种情况该怎么解决。

    产生原因

    1、一个InputStream对象在被读取完成后,将无法被再次读取,始终返回-1;

    2、InputStream并没有实现reset方法(可以重置首次读取的位置),无法实现重置操作;

    解决方法

    一、引入依赖

    1. <dependency>
    2. <groupId>commons-io</groupId>
    3. <artifactId>commons-io</artifactId>
    4. <version>2.7</version>
    5. </dependency>

    二、自定义Wrapper类来包装HttpServletRequest

    我们需要写一个自定义包装类,并继承HttpServletRequestWrapper

    1. import org.apache.commons.io.IOUtils;
    2. import javax.servlet.ReadListener;
    3. import javax.servlet.ServletInputStream;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletRequestWrapper;
    6. import java.io.BufferedReader;
    7. import java.io.IOException;
    8. import java.io.InputStreamReader;
    9. /**
    10. * @描述 包装HttpServletRequest
    11. * MyServletRequestWrapper + RequestReplaceFilter 的作用是:
    12. * 解决异常处理器中拿post请求的json参数时,报request流只能读一次的错
    13. * 原因是 request.getReader() 和 request.getInputStream() 都是只能调用一次
    14. * 所以我这里要使用 HttpServletRequestWrapper 来实现自定义的 MyServletRequestWrapper包装类
    15. * 把request里的 body 保存在 MyServletRequestWrapper中, 并且重写 getInputStream()方法
    16. * 然后所有的request都在RequestReplaceFilter中被转换成了我自定义的HttpServletRequestWrapper
    17. * 然后获取 body时就都是调用 MyServletRequestWrapper中的 getBody()方法了
    18. * @创建人 caoju
    19. */
    20. public class MyServletRequestWrapper extends HttpServletRequestWrapper {
    21. private final byte[] body;
    22. public MyServletRequestWrapper(HttpServletRequest request) throws IOException {
    23. super(request);
    24. body = IOUtils.toByteArray(super.getInputStream());
    25. }
    26. @Override
    27. public BufferedReader getReader() throws IOException {
    28. return new BufferedReader(new InputStreamReader(getInputStream()));
    29. }
    30. @Override
    31. public ServletInputStream getInputStream() throws IOException {
    32. return new RequestBodyCachingInputStream(body);
    33. }
    34. private class RequestBodyCachingInputStream extends ServletInputStream {
    35. private byte[] body;
    36. private int lastIndexRetrieved = -1;
    37. private ReadListener listener;
    38. public RequestBodyCachingInputStream(byte[] body) {
    39. this.body = body;
    40. }
    41. @Override
    42. public int read() throws IOException {
    43. if (isFinished()) {
    44. return -1;
    45. }
    46. int i = body[lastIndexRetrieved + 1];
    47. lastIndexRetrieved++;
    48. if (isFinished() && listener != null) {
    49. try {
    50. listener.onAllDataRead();
    51. } catch (IOException e) {
    52. listener.onError(e);
    53. throw e;
    54. }
    55. }
    56. return i;
    57. }
    58. @Override
    59. public boolean isFinished() {
    60. return lastIndexRetrieved == body.length - 1;
    61. }
    62. @Override
    63. public boolean isReady() {
    64. return isFinished();
    65. }
    66. @Override
    67. public void setReadListener(ReadListener listener) {
    68. if (listener == null) {
    69. throw new IllegalArgumentException("listener cann not be null");
    70. }
    71. if (this.listener != null) {
    72. throw new IllegalArgumentException("listener has been set");
    73. }
    74. this.listener = listener;
    75. if (!isFinished()) {
    76. try {
    77. listener.onAllDataRead();
    78. } catch (IOException e) {
    79. listener.onError(e);
    80. }
    81. } else {
    82. try {
    83. listener.onAllDataRead();
    84. } catch (IOException e) {
    85. listener.onError(e);
    86. }
    87. }
    88. }
    89. @Override
    90. public int available() throws IOException {
    91. return body.length - lastIndexRetrieved - 1;
    92. }
    93. @Override
    94. public void close() throws IOException {
    95. lastIndexRetrieved = body.length - 1;
    96. body = null;
    97. }
    98. }
    99. }

    三、创建过滤器,通过过滤器包装原有的request对象

    1. import org.springframework.stereotype.Component;
    2. import org.springframework.web.filter.OncePerRequestFilter;
    3. import javax.servlet.FilterChain;
    4. import javax.servlet.ServletException;
    5. import javax.servlet.http.HttpServletRequest;
    6. import javax.servlet.http.HttpServletResponse;
    7. import java.io.IOException;
    8. /**
    9. * @描述
    10. * @创建人 caoju
    11. */
    12. @Component
    13. public class RequestReplaceFilter extends OncePerRequestFilter {
    14. @Override
    15. protected void doFilterInternal(HttpServletRequest request,
    16. HttpServletResponse response,
    17. FilterChain filterChain) throws ServletException, IOException {
    18. if (!(request instanceof MyServletRequestWrapper)) {
    19. request = new MyServletRequestWrapper(request);
    20. }
    21. filterChain.doFilter(request, response);
    22. /*//如果有文件上传的业务场景,需要用下面的代码进行处理,不然文件上传的流会有问题
    23. String contentType = request.getContentType();
    24. //如果contentType是空
    25. //或者contentType是多媒体的上传类型则忽略,不进行包装,直接return
    26. if (contentType == null) {
    27. filterChain.doFilter(request, response);
    28. return;
    29. }else if(request.getContentType().startsWith("multipart/")){
    30. filterChain.doFilter(request, response);
    31. return;
    32. }else if (!(request instanceof MyServletRequestWrapper)) {
    33. request = new MyServletRequestWrapper(request);
    34. }
    35. filterChain.doFilter(request, response);
    36. */
    37. }
    38. }

    通过以上几步,我们就实现了把request里的 body 保存在 MyServletRequestWrapper中的效果

    就可以在整个请求链路中任何地方去重复的获取request流了

    四、使用案例

    配置好之后,就可以在整个请求链路中任何地方去重复的获取request流了。

    比如,你可以在请求刚进来时,在过滤器或者拦截器里拿到request对象,再拿到request对象的流数据,去做一些事情,或者你也可以在请求即将结束时,在统一的异常处理器中拿到request对象,拿到request对象流数据里请求的json参数;等等等等,还有其他很多你想使用的场景,都可以这么做。

    下面是在代码中利用RequestContextHolder获取request对象,拿到request对象后就可以获取请求方式、请求url、以及请求参数这些数据了。如果你在某些地方也有需要打印记录请求方式、请求url、请求参数的这些需求,那可以直接复制粘贴我下边的代码就ok了

    1. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    2. String mode = "";
    3. String methodUrl = "";
    4. String param = "";
    5. if (requestAttributes != null) {
    6. ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
    7. HttpServletRequest request = attributes.getRequest();
    8. //请求方式
    9. mode = request.getMethod();
    10. //方法URL
    11. methodUrl = request.getRequestURI();
    12. if(mode.equals(HttpMethod.GET.name())){
    13. param = request.getQueryString();
    14. }
    15. if(mode.equals(HttpMethod.POST.name())){
    16. param = getJsonRequest(request);
    17. }
    18. }
    1. /**
    2. * 获取Request中的JSON字符串
    3. * @param request
    4. * @return
    5. * @throws IOException
    6. */
    7. public static String getJsonRequest(HttpServletRequest request) {
    8. StringBuilder sb = new StringBuilder();
    9. try (BufferedReader reader = request.getReader();) {
    10. char[] buff = new char[1024];
    11. int len;
    12. while ((len = reader.read(buff)) != -1) {
    13. sb.append(buff, 0, len);
    14. }
    15. } catch (IOException e) {
    16. log.error("POST请求参数获取异常", e);
    17. }
    18. return sb.toString();
    19. }

    ok,到这里解决request流只能获取一次的问题就搞定了

    希望对你有所帮助

  • 相关阅读:
    谷歌开发者社区推荐:《Jetpack Compose 从入门到实战》新书上架,带你踏上 Compose 开发之旅~
    maven仓库-阿里镜像-下载问题
    SpringMVC拦截器
    PRC是什么 | 图解系列
    【感性认识】嵌入式开发有何不同
    Codeforces Global Round 21 B. NIT Destroys the Universe
    Antlr4 语法存在错误但语法分析器不报错的问题
    设计模式(12)状态模式
    【深度学习】目标检测,Faster-RCNN算法训练,使用mmdetection训练
    【沐风老师】3DMAX一键生成圣诞树建模插件使用教程
  • 原文地址:https://blog.csdn.net/ju_362204801/article/details/126867741