起因
在 web 程序中使用多线程处理任务,
Caused by: java.lang.NullPointerException: null
at org.apache.catalina.connector.Request.notifyAttributeAssigned(Request.java:1528)
at org.apache.catalina.connector.Request.setAttribute(Request.java:1514)
at org.apache.catalina.connector.RequestFacade.setAttribute(RequestFacade.java:540)
at javax.servlet.ServletRequestWrapper.setAttribute(ServletRequestWrapper.java:252)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.setCurrentSession(SessionRepositoryFilter.java:264)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:375)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:390)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:217)
at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240)
at hk.com.cre.process.util.SessionUtil.getSession(SessionUtil.java:127)
at hk.com.cre.process.util.SessionUtil.getSessionId(SessionUtil.java:131)
at hk.com.cre.process.interceptor.FeignRequestInterceptor.lambda$headerInterceptor$80(FeignRequestInterceptor.java:28)
at feign.SynchronousMethodHandler.targetRequest(SynchronousMethodHandler.java:158)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:88)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76)
at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:108)
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:301)
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:297)
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46
关键类导致:
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper
new Thread(() ->{
RequestContextHolder.setRequestAttributes(requestAttributes);
// 之后在想通过这个获取 request 里面的东西已经是获取不到了
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
}).start();
public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession() {
// 获取时每次都新创建 session
return this.getSession(true);
}
// 之后会调用
this.setCurrentSession(currentSession);
// 最终会调用 notifyAttributeAssigned()
// 在这个请求里面 context 为空 导致上面的异常
Context context = this.getContext();
第二种写法
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// true 设置子线程共享
RequestContextHolder.setRequestAttributes(requestAttributes,true);
事故测试的方法:
@RequestMapping(value = "/help/demo", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object demo() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(requestAttributes,true);
new Thread(() -> {
try {
// 这里故意让主线程结束,下面就会获取不到值
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// RequestContextHolder.setRequestAttributes(requestAttributes);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 获取参数或者 session 都需要主线程不结束的前提下才能获取到
// 当主请求结束,子线程里面的 request 对象也都会清除数据 .
// 当主请求结束 ,那么 Context context = this.getContext(); 也就是 null 了。
request.getParameter("test")
}).start();
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// try {
// Thread.sleep(20000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
return "demo";
}
源码解读:
// 在 DispatcherServlet extends FrameworkServlet 父类中的源码
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
this.doService(request, response);
} catch (ServletException var17) {
failureCause = var17;
throw var17;
} catch (IOException var18) {
failureCause = var18;
throw var18;
} catch (Throwable var19) {
failureCause = var19;
throw new NestedServletException("Request processing failed", var19);
} finally {
// 重置 previousAttributes
this.resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (this.logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", (Throwable)failureCause);
} else if (asyncManager.isConcurrentHandlingStarted()) {
this.logger.debug("Leaving response open for concurrent processing");
} else {
this.logger.debug("Successfully completed request");
}
}
this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
}
}
引发的问题:这样的话必须等待子线程处理完成,主线程才能结束,这样又变成同步执行了,异步失去了意义。
多线程如何传递 request 参数或者 session ?
- 在主线程中获取到需要的信息,使用参数传递到子线程中
- 包装 request 将数据复制一份在内存中,使用 ThreadLocal