专栏系列文章:SpringCloud系列专栏
系列文章:
SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化
SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约
SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表
SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制
SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群
SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇
SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate
SpringCloud 源码系列(8)— 负载均衡Ribbon 之 核心原理
SpringCloud 源码系列(9)— 负载均衡Ribbon 之 核心组件与配置
SpringCloud 源码系列(10)— 负载均衡Ribbon 之 HTTP客户端组件
SpringCloud 源码系列(11)— 负载均衡Ribbon 之 重试与总结篇
Feign 概述
在使用 Spring Cloud 开发微服务应用时,各个服务提供者都是以HTTP接口的形式对外提供服务,因此在服务消费者调用服务提供者时,底层通过 HTTP Client 的方式访问。我们可以使用JDK原生的 URLConnection、Apache的 HttpClient、OkHttp、Spring 的 RestTemplate 去实现服务间的调用。但是最方便、最优雅的方式是通过 Spring Cloud OpenFeign 进行服务间的调用。
Feign 是一个声明式的 Web Service 客户端,它的目的就是让Web Service调用更加简单。Spring Cloud 对 Feign 进行了增强,使 Feign 支持 Spring MVC 的注解,并整合了 Ribbon、Hystrix 等。Feign还提供了HTTP请求的模板,通过编写简单的接口和注解,就可以定义好HTTP请求的参数、格式、地址等信息。Feign 会完全代理HTTP的请求,在使用过程中我们只需要依赖注入Bean,然后调用对应的方法传递参数即可。Feign 的首要目标是将 Java HTTP 客户端的书写过程变得简单。
Feign 的一些主要特性如下:
- 可插拔的注解支持,包括Feign注解和JAX-RS注解。
- 支持可插拔的HTTP编码器和解码器。
- 支持 Hystrix 和它的Fallback。支持Ribbon的负载均衡。
- 支持HTTP请求和响应的压缩。
GitHub地址:
- OpenFeign 地址: github.com/OpenFeign/f…
- SpringCloud OpenFeign 地址: github.com/spring-clou…
DEMO示例
还是使用前面研究 Eureka 和 Ribbon 时的 demo-producer、demo-consumer 服务来做测试。
1、首先,需要引入 openfeign 的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
spring-cloud-starter-openfeign 会帮我们引入如下依赖,包含了 OpenFeign 的核心组件。
2、在 demo-consumer 服务中,增加一个 Feign 客户端接口,来调用 demo-producer 的接口。
@FeignClient(value = "demo-producer")
public interface ProducerFeignClient {
@GetMapping("/v1/user/{id}")
ResponseEntity<User> getUserById(@PathVariable Long id, @RequestParam(required = false) String name);
@PostMapping("/v1/user")
ResponseEntity<User> createUser(@RequestBody User user);
}
3、在启动类加上 @EnableFeignClients 注解来启用 Feign 客户端。
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {
//....
}
4、在接口中注入 ProducerFeignClient 就可以使用 Feign 客户端接口来调用远程服务了。
@RestController
public class FeignController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ProducerFeignClient producerFeignClient;
@GetMapping("/v1/user/query")
public ResponseEntity<User> queryUser() {
ResponseEntity<User> result = producerFeignClient.getUserById(1L, "tom");
User user = result.getBody();
logger.info("query user: {}", user);
return ResponseEntity.ok(user);
}
@GetMapping("/v1/user/create")
public ResponseEntity<User> createUser() {
ResponseEntity<User> result = producerFeignClient.createUser(new User(10L, "Jerry", 20));
User user = result.getBody();
logger.info("create user: {}", user);
return ResponseEntity.ok(user);
}
}
5、在 demo-producer 服务增加 UserController 接口供消费者调用
@RestController
public class UserController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@PostMapping("/v1/user/{id}")
public ResponseEntity<User> queryUser(@PathVariable Long id, @RequestParam String name) {
logger.info("query params: id :{}, name:{}", id, name);
return ResponseEntity.ok(new User(id, name, 10));
}
@PostMapping("/v1/user/{id}")
public ResponseEntity<User> createUser(@RequestBody User user) {
logger.info("create params: {}", user);
return ResponseEntity.ok(user);
}
}
6、测试
先把把注册中心启起来,然后 demo-producer 启两个实例,再启动 demo-consumer,调用 demo-consumer 的接口测试,会发现,ProducerFeignClient 的调用会轮询到 demo-consumer 的两个实例上。
通过简单的测试可以发现,Feign 使得 Java HTTP 客户端的书写过程变得非常简单,就像开发接口一样。另外,Feign 底层一定整合了 Ribbon,@FeignClient 指定了服务名称,请求最终一定是通过 Ribbon 的 ILoadBalancer 组件进行负载均衡的。
FeignClient 注解
通过前面的DEMO可以发现,使用 Feign 最核心的应该就是 @EnableFeignClients 和 @FeignClient 这两个注解,@FeignClient 加在客户端接口类上,@EnableFeignClients 加在启动类上,就是用来扫描加了 @FeignClient 接口的类。我们研究源码就从这两个入口开始。
要知道接口是不能直接注入和调用的,那么一定是 @EnableFeignClients 扫描到 @FeignClient 注解的接口后,基于这个接口生成了动态代理对象,并注入到 Spring IOC 容器中,才可以被注入使用。最终呢,一定会通过 Ribbon 负载均衡获取一个 Server,然后重构 URI,再发起最终的HTTP调用。
@EnableFeignClients 注解
首先看 @EnableFeignClients 的类注释,注释就已经说明了,这个注解就是用来扫描 @FeignClient 注解的接口的,那么核心的逻辑应该就是在 @Import 导入的类 FeignClientsRegistrar 中的。
EnableFeignClients 的主要属性有如下:
value、basePackages: 配置扫描 @FeignClient 的包路径clients:直接指定扫描的 @FeignClient 接口defaultConfiguration:配置 Feign 客户端全局默认配置类,从注释可以得知,默认的全局配置类是 FeignClientsConfiguration
package org.springframework.cloud.openfeign;
/**
* Scans for interfaces that declare they are feign clients (via
* {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
* Configures component scanning directives for use with
* {@link org.springframework.context.annotation.Configuration}
* <code>@Configuration</code> classes.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
// 指定扫描 @FeignClient 包所在目录
String[] value() default {};
// 指定扫描 @FeignClient 包所在目录
String[] basePackages() default {};
// 指定标记接口来扫描包
Class<?>[] basePackageClasses() default {};
// Feign 客户端全局默认配置类
/**
* A custom <code>@Configuration</code> for all feign clients. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
* @return list of default configurations
*/
Class<?>[] defaultConfiguration() default {};
// 直接指定 @FeignClient 注解的类,这时就会禁用类路径扫描
Class<?>[] clients() default {};
}
@FeignClient 注解
首先看 @FeignClient 的类注释,注释说明 @FeignClient 注解就是声明一个 REST 客户端接口,而且会创建一个可以注入的组件,应该就是动态代理的bean。而且如果Ribbon可用,然后就可以用Ribbon做负载均衡,这个负载均衡可以用 @RibbonClient 定制配置类,名称一样就行。
FeignClient 注解被 @Target(ElementType.TYPE) 修饰,表示 FeignClient 注解的作用目标在类或接口上,后面从源码可以看到,@FeignClient 其实只能加在接口上面。@Retention(RetentionPolicy.RUNTIME) 注解表明该注解会在 Class 字节码文件中存在,在运行时可以通过反射获取到。
@FeignClient 注解用于创建声明式 API 接口,该接口是 RESTful 风格的。Feign 被设计成插拔式的,可以注入其他组件和 Feign 一起使用。最典型的是如果 Ribbon 可用,Feign 会和Ribbon 相结合进行负载均衡。
FeignClient 主要有如下属性:
name:指定 FeignClient 的名称,如果项目使用了 Ribbon,name 属性会作为微服务的名称,用于服务发现。url:url 一般用于调试,可以手动指定 @FeignClient 调用的地址。decode404:当发生404错误时,如果该字段为true,会调用 decoder 进行解码,否则抛出 FeignException。configuration:FeignClient 配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contractofallback:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback 指定的类必须实现 @FeignClient 标记的接口。fallbackFactory:工厂类,用于生成 fallback 类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。path:定义当前 FeignClient 的统一前缀。
package org.springframework.cloud.openfeign;
/**
* Annotation for interfaces declaring that a REST client with that interface should be
* created (e.g. for autowiring into another component). If ribbon is available it will be
* used to load balance the backend requests, and the load balancer can be configured
* using a <code>@RibbonClient</code> with the same name (i.e. value) as the feign client.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
// 指定服务名称
@AliasFor("name")
String value() default "";
// 指定服务名称,已过期
@Deprecated
String serviceId() default "";
// FeignClient 接口生成的动态代理的bean名称
String contextId() default "";
// 指定服务名称
@AliasFor("value")
String name() default "";
// @Qualifier 标记
String qualifier() default "";
// 如果不使用Ribbon负载均衡,就需要使用url返回一个绝对地址
String url() default "";
// 404 默认抛出 FeignExceptions 异常,设置为true则替换为404异常
boolean decode404() default false;
// Feign客户端配置类,可以定制 Decoder、Encoder、Contract
/**
* A custom configuration class for the feign client. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
* @return list of configurations for feign client
*/
Class<?>[] configuration() default {};
// FeignClient 接口的回调类,必须实现客户端接口,并注册为一个bean对象。
// 求失败或降级时就会进入回调方法中
/**
* Fallback class for the specified Feign client interface. The fallback class must
* implement the interface annotated by this annotation and be a valid spring bean.
* @return fallback class for the specified Feign client interface
*/
Class<?> fallback() default void.class;
// 回调类创建工厂
Class<?> fallbackFactory() default void.class;
// URL前缀
String path() default "";
// 定义为 primary bean
boolean primary() default true;
}
FeignClient 核心组件
从上面已经得知,FeignClient 的默认配置类为 FeignClientsConfiguration,这个类在 spring-cloud-openfeign-core 的 jar 包下,并且每个 FeignClient 都可以定义各自的配置类。
打开这个类,可以发现这个类注入了很多 Feign 相关的配置 Bean,包括 Retryer、FeignLoggerFactory、Decoder、Encoder、Contract 等,这些类在没有 Bean 被注入的情况下,会自动注入默认配置的 Bean。
package org.springframework.cloud.openfeign;
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();
@Autowired(required = false)
private Logger logger;
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
return springEncoder(formWriterProvider);
}
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable(
ObjectProvider<AbstractFormWriter> formWriterProvider) {
//...
return encoder;
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(this.logger);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
//...
}
这些其实就是 Feign 的核心组件了,对应的默认实现类如下。
Feign 配置
代码配置类
前面提到过 Feign 的核心组件,如果想自定义这些组件,可增加一个配置类,然后配置到 @FeignClient 的 configuration 上。
1、先定义一个配置类
public class ProducerFeignConfiguration {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default();
}
}
2、配置到 @FeignClient 中
@FeignClient(value = "demo-producer", configuration = ProducerFeignConfiguration.class)
public interface ProducerFeignClient {
//...
}
全局配置
前面已经了解到,@EnableFeignClients 的 defaultConfiguration 可以配置全局的默认配置bean对象。也可以使用 application.yml 文件来配置。
feign:
client:
config:
# 默认全局配置
default:
connectTimeout: 1000
readTimeout: 1000
loggerLevel: basic
指定客户端配置
@FeignClient 的 configuration 可以配置客户端特定的配置类,也可以使用 application.yml 配置。
feign:
client:
config:
# 指定客户端名称
demo-producer:
# 连接超时时间
connectTimeout: 5000
# 读取超时时间
readTimeout: 5000
# Feign日志级别
loggerLevel: full
# Feign的错误解码器
errorDecoder: com.example.simpleErrorDecoder
# 配置拦截器
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
# 404是否解码
decode404: false
#Feign的编码器
encoder: com.example.simpleEncoder
#Feign的解码器
decoder: com.example.simpleDecoder
#Feign的Contract配置
contract: com.example.simpleContract
注意,如果通过Java代码的方式配置过 Feign,然后又通过属性文件的方式配置 Feign,属性文件中Feign的配置会覆盖Java代码的配置。但是可以配置 feign.client.default-to-properties=false 来改变Feign配置生效的优先级,使得优先使用Java代码的配置。
开启压缩配置
Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率。
feign:
compression:
request:
# 配置请求GZIP压缩
enabled: true
# 配置压缩支持的 MIME TYPE
mime-types: text/xml,application/xml,application/json
# 配置压缩数据大小的下限
min-request-size: 2048
response:
# 配置响应GZIP压缩
enabled: true
FeignClient 开启日志
Feign 为每一个 FeignClient 都提供了一-个 feign.Logger 实例,可以在配置中开启日志。但是生产环境一般不要开启日志,因为接口调用可能会产生大量日志,一般在开发环境调试开启即可。
通过配置文件开启日志
首先设置客户端的 loggerLevel,然后配置 logging.level 日志级别为 debug。
feign:
client:
config:
demo-producer:
# Feign日志级别
loggerLevel: full
logging:
level:
# 设置日志输出级别
com.lyyzoo.sunny.register.feign: debug
之后调用 FeignClient 就可以看到接口调用日志了:
2020-12-30 15:33:02.459 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] ---> GET http://demo-producer/v1/user/1?name=tom HTTP/1.1
2020-12-30 15:33:02.459 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] ---> END HTTP (0-byte body)
2020-12-30 15:33:02.462 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] <--- HTTP/1.1 200 (3ms)
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] connection: keep-alive
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] content-type: application/json
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] date: Wed, 30 Dec 2020 07:33:02 GMT
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] keep-alive: timeout=60
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] transfer-encoding: chunked
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById]
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] {"id":1,"name":"tom","age":10}
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient : [ProducerFeignClient#getUserById] <--- END HTTP (30-byte body)
2020-12-30 15:33:02.463 INFO 2720 --- [nio-8020-exec-6] c.l.s.r.controller.FeignController : query user: User{id=1, name='tom', age=10}
通过Java代码开启日志
首先还是需要设置日志输出级别:
logging:
level:
# 设置日志输出级别
com.lyyzoo.sunny.register.feign: debug
然后配置一个 feign.Logger.Level 对象,指定输出所有日志就可以了。
@Bean
public feign.Logger.Level loggerLevel() {
return Logger.Level.FULL;
}
Logger.Level 的具体级别如下:
public enum Level {
// 不打印任何日志
NONE,
// 只打印请求的方法和URL,以及响应状态码和执行时间
BASIC,
// 在BASIC的基础上,打印请求头和响应头信息
HEADERS,
// 记录所有请求与相应的明细,包含请求头、请求体、元数据
FULL
}