[ 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 129 天前
老表也不知道,最近刚涉猎微服务, 所以这是个疑问句, 老表怎么看
|
9
keshao OP @yudoo 目前推荐使用 TransmitterableThreadLocal ,目前 Java 的微服务还都以线程池为主。JDK 自带的 InheritableThreadLocal 不支持线程池的局部变量传递,仅支持 new Thread()的方式进行传递父子信息,TransmitterableThreadLocal 是支持的而且有很多种方式。比如修饰线程池、Java Agent 方式去支持。具体你可以看看这个: https://github.com/alibaba/transmittable-thread-local
|
10
yudoo 128 天前
|