一、前言
在前面的文章:
SpringCloud之Feign实现声明式客户端负载均衡详细案例
我们聊了OpenFeign的概述、为什么会使用Feign代替Ribbon、Feign和OpenFeign的区别、以及详细的OpenFeign实现声明式客户端负载均衡案例。
在一些业务场景中,微服务间相互调用需要做鉴权,以保证我们服务的安全性。即:服务A调用服务B的时候需要将服务B的一些鉴权信息传递给服务B,从而保证服务B的调用也可以通过鉴权,进而保证整个服务调用链的安全。
本文我们就讨论如果通过openfeign的拦截器RequestInterceptor
实现服务调用链中上下游服务请求头数据的传递。
二、实现RequestInterceptor
通过RequestInterceptor 拦截器拦截我们的openfeign服务请求,将上游服务的请求头或者请求体中的数据封装到我们的openfeign调用的请求模板中,从而实现上游数据的传递。
1、RequestInterceptor实现类
1)RequestInterceptor实现类
package com.saint.feign.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Objects;
/**
* 自定义的Feign拦截器
*
* @author Saint
*/
@Slf4j
public class MyFeignRequestInterceptor implements RequestInterceptor {
/**
* 这里可以实现对请求的拦截,对请求添加一些额外信息之类的
*
* @param requestTemplate
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 1. obtain request
final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// 2. 兼容hystrix限流后,获取不到ServletRequestAttributes的问题(使拦截器直接失效)
if (Objects.isNull(attributes)) {
log.error("MyFeignRequestInterceptor is invalid!");
return;
}
HttpServletRequest request = attributes.getRequest();
// 2. obtain request headers,and put it into openFeign RequestTemplate
Enumeration<String> headerNames = request.getHeaderNames();
if (Objects.nonNull(headerNames)) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
requestTemplate.header(name, value);
}
}
// todo 需要传递请求参数时放开
// 3. obtain request body, and put it into openFeign RequestTemplate
// Enumeration<String> bodyNames = request.getParameterNames();
// StringBuffer body = new StringBuffer();
// if (bodyNames != null) {
// while (bodyNames.hasMoreElements()) {
// String name = bodyNames.nextElement();
// String value = request.getParameter(name);
// body.append(name).append("=").append(value).append("&");
// }
// }
// if (body.length() != 0) {
// body.deleteCharAt(body.length() - 1);
// requestTemplate.body(body.toString());
// log.info("openfeign interceptor body:{}", body.toString());
// }
}
}
2)使RequestInterceptor生效(均已验证)
使RequestInterceptor生效的方式有四种;
1> 代码方式全局生效
直接在Spring可以扫描到的路径使用@Bean方法将RequestInterceptor实现类
注入到Spring容器;
package com.saint.feign.config;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Saint
*/
@Configuration
public class MyConfiguration {
@Bean
public RequestInterceptor requestInterceptor() {
return new MyFeignRequestInterceptor();
}
}
2> 配置方式全局生效
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
# 拦截器配置(和@Bean的方式二选一)
requestInterceptors:
- com.saint.feign.config.MyFeignRequestInterceptor
3> 代码方式针对某个服务生效
直接在@FeignClient注解中指定configuration属性为RequestInterceptor实现类
;
4、配置方式针对某个服务生效
feign:
client:
config:
SERVICE-A:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
# 拦截器配置(和@Bean的方式二选一)
requestInterceptors:
- com.saint.feign.config.MyFeignRequestInterceptor
2、效果验证
1)feign-server
服务改造
在文章 SpringCloud之Feign实现声明式客户端负载均衡详细案例的基础下,我们修改feign-server
项目,添加一个MVC拦截器(用于获取请求头中的数据)
1> MvcInterceptor
package com.saint.feign.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义MVC拦截器
*
* @author Saint
*/
@Slf4j
public class MvcInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("token-saint");
log.info("obtain token is : {}", token);
return true;
}
}
2> MvcInterceptorConfig
设置MVC拦截器会拦截哪些路径的请求,这里是所有的请求全部拦截。
package com.saint.feign.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* MVC拦截器配置
*
* @author Saint
*/
@Configuration
public class MvcInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MvcInterceptor())
.addPathPatterns("/**");
}
}
2)结果验证
1> 执行请求:
2> feign-consumer中的日志:
3> feign-server中的日志:
==结果显示,RequestInterceptor生效了==
三、结合Hystrix限流使用时的坑(仅做记录)
==此处OpenFeign依赖的SpringCloud版本是2020.X之前。==
在application.yaml文件中做如下配置开启了Hystrix限流:
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
做完上述配置后,Feign接口的熔断机制为:线程模式;
如果我们自定义了一个RequestInterceptor实现类
,就会导致hystrix熔断机制失效,接口调用异常(404、null);
1、原因分析
- 在feign调用之前,会走RequestInterceptor拦截器,拦截器中使用了
ServletRequestAttributes
获取请求数据; - 默认feign使用的是线程池模式,当开启熔断的时候,负责熔断的线程和执行Feign接口的线程不是同一个线程,ServletRequestAttributes取到的将会是空值。
2、解决方案
将hystrix熔断方式从线程模式改为信号量模式;
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
strategy: SEMAPHORE
3、Hystrix线程和信号量隔离区别
4、线程和信号量隔离的使用场景?
1> 线程池隔离
- 请求并发量大,并且耗时长(一般是计算量大或者读数据库);
- 采用线程池隔离,可以保证大量的容器线程可用,不会由于其他服务原因,一直处于阻塞或者等待状态,快速失败返回。
2> 信号量隔离
- 请求并发量大,并且耗时短(一般是计算量小,或读缓存);
- 采用信号量隔离时的服务的返回往往非常快,不会占用容器线程太长时间;
- 其减少了线程切换的一些开销,提高了缓存服务的效率 。