前言

在SpringCloud微服务架构的项目中,服务之间的调用是通过Feign客户端实现。默认情况下在使用Feign客户端时,Feign 调用远程服务存在Header请求头参数丢失问题,例如一个订单服务Order和一个商品服务Product,调用关系为: 用户下单调用订单服务,订单库创建一笔订单,同时订单调用商品服务扣减库存数量;在订单服务通过Feign调用商品服务中扣减库存的接口时,由于Feign是一个伪HTTP客户端,这时相当于重新发起一个HTTP请求,会出现请求头Header参数丢失问题,那么下面给大家介绍一下如何解决这种不足。

一、解决方案

1. 需要实现Feign提供的RequestInterceptor

首先需要新建一个类FeignRequestInterceptor拦截器实现Feign源码中提供的RequestInterceptor,重写apply方法,在apply方法方法中通过逻辑代码实现获取header请求头参数信息,实现往下一个调用链传递。

代码实现如下:

  • FeignRequestInterceptor拦截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

/**
 * @desc:  微服务之间使用Feign调用接口时, 透传header参数信息
 * @author: cao_wencao
 * @date: 2020-09-25 16:36
 */
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                String values = request.getHeader(name);
                requestTemplate.header(name, values);
            }
        }
    }
}

二、如何配置拦截器生效

上述通过自定义FeignRequestInterceptor实现了Feign的RequestInterceptor,那么如何修改配置让自定义的拦截器生效呢???

@FeignClient(value = "product-service",configuration = FeignRequestInterceptor.class)

1. feign调用统一设置请求头(方式一)

@FeignClient注解中有个configuration配置属性,通过configuration可指定自定义的拦截器FeignRequestInterceptor,那么这里拿出一个项目中订单服务调用商品服务作为例子,贴出商品服务的设置方式,如下:

  • IProductService
import com.example.common.product.entity.Product;
import com.example.order.config.FeignRequestInterceptor;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-09-22 23:43
 */
@FeignClient(value = "product-service",configuration = FeignRequestInterceptor.class)
public interface ProductService {
    //@FeignClient的value +  @RequestMapping的value值  其实就是完成的请求地址  "http://product-service/product/" + pid
    //指定请求的URI部分
    @RequestMapping("/product/product/{pid}")
    Product findByPid(@PathVariable Integer pid);

    //扣减库存,模拟全局事务提交
    //参数一: 商品标识
    //参数二:扣减数量
    @RequestMapping("/product/reduceInventory/commit")
    void reduceInventoryCommit(@RequestParam("pid") Integer pid,
                               @RequestParam("number") Integer number);

    //扣减库存,模拟全局事务回滚
    //参数一: 商品标识
    //参数二:扣减数量
    @RequestMapping("/product/reduceInventory/rollback")
    void reduceInventoryRollback(@RequestParam("pid") Integer pid,
                         @RequestParam("number") Integer number);
}
  • 下游服务如何获取请求头参数

这里以订单服务调用商品服务为例,那么商品服务就是调用链的下游,属于被调用方, 商品服务实现类ProductController获取从订单服务的Header传递过来的Token,可通过如下方式直接从请求头获取。

String token = ServletUtils.getRequest().getHeader(“token”);

/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-09-22 23:16
 */
@RestController
@Slf4j
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 扣减库存,正常->模拟全局事务提交
     * @param pid
     * @param number
     */
    @RequestMapping("/reduceInventory/commit")
    public void reduceInventoryCommit(Integer pid, Integer number) {
        String token = ServletUtils.getRequest().getHeader("token");
        log.info("从head请求头透传过来的值为token:"+ token);
        productService.reduceInventoryCommit(pid, number);
    }
}    
  • ServletUtils工具类
public class ServletUtils {
    /**
     * 获取String参数
     */
    public static String getParameter(String name) {
        return getRequest().getParameter(name);
    }

    /**
     * 获取String参数
     */
    public static String getParameter(String name, String defaultValue) {
        return Convert.toStr(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name) {
        return Convert.toInt(getRequest().getParameter(name));
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name, Integer defaultValue) {
        return Convert.toInt(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse() {
        return getRequestAttributes().getResponse();
    }

    /**
     * 获取session
     */
    public static HttpSession getSession() {
        return getRequest().getSession();
    }

    public static ServletRequestAttributes getRequestAttributes() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 是否是Ajax异步请求
     *
     * @param request
     */
    public static boolean isAjaxRequest(HttpServletRequest request) {
        String accept = request.getHeader("accept");
        if (accept != null && accept.indexOf("application/json") != -1) {
            return true;
        }

        String xRequestedWith = request.getHeader("X-Requested-With");
        if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) {
            return true;
        }

        String uri = request.getRequestURI();
        if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) {
            return true;
        }

        String ajax = request.getParameter("__ajax");
        return StringUtils.inStringIgnoreCase(ajax, "json", "xml");
    }
}
  • application.yml
#配置让所有 FeignClient,使用FeignRequestInterceptor
feign:
  hystrix:
    enabled: true
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
        requestInterceptors: com.example.order.config.FeignRequestInterceptor
        
#修改默认隔离策略为信号量模式
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE

也可以配置让 某一个 FeignClient 使用这个 FeignRequestInterceptor,具体如下:

#配置让某个 FeignClient,使用FeignRequestInterceptor
feign:
  hystrix:
    enabled: true
  client:
    config:
      xxxx: ##表示远程服务名
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
        requestInterceptors: com.example.order.config.FeignRequestInterceptor
        
#修改默认隔离策略为信号量模式
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE

解释一下下面这行配置的含义:

在转发Feign的请求头的时候, 如果开启了Hystrix, Hystrix的默认隔离策略是Thread(线程隔离策略), 因此转发拦截器内是无法获取到请求的请求头信息的。

可以修改默认隔离策略为信号量模式,但是SpringCloud官方并不推荐,所以后面介绍自定义策略这种方式。
在这里插入图片描述

2. 自定义Hystrix熔断策略(方式二)——推荐此方式

网上翻阅博客,看到一个大佬介绍了一种使用自定义Hystrix熔断策略的方式,此方式最为推荐,和第一种方法的区别就是不需要在application.yml配置文件中添加修改Hystrix熔断策略的配置属性,但是FeignRequestInterceptor这个还是要的,此种方式也可以生效。

  • 代码实现如下:
package com.jiuyv.etcfront.sdkback.gateway.config;

import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import com.netflix.hystrix.strategy.properties.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @desc: 自定义Hystrix熔断策略
 * @author caowencao
 * @date 2019/3/18 10:09
 */
@Slf4j
@Configuration
public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    private HystrixConcurrencyStrategy delegate;

    public FeignHystrixConcurrencyStrategy() {
        try {
            this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.delegate instanceof FeignHystrixConcurrencyStrategy) {
                // Welcome to singleton hell...
                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 ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixThreadPoolProperties threadPoolProperties) {
        return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties);
    }

    @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);
    }

    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();
            }
        }
    }


}

三、我的解决方式

1. 贴图

考虑到有些小伙伴看了这篇文章,仍旧遇到Feign不生效的情况,我这里分享一下我实际项目中的解决方式,生效无问题。
在这里插入图片描述
查看上图项目结构,将自定义Feign拦截器实现请求头参数无缝传递的类和自定义Hystrix熔断策略的类放到项目的公用模块: xxxx-common 模块中,然后在需要传递请求头参数的子服务中引入公用模块 xxxx-common 即可,这样一来项目启动时可将其以Bean的形式注入到容器,在微服务调用链中可获取到请求头传递过来的参数信息。

2. 下游服务如何获取请求头参数

这里以订单服务调用商品服务为例,那么商品服务就是调用链的下游,属于被调用方,获取从订单服务请求头传递过来的请求头参数方式如下:

  • 商品服务实现类ProductController

String token = ServletUtils.getRequest().getHeader(“token”);

/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-09-22 23:16
 */
@RestController
@Slf4j
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 扣减库存,正常->模拟全局事务提交
     * @param pid
     * @param number
     */
    @RequestMapping("/reduceInventory/commit")
    public void reduceInventoryCommit(Integer pid, Integer number) {
        String token = ServletUtils.getRequest().getHeader("token");
        log.info("从head请求头透传过来的值为token:"+ token);
        productService.reduceInventoryCommit(pid, number);
    }
}    

总结

通过上述为大家总结的几点方式,我们可完美的解决Feign客户端在调用服务时丢失了Header参数的问题,有什么疑问,见下方评论,一起交流。

参考

https://blog.csdn.net/crystalqy/article/details/79083857
https://blog.csdn.net/lidai352710967/article/details/88680173

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐