V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
keshao
V2EX  ›  问与答

[SpringBoot] 对开启 debug 模式后放在 Threadlocal 对象中 HttpServletRequest#getInputStream() 无法获取的疑问

  •  
  •   keshao · 2021-10-27 14:16:33 +08:00 · 949 次点击
    这是一个创建于 902 天前的主题,其中的信息可能已经有所发展或是发生改变。

    [ SpringBoot ] 对开启 debug 模式后放在 Threadlocal 对象中 HttpServletRequest#getInputStream() 无法获取的疑问

    各路大神,感谢花时间来一起讨论。我们的业务场景如下:

    1. 服务收到调用,先走 Filter ,并且拿到当前请求的 request 对象,因为是 tomcat 的 nio 线程池去负责调用相关业务日志代码,如果别的线程想使用当前线程对象就需要进行链式传递,所以就使用了阿里的 TTL ( TransmittableThreadLocal )进行全局的 request 传递,方便各个线程之间的 request 信息获取。
    2. 因为已经拿到 request 对象,所以我们有一个系统日志的需求。落库接口的访问 IP 、参数、返回结果、请求用户等等,这些都是从 request 对象中获取的。
    3. 具体实现采用了 Spring 的 AOP + 注解的形式
      • 命中标注注解的 controller 方法
      • AOP 去解析 request 对象,解析出来 body 、params 、url 、ip 等

    代码如下:

    1. 服务收到调用,先走 Filter , 并且拿到 request, 放入 Threadlocal 中,因为是线程池之间的传递所以使用了阿里的 ttl 进行全局的 Request 传递 当前线程: tomcat 线程

          /** Servlet 属性全局传递 ThreadLocal 前主要用于未来的分布式跟踪,以及线程池之间属性传递 */
          public static final ThreadLocal<HttpServletRequest> GLOBAL_SERVLET_REQUEST =
                  new TransmittableThreadLocal<>();
      
           
          @Override
          protected void doFilterInternal(
                  HttpServletRequest request,
                  @Nullable HttpServletResponse response,
                  @Nullable FilterChain filterChain)
                  throws ServletException, IOException {
              // 先清除 threadLocal 类中的变量
              GLOBAL_SERVLET_REQUEST.remove();
              // 重新放入 request 对象
              GLOBAL_SERVLET_REQUEST.set(request);
              //传递至下一个链中
              Objects.requireNonNull(filterChain).doFilter(request, response);
          }
      
    2. 使用 Spring 的 Aop + 注解形式 去拦截 controller 并异步调用请求日志的落库,这时候进行了线程池隔离。日志专用线程池落库 当前线程: tomcat 线程

          /**
           * 处理完请求后执行
           *
           * @param joinPoint 切点
           */
          @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
          public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
              // 调用异步写入日志
              systemLogService.executeSaveLog(joinPoint, null, jsonResult);
          }
      
    3. 因为这是一个独立的的线程池,也就是一个新的线程在处理这些。所以我必须把 request 传递进来我才可以获取到相关 request 的信息

    4. 我们都知道在 http 的 body 中,被 Java EE 的规范封装在了 HttpServletRequest 父类的 getInputStream()方法中,所以我们可以从这里获取到 body 中相关的内容

      @ToString
      public class LocalServletUtils extends AbstractServletUtils {
      
          /**
           * 从全局的 threadLocal 中获取
           *
           * @return HttpServletRequest
           */
          @Override
          public HttpServletRequest getRequest() {
              return GlobalRequestContextFilter.GLOBAL_SERVLET_REQUEST.get();
          }
      
          
          /**
           * 从 request 中获取 body
           * 使用了模板方法模式,方便预览直接粘贴在此处了
           * @return HttpServletRequest
           */
          public String getBody() {
              try {
                  BufferedReader reader =
                          new BufferedReader(new InputStreamReader(getRequest().getInputStream()));
                  //https://github.com/dromara/hutool/blob/0d8dfb73d87c28d2633a7826cc9a16f8a476372d/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java#L423
                  return IoUtil.read(reader);
              } catch (Exception e) {
                  return "get body error";
              }
          }
          
      }
      
      
      
          hutool io IoUtil code: 
          
      	/**
      	 * 从 Reader 中读取 String ,读取完毕后并不关闭 Reader
      	 *
      	 * @param reader Reader
      	 * @return String
      	 * @throws IORuntimeException IO 异常
      	 */
      	public static String read(Reader reader) throws IORuntimeException {
      		final StringBuilder builder = StrUtil.builder();
      		final CharBuffer buffer = CharBuffer.allocate(DEFAULT_BUFFER_SIZE);
      		try {
      			while (-1 != reader.read(buffer)) {
      				builder.append(buffer.flip().toString());
      			}
      		} catch (IOException e) {
      			throw new IORuntimeException(e);
      		}
      		return builder.toString();
      	}
      
    5. 日志的落库使用了 @Async 结合日志专用线程池去处理日志的落库 当前线程: 日志线程

          @Async(AsyncConfiguration.LOG_EXECUTOR)
          public void executeSaveLog(JoinPoint joinPoint, Exception e, Object json) {
              // 从 ThreadLocal 中获取 ServletUtils 工具类实例,用于获取 request 中的数据
              AbstractServletUtils servletUtils = new LocalServletUtils();
              //具体的业务代码,在这里获取 body,就在这里 request 对象忽悠
              servletUtils.getBody();      
          }
      

    但是以上代码,有几种情况

    • 我本地可使用正常的 Run 模式是可以正常使用的,而且打了 200 个请求过来没看见 error
    • 我本地 Debug 模式就无法获取到正常的 request 对象了。。黑人问号??
    • 这代码在我们的机器上也是几率性的,有时候可用有时候就不可用。。

    始终没搞明白这是为什么。。。

    5 条回复    2021-11-10 22:55:04 +08:00
    Uyuhz
        1
    Uyuhz  
       2021-10-27 14:52:04 +08:00
    应该是当前线程先于日志线程结束,当前线程将 request 对象清空了?
    keshao
        2
    keshao  
    OP
       2021-10-27 14:55:31 +08:00
    @Uyuhz 我现在也是怀疑这个,完全 copy 一份 request 是可以解决的应该。想看看别人的想法~🍧
    Uyuhz
        3
    Uyuhz  
       2021-10-27 14:59:28 +08:00
    @keshao 我之前做类似需求的时候最开始也是想直接传递 request 对象,后来 debug 了半天 request 对象里全是 null ,我就直接先从当前线程的 request 中读取信息来传递了。
    wolfie
        4
    wolfie  
       2021-10-27 15:27:01 +08:00
    线程池怎么定义的? AsyncConfiguration.LOG_EXECUTOR

    是不是 debug 模式下,事先将 inputstream 消耗过了
    keshao
        5
    keshao  
    OP
       2021-11-10 22:55:04 +08:00
    @Uyuhz 是的,这个需求后来还是通过参数值传递的方式去解决了,对整体的 log 模块做了一部分的重构。还有一些遗留问题哈哈~~ 但是问题其实跟楼下老哥说的一样,在 Thread 端#init () 其实就是引用传递。spring mvc 组件在使用完成后会直接 remove 掉 request 对象,所以出现了 debug 之后请求处理完这个 request 就是 null 的情况。
    @wolfie 是的,有一部分原因是你提出的思路~ 看了很多源码跟搜索引擎才找到了答案
    最后,由衷的感谢两位小哥的帮助,最近太忙了没上太多 v2 ,嘿嘿😊
    另外还想对自己说一句: 再设计异步功能的时候看着点~ 不能瞎操作了哈哈
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1275 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 17:59 · PVG 01:59 · LAX 10:59 · JFK 13:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.