总结:异步操作时,因主线程结束,请求被销毁,调用feign接口而导致的几个问题。经过查找网上的资料,发现这个问题也出现在定时任务调用feign上,基本上网大佬总结的方法我都试过了,但是多数大佬的业务场景是主线程并没有在异步线程结束前被销毁,因此他们可以直接开启线程请求共享就好了。
尝试多总方法后,提炼出两种可以通用的方法
1. 继承RequestAttributes类,在子线程中new出来设置到子线程的RequestAttributes,然后创建一个空的SecurityContext,将主线程的context赋值进去
2.请求进来后强制返回响应,后面的代码继续执行(不推荐)
3.定时任务的问题需要重写apply方法
以下是具体过程:
----碰到个需求需要异步执行,直接
@Async注解整上,霹雳吧啦一顿操作,postman发送请求,舒服!
恩??经典空指针??就知道没这么简单。
定位报错代码
//获取当前登录用户信息
User user = SecurityUtils.getUser();
经过查找资料一顿分析,应该是主线程结束后,请求被销毁,导致获取不到用户信息。
好了知道问题在哪里就好解决了,SecurityContext跟随主线程请求一起被销毁了,参考网上各位大佬的办法,那就把SecurityContext的值从主线程传递给子线程
//从主线程获取SecurityContext
final SecurityContext context = SecurityContextHolder.getContext();
//在子线程中设置SecurityContext
SecurityContextHolder.setContext(context );
再次调试发现异步线程可以获取到当前用户信息了,本以为万事大吉了,结果控制台又爆红啦!!!
com.netflix.hystrix.exception.HystrixRuntimeException: Remotexxxxxxx#getxxxxxxx(Integer,String,String,String) failed and fallback failed.
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:832)
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:807)Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:42)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
... 80 moreError creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
由于异步线程中有调用另外一个微服务的方法所以调用了feign,根据控制台的提示,发现是在构造请求时,在还原token的方法中引用了非本次请求的变量(大概是这个意思),所以报错了,好吧,再上csdn查查有没有前辈(倒霉蛋)也遇到过这个问题,没有。。。。
好吧只能自力更生了,既然主线程的请求都被销毁了不能用了 ,那我就再自己创建一个,在赋值不就得了,ok,曙光就在眼前!!
accessTokenContextRelay.copyToken();
在异步线程中创建一个空的SecurityContext,然后再把主线程的context赋值进去,完美!
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
emptyContext.setAuthentication(context.getAuthentication());
SecurityContextHolder.setContext(emptyContext);
这个就不用了
// SecurityContextHolder.setContext(context);
。。。。发现还是报错,推测是主线程被销毁了的原因,那就再整个请求,这是我参考别的大佬的代码
public class FeignRequestScopeAttr implements RequestAttributes {
private Map<String, Object> requestAttributeMap = new HashMap<>();
@Override
public Object getAttribute(String name, int scope) {
if(scope== RequestAttributes.SCOPE_REQUEST) {
return this.requestAttributeMap.get(name);
}
return null;
}
@Override
public void setAttribute(String name, Object value, int scope) {
if(scope== RequestAttributes.SCOPE_REQUEST){
this.requestAttributeMap.put(name, value);
}
}
@Override
public void removeAttribute(String name, int scope) {
if(scope== RequestAttributes.SCOPE_REQUEST) {
this.requestAttributeMap.remove(name);
}
}
@Override
public String[] getAttributeNames(int scope) {
if(scope== RequestAttributes.SCOPE_REQUEST) {
return this.requestAttributeMap.keySet().toArray(new String[0]);
}
return new String[0];
}
@Override
public void registerDestructionCallback(String name, Runnable callback, int scope) {
// Not Supported
}
@Override
public Object resolveReference(String key) {
// Not supported
return null;
}
@Override
public String getSessionId() {
return null;
}
@Override
public Object getSessionMutex() {
return null;
}
}
//异步线程新设置一个请求
RequestContextHolder.setRequestAttributes(new FeignRequestScopeAttr());
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
emptyContext.setAuthentication(context.getAuthentication());
SecurityContextHolder.setContext(emptyContext);
至此问题解决。
再记录下另一个方法
AsyncContext asyncContext = request.startAsync();
HttpServletResponse response = WebUtils.getResponse();
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.setHeader(HttpHeaders.CONNECTION,"close");
PrintWriter out = response.getWriter();
out.print(RVo.ok());//设置响应报文
out.close();//强制返回
//已经返回给前端,但下面的代码还能执行
//继续执行业务代码。。。
另外可以将请求头放在新的请求中。
感谢各位大佬指点!