SpringCloud Alibaba微服务实战之远程Feign请求头丢失问题解决方案
作者:竹林幽深
导读:在上一篇文章中我们讲解了如何利用Spring Security OAuth2
实现微服务统一认证,今天继续讲解Feign
远程调用和异步调用请求头丢失问题。
简介
之前演示只是测试了调用用户微服务下查询接口,并没有在用户服务再调用其他微服务接口,调测看着一切都很正常,但今天测试了用户微服务调用商品微服务,出现了异常:
Debug跟踪,发现用户微服务正常接收到token,而远程调用商品微服务token消失不见了:
原因
源码:ReflectiveFeign.class
:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (!"equals".equals(method.getName())) { if ("hashCode".equals(method.getName())) { return this.hashCode(); } else { return "toString".equals(method.getName()) ? this.toString() : ((InvocationHandlerFactory.MethodHandler)this.dispatch.get(method)).invoke(args); } } else { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return this.equals(otherHandler); } catch (IllegalArgumentException var5) { return false; } } }
进入((InvocationHandlerFactory.MethodHandler)this.dispatch.get(method)).invoke(args)
:
public Object invoke(Object[] argv) throws Throwable { //就是在这 构建了一个新的RequestTemplate ,而浏览器带给我们的请求头都会丢失 RequestTemplate template = this.buildTemplateFromArgs.create(argv); Request.Options options = this.findOptions(argv); Retryer retryer = this.retryer.clone(); while(true) { try { // 在这即将执行该方法 return this.executeAndDecode(template, options); } catch (RetryableException var9) { RetryableException e = var9; try { retryer.continueOrPropagate(e); } catch (RetryableException var8) { Throwable cause = var8.getCause(); if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) { throw cause; } throw var8; } if (this.logLevel != Level.NONE) { this.logger.logRetry(this.metadata.configKey(), this.logLevel); } } } }
进入this.executeAndDecode(template, options)
:
Object executeAndDecode(RequestTemplate template, Request.Options options) throws Throwable { //这里 它会对我们的请求进行一些包装 Request request = this.targetRequest(template); if (this.logLevel != Level.NONE) { this.logger.logRequest(this.metadata.configKey(), this.logLevel, request); } long start = System.nanoTime(); Response response; try { response = this.client.execute(request, options); response = response.toBuilder().request(request).requestTemplate(template).build(); } catch (IOException var12) { if (this.logLevel != Level.NONE) { this.logger.logIOException(this.metadata.configKey(), this.logLevel, var12, this.elapsedTime(start)); } throw FeignException.errorExecuting(request, var12); } ....... }
进入this.targetRequest(template)
:
Request targetRequest(RequestTemplate template) { //拿到对应的所有请求拦截器的迭代器 Iterator var2 = this.requestInterceptors.iterator(); //遍历所有的请求拦截器 while(var2.hasNext()) { RequestInterceptor interceptor = (RequestInterceptor)var2.next(); //这里是每个请求拦截器 依次对该方法进行包装 interceptor.apply(template); } return this.target.apply(template); }
进入interceptor.apply(template)
:
public interface RequestInterceptor { void apply(RequestTemplate var1); }
发现它是一个接口,所以可以重写一下这个方法对我们的请求做一些包装,借鉴一下别的实现方法:
方案
/** * 微服务之间feign调用请求头丢失的问题 * @author yian * @since 2023-04-05 */ @Configuration @Slf4j public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { HttpServletRequest httpServletRequest = getHttpServletRequest(); if(httpServletRequest!=null){ Map<String, String> headers = getHeaders(httpServletRequest); // 传递所有请求头,防止部分丢失 //此处也可以只传递认证的header //requestTemplate.header("json-token", request.getHeader("json-token")); for (Map.Entry<String, String> entry : headers.entrySet()) { template.header(entry.getKey(), entry.getValue()); } log.debug("FeignRequestInterceptor:{}", template.toString()); } } private HttpServletRequest getHttpServletRequest() { try { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } catch (Exception e) { return null; } } /** * 获取原请求头 */ private Map<String, String> getHeaders(HttpServletRequest request) { Map<String, String> map = new LinkedHashMap<>(); Enumeration<String> enumeration = request.getHeaderNames(); if(enumeration!=null){ while (enumeration.hasMoreElements()) { String key = enumeration.nextElement(); String value = request.getHeader(key); map.put(key, value); } } return map; } }
补充说明
实际开发中,在业务复杂情况下难免使用异步编排的方式实现,这个时候你会发现请求头又丢失了。
源码:RequestContextHolder.class
:
@Nullable public static RequestAttributes getRequestAttributes() { RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get(); if (attributes == null) { attributes = (RequestAttributes)inheritableRequestAttributesHolder.get(); } return attributes; }
查看requestAttributesHolder
变量:
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
查看NamedThreadLocal
:
public class NamedThreadLocal<T> extends ThreadLocal<T> { private final String name; public NamedThreadLocal(String name) { Assert.hasText(name, "Name must not be empty"); this.name = name; } public String toString() { return this.name; } }
ThreadLocal
是一个线程局部变量,在不同线程之间是独立的所以我们获取不到原先主线程的请求属性。
方案
//获取之前的请求 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture<Void> getAddress = CompletableFuture.runAsync(() -> { System.out.println(Thread.currentThread().getId()); //每一个线程都来共享之前请求的数据 RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId()); confirmVo.setAddress(address); }, executor);
至此我们已经解决了Feign远程以及异步编排下导致的请求头丢失问题。
到此这篇关于SpringCloud Alibaba微服务实战之远程Feign请求头丢失的文章就介绍到这了,更多相关SpringCloud Alibaba Feign请求头内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!