今天在我的电商项目的订单服务中遇到了两个问题:
- 远程调用丢失请求头
- 空指针异常
事情的经过是这样的:
结算购物车中的商品创建一个订单时,首先要判断用户的登录状态,如果用户未登录的话就不能创建订单,而用户的登录状态信息在发起创建订单请求的时候保存在了 ThreadLocal 中,很容易就获取到了。接下来远程调用了购物车服务来查询该商品的相关信息,以及远程调用了会员服务来查询用户的收货地址,将这些信息封装为一个订单Vo然后提交,一整套操作下来简简单单非常nice,信心满满地启动了项目。
就在我重新启动项目之后,发现,咦,明明我已经是登录状态了,但是订单却创建失败了,我几乎没有想到问题就出在登录状态信息上,我看了半天代码也没有找到原因,然后开启了疯狂DeBug模式,疯狂打断点,终于,在某一个时刻,我发现,调用远程服务时竟然判断我是未登录状态 ??????难道是远程调用这里出问题了?
接下来又开启了疯狂的面向百度编程,浏览各大网站,查看了大佬们对这个问题的办法之后,我有了思绪,我决定深入Feign底层看一看,远程调用时究竟发生了什么。
呈上源码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
远程调用时首先经过一系列判断,判断是否需要调用,如果需要就进入 dispatch.get(method).invoke(args)
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
这里拿到了一个重试器,如果成功的话就不需要重试,直接进入 executeAndDecode(template)
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
这个方法首先要传入一个请求模板 template ,template中声明了请求地URL地址以及请求头,根据这个模板得到了一个请求
Request request = targetRequest(template);
我们来看看这个请求是如何创建的
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}
发现这里使用了 RequestInterceptor 拦截器中的 apply() 方法去构建请求,而默认构建出来的请求是空的,也就是说这个请求根本就没有获取到我们主线程ThreadLocal用户登录信息。
所以自定义一个 RequestInterceptor ,去重写 apply() 方法,将我们主线程 ThreadLocal 中的用户登录信息保存在 Feign 创建的这个远程调用请求中
下面是我自定义的 Requestinterceptor
@Configuration
public class MyFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
有了自定义的这个 Requestinterceptor ,在远程调用时,就使用了这个 Requestinterceptor 来创建远程调用请求,从而解决了远程调用时请求头丢失的问题。
接下来我想对这两个查询任务做个优化,所以创建了一个线程池,对这两个远程调用的查询任务使用异步执行的方式去做,然后获取到这两个异步任务执行的结果返回,这样几乎可以节省一半的查询时间。可是在就我重新启动项目时,发现报了空指针异常??????(心态崩了!)
为什么呢?查看了报错信息,发现是在自定义的 RequestInterceptor 重写的apply() 方法中获取cookie时空指针了。也就是这行代码:
String cookie = request.getHeader("Cookie");
想来简单,原因是 ThreadLocal 是不同线程之间不共享的,所以在使用线程池异步执行远程调用时不能获取到主线程ThreadLocal 中保存的用户登录信息。
这还不简简单单,直接在每一个异步任务中重新获取一下主线程 ThreadLocal 中的信息不就OK了么。
又是饱经风霜的一天,明天加油!