當今是微服務橫行的時代,各個微服務之間相互調用是一件再平常不過的時候。在采用HTTP協議進行通信的微服務中,我們自己可能去封裝一個HttpClient工具類去進行服務間的調用,封裝一個HttpClient工具,我們就需要考慮一下這些事情:
- 我們在發送一個HTTP請求時,我們需要選擇請求方式GET、POST、DELETE等,我們需要構建請求參數、構建請求頭信息等,那么作為一個工具類我們是不是也要提供各種參數的靈活配置;
- 因為采用restful API 風格的HTTP請求參數和返回數據都是字符串的格式,那我們是否需要考慮序列化和反序列化問題;
- 當同一個服務部署到多台服務器的時候,我們是不是應該采用輪詢或者隨機的方式去選擇服務器,這也就是我們常說的負載均衡。從另一方面來說我們的核心是解決服務間的調用,但是我們在設計一個通用HttpClient工具的時候是否也應該支持負載均衡,以及如何和負載均衡高度解耦。
為此,大名鼎鼎的Feign應時而生,我們在學習Feign的實現的時候,我們應該帶着這些問題去學習Feign的實現原理。
一、什么是Feign
Feign 是聲明式 Web 服務客戶端,它使編寫 Web 服務客戶端更加容易 Feign 不做任何請求處理,通過處理注解相關信息生成 Request,並對調用返回的數據進行解碼,從而實現簡化HTTP API 的開發。當然你也可以直接使用 Apache HttpClient 來實現Web服務器客戶端,但是 Feign 的目的是盡量的減少資源和代碼來實現和 HTTP API 的連接。通過自定義的編碼解碼器以及錯誤處理,你可以編寫任何基於文本的 HTTP API。
如果要使用 Feign,需要創建一個接口並對其添加 Feign 相關注解,另外 Feign 還支持可插拔編碼器和解碼器,致力於打造一個輕量級 HTTP 客戶端。
如果你想直接使用原生的Feign的話,你可以去參考Feign配置使用,下面就是Feign針對一個HTTP API的接口定義:
interface GitHub { // RequestLine注解聲明請求方法和請求地址,可以允許有查詢參數
@RequestLine("GET /user/list") List<User> list(); }
目前由於Spring Cloud微服務的廣泛使用,廣大開發者更傾向於使用spring-cloud-starter-openfeign,Spring Cloud 添加了對 Spring MVC 注解的支持,在微服務中我們的接口定義有所變化:
@FeignClient(name="服務名",contextId="唯一標識") interface GitHub { @GetMapping("/user/list") List<User> list(); }
二、Feign 和 Openfeign 的區別
Feign 最早是由 Netflix 公司進行維護的,后來 Netflix 不再對其進行維護,最終 Feign 由社區進行維護,更名為 Openfeign。
2.1 Starter OpenFeign
當然了,基於 SpringCloud 團隊對 Netflix 的情有獨鍾,你出了這么好用的輕量級 HTTP 客戶端,我這老大哥不得支持一下,所以就有了基於 Feign 封裝的 Starter。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
這個包引入了如下依賴:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-openfeign-core</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.1.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> <version>10.4.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-slf4j</artifactId> <version>10.4.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-hystrix</artifactId> <version>10.4.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-java8</artifactId> <version>10.4.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-archaius</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> <optional>true</optional> </dependency>
這里面有兩個非常重要的包:
- 一個是spring-cloud-openfeign-core,這個包是SpringCloud支持Feign的核心包,Spring Cloud 添加了對 Spring MVC 注解的支持(通過SpringMvcContract實現),並支持使用 Spring Web 中默認使用的相同 HttpMessageConverters。另外,Spring Cloud同時集成了Ribbon和Eureka以及Spring Cloud LoadBalancer,以在使用 Feign 時提供負載均衡的 HTTP客戶端。針對於注冊中心的支持,包含但不限於 Eureka,比如 Consul、Naocs 等注冊中心均支持。
- 另一個包是feign-core,也就是feign的原生包,具體使用細節可以參考Feign配置使用。通俗點說,spring-cloud-openfeign-core就是通過一系列的配置創建Feign.builder()實例的過程。
在我們 SpringCloud 項目開發過程中,使用的大多都是這個 Starter Feign。
2.2 demo
為了方便大家理解,這里寫出對應的生產方、消費方 Demo 代碼,以及使用的注冊中心。
生產者服務:添加 Nacos 服務注冊發現注解以及發布出 HTTP 接口服務
@EnableDiscoveryClient @SpringBootApplication public class NacosProduceApplication { public static void main(String[] args) { SpringApplication.run(NacosProduceApplication.class, args); } @RestController static class TestController { @GetMapping("/hello") public String hello(@RequestParam("name") String name) { return "hello " + name; } } }
消費者服務:
定義 FeignClient 消費服務接口
@FeignClient(name= "nacos-produce",contextId="DemoFeignClient") public interface DemoFeignClient { @GetMapping("/hello") String sayHello(@RequestParam("name") String name); }
因為生產者使用 Nacos,所以消費者除了開啟 Feign 注解,同時也要開啟 Naocs 服務注冊發現。
@RestController @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class NacosConsumeApplication { public static void main(String[] args) { SpringApplication.run(NacosConsumeApplication.class, args); } @Autowired private DemoFeignClient demoFeignClient; @GetMapping("/test") public String test() { String result = demoFeignClient.sayHello("xxxxx"); return result; } }
三、Feign 的啟動原理
下文中調試中使用的代碼並不是demo中的代碼,不過和demo使用的類似,只是業務系統更加復雜而已。
我們在 SpringCloud 的使用過程中,如果想要啟動某個組件,一般都是 @Enable... 這種方式注入,Feign 也不例外,我們需要在類上標記此注解 @EnableFeignClients。
3.1 @EnableFeignClients
EnableFeignClients注解,用於掃描使用@FeignClient注解標注的接口, 而該功能是通過@Import(FeignClientsRegistrar.class)完成。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { String[] value() default {}; String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] defaultConfiguration() default {}; Class<?>[] clients() default {}; }
FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar, 用以完成相關Bean注冊。
ImportBeanDefinitionRegistrar 負責動態注入 IOC Bean,分別注入 Feign 配置類、FeignClient。
class FeignClientsRegistrarimplements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware{ @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); } ... }
3.2 registerDefaultConfiguration(metadata, registry)

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")); } } private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(FeignClientSpecification.class); builder.addConstructorArgValue(name); builder.addConstructorArgValue(configuration); registry.registerBeanDefinition( name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); }
1. 獲取 @EnableFeignClients 注解上的屬性以及對應 value。
2.使用BeanDefinitionBuilder構造器為FeignClientSpecification類生成BeanDefinition,這個BeanDefinition是對FeignClientSpecification Bean的定義,保存了FeignClientSpecification Bean 的各種信息,如屬性、構造方法參數等。其中@EnableFeignClients 注解上的defaultConfiguration屬性就是作為構造方法參數傳入的。而bean名稱為 default. + @EnableFeignClients 修飾類(一般是啟動類)全限定名稱 + FeignClientSpecification
3.@EnableFeignClients defaultConfiguration 默認為 {},如果沒有相關配置,默認使用 FeignClientsConfiguration 並結合 name 填充到 FeignClientSpecification,最終會被注冊為 IOC Bean
總結下來,就是根據@EnableFeignClients中屬性defaultConfiguration,為FeignClientSpecification類型生成BeanDefinition,並注入Spriing容器中。
3.3 registerFeignClients(metadata, registry)

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); } } } }
1. 掃描使用FeignClient注解標注的接口,獲取basePackages掃描路徑 。
2.根據獲取到的掃描路徑,然后根據掃描路徑,獲取該路徑將其子路徑下,使用FeignClient注解標記的接口。
3.遍歷每一個FeignClient注解的類:
- 收集接口FeignClient注解屬性信息,並根據 configuration 屬性去創建接口級的 FeignClientSpecification BeanDefinition,然后注入Spring容器。
- 生成FeignClientFactoryBean 類型的BeanDefinition,並將 @FeignClient 的屬性設置到 FeignClientFactoryBean 對象上,然后注入Spring容器。
其中需要注意,在將@FeignClient 的屬性設置到 FeignClientFactoryBean 對象,會將@FeignClient的修飾的類的className作為type屬性,傳遞給FeignClientFactoryBean,后續正是通過這個,創建對應的代理類。
總結下來,就是為一個@FeignClient創建一個FeignClientSpecification、FeignClientFactoryBean,FeignClientSpecification保存這個@FeignClient的configuration 屬性信息,而FeignClientFactoryBean中收集了這個FeignClient其他的屬性。
由於FeignClientFactoryBean 繼承自 FactoryBean,也就是說,當我們定義 @FeignClient 修飾接口時,注冊到 IOC 容器中 Bean 類型變成了 FeignClientFactoryBean,在 Spring 中,FactoryBean 是一個工廠 Bean,用來創建代理 Bean。工廠 Bean 是一種特殊的 Bean,對於需要獲取 Bean 的消費者而言,它是不知道 Bean 是普通 Bean 或是工廠 Bean 的。工廠 Bean 返回的實例不是工廠 Bean 本身,該實例是由工廠 Bean 中 FactoryBean#getObject 邏輯所創建的。更多FactoryBean相關的信息,可以閱讀我之前的博客。
四、 FeignClient創建過程分析
上面說到 @FeignClient 修飾的接口最終填充到 IOC 容器的類型是 FeignClientFactoryBean,先來看下它是什么。
4.1 FactoryBean 接口特征
1 .它會在類初始化時執行一段邏輯,依據InitializingBean 接口。
2.如果它被別的類 @Autowired 進行注入,返回的不是它本身,而是 FactoryBean#getObject 返回的類,依據 Spring FactoryBean 接口。
3.它能夠獲取 Spring 上下文對象,依據 Spring ApplicationContextAware 接口。
先來看它的初始化邏輯都執行了什么:
@Override public void afterPropertiesSet() { Assert.hasText(contextId, "Context id must be set"); Assert.hasText(name, "Name must be set"); }
沒有特別的操作,只是使用斷言工具類判斷兩個字段不為空。ApplicationContextAware 也沒什么說的,獲取上下文對象賦值到對象的局部變量里,重點以及關鍵就是 FactoryBean#getObject 方法。
@Override public Object getObject() throws Exception { return getTarget(); }
4.2 FeignClientFactoryBean#getTarget
getTarget 源碼方法還是挺長的,這里采用分段的形式展示:
<T> T getTarget() { // 從 IOC 容器獲取 FeignContext
FeignContext context = applicationContext.getBean(FeignContext.class); // 通過 context 創建 Feign 構造器
Feign.Builder builder = feign(context); ... }
這里提出一個疑問?FeignContext 什么時候、在哪里被注入到 Spring 容器里的?
用了 SpringBoot 怎么會不使用自動裝配的功能呢,FeignContext 就是在 FeignAutoConfiguration 中被成功創建。
在FeignAutoConfiguration中,向Spring容器注入FeignContext :
並設置其配置為configurations ,而configurations 是通過@Autowired注入,即List<FeignClientSpecification>集合。
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class }) @Import(DefaultGzipDecoderConfiguration.class) public class FeignAutoConfiguration { @Autowired(required = false) private List<FeignClientSpecification> configurations = new ArrayList<>(); @Bean public HasFeatures feignFeature() { return HasFeatures.namedFeature("Feign", Feign.class); } @Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); context.setConfigurations(this.configurations); return context; } ... }
4.3 FeignClientFactoryBean#feign
protected Feign.Builder feign(FeignContext context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); // @formatter:off
Feign.Builder builder = get(context, Feign.Builder.class) // required values
.logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // @formatter:on
configureFeign(context, builder); return builder; }
feign 方法里日志工廠、編碼、解碼等類均是通過FeignClientFactoryBean#get(...) 方法得到。
protected <T> T get(FeignContext context, Class<T> type) { T instance = context.getInstance(this.contextId, type); if (instance == null) { throw new IllegalStateException( "No bean found of type " + type + " for " + this.contextId); } return instance; }
//FeignContext方法 public <T> T getInstance(String name, Class<T> type) { //根據name獲取context實例
AnnotationConfigApplicationContext context = getContext(name); //根據type類型從子容器獲取Bean實例
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { return context.getBean(type); } return null; }
這里涉及到 Spring 父子容器的概念,默認子容器FeignContext#contexts為空,獲取不到服務名對應 context 則使用FeignContext#createContext新建。
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
protected AnnotationConfigApplicationContext getContext(String name) { if (!this.contexts.containsKey(name)) { synchronized (this.contexts) { if (!this.contexts.containsKey(name)) { this.contexts.put(name, createContext(name)); } } } return this.contexts.get(name); }
子容器context創建完之后,如果@FeignClient中配置有configuration。會向子容器中注入一個 configuration屬性指定的類型的 Bean。因此我們可以通過configuration對每個@FeignClient做定制化配置、比如Encoder、Decoder、FeignLoggerFactory等等。
//這里的name是@FeignContent中的contentId值 protected AnnotationConfigApplicationContext createContext(String name) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); //@FeignClient沒有配置configuration屬性 不會執行 this.configurations 保存的是FeignClientConfiguration類型的列表,也就是之前我們介紹到的注入Spring容器中的FeignClient配置 if (this.configurations.containsKey(name)) { for (Class<?> configuration : this.configurations.get(name) .getConfiguration()) { context.register(configuration); } } for (Map.Entry<String, C> entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { // @EnableFeignClient沒有配置defaultConfiguration屬性 不會執行 for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } } // 注入默認配置類FeignClientsConfiguration,會注入默認的feignEncoder、feignDecoder等 context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType); context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( this.propertySourceName, Collections.<String, Object>singletonMap(this.propertyName, name))); if (this.parent != null) { // Uses Environment from parent as well as beans //設置父容器、子容器不存在去父容器查找 context.setParent(this.parent); // jdk11 issue // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101 context.setClassLoader(this.parent.getClassLoader()); } context.setDisplayName(generateDisplayName(name)); context.refresh(); return context; }
關於父子類容器對應關系,以及提供 @FeignClient 服務對應子容器的關系(每一個服務對應一個子容器實例)
需要注意的是上圖中的Product1、Product2、Product3並不是說就有三個微服務。而是說有三個@FeignClien服務,三個服務可以對應一個微服務,比如下面這種:
Client 1 @FeignClient(name = "optimization-user",contextId="1") public interface UserRemoteClient { @GetMapping("/user/get") public User getUser(@RequestParam("id") int id); } Client 2 @FeignClient(name = "optimization-user",,contextId="2") public interface UserRemoteClient2 { @GetMapping("/user2/get") public User getUser(@RequestParam("id") int id); } Client 3 @FeignClient(name = "optimization-user",,contextId="3") public interface UserRemoteClient2 { @GetMapping("/user2/get") public User getUser(@RequestParam("id") int id); }
回到FeignContext#getInstance 方法,子容器此時已加載對應 Bean,直接通過 getBean 獲取 FeignLoggerFactory。
如法炮制,Feign.Builder、Encoder、Decoder、Contract 都可以通過子容器獲取對應 Bean。
protected Feign.Builder feign(FeignContext context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); // @formatter:off
Feign.Builder builder = get(context, Feign.Builder.class) // required values
.logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // @formatter:on
configureFeign(context, builder); return builder; }
configureFeign 方法主要進行一些配置賦值,比如超時、重試、404 配置等,就不再細說了。
到這里有必要總結一下創建 Spring 代理工廠的前半場代碼 :
1. 注入@FeignClient 服務時,其實注入的是 FactoryBean#getObject 返回代理工廠對象。
2.通過 IOC 容器獲取 FeignContext 上下文。
3.,創建 Feign.Builder 對象時會創建 Feign 服務對應的子容器。
4.從子容器中獲取日志工廠、編碼器、解碼器等 Bean 為 Feign.Builder 設置配置,比如超時時間、日志級別等屬性,每一個服務都可以個性化設置。
4.4 動態生成代理
接下來是最最最重要的地方了,繼續FeignClientFactoryBean#getTarget
<T> T getTarget() { FeignContext context = this.applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); //判斷@FeignClient url屬性是否存在
if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { this.url = "http://" + this.name; } else { this.url = this.name; } this.url += cleanPath(); //type就是@FeignClient注解修飾的接口類型 //name:@FeignClient name屬性,ribbon通過這個到注冊中心獲取服務信息
return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } //存在的話,就不使用負載均衡以及注冊中心了
... }
因為我們在 @FeignClient 注解是使用 name 而不是 url,所以會執行負載均衡策略的分支。FeignClientFactoryBean#loadBalance:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
//從 Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); }
Client: Feign 發送請求以及接收響應等都是由 Client 完成,該類默認 Client.Default,另外支持 HttpClient、OkHttp 等客戶端,如:
Feign.builder().client(new OkHttpClient())
代碼中的 Client、Targeter 在自動裝配時注冊,配合上文中的父子容器理論,這兩個 Bean 在父容器中存在,所以子容器也可以獲取到。FeignClientFactoryBean#getOptional,getOptional和get的區別在於一個是可選,一個是必須的,get中如果從子容器獲取不到指定的bean實例,會拋出異常,而getOptional不會:
protected <T> T getOptional(FeignContext context, Class<T> type) { return context.getInstance(this.contextId, type); }
因為我們並沒有對 Hystix 進行設置,所以Targeter#target走入Feign#target分支:
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { return feign.target(target); } feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign; String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName() : factory.getContextId(); SetterFactory setterFactory = getOptional(name, context, SetterFactory.class); if (setterFactory != null) { builder.setterFactory(setterFactory); } Class<?> fallback = factory.getFallback(); if (fallback != void.class) { return targetWithFallback(name, context, target, builder, fallback); } Class<?> fallbackFactory = factory.getFallbackFactory(); if (fallbackFactory != void.class) { return targetWithFallbackFactory(name, context, target, builder, fallbackFactory); } return feign.target(target); }
Feign#target中首先會創建反射類 ReflectiveFeign,其中ReflectiveFeign是Feign的實現類:
然后調用ReflectiveFeign#newInstance(target)執行創建實例類:
public <T> T target(Target<T> target) { return build().newInstance(target); }
public Feign build() { SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404, closeAfterDecode, propagationPolicy); ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); }
ReflectiveFeign#newInstance 方法對 @FeignClient 修飾的接口中 SpringMvc 等配置進行解析轉換,對接口類中的方法進行歸類,生成動態代理類
public <T> T newInstance(Target<T> target) { //將裝飾了@FeignClient的接口方法封裝為方法處理器,包括Spring MVC注解邏輯處理
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); //接口方法對應的MethodHandler
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); //添加JDK8以后出現的接口中默認方法
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); //1.如果是object 方法跳過 2.default方法添加defaultMethodHandlers 3、否則添加methodToHandler
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))); } } //根據targert、methodToHandler創建InvocationHandler
InvocationHandler handler = factory.create(target, methodToHandler); //根據JDK Proxy創建動態代理類
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; }
可以看出 Feign 創建動態代理類的方式和 Mybatis Mapper 處理方式是一致的,因為兩者都沒有實現類 。
根據 ReflectiveFeign#newInstance 方法按照行為大致划分,共做了四件事 處理:
1. 將@FeignClient 接口方法封裝為 MethodHandler 包裝類,每一個方法對應一個MethodHandler,MethodHandler的實現類是SynchronousMethodHandler。
public MethodHandler create(Target<?> target, MethodMetadata md, RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, buildTemplateFromArgs, options, decoder, errorDecoder, decode404, closeAfterDecode, propagationPolicy, forceDecoding); }
可以看到每個MethodHandler都包含了客戶端、日志、請求模板、編碼解碼器等參數,通過這些參數就可以構建相應接口的Http請求。
2. 遍歷接口中所有方法,過濾 Object 方法,並將默認方法以及 FeignClient 方法分類。
3. 創建動態代理對應的 InvocationHandler ,默認InvocationHandler 的實現類為ReflectiveFeign.FeignInvocationHandler,然后利用Proxy.newProxyInstance創建 Proxy 實例。
4. 接口內 default 方法綁定動態代理類。
其中FeignInvocationHandler實現如下:
static class FeignInvocationHandler implements InvocationHandler { private final Target target; private final Map<Method, MethodHandler> dispatch; FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) { this.target = checkNotNull(target, "target"); this.dispatch = checkNotNull(dispatch, "dispatch for %s", target); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { return hashCode(); } else if ("toString".equals(method.getName())) { return toString(); } return dispatch.get(method).invoke(args); }
dispath 是緩存的method 以及 method對應的MethodHandler
我們調用的遠程接口用的是SynchronousMethodHandler實現,該類將方法參數、方法返回值、參數集合、請求類型、請求路徑進行解析存儲。
到這里我們已經很清楚Feign 的工作方式了。前面那么多封裝鋪墊,封裝個性化配置等等,最終確定收尾的是創建動態代理類。
也就是說在我們調用 feign接口時,會被 ReflectiveFeign.FeignInvocationHandler#invoke 攔截,最后會被SynchronousMethodHandler#invoke方法處理。
4.5 調試運行
既然已經明白了調用流程,那就正兒八經的試一哈,就會發現我們通過FeignClient服務發送的請求,最終會被SynchronousMethodHandler#invoke方法處理,該方法首先會根據請求參數構建請求模板:
public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template, options); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } }
RequestTemplate:構建 Request 模版類。Options:存放連接、超時時間等配置類。Retryer:失敗重試策略類。
跟蹤代碼發現,首先根據請求模板RequestTemplate構建Request實例,然后調用的SynchronousMethodHandler持有的Client的實例的execute方法。
Client默認是通過FeignRibbonClientAutoConfiguration進行注入的:
@ConditionalOnClass({ ILoadBalancer.class, Feign.class }) @ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled", matchIfMissing = true) @Configuration(proxyBeanMethods = false) @AutoConfigureBefore(FeignAutoConfiguration.class) @EnableConfigurationProperties({ FeignHttpClientProperties.class }) // Order is important here, last should be the default, first should be optional // see // https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653 @Import({ HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class }) public class FeignRibbonClientAutoConfiguration { ... }
如果前面的兩個配置類的條件沒有滿足,feign.Client 的 IOC 容器實例沒有裝配,則:
1. 創建一個 Client.Default 默認客戶端實例,該實例的內部,使用HttpURLConnnection 完成URL請求處理;
2. 創建一個 LoadBalancerFeignClient 負載均衡客戶端實例,將 Client.Default 實例包裝起來,然后返回LoadBalancerFeignClient 客戶端實例,作為 feign.Client 類型的Spring IOC 容器實例。
@Configuration class DefaultFeignLoadBalancedConfiguration { @Bean @ConditionalOnMissingBean public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory, clientFactory); } }
LoadBalancerFeignClient 也是一個feign.Client 客戶端實現類。內部先使用 Ribbon 負載均衡算法計算server服務器,然后使用包裝的 delegate 客戶端實例,去完成 HTTP URL請求處理。
當然我們還可以上面兩種,具體配置可以參考Feign、httpclient、OkHttp3 結合使用:
- ApacheHttpClient 類:內部使用 Apache httpclient 開源組件完成HTTP URL請求處理的feign.Client 客戶端實現類;
- OkHttpClient類:內部使用 OkHttp3 開源組件完成HTTP URL請求處理的feign.Client 客戶端實現類。
我們調用feign接口的某一個方法,最終調用的LoadBalancerFeignClient.execute()方法。
4.6 LoadBalancerFeignClient#execute
public Response execute(Request request, Request.Options options) throws IOException { try { // URL 處理
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); } }
從上面的代碼可以看到,lbClient(clientName) 創建了一個負載均衡的客戶端,它實際上就是生成的如下所述的類:
public class FeignLoadBalancer extends AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse>
熟悉ribbon的朋友應該知道AbstractLoadBalancerAwareClient 就是Ribbon負載均衡調用的父類。具體的負載均衡實現策略,可以移步微服務通信之ribbon實現原理。至此我們可以得出結論:feign集成負載均衡是通過將FeignLoadBalancer作為調用feign接口的實際執行者,從而達到負載均衡的效果。可以看到這里與Ribbon高度的解耦,相當於我們獲取了服務名、調用地址、調用參數后,最終交由一個執行器去調用。執行器並不關心參數從何而來,這里基於Ribbon提供的執行器實現只是根據傳遞的服務名找到了一個正確的實例去調用而已。
五、總結
到這里Feign的介紹就結束了,我們使用一張Feign遠程調用的基本流程總結一下 Feign 調用鏈(圖片來自Feign原理 (圖解)):
參考文章:
【1】花一個周末,掌握 SpringCloud OpenFeign 核心原理(大部分內容轉載該文)
【2】Feign配置使用
【3】Spring Cloud Feign 如何自定義編碼器、解碼器和客戶端
【5】Feign原理 (圖解)