一、场景
在异步方法中使用了feign调用,发现提示“您还未登录或登录已失效”。那原因很明了就是我的登录信息没办法传入到feign的调用方法里。
二、考虑的解决办法
1)尝试一:ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
sendAsyncService.statisticsSendSms(idList, userTokenValue, sendSMSNotificationParams, coreDescriptionParams, attributes);
public void statisticsSendSms(List<Long> reportIdList,
UserTokenValue userTokenValue,
SendSMSNotificationParams sendSMSNotificationParams,
CoreDescriptionParams coreDescriptionParams,
ServletRequestAttributes attributes) {
// 设置子线程共享
RequestContextHolder.setRequestAttributes(attributes, true);
//figen 调用,需要token
ResponseResult<String> responseResult = smsApi.sendSMSNotification(sendSMSNotificationParams);
这种情况犹豫反复在测试环境测试,都无任何问题,心里自信满满,但是上线后,却暴漏了,依旧提示“您还未登录或登录已失效”。这是什么情况?
1-1)RequestContextHolder跨线程获取不到requests请求对象的原因
为何会失败呢?因为异步注解,顾名思义,是开启了一个新的线程去执行一些代码。
/**
* 给当前线程绑定属性
* @param inheritable 是否要将属性暴露给子线程
* */
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
那源代码理解,已经将属性暴漏给子线程了,为何上线后依旧是获取不到呢?那其实从根本思考,主线程加入相应速度快于子线程呢,结果会是如何?结果就是主线程结束,所有的信息都已经销毁了,谁还管你子线程是否执行没执行呢,哈哈哈。测试环境未复现,是因为环境配置低,可能测试的几次都一定概率的主线程相应时间大于了子线程的相应时间,到了线上,由于高配置的环境,这种平衡性被打破了,主线程先行一步了。
缘由了解了,解决的办法也就来了----肯定就是让主线程等一下,等子线程执行完再结束,可是主线程会那么礼貌吗?该如何解决这个问题呢?
2)自定义异步线程池,配置中做相关处理
// 解决使用@Async注解,获取不到上下文信息的问题
executor.setTaskDecorator(runnable -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
AsyncContext asyncContext;
if (attributes!=null){
HttpServletRequest request=attributes.getRequest();
asyncContext=request.startAsync();
} else {
asyncContext = null;
}
return ()->{
try {
// 我们set 进去 ,其实是一个ThreadLocal维护的.
RequestContextHolder.setRequestAttributes(attributes);
runnable.run();
}finally {
RequestContextHolder.resetRequestAttributes();
if (asyncContext!=null){
asyncContext.complete();
}
}
};
});
2-1)Servlet的异步处理
是由AsyncContext接口来实现的。
AsyncContext asyncContext;
if (attributes!=null){
HttpServletRequest request=attributes.getRequest();
asyncContext=request.startAsync();
} else {
asyncContext = null;
}
2-3)异步注解使用了自定义线程池,避免一个方法多个异步注解使用
@Async("asyncServiceExecutor")
AsyncContext asyncContext;
if (attributes!=null){
HttpServletRequest request=attributes.getRequest();
asyncContext=request.startAsync();
} else {
asyncContext = null;
}
由于线程池中我们使用了Servlet的异步处理来 set我们需要的token信息,所以避免重复调用。多次调用会报错,即使不影响信息获取。
在Servlet中多次调用自定义线程池可能导致错误,这通常是因为Servlet的单实例模式导致的线程安全问题。Servlet容器通常会为每个Servlet维护一个实例,当多个请求同时访问同一个Servlet实例时,可能会出现竞态条件,比如多个线程尝试使用相同的资源或状态。
三、错误使用记录
1)java.lang.IllegalStateException: Calling [asyncStart()] is not valid for a request with Async state [STARTING]
1-1)报错解析:
表明你正在尝试对一个已经被dispatch()方法分派的异步请求执行asyncStart()操作,而这不被允许。
在Java的Servlet API中,异步处理允许请求处理过程中转移到其他线程继续执行,而不阻塞原始请求线程。当你调用request.startAsync()或request.getAsyncContext()时,会创建一个AsyncContext对象。在这个对象被分派之后,原始请求线程可能已经结束,而新的线程继续处理异步的任务。
解决这个问题的方法是确保在调用asyncStart()之前不要对AsyncContext进行分派操作。如果你需要在异步处理中启动新的线程,应该在调用request.startAsync()之后立即进行,并确保不会有进一步的分派发生。