前面已經學習了兩個Spring Cloud 組件:
- Eureka:實現服務注冊功能;
- Ribbon:提供基於RestTemplate的HTTP客戶端並且支持服務負載均衡功能。
通過這兩個組件我們暫時可以完成服務注冊和可配置負載均衡的服務調用。今天我們要學習的是Feign,那么Feign解決了什么問題呢?
相對於Eureka,Ribbon來說,Feign的地位好像不是那么重要,Feign是一個聲明式的REST客戶端,它的目的就是讓REST調用更加簡單。通過提供HTTP請求模板,讓Ribbon請求的書寫更加簡單和便捷。另外,在Feign中整合了Ribbon,從而不需要顯式的聲明Ribbon的jar包。
前面在使用Ribbon+RestTemplate時,利用RestTemplate對http請求的封裝處理,形成了一套模版化的調用方法。但是在實際開發中,由於對服務依賴的調用可能不止一處,往往一個接口會被多處調用,所以通常都會針對每個微服務自行封裝一些客戶端類來包裝這些依賴服務的調用。所以,Feign在此基礎上做了進一步封裝,由他來幫助我們定義和實現依賴服務接口的定義。在Feign的實現下,我們只需創建一個接口並使用注解的方式來配置它(以前是Dao接口上面標注Mapper注解,現在是一個微服務接口上面標注一個Feign注解即可),即可完成對服務提供方的接口綁定,簡化了使用Spring Cloud Ribbon時,自動封裝服務調用客戶端的開發量。
Feign的使用:
1.引入依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
另外,我們需要添加spring-cloud-dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.rickiyang.learn</groupId>
<artifactId>feign-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.接下來,我們需要在主類中添加 @EnableFeignClients:
package com.rickiyang.learn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class, args);
}
}
3.再來看一下用Feign寫的HTTP請求的格式:
package com.rickiyang.learn.service;
import com.rickiyang.learn.entity.Person;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* @author: rickiyang
* @date: 2019/10/5
* @description:
*/
@FeignClient(name= "eureka-client")
public interface HelloRemote {
@RequestMapping(value = "/hello/{name}")
String hello(@PathVariable(value = "name") String name);
@PostMapping(value ="/add",produces = "application/json; charset=UTF-8")
String addPerson(@RequestBody Person person);
@GetMapping("/getPerson/{id}")
String getPerson(@PathVariable("id") Integer id);
}
用FeignClient注解申明要調用的服務是哪個,該服務中的方法都有我們常見的Restful方式的API來聲明,這種方式大家是不是感覺像是在寫Restful接口一樣。
代碼示例:點擊這里。
note:
示例代碼的正確打開方式:先啟動服務端,然后啟動一個client端,再次啟動 feign-consumer,調用 feign-consumer中的接口即可。
還記得在Ribbon學習的時候使用RestTemplate發起HTTP請求的方式嗎:
restTemplate.getForEntity("http://eureka-client/hello/" + name, String.class).getBody();
將整個的請求URL和參數都放在一起,雖然沒有什么問題,總歸不是那么優雅。使用Feign之后你可以使用Restful方式進行調用,寫起來也會更加清晰。
Feign調用過程分析
上面簡單介紹了Feign的使用方式,大家可以結合着代碼示例運行一下,了解基本的使用方式。接下來我們一起分析Feign的調用過程,我們帶着兩個問題去跟蹤:
1.請求如何被Feign 統一托管;
2.Feign如何分發請求。
這兩個問題應該就涵蓋了Feign的功能,下面就出發去一探究竟。
我們還和以前一樣從一個入口進行深入,首先看啟動類上的 @EnableFeignClients 注解:
package org.springframework.cloud.openfeign;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Scans for interfaces that declare they are feign clients (via {@link FeignClient
* <code>@FeignClient</code>}). Configures component scanning directives for use with
* {@link org.springframework.context.annotation.Configuration
* <code>@Configuration</code>} classes.
*
* @author Spencer Gibb
* @author Dave Syer
* @since 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//等價於basePackages屬性,更簡潔的方式
String[] value() default {};
//指定多個包名進行掃描
String[] basePackages() default {};
//指定多個類或接口的class,掃描時會在這些指定的類和接口所屬的包進行掃描
Class<?>[] basePackageClasses() default {};
//為所有的Feign Client設置默認配置類
Class<?>[] defaultConfiguration() default {};
//指定用@FeignClient注釋的類列表。如果該項配置不為空,則不會進行類路徑掃描
Class<?>[] clients() default {};
}
注釋上說了該注解用於掃描 FeignClient 聲明的類。我們用 FeignClient 注解去聲明一個 Eureka 客戶端,那么猜想這里應該是取到我們聲明的Eureka client名稱,然后去訪問Eureka server獲取服務提供者。
同樣的,為所有Feign Client 也支持文件屬性的配置,如下 :
feign:
client:
config:
# 默認為所有的feign client做配置(注意和上例github-client是同級的)
default:
connectTimeout: 5000 # 連接超時時間
readTimeout: 5000 # 讀超時時間設置
注 : 如果通過Java代碼進行了配置,又通過配置文件進行了配置,則配置文件的中的Feign配置會覆蓋Java代碼的配置。
但也可以設置feign.client.defalult-to-properties=false,禁用掉feign配置文件的方式讓Java配置生效。
注意到類頭聲明的 @Import 注解引用的 FeignClientsRegistrar 類,這個類的作用是在 EnableFeignClients 初始化的時候掃描該注解對應的配置。
接着看 FeignClient 注解:
package org.springframework.cloud.openfeign;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
//指定Feign Client的名稱,如果項目使用了 Ribbon,name屬性會作為微服務的名稱,用於服務發現
@AliasFor("name")
String value() default "";
//用serviceId做服務發現已經被廢棄,所以不推薦使用該配置
@Deprecated
String serviceId() default "";
//指定Feign Client的serviceId,如果項目使用了 Ribbon,將使用serviceId用於服務發現,但上面可以看到serviceId做服務發現已經被廢棄,所以也不推薦使用該配置
@AliasFor("value")
String name() default "";
//為Feign Client 新增注解@Qualifier
String qualifier() default "";
//請求地址的絕對URL,或者解析的主機名
String url() default "";
//調用該feign client發生了常見的404錯誤時,是否調用decoder進行解碼異常信息返回,否則拋出FeignException
boolean decode404() default false;
//Feign Client設置默認配置類
Class<?>[] configuration() default {};
//定義容錯的處理類,當調用遠程接口失敗或超時時,會調用對應接口的容錯邏輯,fallback 指定的類必須實現@FeignClient標記的接口。實現的法方法即對應接口的容錯處理邏輯
Class<?> fallback() default void.class;
//工廠類,用於生成fallback 類示例,通過這個屬性我們可以實現每個接口通用的容錯邏輯,減少重復的代碼
Class<?> fallbackFactory() default void.class;
//定義當前FeignClient的所有方法映射加統一前綴
String path() default "";
//是否將此Feign代理標記為一個Primary Bean,默認為ture
boolean primary() default true;
}
同樣在 FeignClientsRegistrar 類中也會去掃描 FeignClient 注解對應的配置信息。我們直接看 FeignClientsRegistrar 的邏輯:
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware, EnvironmentAware {
// patterned after Spring Integration IntegrationComponentScanRegistrar
// and RibbonClientsConfigurationRegistgrar
private ResourceLoader resourceLoader;
private Environment environment;
public FeignClientsRegistrar() {
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
//在這個重載的方法里面做了兩件事情:
//1.將EnableFeignClients注解對應的配置屬性注入
//2.將FeignClient注解對應的屬性注入
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注入EnableFeignClients注解對應的配置屬性
registerDefaultConfiguration(metadata, registry);
//注入FeignClient注解對應的屬性
registerFeignClients(metadata, registry);
}
/**
* 拿到 EnableFeignClients注解 defaultConfiguration 字段的值
* 然后進行注入
*
**/
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"));
}
}
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 獲取ClassPath掃描器
ClassPathScanningCandidateComponentProvider scanner = getScanner();
// 為掃描器設置資源加載器
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
// 1. 從@EnableFeignClients注解中獲取到配置的各個屬性值
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
// 2. 注解類型過濾器,只過濾@FeignClient
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
// 3. 從1. 中的屬性值中獲取clients屬性的值
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
// 掃描器設置過濾器且獲取需要掃描的基礎包集合
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}else {
// clients屬性值不為null,則將其clazz路徑轉為包路徑
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)));
}
// 3. 掃描基礎包,且滿足過濾條件下的接口封裝成BeanDefinition
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
// 遍歷掃描到的bean定義
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 並校驗掃描到的bean定義類是一個接口
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
// 獲取@FeignClient注解上的各個屬性值
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
// 可以看到這里也注冊了一個FeignClient的配置bean
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注冊bean定義到spring中
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
/**
* 注冊bean
**/
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
// 1.獲取類名稱,也就是本例中的FeignService接口
String className = annotationMetadata.getClassName();
// 2.BeanDefinitionBuilder的主要作用就是構建一個AbstractBeanDefinition
// AbstractBeanDefinition類最終被構建成一個BeanDefinitionHolder
// 然后注冊到Spring中
// 注意:beanDefinition類為FeignClientFactoryBean,故在Spring獲取類的時候實際返回的是
// FeignClientFactoryBean類
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
// 3.添加FeignClientFactoryBean的屬性,
// 這些屬性也都是我們在@FeignClient中定義的屬性
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);
// 4.設置別名 name就是我們在@FeignClient中定義的name屬性
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;
}
// 5.定義BeanDefinitionHolder,
// 在本例中 名稱為FeignService,類為FeignClientFactoryBean
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
private void validate(Map<String, Object> attributes) {
AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
// This blows up if an aliased property is overspecified
// FIXME annotation.getAliasedString("name", FeignClient.class, null);
Assert.isTrue(
!annotation.getClass("fallback").isInterface(),
"Fallback class must implement the interface annotated by @FeignClient"
);
Assert.isTrue(
!annotation.getClass("fallbackFactory").isInterface(),
"Fallback factory must produce instances of fallback classes that implement the interface annotated by @FeignClient"
);
}
......
......
......
}
在這里做了兩件事情:
- 將EnableFeignClients注解對應的配置屬性注入;
- 將FeignClient注解對應的屬性注入。
生成FeignClient對應的bean,注入到Spring 的IOC容器。
在registerFeignClient方法中構造了一個BeanDefinitionBuilder對象,BeanDefinitionBuilder的主要作用就是構建一個AbstractBeanDefinition,AbstractBeanDefinition類最終被構建成一個BeanDefinitionHolder 然后注冊到Spring中。
beanDefinition類為FeignClientFactoryBean,故在Spring獲取類的時候實際返回的是FeignClientFactoryBean類。
FeignClientFactoryBean
作為一個實現了FactoryBean
的工廠類,那么每次在Spring Context 創建實體類的時候會調用它的getObject()
方法。
public Object getObject() throws Exception {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
url += cleanPath();
return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not lod balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}
這里的getObject()
其實就是將@FeinClient
中設置value值進行組裝起來,此時或許會有疑問,因為在配置FeignClientFactoryBean
類時特意說過並沒有將Configuration傳過來,那么Configuration中的屬性是如何配置的呢?看其第一句是:
FeignContext context = applicationContext.getBean(FeignContext.class);
從Spring容器中獲取FeignContext.class
的類,我們可以看下這個類是從哪加載的。點擊該類查看被引用的地方,可以找到在FeignAutoConfiguration
類中有聲明bean:
@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.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;
}
......
......
......
}
從上面的代碼中可以看到在set屬性的時候將 FeignClientSpecification 類型的類全部加入此類的屬性中。還記得在上面分析registerFeignClients
方法的時候里面有一行代碼調用:registerClientConfiguration()
方法:
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());
}
在注冊BeanDefinition的時候, configuration 其實也被作為參數,傳給了 FeignClientSpecification。 所以這時候在FeignContext中是帶着configuration配置信息的。
至此我們已經完成了配置屬性的裝配工作,那么是如何執行的呢?我們可以看getObject()
最后一句可以看到返回了Targeter.target
的方法。
return targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url));
那么這個Targeter
是哪來的?我們還是看上面的FeignAutoConfiguration
類,可以看到其中有兩個Targeter
類,一個是DefaultTargeter
,一個是HystrixTargeter
。當配置了feign.hystrix.enabled = true
的時候,Spring容器中就會配置HystrixTargeter
此類,如果為false那么Spring容器中配置的就是DefaultTargeter
。
我們以DefaultTargeter
為例介紹一下接下來是如何通過創建代理對象的:
class DefaultTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(target);
}
}
public static class Builder {
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);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
}
}
在target方法中有個參數:Feign.Builder:
public static class Builder {
private final List<RequestInterceptor> requestInterceptors =
new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
private Contract contract = new Contract.Default();
private Client client = new Client.Default(null, null);
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
private InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
private boolean decode404;
......
......
......
}
構建feign.builder時會向FeignContext獲取配置的Encoder,Decoder等各種信息 。Builder中的參數來自於配置文件的 feign.client.config里面的屬性。
查看ReflectiveFeign
類中newInstance
方法是返回一個代理對象:
public <T> T newInstance(Target<T> target) {
//為每個方法創建一個SynchronousMethodHandler對象,並放在 Map 里面
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)) {
//如果是 default 方法,說明已經有實現了,用 DefaultHandler
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
//否則就用上面的 SynchronousMethodHandler
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
// 設置攔截器
// 創建動態代理,factory 是 InvocationHandlerFactory.Default,創建出來的是
// ReflectiveFeign.FeignInvocationHanlder,也就是說后續對方法的調用都會進入到該對象的 inovke 方法
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;
}
這個方法大概的邏輯是:
- 根據target,解析生成
MethodHandler
對象; - 對
MethodHandler
對象進行分類整理,整理成兩類:default 方法和 SynchronousMethodHandler 方法; - 通過jdk動態代理生成代理對象,這里是最關鍵的地方;
- 將
DefaultMethodHandler
綁定到代理對象。
最終都是執行了SynchronousMethodHandler
攔截器中的invoke
方法:
@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;
}
}
}
invoke
方法方法首先生成 RequestTemplate 對象,應用 encoder,decoder 以及 retry 等配置,下面有一個死循環調用:executeAndDecode,從名字上看就是執行調用邏輯並對返回結果解析。
Object executeAndDecode(RequestTemplate template) throws Throwable {
//根據 RequestTemplate生成Request對象
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
// 調用client對象的execute()方法執行http調用邏輯,
//execute()內部可能設置request對象,也可能不設置,所以需要response.toBuilder().request(request).build();這一行代碼
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 10
response.toBuilder().request(request).build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
// IOException的時候,包裝成 RetryableException異常,上面的while循環 catch里捕捉的就是這個異常
throw errorExecuting(request, e);
}
//統計 執行調用花費的時間
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
boolean shouldClose = true;
try {
if (logLevel != Logger.Level.NONE) {
response =
logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
// ensure the request is set. TODO: remove in Feign 10
response.toBuilder().request(request).build();
}
//如果元數據返回類型是 Response,直接返回回去即可,不需要decode()解碼
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
if (response.body().length() == null ||
response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
shouldClose = false;
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
//主要對2xx和404等進行解碼,404需要特別的開關控制。其他情況,使用errorDecoder進行解碼,以異常的方式返回
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
return decode(response);
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
return decode(response);
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
if (shouldClose) {
ensureClosed(response.body());
}
}
}
這里主要就是使用:client.execute(request, options) 來發起調用,下面基本都是處理返回結果的邏輯。到此我們的整個調用生態已經解析完畢。
我們可以整理一下上面的分析:
首先調用接口為什么會直接發送請求?
原因就是Spring掃描了@FeignClient
注解,並且根據配置的信息生成代理類,調用的接口實際上調用的是生成的代理類。
其次請求是如何被Feign接管的?
- Feign通過掃描
@EnableFeignClients
注解中配置包路徑,掃描@FeignClient
注解並將注解配置的信息注入到Spring容器中,類型為FeignClientFactoryBean
; - 然后通過
FeignClientFactoryBean
的getObject()
方法得到不同動態代理的類並為每個方法創建一個SynchronousMethodHandler
對象; - 為每一個方法創建一個動態代理對象, 動態代理的實現是
ReflectiveFeign.FeignInvocationHanlder
,代理被調用的時候,會根據當前調用的方法,轉到對應的SynchronousMethodHandler
。
這樣我們發出的請求就能夠被已經配置好各種參數的Feign handler進行處理,從而被Feign托管。
請求如何被Feign分發的?
上一個問題已經回答了Feign將每個方法都封裝成為代理對象,那么當該方法被調用時,真正執行邏輯的是封裝好的代理對象進行處理,執行對應的服務調用邏輯。