TransmittableThreadLocal传递ServletRequestAttributes对象在主线程和线程池,避坑指南

时间:2022-11-19 14:58:03

关于HttpServletRequest对象在主线程和线程池传递过程的问题

一,针对一般对象,解决主线程和线程池内线程对象解决方案是用阿里的插件TransmittableThreadLocal

使用案例

(1)将线程池进行TTL包裹

@Bean

public Executor accountThreadPoolTaskExecutor() {

int corePoolSize = Runtime.getRuntime().availableProcessors() * 2 + 1;

corePoolSize = corePoolSize > PROCESSORS ? corePoolSize : PROCESSORS;



ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(corePoolSize);

executor.setMaxPoolSize(corePoolSize);

executor.setQueueCapacity(QUEUE_CAPACITY);

executor.setThreadNamePrefix(VEHICLE_SERVICE);

executor.setRejectedExecutionHandler((r, threadPoolExecutor) -> {

try {

    threadPoolExecutor.getQueue().put(r);

} catch (InterruptedException e) {

    log.warn("retry put task error message:{}", e);

}

});

executor.initialize();

return TtlExecutors.getTtlExecutor(executor.getThreadPoolExecutor());

}



(2)传递对象用TransmittableThreadLocal进行传递


public class RequestPoolThreadContextHolder {

private static final ThreadLocal<ServletRequestAttributes> CONTEXT_HOLDER = new TransmittableThreadLocal<>();



/**

* 设置请求上下文

* @param requestAttributes 请求上下文

*/

public static void setPoolThreadContext(ServletRequestAttributes requestAttributes) {

    CONTEXT_HOLDER.set(requestAttributes);

}



/**

* 获取请求上下文

* @return 请求上下文

*/

public static ServletRequestAttributes getPoolThreadContext() {

return CONTEXT_HOLDER.get();

}



/**

* 清除请求上下文

*/

public static void clearPoolThreadContext() {

CONTEXT_HOLDER.remove();

}

}



(3)在需要传递的地方主线程设置共享变量


ServletRequestAttributes att = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

//设置主子线程请求上下文

RequestPoolThreadContextHolder.setPoolThreadContext(att);

try {

accountThreadPoolTaskExecutor.execute(() -> {

try {

//将主线程的请求上下文赋值给子线程

RequestContextHolder.setRequestAttributes(RequestPoolThreadContextHolder.getPoolThreadContext(););

//TODO 进行feign请求或者其他操作;

} catch (Exception e) {

System.out.println("异常信息:" + e);

}

});

}finally {

//清除主子线程请求上下文

RequestPoolThreadContextHolder.clearPoolThreadContext();

}




二,但是针对HttpServletRequest的对象的传递要注意,会因为异步调用后如果主线程的request对象可能因为tomcat先返回导致提前销毁,那么线程池内持有的同一引用因此会被改写,那么也就会导致空指针以及安全问题

(1)先了解Request的生命周期如下

TransmittableThreadLocal传递ServletRequestAttributes对象在主线程和线程池,避坑指南


(2)通过上述周期可以理解官方给出了如下警示:(不建议在)

“每个请求对象只有在一个servlet的service方法的范围内有效,或者在一个过滤器的doFilter方法的范围内有效,除非能够异步处理并且请求对象调用startAsync方法。异步处理的时候,请求对象保持有效直到AsyncContext调用了complete方法。容器一般循环利用请求对象,便面创建的请求对象达到最大值。注意在上述描述范围之外调用startAsync方法保持请求对象的引用时不推荐的,因为这样可能有不确定的结果。”

TransmittableThreadLocal传递ServletRequestAttributes对象在主线程和线程池,避坑指南


从中可以得到如下信息:

  • 1 三种情况下request有效:service函数内,doFilter函数内,startAsync起的异步线程
  • 2 在三种情况之外,使用request会产生不确定的结果(indeterminate results)
  • 3 大部分容器在实现servlet的时候,为了提高性能,会复用request对象,但这不是规范里必须的

其中提到的startAsync是servlet 3.0开始有的,它是为了让一个工作线程可以在做IO或类似阻塞线程的操作的时候能干其它的事情,但是它要求异步线程都结束了,才会将请求返回给客户端,本质上还是同步的,只是并行了。所以要想异步的处理Request,必须使用servlet自己的异步机制,但是这样并不能满足我们的需求,因为我们就是为了不让主线程等待。


(3)为了避开这点我们可以在塞入ServletRequest对象的时候对HttpServletRequest对象进行深度拷贝并按需重写getHeader方法来解决

public class RequestPoolThreadContextHolder {

/**

*

*/

private static final ThreadLocal<ServletRequestAttributes> CONTEXT_HOLDER = new TransmittableThreadLocal<>();



/**

* 设置请求上下文

*

* @param requestAttributes 请求上下文

*/

public static void setPoolThreadContext(ServletRequestAttributes requestAttributes) {

ServletRequestAttributes request = new ServletRequestAttributes(new MyServletRequest(requestAttributes.getRequest()));

CONTEXT_HOLDER.set(request);

}



/**

* 获取请求上下文

*

* @return 请求上下文

*/

public static ServletRequestAttributes getPoolThreadContext() {

return CONTEXT_HOLDER.get();

}



/**

* 清除请求上下文

*/

public static void clearPoolThreadContext() {

CONTEXT_HOLDER.remove();

}



/**

* 重新构造请求,重写getHeader方法

*/

static class MyServletRequest extends HttpServletRequestWrapper {

private final HashMap<String,String> headMap;

public MyServletRequest(HttpServletRequest request) {

super(request);

headMap = new HashMap();

Enumeration<String> headNameList = request.getHeaderNames();

while (headNameList.hasMoreElements()){

String key = headNameList.nextElement();

headMap.put(key.toLowerCase(),request.getHeader(key));

}

}

@Override

public String getHeader(String name) {

return headMap.get(name.toLowerCase());

}

}

}