持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情
前言
OpenFeign
全称Spring Cloud OpenFeign,它是Spring官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的Feign。
OpenFeign是 Spring Cloud对Feign的二次封装,它具有Feign的所有功能,并在Feign的基础上增加了对Spring MVC注解的支持,例如 @RequestMapping、@GetMapping和 @PostMapping等。
OpenFeign作为微服务间接口的调用组件,除了需要考虑传递消息体外,还需要考虑到如何在各个服务间传递请求头信息。如果不做任何配置,直接使用openFeign在服务间进行调用就会如下图:
这样会丢失请求头,在企业级的应用中,token是非常重要的请求信息,他会携带权限、用户信息等。
解决方案
方案一 增加接口参数
接口提供方在接口的参数中通过注解的形式增加请求头参数。服务A调用服务B的时候,服务A先获取请求头信息,然后在调用服务B的时候请求头信息作为参数进行传递。
- 服务B接口声明请求头参数
@RequestMapping(value = "/api/test", method = RequestMethod.GET)
public String testFun(@RequestParam String name, @RequestHeader("token") String token) {
if(token == null ){
return "Must defined the uniqueId , it can not be null";
}
log.info(token, "begin testFun... ");
return uniqueId;
}
- 服务A调用服务B的时候显示传递请求头
@FeignClient(value = "DEMO-SERVICE")
public interface CallClient {
/**
* 访问DEMO-SERVICE服务的/api/test接口,通过注解将logId传递给下游服务
*/
@RequestMapping(value = "/api/test", method = RequestMethod.GET)
String callApiTest(@RequestParam(value = "name") String name, @RequestHeader(value = "token") String token);
}
方案弊端:毫无疑问,这方案不好,因为对代码有侵入,需要开发人员每次手动的获取和添加,因此舍弃
方案二 添加拦截器
openFeign提供了扩展点。实现RequestInterceptor,openFeign在远程调用之前会遍历容器中的RequestInterceptor,调用RequestInterceptor的apply方法,创建一个新的Request进行远程服务调用。因此可以通过实现RequestInterceptor给容器中添加自定义的RequestInterceptor实现类,在这个类里面设置需要发送请求时的参数,比如请求头信息,链路追踪信息等。
实现方式一
- 添加配置类,给Spring容器中注入一个RequestInterceptor类型的Bean
import com.intellif.log.LoggerUtilI;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
/**
* 自定义的请求头处理类,处理服务发送时的请求头;
* 将服务接收到的请求头中的uniqueId和token字段取出来,并设置到新的请求头里面去转发给下游服务
* 比如A服务收到一个请求,请求头里面包含uniqueId和token字段,A处理时会使用Feign客户端调用B服务
* 那么uniqueId和token这两个字段就会添加到请求头中一并发给B服务;
*
*/
@Configuration
public class FeignHeadConfiguration {
private final LoggerUtilI logger = LoggerUtilI.getLogger(this.getClass().getName());
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
/**
* 遍历请求头里面的属性字段,将logId和token添加到新的请求头中转发到下游服务
* */
if ("uniqueId".equalsIgnoreCase(name) || "token".equalsIgnoreCase(name)) {
logger.debug("添加自定义请求头key:" + name + ",value:" + value);
requestTemplate.header(name, value);
} else {
logger.debug("FeignHeadConfiguration", "非自定义请求头key:" + name + ",value:" + value + "不需要添加!");
}
}
} else {
logger.warn("FeignHeadConfiguration", "获取请求头失败!");
}
}
};
}
}
- 在@FeignClient注解添加属性configuration,指向RequestInterceptor的Bean
@FeignClient(value = "vip-producer", fallback = UserFallbackService.class,
configuration = RequestInterceptor.class)
- 配置文件application.yml中将hystrix的隔离策略修改魏semaphore 如果使用了hystrix做服务熔断降级。
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
实现方式二
- 定义一个配置类,确保这个类能被Spring能扫描到,这样就能添加到容器中
import cn.hutool.core.util.StrUtil;
import com.xinxin.vip.common.dict.Constants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName FeignConfiguration
* @Description openFeign调用服务时请求头传递MDC,实现服务间链路追踪
* @Author lantianbaiyun
* @Date 2021/3/30
* @Version 1.0
*/
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String headerTraceID = request.getHeader(Constants.HTTP_HEADER_TRACE_ID);
if (StrUtil.isBlank(headerTraceID)) {
headerTraceID = MDC.get(Constants.LOG_TRACE_ID);
}
requestTemplate.header(Constants.HTTP_HEADER_TRACE_ID, headerTraceID);
}
}
- 在@FeignClient添加configuration属性
import com.xinxin.vip.common.message.RequestEntityMessage;
import com.xinxin.vip.common.message.ResponseEntityMessage;
import com.xinxin.vip.consumer.feign.config.FeignConfiguration;
import com.xinxin.vip.consumer.feign.fallback.UserFallbackService;
import com.xinxin.vip.consumer.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
/**
* @ClassName UserService
* @Description openFeign调用生产者接口
* @Author lantianbaiyun
* @Date 2021/3/29
* @Version 1.0
*/
@FeignClient(value = "vip-producer",fallback = UserFallbackService.class,configuration = FeignConfiguration.class)
public interface UserService {
@PostMapping("/producer/create")
ResponseEntityMessage<Map<String, String>> create(@RequestBody RequestEntityMessage<User> user);
}
- 修改配置文件application.yml,添加配置
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
在开启熔断器之后,这里的RequestContextHolder.getRequestAttributes()就是null,RequestContextHolder.getRequestAttributes()该方法是从ThreadLocal变量里面取得对应信息的,这就找到问题原因了,由于Hystrix熔断机制导致的,因为熔断器默认的隔离策略是Thread,也就是线程隔离,实际上接收到请求的对象和这个再发送给服务B请求不是一个线程,怎么办?有一个办法,修改隔离策略hystrix.command.default.execution.isolation.strategy=SEMAPHORE,改为信号量的隔离模式,但是不推荐,因为thread是默认的,而且要命的是信号量模式,熔断器不生效,比如设置了熔断时间hystrix.command.default.execution.isolation.semaphore.timeoutInMilliseconds=5000,五秒,如果B服务里面sleep了10秒,非得等到B执行完毕再返回,因此这个方案也不可取
实现方式三
在方案2的基础上进行的修改,其实也可以归类到方案2中(添加一个实现方式3),但是比较经典所以就单独写个方案总结
RequestInterceptor+自定义Hystrix隔离策略。
研究zipkin日志追踪的时候,看到过Sleuth有自己的熔断机制,用来在thread之间传递Trace信息,Sleuth是可以拿到自己上下文信息的,查看源码找到了
org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixConcurrencyStrategy
这个类,查看SleuthHystrixConcurrencyStrategy的源码,继承了HystrixConcurrencyStrategy
,用来实现了自己的并发策略。如图:
我们熟悉的Spring Security和刚才提到的Sleuth都是使用了自定义的策略,同时由于Hystrix只允许有一个并发策略,因此为了不影响Spring Security和Sleuth,我们可以参考他们的策略实现自己的策略,大致思路: 将现有的并发策略作为新并发策略的成员变量; 在新并发策略中,返回现有并发策略的线程池、Queue;
- 定义一个配置类,确保这个类能被Spring能扫描到,这样就能添加到容器中
import com.intellif.log.LoggerUtilI;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
/**
* 自定义的请求头处理类,处理服务发送时的请求头;
* 将服务接收到的请求头中的uniqueId和token字段取出来,并设置到新的请求头里面去转发给下游服务
* 比如A服务收到一个请求,请求头里面包含uniqueId和token字段,A处理时会使用Feign客户端调用B服务
* 那么uniqueId和token这两个字段就会添加到请求头中一并发给B服务;
*
*/
@Configuration
public class FeignHeadConfiguration {
private final LoggerUtilI logger = LoggerUtilI.getLogger(this.getClass().getName());
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
/**
* 遍历请求头里面的属性字段,将logId和token添加到新的请求头中转发到下游服务
* */
if ("uniqueId".equalsIgnoreCase(name) || "token".equalsIgnoreCase(name)) {
logger.debug("添加自定义请求头key:" + name + ",value:" + value);
requestTemplate.header(name, value);
} else {
logger.debug("FeignHeadConfiguration", "非自定义请求头key:" + name + ",value:" + value + "不需要添加!");
}
}
} else {
logger.warn("FeignHeadConfiguration", "获取请求头失败!");
}
}
};
}
}
- 在@FeignClient注解添加属性configuration,指向RequestInterceptor的Bean
@FeignClient(value = "vip-producer", fallback = UserFallbackService.class,
configuration = FeignConfiguration.class)
- 自定义openfeign的隔离策略
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
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;
/**
* 自定义Feign的隔离策略;
* 在转发Feign的请求头的时候,如果开启了Hystrix,Hystrix的默认隔离策略是Thread(线程
* 隔离策略),因此转发拦截器内是无法获取到请求的请求头信息的,可以修改默认隔离策略为信号
* 量模式:hystrix.command.default.execution.isolation.strategy=SEMAPHORE,这
* 样的话转发线程和请求线程实际上是一个线程,这并不是最好的解决方法,信号量模式也不是官方
* 最为推荐的隔离策略;另一个解决方法就是自定义Hystrix的隔离策略,思路是将现有的并发策略
* 作为新并发策略的成员变量,在新并发策略中,返回现有并发策略的线程池、Queue;将策略加到
* Spring容器即可;
*
* @author mozping
* @version 1.0
* @date 2018/7/5 9:08
* @see FeignHystrixConcurrencyStrategyIntellif
* @since JDK1.8
*/
@Component
public class FeignHystrixConcurrencyStrategyIntellif extends HystrixConcurrencyStrategy {
private static final Logger log = LoggerFactory.getLogger(FeignHystrixConcurrencyStrategyIntellif.class);
private HystrixConcurrencyStrategy delegate;
public FeignHystrixConcurrencyStrategyIntellif() {
try {
this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (this.delegate instanceof FeignHystrixConcurrencyStrategyIntellif) {
// 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();
}
}
}
}
方案3与方案2相比就是将方案2中修改配置文件的步骤,替换成添加自定义策略的配置类。