背景
今天线上发现一个很有趣的问题,我们有一个会员管理的模块,其中有一个会员导入的功能用来帮助商户快速导入会员。商户使用的时候,反馈导入,如果导入会员的时候加了积分,就一直显示再进行中,排查线上日志,提示No thread-bound request found
现场还原
这就很奇怪了,为什么当前线程会没有绑定request,明明已经发送了请求。先看看业务代码
/**
* 导入数据解析
*
* @param request 入参
*/
@PostMapping("/parse-file")
public void parseFile(@Validated @RequestBody FileParseRequest request) {
FileParseParam param = fileImportApiMapper.toFileParseParam(request);
fileImportService.parseFile(param);
}
前端先调用了parse-file接口,后端调用了fileImportService.parseFile方法
public void parseFile(FileParseParam param) {
// ... 校验
// 登录信息
LoginBaseResult loginBaseResult = loginBaseService.getLoginBaseInfo();
// ... 前置业务处理
// 发送MQ处理导入文件
// 发送导入成功回调MQ,通知消费端
String memberImportSuccessMsgKey = FileTemplateConstants.getMemberImportSuccessMsgKey(fid);
memberImportProducer.sendImportMsg(memberImportSuccessMsgKey, fid);
LogUtil.info(log, "CustomerCardService.createMemberCard >> 发送MQ处理导入文件 >> param = {},param = {}", param, memberImportSuccessMsgKey);
}
最后是发送了一条mq消息,进行异步处理,目前逻辑看着都没什么问题,开始消费,创建会员信息
....前置处理完后,创建会员
String masterCardId = MemberIdUtil.createId(IdRuleEnum.MEMBER_CARD);
String masterCardNo = createMasterCardNo(importParam, cardNoTypeEnum);
importParam.setMasterCardNo(masterCardNo);
importParam.setMasterCardId(masterCardId);
// 创建会员信息,不创建会员扩展信息,待C端注册时再补充扩展信息
MemberCustomerDO customerDO = buildMemberCustomerDO(param, importParam);
MemberCustomerCardDO masterCardDO = buildMemberCustomerMasterCardDO(param, importParam);
List<MemberCustomerCardDO> subCardDOList = buildMemberCustomerSubCardDOList(param, importParam);
MemberCustomerIntegralDO customerIntegralDO = buildMemberCustomerIntegralDO(param, importParam);
.... 数据库操作
既然是积分变动的时候,有问题所以直接看积分参数构建的方法就好了
private MemberCustomerIntegralDO buildMemberCustomerIntegralDO(MemberCustomerImportParam param, CustomerImportParam importParam) {
Long integral = Long.valueOf(importParam.getIntegral());
if (integral <= 0) {
return null;
}
String orgId = param.getOrgId();
String storeId = param.getStoreId();
String storeName = param.getStoreName();
String platformId = param.getPlatformId();
String merchantId = param.getMerchantId();
LoginBaseResult loginBaseResult = loginBaseService.getLoginBaseInfo();
....
}
这里果然就发现了问题loginBaseService是我们获取当前用户信息的方法,本质就是从当前的request获取token,根据token去换取当前用户信息。所以导入会员,进行积分操作的时候,会报错
String token = httpServletRequest.getHeader(ApiConstants.TOKEN);
if (StringUtils.isBlank(token)) {
LogUtil.info(log, "LoginBaseService.getLoginBaseInfo >> token is empty from header ");
throw LoginException.LOGIN_ERROR;
}
String redisToken = StrUtil.format(LoginConstants.MEMBER_LOGIN_TOKEN_KEY, token);
String loginJsonString = redisService.get(redisToken);
源码解析
回过头来想,明明发了请求,为什么会提示request未绑定呢? 根据错误提示,找到对应的源码, 发现RequestContextHolder获取不到当前request的属性。从而抛出异常
public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
RequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
if (jsfPresent) {
attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
}
if (attributes == null) {
throw new 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.");
}
}
return attributes;
}
RequestContextHolder顾名思义就是持有上下文的Request容器,很多对于request的操作都是基于RequestContextHolder的。 点开getRequestAttributes()方法,会发现它只是调用了两个threadLocal对象
// 当前存储的reqeust对象
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
// 可被子线程获取的request对象
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
@Nullable
public static RequestAttributes getRequestAttributes() {
// 获取当前线程request对象
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
// 获取父线程的request对象
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
看到这里又疑惑了,request是什么时候设置的呢? 关于springmvc具体我就不展开了,我们只要知道springmvc简单理解就是重写了web容器的过滤器等,进行了很多扩展,这里放个类图供大家简单理解
当我们请求web容器的时候,会请求到HttpServlet的service方法,根据不同的请求类型,进行分发
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
由于这次请求是post请求,所以只用关注doPost方法即可,会发现springmvc也对对应的请求类型的方法进行重写
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 处理request请求
processRequest(request, response);
}
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
// 获取国际化上下文配置
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
// 构建配置
LocaleContext localeContext = buildLocaleContext(request);
// 获取request中的属性,当前线程第一次请求的是为空的
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
// 获取异步执行器
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
// 将当前request绑定到当前线程中
initContextHolders(request, localeContext, requestAttributes);
try {
// 调用核心应用DispatcherServlet进行增强
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
... 后置业务处理
}
}
我们只需要看initContextHolders即可,我们会发现他调用了RequestContextHolder.setRequestAttributes
private void initContextHolders(HttpServletRequest request,
@Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
if (localeContext != null) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
}
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
}
点开setRequestAttributes,一切都完美的闭环了,这不就是我们最开始要获取的request对象嘛
// 当前存储的reqeust对象
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
// 可被子线程获取的request对象
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
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();
}
}
}
总结
总结一下,每当我们请求的时候,springmvc会将每次请求的参数,存入到当前线程的threadlocal中。但是如果我们是进行异步操作时,由于threadlocal不是线程间共享的,这时候去获取request数据,就会为空,也就是当前线程没有绑定request。解决方案,可以开启request的线程共享,或者异步操作下,不去进行获取request信息等操作。我的建议时异步操作的时候,不去和springmvc进行耦合,通过中间件进行一些业务关联