[ SpringBoot ] 对开启 debug 模式后放在 Threadlocal 对象中 HttpServletRequest#getInputStream() 无法获取的疑问
各路大神,感谢花时间来一起讨论。我们的业务场景如下:
代码如下:
服务收到调用,先走 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); }
使用 Spring 的 Aop + 注解形式 去拦截 controller 并异步调用请求日志的落库,这时候进行了线程池隔离。日志专用线程池落库 当前线程: tomcat 线程
/** * 处理完请求后执行 * * @param joinPoint 切点 */ @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult") public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) { // 调用异步写入日志 systemLogService.executeSaveLog(joinPoint, null, jsonResult); }
因为这是一个独立的的线程池,也就是一个新的线程在处理这些。所以我必须把 request 传递进来我才可以获取到相关 request 的信息
我们都知道在 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(); }
日志的落库使用了 @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(); }
但是以上代码,有几种情况
始终没搞明白这是为什么。。。
![]() | 1 Uyuhz 2021-10-27 14:52:04 +08:00 应该是当前线程先于日志线程结束,当前线程将 request 对象清空了? |
![]() | 3 Uyuhz 2021-10-27 14:59:28 +08:00 @keshao 我之前做类似需求的时候最开始也是想直接传递 request 对象,后来 debug 了半天 request 对象里全是 null ,我就直接先从当前线程的 request 中读取信息来传递了。 |
![]() | 4 wolfie 2021-10-27 15:27:01 +08:00 线程池怎么定义的? AsyncConfiguration.LOG_EXECUTOR 是不是 debug 模式下,事先将 inputstream 消耗过了 |
![]() | 5 keshao OP @Uyuhz 是的,这个需求后来还是通过参数值传递的方式去解决了,对整体的 log 模块做了一部分的重构。还有一些遗留问题哈哈~~ 但是问题其实跟楼下老哥说的一样,在 Thread 端#init () 其实就是引用传递。spring mvc 组件在使用完成后会直接 remove 掉 request 对象,所以出现了 debug 之后请求处理完这个 request 就是 null 的情况。 @wolfie 是的,有一部分原因是你提出的思路~ 看了很多源码跟搜索引擎才找到了答案 最后,由衷的感谢两位小哥的帮助,最近太忙了没上太多 v2 ,嘿嘿 另外还想对自己说一句: 再设计异步功能的时候看着点~ 不能瞎操作了哈哈 |
![]() | 8 yudoo 2024-07-22 14:11:16 +08:00 老表也不知道,最近刚涉猎微服务, 所以这是个疑问句, 老表怎么看 |
![]() | 9 keshao OP @yudoo 目前推荐使用 TransmitterableThreadLocal ,目前 Java 的微服务还都以线程池为主。JDK 自带的 InheritableThreadLocal 不支持线程池的局部变量传递,仅支持 new Thread()的方式进行传递父子信息,TransmitterableThreadLocal 是支持的而且有很多种方式。比如修饰线程池、Java Agent 方式去支持。具体你可以看看这个: https://github.com/alibaba/transmittable-thread-local |
![]() | 10 yudoo 2024-07-23 15:05:55 +08:00 |