微服務中 Feign調用以及網關Zuul的請求頭相關問題

微服務中 Feign調用以及網關Zuul的請求頭與字段相關問題

前言

在Spring Cloud中使用Feign進行遠端調用時,會發生請求頭信息的丟失,下游服務無法獲取到上游服務請求頭的問題。在搭配Zuul網關後,可能會需要使用前置過濾器對上游服務的請求頭信息進行修改、添加或過濾 再轉至下游服務。

1. Feign遠程調用請求頭丟失

在不進行任何配置的請求下,Feign在進行遠程調用的時候,會發現下游服務無法獲取到上游服務發送的請求頭,而在許多時候,請求頭包含了一些重要信息,如 關聯性ID、授權認證等等。

1.1 原因分析

Feign發起遠程調用,實際上是通過@HystrixCommand命令去發起的,而瞭解Hystrix的調用隔離策略可知,Hsyrix有兩種隔離策略,一種SEMAPHORE(信號量)這是一個輕量級的隔離策略,當上遊服務發生遠程調用時,會直接在原調用線程中直接發起;另一種Thread(線程)這是一個默認的隔離策略,也是Hystrix官方大力推薦地策略,使用THREAD策略會另起一個子線程來進行遠程調用,這樣遠程調用可能發生的錯誤或異常也不會想到原調用線程。

引起下游服務無法接收到請求頭信息的原因有二:其一,在上游服務接收到請求頭新信息之後,並沒有把請求頭信息再封裝到遠程調用的request中,這裏可以通過加入一個攔截器FeignInterceptor來添加;其二,即使添加了FeignInterceptor也會發現下游服務依然無法獲取到請求頭信息,這是由於 FeignInterceptor中,在封裝請求頭信息到request的時候就已經丟失了,在獲取請求頭信息時,一般是通過 RequestContextHolder進行獲取的,而這個類會將RequestContext請求上下文封裝到ThreadLocal中以供使用,而由於Hystrix的默認隔離策略,子線程與父線程之間是無法傳遞ThreadLocal對象的

1.2 解決方案1

知道了原因就可以有兩個解決方案,最簡單粗暴但不推薦的就是改變Hystrix的隔離策略,將默認的Thread策略改爲SEMAPHORE策略,但是既然官方極力不推薦這種,也只做瞭解吧。

① FeignInterceptor攔截器,添加需要轉發的請求頭信息

/**
 * @Author: Jam
 * @Date: 2020/5/30 13:12
 */
@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            //header消息傳遞 本文示例 傳遞四個請求頭信息
            template.header("tmx-correlation-id",request.getHeader("tmx-correlation-id"));
            template.header("tmx-auth-token",request.getHeader("tmx-auth-token"));
            template.header"tmx-user-id",request.getHeader("tmx-user-id"));
            template.header("tmx-org-id",request.getHeader("tmx-org-id"));
        }
    }
}

② 修改@HystrixCommand的執行策略

@HyscrixCommand(commandProperties={@HysrixProperty(name="execution.isolation.strategy",value="SEMAPHORE")})
public void RemoteCall(){
    return feignClient.get();
}

以上爲通過修改Hystrix的隔離策略爲SEMAHORE來解決Feign調用中無法傳遞請求頭。但不推薦,不推薦,不推薦!

1.3 解決方案2

爲了使用Thread策略來執行Hystrix調用,那麼就要將父線程(原調用線程)的RequestContext傳遞到子線程中,Hystrix提供了併發策略機制,完成上下文的傳播,所以只要通過自定義併發策略 就可以讓子線程獲得請求頭信息。tips:Hystrix只允許一個併發策略,所以自定義併發策略時,原先會存在一個用以處理安全的併發策略,所以要把他們進行合併。

① 自定義併發策略FeignConfig,繼承HystrixConcurrencyStrategy類

@Component
public class FeignConfig extends HystrixConcurrencyStrategy {
    private static final Logger log = LoggerFactory.getLogger(FeignConfig.class);
    private HystrixConcurrencyStrategy delegate; 

    public FeignConfig() {
        try {
            this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.delegate instanceof FeignConfig) {
                return;
            }
            HystrixCommandExecutionHook commandExecutionHook =
                    HystrixPlugins.getInstance().getCommandExecutionHook();
            HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
            HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy =
                    HystrixPlugins.getInstance().getPropertiesStrategy();
            this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy);
            // 插件重置
            HystrixPlugins.reset();
            //設置自定義的併發策略
            HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
            HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
            HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
            HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
            HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
        } catch (Exception e) {
            log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
        }
    }

    private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
                                                 HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) {
        if (log.isDebugEnabled()) {
            log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy ["
                    + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher ["
                    + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]");
            log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
        }
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return new WrappedCallable<>(callable, requestAttributes);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize,
                                            HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue);
    }

    @Override
    public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
        return this.delegate.getBlockingQueue(maxQueueSize);
    }

    @Override
    public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
        return this.delegate.getRequestVariable(rv);
    }

    //一個實現Callable的靜態類,這裏完成父子線程的上下文傳播
    static class WrappedCallable<T> implements Callable<T> {
        private final Callable<T> target;
        private final RequestAttributes requestAttributes;

        public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
            this.target = target;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return target.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}

FeignConfig需要做兩件事,要註冊註冊一個新的併發策略,所以要獲取所有Hystrix組件,重置之後,再重新設置,具體操作詳見public Feign()方法,關鍵方法 WrappedCallable方法中的call()方法,這個方法會在@HystrixCommand之前被調用,以此Hystrix管理的子線程即可獲取到父線程的RequestAttributes進而獲取請求頭信息。

② FeignInterceptor攔截器,添加需要轉發的請求頭信息,代碼與1.2中的代碼一致,不再贅述,不要漏了這個攔截器類!自定義的併發策略只是完成了父子線程RequestContext的傳播問題。

2. Zuul對請求頭的加工

本文采用的@EnableZuulProxy,如果是其他啓動類配置,可能會有不適用的情況,未進行嘗試了深究 故方案僅測試了@EnableZuulProxy的配置下。

默認情況下,Zuul會直接轉發來自及上游的請求(請求頭信息也會全部攜帶),而Zuul不允許直接添加或修改請求中的HTTP請求首部,如果想要添加並且在以後的過濾器能再訪問他,需要通過RequestContext.getZuulRequestHeaders和 .addZuulRequestHeaders()進行獲取和添加。這個方法將維護一個單獨的HTTP首部映射,在Zuul服務器調用目標服務時,包含在ZuulRequestHeader映射中的數據將被合併。

示例:添加請求頭信息、修改請求頭信息、過濾(刪除)請求頭信息。

@Component
public class TrackingFilter extends ZuulFilter {
    private static final int FILTER_ORDER=0;
    private static final boolean SHOULD_FILTER=true;
    private static final Logger logger= LoggerFactory.getLogger(TrackingFilter.class);

    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    private boolean isCorrelationIdPresent(){
        HttpServletRequest request=RequestContext.getCurrentContext().getRequest();
        return request.getHeader("tmx-correlation-id") != null;
    }

    @Override
    public Object run() throws ZuulException {
        //run()方法是每次服務通過過濾器時執行的代碼
        if(isCorrelationIdPresent()){
            logger.info("tmx-correlation-id found in tracking filter{}.",RequestContext.getCurrentContext().getRequest().getHeader("tmx-correlation-id"));
            //驗證Zuul是否接受到 tmx-correlation-id請求頭
            RequestContext.getCurrentContext().addZuulRequestHeader("tmx-correlation-id","123");
            //修改 tmx-correlation-id請求頭的值爲 123
            RequestContext.getCurrentContext().addZuulRequestHeader("tmx-zuul","123");
            //添加 tmx-zuul 請求頭 值爲 123
        }else{
            logger.info("沒有啊親");
        }
        RequestContext ctx=RequestContext.getCurrentContext();
        logger.info("Processing incoming request for{}.",ctx.getRequest().getRequestURI());
        return null;
    }

    @Override
    public String filterType() {
        return PRE_TYPE; 
 //   String ERROR_TYPE = "error"; 
 //   String POST_TYPE = "post";
 //  String PRE_TYPE = "pre";  設置爲前置過濾器
 //   String ROUTE_TYPE = "route";
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }
}

上述代碼中通過 addZuulRequestHeader 給請求中 添加以及修改了請求頭信息(如果發現Zuul對請求頭的操作沒有生效,檢查獲取上下文的位置,確保是在run方法中進行的獲取而不是在類屬性裏),若需要過濾掉請求頭的部分信息不轉發至下游,可以通過修改 application.yml文件。

zuul:
  ignored-headers: tmx-user-id #多個時,用“,”間隔開

參考書籍:《Spring微服務實戰》
參考博客:feign調用session丟失解決方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章