[SpringBoot] 对开启 debug 模式后放在 Threadlocal 对象中 HttpServletRequest#getInputStream() 无法获取的疑问 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
keshao
V2EX    问与答

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

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

    [ 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 对象了。。黑人问号??
    • 这代码在我们的机器上也是几率性的,有时候可用有时候就不可用。。

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

    11 条回复    2024-07-31 22:22: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 ,嘿嘿
    另外还想对自己说一句: 再设计异步功能的时候看着点~ 不能瞎操作了哈哈
    yudoo
        6
    yudoo  
       2024-07-19 15:30:41 +08:00
    @keshao 分布式微服务链路追踪是不是不建议使用 TransmitterableThreadLocal 了
    keshao
        7
    keshao  
    OP
       2024-07-22 09:35:31 +08:00
    @yudoo 怎么说老表
    yudoo
        8
    yudoo  
       2024-07-22 14:11:16 +08:00
    老表也不知道,最近刚涉猎微服务, 所以这是个疑问句, 老表怎么看
    keshao
        9
    keshao  
    OP
       2024-07-23 09:39:26 +08:00
    @yudoo 目前推荐使用 TransmitterableThreadLocal ,目前 Java 的微服务还都以线程池为主。JDK 自带的 InheritableThreadLocal 不支持线程池的局部变量传递,仅支持 new Thread()的方式进行传递父子信息,TransmitterableThreadLocal 是支持的而且有很多种方式。比如修饰线程池、Java Agent 方式去支持。具体你可以看看这个: https://github.com/alibaba/transmittable-thread-local
    yudoo
        10
    yudoo  
       2024-07-23 15:05:55 +08:00
    @keshao 谢谢老表, 流水号生成可以用雪花算法吗, 或者 redis
    感觉微服务相关知识好多 什么限流熔断 请求统一处理 结果处理 什么适配层 bff thrift 共同组件
    这些你们是哪里学的呢
    keshao
        11
    keshao  
    OP
       2024-07-31 22:22:04 +08:00
    @yudoo 一般来说 id 这种东西尽量去中心化,一方面可以结耦另一方面可以减少服务的链路等问题,我们一般也在用雪花算法进行处理。

    学习这种的话,还是推荐具体场景去学习学习的快~
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1485 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 16:41 PVG 00:41 LAX 09:41 JFK 12:41
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86