Spring Cloud中Feign客户端调用服务时丢失Header参数的解决方案
前言在SpringCloud微服务架构的项目中,服务之间的调用是通过Feign客户端实现。默认情况下在使用Feign客户端时,Feign 调用远程服务存在Header请求头参数丢失问题,例如一个订单服务Order和一个商品服务Product,调用关系为: 用户下单调用订单服务,订单库创建一笔订单,同时商品服务扣减库存数量;在订单服务通过Feign调用商品服务中扣减库存的接口时,由于Feign是一个
前言
在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
更多推荐
所有评论(0)