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

2021-10-27 14:16:33 +08:00
 keshao

[ 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();      
        }
    

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

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

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

是不是 debug 模式下,事先将 inputstream 消耗过了
keshao
2021-11-10 22:55:04 +08:00
@Uyuhz 是的,这个需求后来还是通过参数值传递的方式去解决了,对整体的 log 模块做了一部分的重构。还有一些遗留问题哈哈~~ 但是问题其实跟楼下老哥说的一样,在 Thread 端#init () 其实就是引用传递。spring mvc 组件在使用完成后会直接 remove 掉 request 对象,所以出现了 debug 之后请求处理完这个 request 就是 null 的情况。
@wolfie 是的,有一部分原因是你提出的思路~ 看了很多源码跟搜索引擎才找到了答案
最后,由衷的感谢两位小哥的帮助,最近太忙了没上太多 v2 ,嘿嘿😊
另外还想对自己说一句: 再设计异步功能的时候看着点~ 不能瞎操作了哈哈
yudoo
132 天前
@keshao 分布式微服务链路追踪是不是不建议使用 TransmitterableThreadLocal 了
keshao
130 天前
@yudoo 怎么说老表
yudoo
129 天前
老表也不知道,最近刚涉猎微服务, 所以这是个疑问句, 老表怎么看
keshao
129 天前
@yudoo 目前推荐使用 TransmitterableThreadLocal ,目前 Java 的微服务还都以线程池为主。JDK 自带的 InheritableThreadLocal 不支持线程池的局部变量传递,仅支持 new Thread()的方式进行传递父子信息,TransmitterableThreadLocal 是支持的而且有很多种方式。比如修饰线程池、Java Agent 方式去支持。具体你可以看看这个: https://github.com/alibaba/transmittable-thread-local
yudoo
128 天前
@keshao 谢谢老表, 流水号生成可以用雪花算法吗, 或者 redis
感觉微服务相关知识好多 什么限流熔断 请求统一处理 结果处理 什么适配层 bff thrift 共同组件
这些你们是哪里学的呢
keshao
120 天前
@yudoo 一般来说 id 这种东西尽量去中心化,一方面可以结耦另一方面可以减少服务的链路等问题,我们一般也在用雪花算法进行处理。

学习这种的话,还是推荐具体场景去学习学习的快~

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/810958

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX