新增 Spring Cloud OpenFeign 源码分析 (#136)

pull/138/head
haitaoss 1 year ago committed by GitHub
parent 3207d4f1bd
commit 58d089b5ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -117,6 +117,11 @@
- [SpringBoot 日志系统](/docs/SpringBoot/SpringBoot-LogSystem.md)
- [SpringBoot ConditionalOnBean](/docs/SpringBoot/SpringBoot-ConditionalOnBean.md)
## Spring Cloud
- [Spring Cloud Commons 源码](docs/SpringCloud/spring-cloud-commons-source-note.md)
- [Spring Cloud OpenFeign 源码](docs/SpringCloud/spring-cloud-openfeign-source-note.md)
### SpringSecurity
- [SpringSecurity 请求全过程解析](/docs/SpringSecurity/SpringSecurity请求全过程解析.md)

@ -23,7 +23,32 @@ SpringCloud 是在 SpringBoot 的基础上构建的。Spring Cloud 以两个库
[前置知识SprinBoot 加载 application.yml 的原理](https://github.com/haitaoss/spring-boot/blob/source-v2.7.8/note/springboot-source-note.md#%E5%B1%9E%E6%80%A7%E6%96%87%E4%BB%B6%E7%9A%84%E5%8A%A0%E8%BD%BD%E9%A1%BA%E5%BA%8F)
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/BootstrapProperties/Main.java)
示例代码
```java
@EnableAutoConfiguration
public class Main {
public static void main(String[] args) {
// 是否创建 bootstrapContext
System.setProperty("spring.cloud.bootstrap.enabled", "true");
// 设置 bootstrapContext 中属性文件的搜索目录 或者是 属性文件
System.setProperty("spring.cloud.bootstrap.location", "");
System.setProperty("spring.cloud.bootstrap.additional-location",
"optional:classpath:/config/haitao/,classpath:/haitao.properties");
// 设置 bootstrapContext 默认属性文件的名字
// System.setProperty("spring.cloud.bootstrap.name", "bootstrap-haitao");
// 设置 profile
// System.setProperty("spring.profiles.active", "haitao");
// 测试读取属性
ConfigurableApplicationContext context = SpringApplication.run(Main.class, args);
ConfigurableEnvironment environment = context.getEnvironment();
Stream.iterate(1, i -> i + 1).limit(5).map(i -> "p" + i).forEach(
name -> System.out.println(String.format("key:%s \t valus: %s", name, environment.getProperty(name))));
}
}
```
BootstrapApplicationListener 是用于完成 SpringCloud 的接入的,主要是完成 bootstrapContext 的创建、bootstrap 属性的加载、设置 bootstrapContext 为父容器。下面是 BootstrapApplicationListener 被触发的入口和核心逻辑
@ -109,7 +134,43 @@ public class BootstrapImportSelectorConfiguration {}
### PropertySourceBootstrapConfiguration
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/BootstrapProperties/BootstrapConfiguration/MyPropertySourceLocator.java)
示例代码
```java
public class MyPropertySourceLocator implements PropertySourceLocator {
public MyPropertySourceLocator() {
System.out.println("MyPropertySourceLocator...构造器");
}
@Resource
private ApplicationContext applicationContext;
@Value("${dynamicConfigFile}")
private String filePath;
@Override
public PropertySource<?> locate(Environment environment) {
PropertySource<?> propertySource;
try {
// 也可以改成网络资源
propertySource = new YamlPropertySourceLoader()
.load("haitao-propertySource", applicationContext.getResource(filePath)).get(0);
} catch (IOException e) {
throw new RuntimeException(e);
}
return propertySource;
}
}
```
`META-INF/spring.factories`
```properties
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
cn.haitaoss.BootstrapProperties.BootstrapConfiguration.MyPropertySourceLocator
```
```java
/**
@ -154,7 +215,39 @@ public class BootstrapImportSelectorConfiguration {}
## @RefreshScope@ConfigurationProperties bean 的更新
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/refresh/Main.java)
示例代码
```java
@SpringBootApplication
public class Main {
/**
* 总结用法:
*
* 可以通过属性 spring.cloud.refresh.refreshable spring.cloud.refresh.extraRefreshable
* 代替 @RefreshScope
*
* 可以设置属性 spring.cloud.refresh.enabled=false 取消 @RefreshScope 的自动注入 是
* spring.cloud.refresh.never-refreshable 属性记录的类就不重会新绑定属性
*/
public static void main(String[] args) {
// TODOHAITAO: 2023/4/6 访问验证属性更新 GET http://127.0.0.1:8080/actuator/refresh
// 启用 bootstrap 属性的加载
System.setProperty("spring.cloud.bootstrap.enabled", "true");
// 通过配置属性的方式扩展bean为 refresh scope 的
System.setProperty("spring.cloud.refresh.refreshable",
Arrays.asList(RefreshScopeBean1.class.getName(), RefreshScopeBean2.class.getName()).stream()
.collect(Collectors.joining(",")));
System.setProperty("spring.cloud.refresh.extraRefreshable",
Arrays.asList(Object.class.getName()).stream().collect(Collectors.joining(",")));
// 设置 bootstrapContext 会默认加载的 bean
System.setProperty("spring.cloud.bootstrap.sources","cn.haitaoss.RefreshScope.config.MyPropertySourceLocator");
}
}
```
```java
/**
@ -438,7 +531,29 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
## LoadBalancerClient
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/ServiceRegisterAndLoadBalance/Main.java)
示例代码
```java
@EnableAutoConfiguration
@RestController
@Import({ LoadBalancerClientConfig.class, LoadBalancerOtherConfig.class })
public class Main extends BaseApp {
public static void main(String[] args) {
/**
* TODOHAITAO: 2023/4/7 验证方式 运行 Main、Client1、Client2 然后访问:
* - 堵塞式 GET http://localhost:8080/s1
* - 响应式 GET http://localhost:8080/2/s1
*/
// 采用那种方式对 RestTemplate 进行增强,看
// org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration
System.setProperty("spring.cloud.loadbalancer.retry.enabled", "false");
System.setProperty("spring.profiles.active", "loadbalance");
ConfigurableApplicationContext context = SpringApplication.run(Main.class);
}
}
```
负载均衡会使用 LoadBalancerClient 来执行请求的,大致逻辑是通过 DiscoveryClient 得到 serviceId 有哪些实例,再通过负载均衡策略的逻辑筛选出唯一的实例,然后根据这个实例的 url 执行请求。
@ -720,7 +835,16 @@ spring.cloud.loadbalancer.retry.backoff.jitter=1
### ReactorLoadBalancer
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/ServiceRegisterAndLoadBalance/loadbalancer/LoadBalancerClientConfig.java)
示例代码
```java
@LoadBalancerClient(name = "s1", configuration = { MyLoadBalancer.class, MyServiceInstanceListSupplier.class })
@LoadBalancerClients({ @LoadBalancerClient(name = "s2", configuration = MyRandomLoadBalancer.class),
@LoadBalancerClient(name = "s3", configuration = MyRoundRobinLoadBalancer.class), })
public class LoadBalancerClientConfig {
}
```
```java
/**
@ -783,7 +907,28 @@ public class LoadBalancerClientConfiguration {
### ServiceInstanceListSupplier
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/ServiceRegisterAndLoadBalance/loadbalancer/MyServiceInstanceListSupplier.java)
示例代码
```java
public class MyServiceInstanceListSupplier {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
//.withDiscoveryClient() // 通过 ReactiveDiscoveryClient 获取 List<ServiceInstance>
.withBlockingDiscoveryClient() // 通过 DiscoveryClient 获取 List<ServiceInstance>
// 下面配置的是通过什么方式 过滤 List<ServiceInstance>
// .withZonePreference() // spring.cloud.loadbalancer.zone" 属性值与 serviceInstance.getMetadata().get("zone") 进行匹配
// .withBlockingHealthChecks() // spring.cloud.loadbalancer.healthCheck.* 属性定义的的规则来过滤
// .withRequestBasedStickySession() spring.cloud.loadbalancer.stickySession.instanceIdCookieName 属性值过滤 serviceInstance.getInstanceId()
// .withSameInstancePreference()
.withCaching() // 会使用到 LoadBalancerCacheManager 缓存 List<ServiceInstance>
.build(context);
}
}
```
```java
/**
@ -803,15 +948,15 @@ public class LoadBalancerClientConfiguration {
```java
public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceInstance>>> {
String getServiceId();
String getServiceId();
default Flux<List<ServiceInstance>> get(Request request) {
return get();
}
default Flux<List<ServiceInstance>> get(Request request) {
return get();
}
static ServiceInstanceListSupplierBuilder builder() {
return new ServiceInstanceListSupplierBuilder();
}
static ServiceInstanceListSupplierBuilder builder() {
return new ServiceInstanceListSupplierBuilder();
}
}
```
@ -820,8 +965,6 @@ public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceI
WebClient.Builder 是执行响应式请求的工具类。下面是让 WebClient.Builder 具有负载均衡能力的实现逻辑。
[示例代码](https://github.com/haitaoss/spring-cloud-commons/tree/source-v3.1.5/source-note-spring-cloud-commons/src/main/java/cn/haitaoss/ServiceRegisterAndLoadBalance/loadbalancer/LoadBalancerOtherConfig.java)
`spring-cloud-commons.jar!/META-INF/spring.factories`的部分内容
```properties

@ -0,0 +1,557 @@
# 说明
Author: [haitaoss](https://github.com/haitaoss)
源码阅读仓库: [spring-cloud-openfeign](https://github.com/haitaoss/spring-cloud-openfeign)
参考资料和需要掌握的知识:
- [SpringBoot 源码分析](https://github.com/haitaoss/spring-boot/blob/source-v2.7.8/note/springboot-source-note.md)
- [Spring 源码分析](https://github.com/haitaoss/spring-framework)
- [Spring Cloud 官网文档](https://docs.spring.io/spring-cloud/docs/2021.0.5/reference/html/)
- [Spring Cloud Commons 官网文档](https://docs.spring.io/spring-cloud-commons/docs/3.1.5/reference/html/)
- [Spring Cloud OpenFeign 官网文档](https://docs.spring.io/spring-cloud-openfeign/docs/3.1.5/reference/html/)
- [Feign 官方文档](https://github.com/OpenFeign/feign#readme)
# Spring Cloud OpenFeign 介绍
[Feign](https://github.com/haitaoss/feign) 是一个声明式的 Web 服务客户端,它使 Java 编写 Web 服务客户端变得更加容易。其实就是通过 JDK 代理生成接口的代理对象,方法的执行就是执行 Http 请求。而 OpenFeign 的作用是通过自动装配将 Feign 集成到应用程序中。主要是有这几个特性:
1. 整合 [Spring Cache](https://github.com/haitaoss/spring-framework/blob/source-v5.3.10/note/spring-source-note.md#cacheinterceptorinvoke) ,代理 FeignClient 接口方法,增加上缓存相关的逻辑。
2. 整合 CircuitBreaker ,代理 FeignClient 接口方法,方法的执行委托给 CircuitBreaker 控制
3. 整合 [spring-cloud-loadbalancer](https://github.com/haitaoss/spring-cloud-commons/blob/source-v3.0.1/note/spring-cloud-commons-source-note.md#loadbalancerclient) ,让 Feign 使用负载均衡的 HTTP 客户端 发送请求
# 核心功能源码分析
## OpenFeign 自动装配原理
`spring-cloud-openfeign-core.jar!META-INF/spring.factories` 的部分内容
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration
```
### FeignAutoConfiguration
主要是注册了 FeignContext、Targeter、CachingCapability、Client
- FeignContext 是用来隔离不同 FeignClient 的容器,每个 FeignClient 有单独的 IOC 容器,容器中默认注册了 FeignClient 需要的 bean。
- Targeter 是用来生成 FeignClient 接口实现类的,只是配置而已。生成接口代理类的逻辑是由 Feign 实现的。
- CachingCapability 是用来配置 Feign.Builder 的,主要是对 InvocationHandlerFactory 进行增强,而 InvocationHandlerFactory 是用来生成 InvocationHandler 从而让方法的执行委托给 [CacheInterceptor](https://github.com/haitaoss/spring-framework/blob/source-v5.3.10/note/spring-source-note.md#cacheinterceptorinvoke) 执行,这是属于 SpringCache 的内容了,不展开说了。
- Client 是执行 HTTP 请求的工具,比如 ApacheHttpClient、OkHttpClient。
```java
/**
* FeignAutoConfiguration
* 注册三个绑定属性的bean @EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class, FeignEncoderProperties.class})
* FeignClientProperties记录 FeignClient 的配置信息,比如 RequestInterceptor 等等
* FeignHttpClientProperties记录 HttpClient 的配置信息最大连接数、连接的ttl等等
* FeignEncoderProperties: 是否从响应头 Content-Type 获取响应体的编码会使用这个编码对响应体解码成字符串默认是UTF-8
*
* 注册 HasFeatures 是用来描述系统有 Feign 的功能,它是给 FeaturesEndpoint 使用的。
*
* 注册 FeignContext 其继承 NamedContextFactory。
* 其作用是会根据 name 创建单独的IOC容器获取bean是从单独的IOC容器中拿。IOC容器默认有两个配置类PropertyPlaceholderAutoConfiguration、FeignClientsConfiguration
* 并依赖 FeignClientSpecification 用来扩展配置类创建的IOC容器会缓存到Map中。
*
* FeignClientsConfiguration 其目的是注册 Decoder、Encoder、Encoder、Contract、FormattingConversionService、
* Retryer、FeignLoggerFactory、FeignClientConfigurer、Feign.Builder 这些bean 而且都有 @ConditionalOnMissingBean 条件,若我们想自定义
* 这些bean可以设置 FeignClientSpecification 扩展配置类,从而让 @ConditionalOnMissingBean 不满足也就不会使用这些默认的bean
*
* 而 FeignClientSpecification 可以通过这两个注解快速配置
* @EnableFeignClients(defaultConfiguration={A.class}) // 这样子是注册全局的FeignContext 创建的所有IOC容器都会使用这个配置类
* @FeignClient(contextId="f1",name="serviceName",configuration={A.class}) // FeignContext 为 f1 创建的IOC容器 会使用这个配置类
* 注contextId 为空 就会使用 name 作为缺省值
*
* 注册 CachingCapability 其实现 Capability 接口,依赖 CacheInterceptor。CachingCapability 是用来增强 Feign.Builder 设置的 InvocationHandlerFactory 的
* 让方法的执行委托给 CacheInterceptor 执行,也就是支持 Spring Cache 的功能
*
* 注册 PageJacksonModule、SortJacksonModule 都是 Module 类型的,这两个东西是用来扩展 jackson 扩展序列化规则的,是为了支持 spring data
*
* 注册 Targeter 类型的bean默认是 DefaultTargeter , 如果容器中有 CircuitBreakerFactory 类型的bean那就会注册 FeignCircuitBreakerTargeter
* Targeter 是用来聚合 FeignClientFactoryBean、Feign.Builder、FeignContext、Target.HardCodedTarget
* 定义了如何生成 Target.HardCodedTarget<T> 泛型的实例
*
* 注册 CircuitBreakerNameResolver 是用来生成断路器name的FeignCircuitBreakerTargeter 会依赖这个bean
*
* 注册 HttpClientConnectionManager 会依赖 FeignHttpClientProperties 来设置连接相关参数
*
* 注册 CloseableHttpClient 是 HttpClient 的实现类,其依赖 HttpClientConnectionManager、FeignHttpClientProperties 设置一些参数
*
* 注册 ApacheHttpClient 是 feign.Client 的实现类,依赖 HttpClient 来执行HTTP请求
*
* 属性 feign.okhttp.enabled == true 会注册
* 注册 ConnectionPool 会依赖 FeignHttpClientProperties 来设置连接相关参数
* 注册 okhttp3.OkHttpClient 其依赖 ConnectionPool、FeignHttpClientProperties 设置一些参数
* 注册 OkHttpClient 是 feign.Client 的实现类,依赖 okhttp3.OkHttpClient 来执行HTTP请求
* */
```
### FeignAcceptGzipEncodingAutoConfiguration
```java
/**
* FeignAcceptGzipEncodingAutoConfiguration
* 注册 FeignAcceptGzipEncodingInterceptor , 它是 RequestInterceptor 的实现类,其目的是给 Request 增加请求头 Accept-Encoding=gzip,deflate
* 注:请求头 Accept-Encoding=gzip,deflate 是告诉服务器 客户端支持 gzip,deflate 压缩
**/
```
```yml
feign:
compression:
# 设置请求头 Accept-Encoding=gzip,deflate 用于告诉服务器 客户端支持 gzip,deflate 压缩
response:
enabled: true
```
### FeignContentGzipEncodingAutoConfiguration
```java
/**
* FeignContentGzipEncodingAutoConfiguration
* 注册 FeignContentGzipEncodingInterceptor , 它是 RequestInterceptor 的实现类,其目的是给 Request 增加请求头 Content-Encoding=gzip,deflate
* 满足这两点才需要增加请求头:
* 1. Content-Type 是属性 feign.compression.request.mimeTypes 包含的值
* 2. Content-Length 大于 属性 feign.compression.request.minRequestSize 的值
*
* 请求头有 Content-Encoding=gzip,deflate 会对请求体进行编码后(压缩)再发送给到服务器(这得看你用的Client是否支持)
* */
```
```yml
feign:
compression:
request:
enabled: true
# FeignClient 执行HTTP请求时Content-Type 、Content-Length 满足这两个条件,就设置请求头 Content-Encoding=gzip,deflate。
# 设置了请求头后 在发送前会对请求体进行压缩
mimeTypes:
- "text/xml"
- "application/xml"
- "application/json"
minRequestSize: 100
```
### FeignLoadBalancerAutoConfiguration
```java
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {}
```
```java
/**
* FeignLoadBalancerAutoConfiguration
*
* 目的都是注册 Client 的实现类,根据条件会注册 FeignBlockingLoadBalancerClient 或者是 RetryableFeignBlockingLoadBalancerClient
* 导入的三个配置类的区别在于:
* - HttpClientFeignLoadBalancerConfiguration 依赖 HttpClient 执行HTTP请求
* - OkHttpFeignLoadBalancerConfiguration 依赖 okhttp3.OkHttpClient 执行HTTP请求
* - DefaultFeignLoadBalancerConfiguration 依赖 Client.Default 执行HTTP请求
* */
```
## @EnableFeignClients@FeignClient
@EnableFeignClients 是用来扫描得到标注了 @FeignClient 的类,将类的信息映射成 BeanDefinition然后注册到 BeanFactory 中。而需要注意的是这个 bean 的实例化是 `FeignClientFactoryBean.getObject()` 得到的。@FeignClient 的注解值主要是映射给`FeignClientFactoryBean`。所以要想知道 `@FeignClient` 是如何实现生成接口代理对象的还得看`FeignClientFactoryBean.getObjec()`
```java
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {}; // 要扫描的包
String[] basePackages() default {}; // 要扫描的包
Class<?>[] basePackageClasses() default {}; // 类所在的包
Class<?>[] defaultConfiguration() default {}; // FeignClient Context 会用到的默认配置类
Class<?>[] clients() default {}; // 指定 FeignClient若指定就不会根据包路径扫描
}
```
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
String contextId() default ""; // FeignClient Context 的ID没指定会使用 name 或者 value 的值。支持占位符解析
@AliasFor("name")
String value() default ""; // 服务名。支持占位符解析
@AliasFor("value")
String name() default ""; // 服务名。支持占位符解析
String qualifier() default ""; // 别名
String url() default ""; // 若指定这个值,那就不会使用 name也就不会变成负载均衡请求了。支持占位符解析
boolean decode404() default false;
Class<?>[] configuration() default {}; // 给 FeignClient Context 设置默认配置类
Class<?> fallback() default void.class; // 使用 {@link FeignCircuitBreaker.Builder} 构造的 FeignClient 才会用到这个属性,是用来给 CircuitBreaker 使用的用于在执行HTTP请求时出错后 的兜底策略。需要注册到容器中才行
Class<?> fallbackFactory() default void.class; // 和 {@link FeignClient#fallback()} 的用法类似,只不过这个是用来创建 fallback的。如果指定了 fallback ,那么这个属性就没用了。需要注册到容器中才行
String path() default ""; // 访问路径是 url + path 。支持占位符解析
boolean primary() default true; // bean是否是 @Primary
}
```
```java
/**
*
* 举例:
* @EnableFeignClients(defaultConfiguration={A.class},clients={A.class})
* @FeignClient(name="s1",configuration={A.class})
* public class Config{}
*
* @EnableFeignClients 上有 @Import(FeignClientsRegistrar.class) 所以解析配置类解析到这个注解时会将 FeignClientsRegistrar
* 注册到BeanFactory中而 FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,所以其接口方法会被回调。
* {@link FeignClientsRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)}
*
* 1. 注册 FeignClient 默认配置
* 获取 defaultConfiguration 注解值映射成 BeanDefinition 注册到 BeanFactory 中
* BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
* builder.addConstructorArgValue("default."+Config.class.getName());
* builder.addConstructorArgValue(defaultConfiguration);
* registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),
* builder.getBeanDefinition());
*
* Tips: FeignContext 继承 NamedContextFactory, 会依赖 FeignClientSpecification 类型的bean 用来配置要生成的IOC容器。
* FeignContext 会使用 beanName是 "default." 前缀的 FeignClientSpecification 作为默认项用来配置要生成的IOC容器
*
* 2. 注册 FeignClient
* 2.1 记录候选的组件 candidateComponents
* 设置 clients 值那就只使用这些值作为 candidateComponents 没有设置 clients 值,那就扫描包下的类,只会收集有 @FeignClient 的类。
* value + basePackages + basePackageClasses 的值作为要扫描的包路径,若这三个注解值都没设置,
* 那就用 @EnableFeignClients 注解所在的配置类的包作为要扫描的包路径
*
* 2.2 遍历 candidateComponents 挨个映射成 BeanDefinition 注册到 BeanFactory 中
* - 校验 @FeignClient 标注的类 不是接口就报错
* - 注册 FeignClient 配置
* String name = getClientName(attributes); // 为空就依次获取属性 contextId -> value -> name -> serviceId 都没设置就报错
* registerClientConfiguration(registry, name, attributes.get("configuration")); // 同上映射成 FeignClientSpecification但是没有 "default." 前缀
*
* - 将注解的值映射到 FeignClientFactoryBean ,然后装饰成 BeanDefinition 注册到BeanFactory中
* String contextId = getContextId(beanFactory, attributes); // 获取属性值,值为空就依次获取: contextId -> serviceId -> name -> value
* String name = getName(attributes); // 同上只不过获取的是serviceId -> name -> value
*
* // 定义 FeignClientFactoryBean
* FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
* factoryBean.setBeanFactory(beanFactory);
* factoryBean.setName(name);
* factoryBean.setContextId(contextId);
* factoryBean.setType(clazz);
* // 根据 属性 feign.client.refresh-enabled 设置
* factoryBean.setRefreshableClient(isClientRefreshEnabled());
*
* // 提供 Supplier BeanFactory实例化会调用 Supplier 得到bean对象
* BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
* // 根据 @FeignClient(url="")的值来设置会解析占位符还会补全http://
* factoryBean.setUrl(getUrl(beanFactory, attributes));
*
* // 获取 @FeignClient(path="")的值来设置,会解析占位符, 会补上前缀/,移除后缀/
* factoryBean.setPath(getPath(beanFactory, attributes));
*
* // 剩下的就是简单读取值然后设置给factoryBean
* factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
* factoryBean.setFallback(attributes.get("fallback"));
* factoryBean.setFallbackFactory(attributes.get("fallbackFactory"));
* return factoryBean.getObject();
* });
*
* // 将BeanDefinition注册到BeanFactory中
* registry.registerBeanDefinition(beanName, definition.getBeanDefinition());
*
* 如果 feign.client.refresh-enabled 是true那就多注册 OptionsFactoryBean 到容器中,而且是 refresh 作用域的
* 当 FeignClientFactoryBean.getObject() 时会拿到 OptionsFactoryBean 用来配置 Feign.Builder
*
* Tips因为每次实例化bean都会重新设置 url、path 的值且支持使用占位符,所以我们可以
* 将 bean 设置成 refresh 作用域的,然后就能实现 url、path 的动态更新
* */
```
## FeignClientFactoryBean
```java
/**
* @FeignClient 注解修饰的接口会注册到BeanFactory中这种bean的实例化是执行 FeignClientFactoryBean.getObject() 得到。
*
* FeignClientFactoryBean 继承 FactoryBean 实现 InitializingBean
* {@link FeignClientFactoryBean#afterPropertiesSet()} 会校验属性 contextId、name 都不能为空
* {@link FeignClientFactoryBean#getType()} 返回的其实就是 @FeignClient 标注的接口类型
* {@link FeignClientFactoryBean#getObject()} 这个才是关键,看这里才能知道是得到接口代理对象的
* */
```
## FeignClientFactoryBean#getObject
其目的是配置 `Feign.Builder`,配置的参数有啥作用去看 [Feign](https://github.com/haitaoss/feign) 就明白了,最后将 `Feign.Builder` 交给 [Targeter](#Targeter) ,由 Targeter 使用 `Feign.Bduiler` 生成接口代理对象。我们可以自定义 Targeter 注册到容器中,让默认注册的失效。
可以使用 FeignClientSpecification、FeignBuilderCustomizer 来配置 `Feign.Builder` 需要的参数
```java
/**
* {@link FeignClientFactoryBean#getObject()}
*
* 1. 从容器中获取 FeignContext
* FeignContext context = beanFactory.getBean(FeignContext.class)
*
* 2. 获取 Feign.Builder 并对其进行配置
* Feign.Builder builder = feign(context);
* 2.1 根据 contextId 从 FeignContext 中获取 Feign.Builder
* 2.2 根据 contextId 从 FeignContext 中获取 FeignLoggerFactory、Encoder、Decoder、Contract... 设置给 Feign.Builder
* 2.3 可以设置 feign.client.config.contextId.xx 属性 和使用 FeignBuilderCustomizer 用来对 Feign.Builder 进行配置
* 会设置很多东西Logger.Level、Retryer、ErrorDecoder、FeignErrorDecoderFactory、Options、RequestInterceptor、QueryMapEncoder、Contract、Encoder、Decoder、ExceptionPropagationPolicy、Capability
*
* 注FeignContext 是 NamedContextFactory 不同的 name 会有单独的IOC容器IOC容器默认会加载的配置类是 FeignClientsConfiguration
*
* 3. url 没有值,那就使用 name + path 拼接成 url然后使用 Target 得到接口的代理对象
* Client client = getOptional(context, Client.class);
* builder.client(client);
* Targeter targeter = get(context, Targeter.class);
*
* return targeter.target(this, builder, context, target);
*
* 4. url 有值,那就使用 url + path 拼接成 url然后使用 Target 得到接口的代理对象这里会对Client进行解构使用非负载均衡的Client
* Client client = getOptional(context, Client.class);
* if (client instanceof FeignBlockingLoadBalancerClient) {
* // 因为提供了Url所以不需要负载均衡的Client所以这里解构拿到 非负载均衡的Client
* client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
* }
* builder.client(client);
* Targeter targeter = get(context, Targeter.class);
* return targeter.target(this, builder, context, target);
*
* */
```
## FeignClientBuilder
这是 OpenFeign 提供的工具类,用于快速生成 FeignClient 接口的代理对象。其本质是通过配置 FeignClientFactoryBean 然后执行 [getObject](#FeignClientFactoryBean#getObject) 得到代理对象。
## Targeter
```java
public interface Targeter {
<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target);
}
```
就是接口规范而已,其目的是规定使用 Feign 的步骤。最终的目的是使用 `Feign.Builder``Target.HardCodedTarget `描述的接口生成代理对象。
我们可以注册 Targeter 到容器中,自定义逻辑,比如 [FeignCircuitBreakerTargeter](#FeignCircuitBreakerTargeter)
## FeignCircuitBreakerTargeter
断路器可以看:
- [spring-cloud-circuitbreaker](https://github.com/haitaoss/spring-cloud-circuitbreaker)
- [Sentinel](https://github.com/haitaoss/Sentinel)
`spring-cloud-openfeign-core.jar!META-INF/spring.factories` 的部分内容
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
```
属性`feign.circuitbreaker.enabled`是 true 且 容器中有 CircuitBreakerFactory 类型的 bean就会注册 FeignCircuitBreakerTargeter 到容器中
```java
public class FeignAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("feign.circuitbreaker.enabled")
protected static class CircuitBreakerPresentFeignTargeterConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(CircuitBreakerFactory.class)
public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory) {
return new FeignCircuitBreakerTargeter(circuitBreakerFactory);
}
}
}
```
FeignCircuitBreakerTargeter 的核心逻辑是为 Feign.Builder 配置 **invocationHandlerFactory** 属性,从而能够将方法的执行委托给 **CircuitBreaker** 执行。
```java
// 伪代码如下
Feign.builder().invocationHandlerFactory(
(target, dispatch) -> new FeignCircuitBreakerInvocationHandler(circuitBreakerFactory,
feignClientName, target, dispatch, nullableFallbackFactory
));
```
```java
class FeignCircuitBreakerTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
// 不是 FeignCircuitBreaker 类型的就不做处理
if (!(feign instanceof FeignCircuitBreaker.Builder)) {
return feign.target(target);
}
FeignCircuitBreaker.Builder builder = (FeignCircuitBreaker.Builder) feign;
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName() : factory.getContextId();
/**
* 前置知识Feign 其实是通过JDK动态代理 为接口创建出代理对象,所以要想拦截方法的执行只需要配置 InvocationHandler 即可。
*
* 下面的几行代码的最终目都是设置 FeignCircuitBreakerInvocationHandler 作为代理对象的 InvocationHandler
* 所以关键还得看 FeignCircuitBreakerInvocationHandler
*
* {@link FeignCircuitBreakerInvocationHandler#invoke(Object, Method, Object[])}
* 大致流程是方法的执行交给 CircuitBreaker 执行 {@link CircuitBreaker#run(Supplier, Function)}
* CircuitBreaker 可以拿到 fallback 或者 fallbackFactory。可以决定什么时候回调 fallback 的逻辑
* */
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
// 存在 fallback 的情况
return targetWithFallback(name, context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
// 存在 fallbackFactory 的情况
return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
}
return builder(name, builder).target(target);
}
}
```
## FeignCircuitBreakerInvocationHandler
[FeignCircuitBreakerTargeter](#FeignCircuitBreakerTargeter) 会配置 FeignCircuitBreakerInvocationHandler 作为 FeignClient 接口代理对象的 InvocationHandler从而将 方法的执行 和 fallback 的执行 交给 CircuitBreaker 来决定,比如方法执行出错了就执行 fallback。
```java
class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
String circuitName = this.feignClientName + "_" + method.getName();
// 通过 CircuitBreakerFactory 得到 CircuitBreaker 实例
CircuitBreaker circuitBreaker = this.factory.create(circuitName);
// 定义方法的执行
Supplier<Object> supplier = asSupplier(method, args);
/**
* 存在 nullableFallbackFactory 就使用
*
* 比如这两种情况 nullableFallbackFactory 才会有值
* @FeignClient(fallback=A.class)
* @FeignClient(fallbackFactory=A.class)
* @FeignClient(fallback=A.class, fallbackFactory=A.class) // 两个都有的情况 只会使用 fallback
* */
if (this.nullableFallbackFactory != null) {
// 使用 nullableFallbackFactory 构造出 fallbackFunction
Function<Throwable, Object> fallbackFunction = throwable -> {
// 通过 nullableFallbackFactory 得到 fallback
Object fallback = this.nullableFallbackFactory.create(throwable);
try {
// 使用 fallback 执行当前出错的方法
return this.fallbackMethodMap.get(method).invoke(fallback, args);
}
catch (Exception e) {
throw new IllegalStateException(e);
}
};
// 使用 circuitBreaker 执行方法
return circuitBreaker.run(supplier, fallbackFunction);
}
// 使用 circuitBreaker 执行方法
return circuitBreaker.run(supplier);
}
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
return () -> {
try {
return this.dispatch.get(method).invoke(args);
}
catch (RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
}
```
## FeignClientsConfiguration
FeignContext 中为 name 创建的 IOC 容器都会使用 FeignClientsConfiguration 作为默认的配置类,这个配置类中定义了配置 Feign.Builder 的参数,其实就是对 Feign 的功能做实现,让 Feign 支持 SpringMVC 的注解等等。
**列举最关键的几个参数对象,并不是全部的代码**
```java
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
/**
* 依赖IOC容器配置的 List<HttpMessageConverter> ,其作用是将 执行 FeignClient 接口的响应体 转成 方法的参数类型
* @return
*/
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}
/**
* 依赖IOC容器配置的 List<HttpMessageConverter> ,其作用是将 FeignClient 接口的参数 设置到请求体中
* @param formWriterProvider
* @return
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
return springEncoder(formWriterProvider);
}
/**
* 扩展 FeignClient 接口 支持的注解。其作用是在执行接口方法时 将特殊注解标注的内容 映射到Request对象中
* 比如设置 请求头、请求路径、查询参数、请求体 等等
* @param feignConversionService
* @return
*/
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
}
/**
* 生成 FeignClient 的 Builder 对象。FeignCircuitBreaker 是 OpenFeign 定义的,
* 用来使用 FeignCircuitBreaker 来执行 FeignClient 接口的方法
* @return
*/
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnBean(CircuitBreakerFactory.class)
public Feign.Builder circuitBreakerFeignBuilder() {
return FeignCircuitBreaker.builder();
}
}
```
Loading…
Cancel
Save