[ SpringBoot ] 对开启 debug 模式后放在 Threadlocal 对象中 HttpServletRequest#getInputStream() 无法获取的疑问
各路大神,感谢花时间来一起讨论。我们的业务场景如下:
- 服务收到调用,先走 Filter ,并且拿到当前请求的 request 对象,因为是 tomcat 的 nio 线程池去负责调用相关业务日志代码,如果别的线程想使用当前线程对象就需要进行链式传递,所以就使用了阿里的 TTL ( TransmittableThreadLocal )进行全局的 request 传递,方便各个线程之间的 request 信息获取。
- 因为已经拿到 request 对象,所以我们有一个系统日志的需求。落库接口的访问 IP 、参数、返回结果、请求用户等等,这些都是从 request 对象中获取的。
-
具体实现采用了 Spring 的 AOP + 注解的形式
- 命中标注注解的 controller 方法
- AOP 去解析 request 对象,解析出来 body 、params 、url 、ip 等
代码如下:
-
服务收到调用,先走 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(); }
但是以上代码,有几种情况
- 我本地可使用正常的 Run 模式是可以正常使用的,而且打了 200 个请求过来没看见 error
- 我本地 Debug 模式就无法获取到正常的 request 对象了。。黑人问号??
- 这代码在我们的机器上也是几率性的,有时候可用有时候就不可用。。
始终没搞明白这是为什么。。。