什么?获取不到request数据了?

526 阅读5分钟

背景

今天线上发现一个很有趣的问题,我们有一个会员管理的模块,其中有一个会员导入的功能用来帮助商户快速导入会员。商户使用的时候,反馈导入,如果导入会员的时候加了积分,就一直显示再进行中,排查线上日志,提示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容器的过滤器等,进行了很多扩展,这里放个类图供大家简单理解 image.png

当我们请求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进行耦合,通过中间件进行一些业务关联