背景
项目中有很多微服务,当一个 http 请求A服务并且带了一些header信息,而A服务需要调用B、C服务,B、C服务也需要获取这些header信息,这种场景在项目中非常常见,为了再调用其它服务时让这些header自动传递到下一个服务,我们写了一个全局的feign RequestInterceptor,在拦截器中通过RequestContextHolder去获取当前请求的header信息,并将这些header信息设置到调用下游服务的接口的header头中。
@Bean
RequestInterceptor globalRequestInterceptor() {
return (template) -> {
WHITE_LIST_HEADER_FIELDS.forEach((headerField) -> {
String headerValue = this.getHeader(headerField);
if (!template.headers().containsKey(headerField) && Objects.nonNull(headerValue)) {
template.header(headerField, new String[]{headerValue});
}
});
};
}
private String getHeader(String header) {
HttpServletRequest request = this.getHttpServletRequest();
return request == null ? null : request.getHeader(header);
}
private HttpServletRequest getHttpServletRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
return attributes == null ? null : attributes.getRequest();
}
复制代码
问题
一个 http 请求过来A服务的a1接口,a1接口有一部分代码逻辑a2是异步的,在异步的a2方法中需要调用B服务。当通过feign调用B服务,走到RequestInterceptor之时,线上环境却偶现获取不到header了。
@Service
public class Service1 {
@Autowired
private Service2 service2;
public void a1() {
// some sync code
service2.a2();
//some sync code
}
}
复制代码
@Service
public class Service2 {
@Autowired
private BClient bClient;
@Async
public void a2(){
// do some thing
bClient.callB1();
}
}
复制代码
public interface BClient {
@PostMapping("/b1")
void callB1();
}
复制代码
分析
首先看到获取RequestAttributes的代码RequestContextHolder.getRequestAttributes(),通过debug发现是在这里获取到的RequestAttributes是null。
进到RequestContextHolder源码可以看到实际上是把requestAttributesHolder或者inheritableRequestAttributesHolder的value返回了,而这两个其实是threadlocal对象,源码的注释也告诉了我们这个方法其实会放回绑定了当前线程的requestAttributes,这就好理解了,由于是threadlocal对象线程之间隔离了,那在子线程中自然就获取不到requestAttribute了。
既然这样最直接的办法在主线程代码获取到header,再通过传参的方式一层一层传到feign client就可以了。但这样做的话每个异步方法都要写类似的代码感觉有点太过麻烦。 再想了想我们让每个子线程执行前去获取主线程的RequestAtribute然后设置到子线程不就好了,直接使用ThreadPoolTaskExecutorsetTaskDecorator的setTaskDecorator来进行装饰就可以了。
threadPoolTaskExecutor.setTaskDecorator((runnable)->{
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(requestAttributes);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
});
复制代码
似乎大功告成了,commit代码本地测试没问题,然而神奇的事情发生了,bug从稳定出现变成了偶现。
看来事情没有这么简单,继续跟踪源码发现实际上spring在RequestContextFilter类的doFilterInternal方法中进行了RequestAttributes的初始化以及销毁。
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
//初始化ServletRequestAttributes到threadlocal
initContextHolders(request, attributes);
try {
filterChain.doFilter(request, response);
}
finally {
//remove掉ThreadLocal<RequestAttributes>
resetContextHolders();
if (logger.isTraceEnabled()) {
logger.trace("Cleared thread-bound request context: " + request);
}
attributes.requestCompleted();
}
}
复制代码
看到这里就明白了,主线程执行完后对ThreadLocal执行了remove操作,当子线程获取RequestAttribute的时候主线程已经执行完了,自然就获取不到了RequestAttribute了。
那在setTaskDecorator的时,将主线程获取到的RequestAttribute拷贝一份再设置到子线程不就好了,这下总没有什么问题了吧,再次自信commit。
很幸运bug再次偶现,但这次不是RequestAttribute获取不到了,而是header变成了null,经过一顿操作,终于发现了其中蹊跷。
tomcat的Http11InputBuffer有个recycle方法,当http请求连接关闭的时候就会去调用,recycle方法里面又会去调用Request的recycle方法,在这个方法里面就会去清除掉contentLength、headers 之类的信息了
/**
* Recycle the input buffer. This should be called when closing the
* connection.
*/
void recycle() {
wrapper = null;
request.recycle();
for (int i = 0; i <= lastActiveFilter; i++) {
activeFilters[i].recycle();
}
byteBuffer.limit(0).position(0);
lastActiveFilter = -1;
parsingHeader = true;
swallowInput = true;
headerParsePos = HeaderParsePosition.HEADER_START;
parsingRequestLine = true;
parsingRequestLinePhase = 0;
parsingRequestLineEol = false;
parsingRequestLineStart = 0;
parsingRequestLineQPos = -1;
headerData.recycle();
}
复制代码
public void recycle() {
bytesRead=0;
contentLength = -1;
contentTypeMB = null;
charset = null;
characterEncoding = null;
expectation = false;
headers.recycle();
trailerFields.clear();
serverNameMB.recycle();
serverPort=-1;
localAddrMB.recycle();
localNameMB.recycle();
localPort = -1;
remoteAddrMB.recycle();
remoteHostMB.recycle();
remotePort = -1;
available = 0;
sendfile = true;
serverCookies.recycle();
parameters.recycle();
pathParameters.clear();
uriMB.recycle();
decodedUriMB.recycle();
queryMB.recycle();
methodMB.recycle();
protoMB.recycle();
schemeMB.recycle();
remoteUser.recycle();
remoteUserNeedsAuthorization = false;
authType.recycle();
attributes.clear();
listener = null;
allDataReadEventSent.set(false);
startTime = -1;
}
复制代码
找到原因了,在这里的话有两个解决思路,一个就是将RequestAttribute对象进行深拷贝再存到子线程的。 另一个思路既然我们现在只需要header的信息,直接在主线程将header信息取出来,自定义一个HeaderContextHolder,再设置到子线程的threadlocal就可以了,我们在拦截器中获取header的时候,先去HeaderContextHolder去取header信息(能取到说明当前线程为主线程),如果HeaderContextHolder没取到再去spring的RequestContextHolder去获取heder。这里我们用第二种思路。
@Slf4j
public class ContextTaskDecorator implements TaskDecorator {
/**
* Bind the given RequestAttributes to the current thread
*/
@Override
public Runnable decorate(Runnable runnable) {
//组装 header map
Map<String, String> map = wrapHeaderMap();
return () -> {
try {
HeaderContextHolder.setContext(map);
runnable.run();
} finally {
HeaderContextHolder.removeContext();
}
};
}
}
复制代码
public class HeaderContextHolder {
private HeaderContextHolder() {
}
private static final ThreadLocal<Map<String, String>> CTX = new ThreadLocal<>();
public static void setContext(Map<String, String> map) {
CTX.set(map);
}
public static void removeContext() {
CTX.remove();
}
public static String getHeader(String key) {
if (isEmpty()) {
return null;
}
return CTX.get().get(key);
}
public static boolean isEmpty() {
return MapUtils.isEmpty(CTX.get());
}
}
复制代码
到此总算解决问题了。