Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。
Feign集成了Ribbon、RestTemplate实现了负载均衡的执行Http调用,只不过对原有的方式(Ribbon+RestTemplate)进行了封装,开发者不必手动使用RestTemplate调服务,而是定义一个接口,在这个接口中标注一个注解即可完成服务调用,这样更加符合面向接口编程的宗旨,简化了开发。
Spring Cloud Feign具备可插拔的注解支持,支持Feign注解、JAX-RS注解和Spring MVC的注解。
但遗憾的是Feign现在停止迭代了,当然现在也是有不少企业在用。
前面介绍过停止迭代的Feign,简单点来说:OpenFeign是springcloud在Feign的基础上支持了 SpringMVC 的注解,如@RequestMapping 等等。OpenFeign的 @FeignClient 可以解析SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
官网地址:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.10.BUILD-SNAPSHOT/reference/html
Feign | openFiegn |
---|---|
Feign是SpringCloud组件中一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 | OpenFeign 是SpringCloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等。OpenFeign 的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 |
Feign的maven依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-feignartifactId>
dependency>
OpenFeign的maven依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
说明:
springcloud F 及F版本以上 springboot 2.0 以上基本上使用openfeign,openfeign 如果从框架结构上看就是2019年feign停更后出现版本,也可以说大多数新项目都用openfeign ,2018年以前的项目在使用feign。
使用Feign涉及两个注解:
查看 FeignClient 注解的源码,其代码如下:
public @interface FeignClient {
@AliasFor("name")
String value() default "";
/** @deprecated */
@Deprecated
String serviceId() default "";
String contextId() default "";
// 指定FeignClient的名称
@AliasFor("value")
String name() default "";
String qualifier() default "";
// 全路径地址或hostname,http或https可选
String url() default "";
// 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
boolean decode404() default false;
// Feign配置类,可以自定义Feign的LogLevel
Class<?>[] configuration() default {};
// 容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑
Class<?> fallback() default void.class;
// 工厂类,用于生成fallback类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
Class<?> fallbackFactory() default void.class;
// 定义当前FeignClient的统一前缀,类似于controller类上的requestMapping
String path() default "";
boolean primary() default true;
}
value 和 name 的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。
serviceId已经废弃了,直接使用name即可。
比如我们有个user服务,但user服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,比如:
Client 1
@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id") int id);
}
Client 2
@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
@GetMapping("/user2/get")
public User getUser(@RequestParam("id") int id);
}
这种情况下启动就会报错了,因为Bean的名称冲突了,具体错误如下:
Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
解决方案可以增加下面的配置,作用是允许出现beanName一样的BeanDefinition。
spring.main.allow-bean-definition-overriding=true
另一种解决方案就是为每个Client手动指定不同的contextId,这样就不会冲突了。
上面给出了Bean名称冲突后的解决方案,下面来分析下contextId在Feign Client的作用,在注册Feign Client Configuration的时候需要一个名称,名称是通过getClientName方法获取的:
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
private String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("name");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("serviceId");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
+ FeignClient.class.getSimpleName());
}
可以看到如果配置了contextId就会用contextId,如果没有配置就会去value然后是name最后是serviceId。默认都没有配置,当出现一个服务有多个Feign Client的时候就会报错了。
其次的作用是在注册FeignClient中,contextId会作为Client 别名的一部分,如果配置了qualifier优先用qualifier作为别名。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// 拼接别名
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
// null
beanDefinition.setPrimary(primary);
// 配置了qualifier优先用qualifier
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
url用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用。
使用示列
@FeignClient(name = "optimization-user", url = "http://localhost:8085")
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id") int id);
}
当调用请求发生404错误时,decode404的值为true,那么会执行decoder解码,否则抛出异常。
解码也就是会返回固定的数据格式给你:
{"timestamp":"2020-01-05T09:18:13.154+0000","status":404,"error":"Not Found","message":"No message available","path":"/user/get11"}
抛异常的话就是异常信息了,如果配置了fallback那么就会执行回退逻辑。
configuration是配置Feign配置类,在配置类中可以自定义Feign的Encoder、Decoder、LogLevel、Contract等。
configuration定义
// 不要添加@Configuration,不然会被作为全局配置文件共享
public class FeignConfiguration {
@Bean
public Logger.Level getLoggerLevel() {
return Logger.Level.FULL;
}
// Contract,feignDecoder,feignEncoder.....
}
使用示列
@FeignClient(value = "optimization-user", configuration = FeignConfiguration.class)
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id")int id);
}
定义容错的处理类,也就是回退逻辑,fallback的类必须实现Feign Client的接口,无法知道熔断的异常信息。
fallback定义
@Component
public class UserRemoteClientFallback implements UserRemoteClient {
@Override
public User getUser(int id) {
return new User(0, "默认fallback");
}
}
使用示列
@FeignClient(value = "optimization-user", fallback = UserRemoteClientFallback.class)
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id")int id);
}
也是容错的处理,可以知道熔断的异常信息。
fallbackFactory定义
@Component
public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);
@Override
public UserRemoteClient create(Throwable cause) {
return new UserRemoteClient() {
@Override
public User getUser(int id) {
logger.error("UserRemoteClient.getUser异常", cause);
return new User(0, "默认");
}
};
}
}
使用示列
@FeignClient(value = "optimization-user", fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id")int id);
}
path定义当前FeignClient访问接口时的统一前缀,比如接口地址是/user/get, 如果你定义了前缀是user, 那么具体方法上的路径就只需要写/get 即可。
使用示列
@FeignClient(name = "optimization-user", path="user")
public interface UserRemoteClient {
@GetMapping("/get")
public User getUser(@RequestParam("id") int id);
}
primary对应的是 @Primary 注解,默认为 true,官方这样设置也是有原因的。当我们的Feign实现了fallback后,也就意味着Feign Client有多个相同的Bean在Spring容器中,当我们在使用@Autowired进行注入的时候,不知道注入哪个,所以我们需要设置一个优先级高的,@Primary 注解就是干这件事情的。
qualifier 对应的是 @Qualifier 注解,使用场景跟上面的primary关系很淡,一般场景直接 @Autowired 直接注入就可以了。
如果我们的Feign Client有fallback实现,默认@FeignClient注解的primary=true, 意味着我们使用@Autowired注入是没有问题的,会优先注入你的Feign Client。
如果你鬼斧神差的把primary设置成false了,直接用@Autowired注入的地方就会报错,不知道要注入哪个对象。
解决方案很明显,你可以将primary设置成true即可,如果由于某些特殊原因,你必须得去掉primary=true的设置,这种情况下我们怎么进行注入,我们可以配置一个qualifier,然后使用@Qualifier注解进行注入,示列如下:
Feign Client定义
@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {
@GetMapping("/get")
public User getUser(@RequestParam("id") int id);
}
Feign Client注入
@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;
既然是微服务之间的相互调用,那么一定会有服务提供者了,创建openFeign-provider9005,注册进入Nacos中,配置如下:
server:
port: 9005
spring:
application:
## 指定服务名称,在nacos中的名字
name: openFeign-provider
cloud:
nacos:
discovery:
# nacos的服务地址,nacos-server中IP地址:端口号
server-addr: 127.0.0.1:8848
management:
endpoints:
web:
exposure:
## yml文件中存在特殊字符,必须用单引号包含,否则启动报错
include: '*'
新建一个模块openFeign-consumer9006作为消费者服务,步骤如下。
除了Nacos的注册中心的依赖,还要添加openFeign的依赖,如下:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
老套路了,在Spring boot 主启动类上添加一个注解@EnableFeignClients,开启openFeign功能,如下:
@SpringBootApplication
@EnableDiscoveryClient
// basePackages 属性指定具体的feign接口包路径
@EnableFeignClients(basePackages = {"cn.*.springcloud.*"})
public class OpenFeignConsumer9006Application
{
public static void main(String[] args) {
SpringApplication.run(OpenFeignConsumer9006Application.class, args);
}
}
新建一个openFeign接口,使用@FeignClient注解标注,如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
}
注意:该注解@FeignClient中的value属性指定了服务提供者在nacos注册中心的服务名。
新建一个controller用来调试接口,直接调用openFeign的接口,如下:
@RestController
@RequestMapping("/openfeign")
public class OpenFeignController {
}
好了,至此一个openFeign的微服务就搭建好了,并未实现具体的功能,下面一点点实现。
开发中接口传参的方式有很多,但是在openFeign中的传参是有一定规则的,下面详细介绍。
这个也是接口开发中常用的传参规则,在Spring Boot 中通过@RequestBody标识入参。
provider接口中JSON传参方法如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order2")
public Order createOrder2(@RequestBody Order order){
return order;
}
}
consumer中openFeign接口中传参代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,这里的@RequestBody可以不填
* 方法名称任意
*/
@PostMapping("/openfeign/provider/order2")
Order createOrder2(@RequestBody Order order);
}
注意:openFeign默认的传参方式就是JSON传参(@RequestBody),因此定义接口的时候可以不用@RequestBody注解标注,不过为了规范,一般都填上。
这种传参方式也是比较常用,参数使用POJO对象接收。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order1")
public Order createOrder1(Order order){
return order;
}
}
consumer消费者openFeign代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,如果通过POJO表单传参的,使用@SpringQueryMap标注
*/
@PostMapping("/openfeign/provider/order1")
Order createOrder1(@SpringQueryMap Order order);
}
网上很多人疑惑POJO表单方式如何传参,官方文档明确给出了解决方案,如下:
openFeign提供了一个注解@SpringQueryMap完美解决POJO表单传参。
此种方式针对restful方式中的GET请求,也是比较常用请求方式。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@GetMapping("/test/{id}")
public String test(@PathVariable("id")Integer id){
return "accept one msg id="+id;
}
consumer消费者openFeign接口如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
@GetMapping("/openfeign/provider/test/{id}")
String get(@PathVariable("id")Integer id);
}
使用注解@PathVariable接收url中的占位符,这种方式很好理解,@PathVariable参数名一定要写。
此种方式传参不建议使用,但是也有很多开发在用。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/test2")
public String test2(String id,String name){
return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}
}
consumer消费者openFeign接口传参如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 必须要@RequestParam注解标注,且value属性必须填上参数名
* 方法参数名可以任意,但是@RequestParam注解中的value属性必须和provider中的参数名相同
*/
@PostMapping("/openfeign/provider/test2")
String test(@RequestParam("id") String arg1,@RequestParam("name") String arg2);
}
想要理解超时处理,先看一个例子:我将provider服务接口睡眠3秒钟,接口如下:
@PostMapping("/test2")
public String test2(String id,String name) throws InterruptedException {
Thread.sleep(3000);
return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}
此时,我们调用consumer的openFeign接口返回结果如下图:
很明显的看出程序异常了,返回了接口调用超时。为什么呢?
openFeign其实是有默认的超时时间的,默认分别是连接超时时间10秒、读超时时间60秒,源码在feign.Request.Options#Options()这个方法中,如下图:
那么问题来了:为什么我只设置了睡眠3秒就报超时呢?
其实openFeign集成了Ribbon,Ribbon的默认超时连接时间、读超时时间都是是1秒,源码在org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute()方法中,如下图:
源码大致意思:如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。
理解了超时设置的原理,由之产生两种方案也是很明了了,如下:
设置很简单,在配置文件中添加如下设置:
ribbon:
# 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
ReadTimeout: 5000
# 指的是建立链接后从服务器读取可用资源所用的时间
ConectTimeout: 5000
openFeign设置超时时间非常简单,只需要在配置文件中配置,如下:
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
default设置的是全局超时时间,对所有的openFeign接口服务都生效
但是正常的业务逻辑中可能涉及到多个openFeign接口的调用,如下图:
上图中的伪代码如下:
public T invoke(){
//1. 调用serviceA
serviceA();
//2. 调用serviceA
serviceB();
//3. 调用serviceA
serviceC();
}
那么上面配置的全局超时时间能不能通过呢?很显然是serviceA、serviceB能够成功调用,但是serviceC并不能成功执行,肯定报超时。
此时我们可以给serviceC这个服务单独配置一个超时时间,配置如下:
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
## 为serviceC这个服务单独配置超时时间
serviceC:
connectTimeout: 30000
readTimeout: 30000
注意:单个配置的超时时间将会覆盖全局配置。
openFeign虽然提供了日志增强功能,但是默认是不显示任何日志的,不过开发者在调试阶段可以自己配置日志的级别。
openFeign的日志级别如下:
配置起来也很简单,步骤如下:
需要自定义一个配置类,在其中设置日志级别,如下:
注意:这里的logger是feign包里的。
只需要在配置文件中调整指定包或者openFeign的接口日志级别,如下:
logging:
level:
cn.myjszl.service: debug
这里的 cn.myjszl.service 是openFeign接口所在的包名,当然你也可以配置一个特定的openFeign接口。
Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。
在生产环境中,通常不使用默认的http client,通常有如下两种选择:
至于哪个更好,其实各有千秋,我比较倾向于ApacheHttpClient,毕竟老牌子了,稳定性不在话下。那么如何替换掉呢?其实很简单,下面演示使用ApacheHttpClient替换
在openFeign接口服务的pom文件添加如下依赖:
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
为什么要添加上面的依赖呢?从源码中不难看出,请看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration这个类,代码如下:
上述红色框中的生成条件,其中的@ConditionalOnClass(ApacheHttpClient.class),必须要有ApacheHttpClient这个类才会生效,并且feign.httpclient.enabled这个配置要设置为true。
feign:
client:
httpclient:
# 开启 Http Client
enabled: true
其实很简单,在feign.SynchronousMethodHandler#executeAndDecode()这个方法中可以清楚的看出调用哪个client,如下图:
上图中可以看到最终调用的是ApacheHttpClient。
在讲如何优化之前先来看一下GZIP 压缩算法,概念如下:
gzip是一种数据格式,采用用deflate算法压缩数据;gzip是一种流行的数据压缩算法,应用十分广泛,尤其是在Linux平台。
当GZIP压缩到一个纯文本数据时,效果是非常明显的,大约可以减少70%以上的数据大小。
网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快的好处不言而喻,除了节省流量,改善用户的浏览体验外,另一个潜在的好处是GZIP与搜索引擎的抓取工具有着更好的关系。例如 Google就可以通过直接读取GZIP文件来比普通手工抓取更快地检索网页。
openFeign支持请求/响应开启GZIP压缩,整体的流程如下图:
上图中涉及到GZIP传输的只有两块,分别是Application client -> Application Service、 Application Service->Application client。
注意:openFeign支持的GZIP仅仅是在openFeign接口的请求和响应,即是openFeign消费者调用服务提供者的接口。
openFeign开启GZIP步骤也是很简单,只需要在配置文件中开启如下配置:
feign:
## 开启压缩
compression:
request:
enabled: true
## 开启压缩的阈值,单位字节,默认2048,即是2k,这里为了演示效果设置成10字节
min-request-size: 10
mime-types: text/xml,application/xml,application/json
response:
enabled: true
上述配置完成之后,发出请求,可以清楚看到请求头中已经携带了GZIP压缩,如下图:
Feign有提供一个接口,RequestInterceptor;
只要实现这个接口,简单做一些处理,比如说我们验证请求头的token叫 X-Token,我们就先取出当前请求的token,然后放到feign请求头上;
public class TokenRelayRequestIntecepor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//1.使用静态方法获取到token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Token");
//2.将token放在RequestTemplate中把token传递到下一个服务中,放在请求头里
//不为空才传递
if (StringUtils.isNotBlank(token)){
template.header("X-Token",token);
}
}
}
feign:
client:
config:
#全局配置
default:
requestInterceptors:
- 全路径包名.TokenRelayRequestIntecepor
在@FeignClient接口里添加configuration = {TokenRelayRequestIntecepor.class}
@FeignClient(value="被调用的服务名",configuration={TokenRelayRequestIntecepor.class})
常见的熔断降级框架有Hystrix、Sentinel,openFeign默认支持的就是Hystrix,这个在官方文档上就有体现。
feign:
hystrix:
# 开启熔断器
enabled: true
但是阿里的Sentinel无论是功能特性、简单易上手等各方面都完全秒杀Hystrix,使用openFeign+Sentinel进行整合实现服务降级。
在openFeign-consumer9006消费者的pom文件添加sentinel依赖(由于使用了聚合模块,不指定版本号),如下:
>
>com.alibaba.cloud >
>spring-cloud-starter-alibaba-sentinel >
>
feign:
sentinel:
enabled: true
这个类一定要和openFeign接口实现同一个类,如下图:
OpenFeignFallbackService这个是降级回调的类,一旦OpenFeignService中对应得接口出现了异常则会调用这个类中对应得方法进行降级处理。
在@FeignClient中添加fallback属性,属性值是降级回调的类,如下:
@FeignClient(value = "openFeign-provider",fallback = OpenFeignFallbackService.class)
public interface OpenFeignService {}
fallbackFactory:工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
@Component
public class OpenFeignFallbackFactory implements FallbackFactory<OpenFeignService > {
@Override
public OpenFeignService create(Throwable throwable) {
return new OpenFeignService () {
@Override
public String batchOrder(List<Order> list) {
System.out.println("服务错误,降级"+ throwable.getMessage());
return null;
}
};
}
}
@FeignClient(value = "openFeign-provider",fallbackFactory = OpenFeignFallbackFactory.class)
public interface OpenFeignService {}
注:同一个@FeignClient里,fallback 和 fallbackFactory 不能同时使用。
说到 OpenFeign就不得不提 Ribbon,OpenFeign默认将Ribbon作为负载均衡器,直接内置了 Ribbon。在导入OpenFeign 依赖后无需专门导入Ribbon 依赖。
Ribbon 是 Netflix 公司的一个开源的负载均衡项目,一个客户端负载均衡器,运行在消费者端。简单来说就是在消费者端配置对提供者的负载均衡器。这点与 Dubbo略有不同,Dubbo 在消费者端与提供者端均可配置负载均衡器。
若要更换负载均衡策略,则首先要了解负载均衡策略的定义接口 IRule。Ribbon 默认采用的是 RoundRobinRule,即轮询策略。但通过修改消费者工程的配置文件,或修改消费者的启动类或 JavaConfig 类可以实现更换负载均衡策略的目的。
1、修改配置文件
修改配置文件,在其中添加如下内容,指定要使用的负载均衡策略
<clientName>.<clientConfigNameSpace>.NFLoadBalancerRuleClassName
该方式的好处是,可以为不同的微服务指定相应的负载均衡策略。
providerFeignService:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
com.netflix.loadbalancer.RandomRule 选择对应的策略完整包路径
2、修改JavaConfig类
在 JavaConfig 类中添加负载 Bean 方法。全局所有feign对应服务都可以生效。
@Configuration
public class FeignConfiguration {
/**
* 配置随机的负载均衡策略
* 特点:对所有的服务都生效
*/
@Bean
public IRule loadBalancedRule() {
return new RandomRule();
}
}
1、定义一个负载均衡策略
该负载均衡策略的思路是:从所有可用的 provider 中排除掉指定端口号的provider,剩余 provider 进行随机选择。
/**
* 自定义负载均衡算法
*/
public class CustomRule implements IRule {
private ILoadBalancer lb;
private List<Integer> excludePorts;
public CustomRule() {
}
public CustomRule(List<Integer> excludePorts) {
this.excludePorts = excludePorts;
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer() {
return lb;
}
/**
* 目标:自定义负载均衡策略:从所有可用的provider中排除掉指定端口号的provider,剩余provider进行随机选择
* 实现步骤:
* 1.获取到所有Server
* 2.从所有Server中排除掉指定端口的Server后,剩余的Server
* 3.从剩余Server中随机选择一个Server
*/
@Override
public Server choose(Object key) {
// 1.获取到所有Server
List<Server> servers = lb.getReachableServers();
// 2.从所有Server中排除掉指定端口的Server后,剩余的Server
List<Server> availableServers = this.getAvailableServers(servers);
// 3.从剩余Server中随机选择一个Server
return this.getAvailableRandomServers(availableServers);
}
private List<Server> getAvailableServers(List<Server> servers) {
// 若没有指定要排除的port,则返回所有Server
if(excludePorts == null || excludePorts.size() == 0) {
return servers;
}
List<Server> aservers = servers.stream()
// filter()
// noneMatch() 只有当流中所有元素都没有匹配上时,才返回true,只要有一个匹配上了,则返回false
.filter(server -> excludePorts.stream().noneMatch(port -> server.getPort() == port))
.collect(Collectors.toList());
return aservers;
}
private Server getAvailableRandomServers(List<Server> availableServers) {
// 获取一个[0,availableServers.size())的随机数
int index = new Random().nextInt(availableServers.size());
return availableServers.get(index);
}
}
2、修改JavaConfig类,使用自定义的负载均衡策略
@Configuration
public class FeignConfiguration {
@Bean
public IRule loadBalancedRule() {
List<Integer> list = new ArrayList<>();
list.add(8081);//排除访问端口
return new CustomRule(list);
}
}
算法 | 说明 |
---|---|
RoundRobinRule | 轮询策略:Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的provider,其最多轮询 10 轮(代码中写死的,不能修改)。若还未找到,则返回 null。 |
RandomRule | 随机策略:从所有可用的 provider 中随机选择一个。 |
RetryRule | 重试策略:先按照 RoundRobinRule 策略获取 server,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。 |
BestAvailableRule | 最可用策略:选择并发量最小的 provider,即连接的消费者数量最少的provider。其会遍历服务列表中的每一个server,选择当前连接数量minimalConcurrentConnections 最小的server。 |
AvailabilityFilteringRule | 可用过滤算法:该算法规则是过滤掉处于熔断状态的 server 与已经超过连接极限的server,对剩余 server 采用轮询策略。 |
由于 Netflix 对于 Ribbon 的维护已经暂停,所以 Spring Cloud 对于负载均衡建议使用由其自己定义的 Spring Cloud LoadBalancer。对于Spring Cloud LoadBalancer 的使用非常简单。
1、关闭Ribbon的负载均衡器
spring:
application:
name: consumer01-depart
cloud:
loadbalancer:
# 关闭Ribbon的负载均衡器
ribbon:
enabled: false
2、pom中添加LoadBalancer依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
<version>2.2.3.RELEASEversion>
dependency>
默认的配置类为FeignClientsConfiguration,这个类在spring-cloud-netflix-core的jar包下,打开这个类,可以发现它是一个配置类,注入了很多的相关配置的bean,包括feignRetryer、FeignLoggerFactory、FormattingConversionService等,其中还包括了Decoder、Encoder、Contract,如果这三个bean在没有注入的情况下,会自动注入默认的配置。
代码如下:
@Configuration
public class FeignClientsConfiguration {
...//省略代码
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters));
}
@Bean
@ConditionalOnMissingBean
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
...//省略代码
}
重写配置:
你可以重写FeignClientsConfiguration中的bean,从而达到自定义配置的目的,比如FeignClientsConfiguration的默认重试次数为Retryer.NEVER_RETRY,即不重试,那么希望做到重写,写个配置文件,注入feignRetryer的bean,代码如下:
@Configuration
public class FeignConfig {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}
在上述代码更改了该FeignClient的重试次数,重试间隔为100ms,最大重试时间为1s,重试次数为5次。
feign是一个伪客户端,即它不做任何的请求处理。Feign通过处理注解生成request,从而实现简化HTTP API开发的目的,即开发人员可以使用注解的方式定制request api模板,在发送http request请求之前,feign通过处理注解的方式替换掉request模板中的参数,这种实现方式显得更为直接、可理解。
通过包扫描注入FeignClient的bean,该源码在FeignClientsRegistrar类:
首先在启动配置上检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解接口。代码如下:
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
程序启动后通过包扫描,当类有@FeignClient注解,将注解的信息取出,连同类名一起取出,赋给BeanDefinitionBuilder,然后根据BeanDefinitionBuilder得到beanDefinition,最后beanDefinition式注入到ioc容器中,源码如下:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
注入bean之后,通过jdk的代理,当请求Feign Client的方法时会被拦截,代码在ReflectiveFeign类,代码如下:
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if(Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
在SynchronousMethodHandler类进行拦截处理,当被FeignClient的方法被拦截会根据参数生成RequestTemplate对象,该对象就是http请求的模板,代码如下:
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
其中有个executeAndDecode()方法,该方法是通RequestTemplate生成Request请求对象,然后根据用client获取response。
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
...//省略代码
response = client.execute(request, options);
...//省略代码
}
其中Client组件是一个非常重要的组件,Feign最终发送request请求以及接收response响应,都是由Client组件完成的,其中Client的实现类,只要有Client.Default,该类由HttpURLConnnection实现网络请求,另外还支持HttpClient、Okhttp.
首先来看以下在FeignRibbonClient的自动配置类,FeignRibbonClientAutoConfiguration ,主要在工程启动的时候注入一些bean,其代码如下:
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null),
cachingFactory, clientFactory);
}
}
在缺失配置feignClient的情况下,会自动注入new Client.Default(),跟踪Client.Default()源码,它使用的网络请求框架为HttpURLConnection,代码如下:
@Override
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection).toBuilder().request(request).build();
}
怎么在feign中使用HttpClient,查看FeignRibbonClientAutoConfiguration的源码
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration {
...//省略代码
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignLoadBalancedConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
ApacheHttpClient delegate;
if (this.httpClient != null) {
delegate = new ApacheHttpClient(this.httpClient);
}
else {
delegate = new ApacheHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
...//省略代码
}
从代码@ConditionalOnClass(ApacheHttpClient.class)注解可知道,只需要在pom文件加上HttpClient的classpath就行了,另外需要在配置文件上加上feign.httpclient.enabled为true,从 @ConditionalOnProperty注解可知,这个可以不写,在默认的情况下就为true.
在pom文件加上:
<dependency>
<groupId>com.netflix.feigngroupId>
<artifactId>feign-httpclientartifactId>
<version>RELEASEversion>
dependency>
同理,如果想要feign使用Okhttp,则只需要在pom文件上加上feign-okhttp的依赖:
<dependency>
<groupId>com.netflix.feigngroupId>
<artifactId>feign-okhttpartifactId>
<version>RELEASEversion>
dependency>
通过上述的FeignRibbonClientAutoConfiguration类配置Client的类型(httpurlconnection,okhttp和httpclient)时候,可知最终向容器注入的是LoadBalancerFeignClient,即负载均衡客户端。现在来看下LoadBalancerFeignClient的代码:
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
其中有个executeWithLoadBalancer()方法,即通过负载均衡的方式请求。
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri())
.build();
try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
其中服务在submit()方法上,点击submit进入具体的方法,这个方法是LoadBalancerCommand的方法:
Observable<T> o =
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
context.setServer(server);
}}
上述代码中有个selectServe(),该方法是选择服务的进行负载均衡的方法,代码如下:
private Observable<Server> selectServer() {
return Observable.create(new OnSubscribe<Server>() {
@Override
public void call(Subscriber<? super Server> next) {
try {
Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);
next.onNext(server);
next.onCompleted();
} catch (Exception e) {
next.onError(e);
}
}
});
}
最终负载均衡交给loadBalancerContext来处理,即之前讲述的Ribbon,在这里不再重复。
总到来说,Feign的源码实现的过程如下:
比较内容 | Dubbo | Feign |
---|---|---|
协议 | 支持多传输协议(Dubbo、Rmi、http、redis等等),可以根据业务场景选择最佳的方式。非常灵活。默认的Dubbo协议:利用Netty,TCP传输,单一、异步、长连接,适合数据量小、高并发和服务提供者远远少于消费者的场景。 | 基于Http传输协议,短连接,不适合高并发的访问。 |
负载均衡 | 1.支持4种算法(随机、轮询、活跃度、Hash一致性),而且算法里面引入权重的概念。2.配置的形式不仅支持代码配置,还支持Dubbo控制台灵活动态配置。3.负载均衡的算法可以精准到某个服务接口的某个方法。 | 只支持N种策略:轮询、随机、ResponseTime加权。负载均衡算法是Client级别的。 |
容错策略 | 支持多种容错策略:failover、failfast、brodecast、forking等,也引入了retry次数、timeout等配置参数。 | 利用熔断机制来实现容错的,处理的方式不一样。 |
参考:
https://zhuanlan.zhihu.com/p/101383417?from=singlemessage
https://www.cnblogs.com/liconglong/p/15408858.html
https://mp.weixin.qq.com/s/0DY90tn5HNjo1LfZVPIUJA
https://cloud.tencent.com/developer/article/1009212