前面已經學習了Ribbon,從Eureka獲取服務的實例在通過RestTemplate調用,並轉換成需要的對象
如:List<Product> list = restTemplate.exchange(PRODUCT_LIST_URL,HttpMethod.GET,new HttpEntity<Object>(httpHeaders), List.class).
getBody();
可以發現所有的數據調用和轉換都是由用戶直接來完成的,我們可能不想直接訪問Rest接口,如果轉換回來的直接是對象而不需要直接使用RestTemplate進行轉換就好了,這個時候就需要使用Feign了。
代碼Git地址:https://gitee.com/hankin_chj/springcloud-micro-service.git
注意:Feign不建議大家使用,流程簡單併發不高的方法可以用一用。
一、Feign基本使用
1、創建新模塊Feign
複製springcloud-micro-consumer成一個新的模塊springcloud-micro-consumer-feign
Feign服務修改pom文件,增加對feign的支持:
<dependencies>
<dependency>
<groupId>com.chj</groupId>
<artifactId>springcloud-micro-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- eureka中已經內置集成了ribbon,所以並不需要顯示引入ribbon的依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 增加對feign的支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
注意:這裏又有版本問題,如果是Edgware或之前的版本,使用下面的版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
feign的操作其實需要ribbon的支持。
2、新建springcloud-micro-service-feign模塊
2.1、這個模塊專門定義客戶端的調用接口,修改pom文件如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.chj</groupId>
<artifactId>springcloud-micro-api</artifactId>
</dependency>
</dependencies>
2.2、安全服務提供方的認證問題
service-feign模塊如果要通過Feign進行遠程調用,依然需要安全服務提供方的認證問題,不過在feign裏面已經集成了這塊功能,代碼如下:
@Configuration
public class FeignClientConfig {
@Bean
public BasicAuthRequestInterceptor getBasicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("admin", "admin");
}
}
2.3、service-feign模塊新建一個IProductClientService接口
@FeignClient(name = "SPRINGCLOUD-MICRO-PRODUCT", configuration = FeignClientConfig.class)
public interface IProductClientService {
@RequestMapping("/prodcut/get/{id}")
public Product getProduct(@PathVariable("id")long id);
@RequestMapping("/prodcut/list")
public List<Product> listProduct() ;
@RequestMapping("/prodcut/add")
public boolean addPorduct(Product product) ;
}
3、模塊springcloud-micro-consumer-feign配置
3.1、consumer-feign引入service-feign包
springcloud-micro-consumer-feign模塊修改pom文件,引入springcloud-micro-service-feign包:
<!-- feign接口模塊 -->
<dependency>
<groupId>com.chj</groupId>
<artifactId>springcloud-micro-service-feign</artifactId>
</dependency>
3.2、consumer-feign模塊刪除RestConfig.java類
springcloud-micro-consumer-feign模塊由於microcloud-service裏面已經做了安全驗證,並且後面並不直接使用RestTemplate ,所以刪除RestConfig.java類。
3.3、consumer-feign模塊修改ConsumerProductController
springcloud-micro-consumer-feign模塊修改ConsumerProductController,這個時候直接使用microcloud-service定義的服務就可以了,具體實現如下:
@RestController
@RequestMapping("/consumer")
public class ConsumerProductController {
@Resource
private IProductClientService iProductClientService;
@RequestMapping("/product/get")
public Object getProduct(long id) {
return iProductClientService.getProduct(id);
}
@RequestMapping("/product/list")
public Object listProduct() {
return iProductClientService.listProduct();
}
@RequestMapping("/product/add")
public Object addPorduct(Product product) {
return iProductClientService.addPorduct(product);
}
}
由此可見,這個時候ConsumerProductController的代碼已經簡潔了不少。
3.4、consumer-feign模塊修改程序主類
由於使用了feign接口功能,需要添加@EnableFeignClients()註解:
@SpringBootApplication
@EnableEurekaClient
//@RibbonClient(name ="SPRINGCLOUD-MICRO-PRODUCT" ,configuration = RibbonConfig.class)
@EnableFeignClients("com.chj.service") // 掃描feign接口服務的service包
public class ConsumerFeignApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerFeignApp.class,args);
}
}
4、啓動測試:
訪問地址:http://localhost/consumer/product/list
可以發現Feign在調用接口的時候自帶負載均衡,並且使用了默認的輪詢方式,這也不奇怪,因爲Fegin裏面內置就使用了Ribbon,可以做個測試,看下是否真的如此consumer-feign修改程序主類ConsumerFeignApp。
添加註解:@RibbonClient(name ="MICROCLOUD-PROVIDER-PRODUCT" ,configuration = RibbonConfig.class)
啓動測試:http://localhost/consumer/product/list
可以發現,現在的路由規則以及變成了隨機訪問
二、其他配置
1、數據壓縮
前面我們已經知道Feign之中最核心的作用就是將Rest服務的信息轉化爲接口,這其中還有其他的一些地方應該要考慮,比如,數據的壓縮。Rest協議更多的傳輸的是文本,JSON或者XML,如果用戶發送的請求很大,這個時候有必要對數據進行壓縮處理,好在feign本身就提供了壓縮的支持。
FeignContentGzipEncodingAutoConfiguration可以先看下這個類:
@Configuration
@EnableConfigurationProperties({FeignClientEncodingProperties.class})
@ConditionalOnClass({Feign.class})
@ConditionalOnBean({Client.class})
@ConditionalOnMissingBean(
type = {"okhttp3.OkHttpClient"}
)
@ConditionalOnProperty(
value = {"feign.compression.request.enabled"},
matchIfMissing = false
)
@AutoConfigureAfter({FeignAutoConfiguration.class})
public class FeignContentGzipEncodingAutoConfiguration {
雖然Feign支持壓縮,但默認是不開啓的,再看下FeignClientEncodingProperties,可以根據這裏面的屬性進行相關壓縮的配置。
@ConfigurationProperties("feign.compression.request")
public class FeignClientEncodingProperties {
private String[] mimeTypes = new String[]{"text/xml", "application/xml", "application/json"};
private int minRequestSize = 2048;
consumer-feign模塊修改application.yml配置文件:
feign:
compression:
request:
enabled: true
mime-types: # 可以被壓縮的類型
- text/xml
- application/xml
- application/json
min-request-size: 2048 # 超過2048的字節進行壓縮
2、日誌配置
在構建@FeignClient註解修飾的服務客戶端時,會爲一個客戶端都創建一個feign.Logger實例,可以利用日誌來分析Feign的請求細節,不過默認Feign的日誌是不開啓的。
2.1、consumer-feign模塊修改
修改application.yml配置文件,增加日誌信息:
logging:
level:
com.chj.service: DEBUG
只添加上面配置還無法實現對DEBUG日誌的輸出,以因爲Feign客戶端默認的logger.level對象定義爲none級別,所以不會記錄feign調用過程中的信息。
public static enum Level {
NONE, // 默認
BASIC,
HEADERS,
FULL;
private Level() {
}
}
2.2、service-feign模塊修改FeignClientConfig,開啓日誌輸出
@Configuration
public class FeignClientConfig {
@Bean
public BasicAuthRequestInterceptor getBasicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("admin", "admin");
}
@Bean
public Logger.Level getFeignLoggerLevel() {
return feign.Logger.Level.FULL ;
}
}
2.3、啓動訪問,查看日誌輸出
訪問誒之:localhost/consumer/product/list
日誌打印結果:
2019-11-21 01:41:46.696 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] ---> GET http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/list HTTP/1.1
2019-11-21 01:41:46.697 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] Authorization: Basic YWRtaW46YWRtaW4=
2019-11-21 01:41:46.697 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] ---> END HTTP (0-byte body)
2019-11-21 01:41:46.782 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] <--- HTTP/1.1 200 (85ms)
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] cache-control: no-cache, no-store, max-age=0, must-revalidate
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] content-type: application/json;charset=UTF-8
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] date: Wed, 20 Nov 2019 17:41:46 GMT
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] expires: 0
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] pragma: no-cache
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] transfer-encoding: chunked
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] x-content-type-options: nosniff
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] x-frame-options: DENY
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] x-xss-protection: 1; mode=block
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct]
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] [{"productId":null,"productName":"java編程
","productDesc":"springcloud"},{"productId":null,"productName":"Springboot","productDesc":"springcloud"},{"productId":null,"productName":"西遊記","productDesc":"springcloud"},{"productId":null,"productName":"水滸傳
","productDesc":"springcloud"},{"productId":null,"productName":"西廂記","productDesc":"springcloud"}]
2019-11-21 01:41:46.783 DEBUG 313788 --- [p-nio-80-exec-4] com.chj.service.IProductClientService : [IProductClientService#listProduct] <--- END HTTP (368-byte body)
3、總結
·當使用Feign要通過接口的方法訪問Rest服務的時候會根據設置的服務類型發出請求,這個請求是發送給Eureka。
·隨後由於配置了授權處理,所以繼續發送授權信息(Authorization),其實在外面使用RestTemplate的時候也是這麼做的,可以對應日誌的加密內容和直接訪問其實是一樣的。
·在進行服務調用的時候Feign融合了Ribbon技術,所以也支持有負載均衡的處理。
Feign = RestTempate + HttpHeader + Ribbon + Eureka 綜合體,使用feign大大增加了代碼的靈活程度。
三、Feign源碼解析
1、代碼入口分析
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name ="SPRINGCLOUD-MICRO-PRODUCT" ,configuration = RibbonConfig.class)
@EnableFeignClients("com.chj.service") // 掃描feign接口服務的service包
public class ConsumerFeignApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerFeignApp.class,args);
}
}
在@EnableFeignClients標籤中,import了一個FeignClientsRegistrar類:
@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 {};
}
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerBeanDefinitions方法裏面有兩個核心方法,它會在AbstractApplicationContext#refresh()方法中被調用,這部分可以參考spring的源代碼。
核心方法registerDefaultConfiguration():從EnableFeignClients的屬性值來構建Feign的Configuration。
核心方法registerFeignClients():掃描package,註冊被@FeignClient修飾的接口類Bean的信息。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//TODO 從EnableFeignClients的屬性值來構建Feign的Configuration
registerDefaultConfiguration(metadata, registry);
//TODO 掃描package,註冊被@FeignClient修飾的接口類Bean的信息
registerFeignClients(metadata, registry);
}
2、核心方法registerDefaultConfiguration分析
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerDefaultConfiguration
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//TODO 獲取到metadata中關於EnableFeignClients的屬性值鍵值對。
Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
//TODO 如果配置了defaultConfiguration 進行配置,如果沒有使用默認的configuration
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}else {
name = "default." + metadata.getClassName();
}
//TODO 進行註冊
registerClientConfiguration(registry, name,defaultAttrs.get("defaultConfiguration"));
}
}
2.1、registerClientConfiguration進行註冊方法分析
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration接下來進行註冊:
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,Object configuration) {
//使用BeanDefinitionBuilder來生成BeanDefinition,並把它進行註冊
BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition( name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
方法的入參BeanDefinitionRegistry是spring框架用於動態註冊BeanDefinition信息的接口,調用registerBeanDefinition方法可以將BeanDefinition註冊到Spring容器中,其中name屬性就是註冊的BeanDefinition的名稱,在這裏它註冊了一個FeignClientSpecification的對象。
FeignClientSpecification實現了NamedContextFactory.Specification接口,它是Feign實例化的重要一環,在上面的方法中,它持有自定義配置的組件實例,SpringCloud使用NamedContextFactory創建一些列的運行上下文ApplicationContext來讓對應的Specification在這些上下文中創建實例對象。
2.2、NamedContextFactory有3個功能:
·創建AnnotationConfigApplicationContext上下文。
·在上下文中創建並獲取bean實例。
·當上下文銷燬時清除其中的feign實例。
NamedContextFactory有個非常重要的子類FeignContext,用於存儲各種OpenFeign的組件實例。
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
2.3、FeignContext是哪裏構建的呢?
配置見:pring-cloud-openfeign-core-2.0.0.RELEASE.jar!\META-INF\spring.factories
2.4、啓動配置類FeignAutoConfiguration代碼分析:
org.springframework.cloud.openfeign.FeignAutoConfiguration#feignContext方法代碼如下:
@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;
}
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
//將默認的FeignClientsConfiguration作爲參數傳遞給構造函數
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
FeignContext創建的時候會將之前FeignClientSpecification通過setConfigurations設置給context上下文。
2.5、NamedContextFactory的createContext方法解析:
代碼詳見:org.springframework.cloud.context.named.NamedContextFactory#createContext方法。
FeignContext的父類的createContext方法會將創建AnnotationConfigApplicationContext實例,這實例將作爲當前上下文的子上下文,用於關聯feign組件的不同實例。
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//TODO 獲取name所對應的configuration,如果有就註冊到子context中
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
context.register(configuration);
}
}
//TODO 註冊default的Configuration,也就是 FeignClientsRegistrar類中registerDefaultConfiguration方法中註冊的Configuration
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
//TODO 註冊PropertyPlaceholderAutoConfiguration
context.register(PropertyPlaceholderAutoConfiguration.class,this.defaultConfigType);
//TODO 設置Environment的propertySources屬性源
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);
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}
由於NamedContextFactory實現了DisposableBean,所以當實例消亡的時候會調用
@Override
public void destroy() {
Collection<AnnotationConfigApplicationContext> values = this.contexts.values();
for (AnnotationConfigApplicationContext context : values) {
context.close();
}
this.contexts.clear();
}
2.6、總結一下:
NamedContextFactory會創建出AnnotationConfigApplicationContext實例,並以name作爲唯一標識,然後每個AnnotationConfigApplicationContext實例都會註冊部分配置類,從而可以給出一系列的基於配置類生成的組件實例,這樣就可以基於name來管理一系列的組件實例,爲不同的FeignClient準備不同配置組件實例。
3、核心方法registerFeignClients()分析
3.1、registerFeignClients源碼分析
FeignClientsRegistrar做的第二件事就是掃描指定包下的類文件,註冊@FeignClient修飾的接口類信息。
org.springframework.cloud.openfeign.FeignClientsRegistrar.registerFeignClients(metadata, registry)源碼:
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//TODO 生成自定義的ClassPathScanningCandidateComponentProvider
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
//TODO 獲取EnableFeignClients所有屬性的鍵值對
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
//TODO 依照AnnotationTypeFilter 來進行過濾,只會掃描出EnableFeignClients修飾的類
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
final Class<?>[] clients = attrs == null ? null: (Class<?>[]) attrs.get("clients");
//TODO 如果沒有配置clients屬性,那麼就要掃描basePackages
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
} else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
//TODO 遍歷上訴過程中獲取basePackages 的列表
for (String basePackage : basePackages) {
//TODO 獲取basePackage中所有的BeanDefinition
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");
//TODO 從這些BeanDefinition中獲取FeignClient的屬性值
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
//TODO 對單獨的某個FeignClient的configuration進行配置
registerClientConfiguration(registry, name,attributes.get("configuration"));
//TODO 註冊
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
接下來分析註冊方法registerFeignClient()如下:
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
總結:
FeignClientsRegistrar方法會依據@EnableFeignClients的屬性獲取要掃描的包路徑信息,然後獲取這些包下被@FeignClient註解修飾的接口的BeanDefinition。例如springcloud-micro-service-feign模塊的IProductClientService接口實現:
@FeignClient(name = "SPRINGCLOUD-MICRO-PRODUCT", configuration = FeignClientConfig.class)
public interface IProductClientService {
3.2、實例化@FeignClient修飾的接口
FeignClientFactoryBean是工廠類,Spring容器通過調用它的getObject方法來獲取對應的bean實例,被@FeignClient修飾的接口都是通過FeignClientFactoryBean的getObject方法來進行實例化的。
源碼見org.springframework.cloud.openfeign.FeignClientFactoryBean#getObject方法:
@Override
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();
//調用FeignContext的getInstance方法獲取Client對象
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
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));
}
3.3、調用FeignContext的getInstance方法獲取Client對象
protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(this.name, type);
}
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,type).length > 0) {
return context.getBean(type);
}
return null;
}
3.4、DefaultTargeter的實現
Targeter是一個接口,它的target方法會生成對應實例對象,它有兩個實現類,分表爲DefaultTargeter和HystrixTargeter,fegign使用HystrixTargeter這一層抽象來封裝關於Hystrix的實現,DefaultTargeter的實現如下所示,只是調用了Feign.Builder的target方法
class DefaultTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(target);
}
}
Feign.Builder負責生成被@FeignClient修飾的FeignClient接口類實例,它通過JAVA反射機制構建:
代碼見:feign-core-9.5.1.jar!\feign\Feign.target方法:
public <T> T target(Target<T> target) {
return this.build().newInstance(target);
}
public Feign build() {
Factory synchronousMethodHandlerFactory = new Factory(this.client, this.retryer,
this.requestInterceptors, this.logger, this.logLevel, this.decode404);
ParseHandlersByName handlersByName = new ParseHandlersByName(this.contract, this.options, this.encoder,
this.decoder, this.errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory);
}
3.5、實例化方法ReflectiveFeign
feign-core-9.5.1.jar!\feign\ReflectiveFeign.newInstance()方法分析:
這方法主要做兩件事:
1)掃描FeignClient接口類的所有函數,生成對應的Handler
2)使用Proxy生成FeignClient的實例對象
public <T> T newInstance(Target<T> target) {
//TODO arseHandlersByName.appy方法填充信息
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)) {
//TODO 爲每一個默認方法生成一個DefaultMethodHandler
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
//TODO 生成java InvocationHandler 是個動態代理的類
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;
}
3.6、掃描FeignClient接口類的所有函數,生成對應的Handler
在掃描FeignClient接口類所有函數生成對應的Handle過程中,feign會生成調用該函數時發送網絡請求的模板,也就是RequestTemplate實例,RequestTemplate中包含了發送網絡請求的URL和產生填充信息,@RequestMapping、@RequestVariable等註解信息也會包含到RequestTemplate中,這一具體過程就是ParseHandlersByName.apply()方法來實現的。
1)feign.ReflectiveFeign.ParseHandlersByName#apply方法分析:
public Map<String, MethodHandler> apply(Target key) {
//獲取type中所有的方法信息,會根據註解生成每個方法的RequestTemplate
List<MethodMetadata> metadata = this.contract.parseAndValidatateMetadata(key.type());
Map<String, MethodHandler> result = new LinkedHashMap();
MethodMetadata md;
Object buildTemplate;
for(Iterator var4 = metadata.iterator(); var4.hasNext(); result.put(md.configKey(),
this.factory.create(key, md, (Factory)buildTemplate,
this.options, this.decoder, this.errorDecoder))) {
md = (MethodMetadata)var4.next();
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate = new ReflectiveFeign.BuildFormEncodedTemplateFromArgs(md, this.encoder);
} else if (md.bodyIndex() != null) {
buildTemplate = new ReflectiveFeign.BuildEncodedTemplateFromArgs(md, this.encoder);
} else {
buildTemplate = new ReflectiveFeign.BuildTemplateByResolvingArgs(md);
}
}
return result;
}
}
Contract的默認實現是org.springframework.cloud.openfeign.support.SpringMvcContract其基類爲Contract.BaseContract。
public abstract static class BaseContract implements Contract {
2)parseAndValidateMetadata()方法代碼解析
org.springframework.cloud.openfeign.support.SpringMvcContract.parseAndValidateMetadata()方法會與HTTP請求相關的所有函數的基本信息和註解信息
@Override
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
this.processedMethods.put(Feign.configKey(targetType, method), method);
//調用父類BaseContract的函數
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class);
//處理RequestMapping註解
if (classAnnotation != null) {
if (!md.template().headers().containsKey(ACCEPT)) {
parseProduces(md, method, classAnnotation);
}
if (!md.template().headers().containsKey(CONTENT_TYPE)) {
parseConsumes(md, method, classAnnotation);
}
parseHeaders(md, method, classAnnotation);
}
return md;
}
3)feign.Contract.BaseContract#parseAndValidateMetadata 父類的parseAndValidateMetadata方法會依次解析接口類中的註解,方法中的註解,和各種參數註解,並將這些註解包含的信息封裝到MethodMetadata 對象中,然後返回。
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
MethodMetadata data = new MethodMetadata();
//TODO 函數的返回值
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method));
//TODO 獲取並處理修飾class的註解信息
if(targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
//TODO 調用子類processAnnotationOnClass的實現
processAnnotationOnClass(data, targetType);
//TODO 處理修飾method的註解信息
for (Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
checkState(data.template().method() != null,
"Method %s not annotated with HTTP method type (ex. GET, POST)",method.getName());
//TODO 方法參數的類型
Class<?>[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
//TODO 方法參數的註解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
//TODO 依次處理各個函數參數的註解
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation) {
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters.");
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
if (data.headerMapIndex() != null) {
checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()], genericParameterTypes[data.headerMapIndex()]);
}
if (data.queryMapIndex() != null) {
checkMapString("QueryMap", parameterTypes[data.queryMapIndex()], genericParameterTypes[data.queryMapIndex()]);
}
return data;
}
3.7、使用Proxy生成FeignClient的實例對象
1)實例化方法分析feign.ReflectiveFeign#newInstance:
InvocationHandler handler = factory.create(target, methodToHandler);中另外一件事情是OpenFeign使用Proxy.newProxyInstance方法來創建FeignClient接口類的實例,然後將InvocationHandle綁定到接口實例上,用於處理接口類函數調用。
feign.InvocationHandlerFactory.Default()方法:
public interface InvocationHandlerFactory {
InvocationHandler create(Target var1, Map<Method, InvocationHandlerFactory.MethodHandler> var2);
public static final class Default implements InvocationHandlerFactory {
public Default() {
}
public InvocationHandler create(Target target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
return new FeignInvocationHandler(target, dispatch);
}
}
public interface MethodHandler {
Object invoke(Object[] var1) throws Throwable;
}
}
Default 實現了InvocationHandlerFactory 接口,create方法其實返回FeignInvocationHandler的實例。
2)ReflectiveFeign.FeignInvocationHandler.invoke()方法源碼如下:
static class FeignInvocationHandler implements InvocationHandler {
private final Target target;
private final Map<Method, MethodHandler> dispatch;
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = (Target)Util.checkNotNull(target, "target", new Object[0]);
this.dispatch = (Map)Util.checkNotNull(dispatch, "dispatch for %s", new Object[]{target});
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!"equals".equals(method.getName())) {
if ("hashCode".equals(method.getName())) {
return this.hashCode();
} else {
return "toString".equals(method.getName()) ? this.toString() :
((MethodHandler)this.dispatch.get(method)).invoke(args);
}
} else {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return this.equals(otherHandler);
} catch (IllegalArgumentException var5) {
return false;
}
}
}
注意:((MethodHandler)this.dispatch.get(method)).invoke(args),Invoke方法會根據函數名稱來調用不同的MethodHandle實例invoke方法。
3.8、網絡請求
接下來就直接使用FeignClient接口類的實例,調用它的函數來發送網絡請求,發送網絡請求可以分爲3個階段:
1)是將函數實際參數添加到RequestTemplate中
2)調用Target生成具體的Request對象
3)調用Client來發送網絡請求,將Response轉化爲對象返回
首先分析feign.SynchronousMethodHandler#invoke()方法:
public Object invoke(Object[] argv) throws Throwable {
//根據函數參數創建RequestTemplate
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
return this.executeAndDecode(template);
} catch (RetryableException var5) {
retryer.continueOrPropagate(var5);
if (this.logLevel != Level.NONE) {
this.logger.logRetry(this.metadata.configKey(), this.logLevel);
}
}
}
}
3.9、根據函數參數創建RequestTemplate.create()
@Override
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = new RequestTemplate(metadata.template());
//TODO 設置url
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.insert(0, String.valueOf(argv[urlIndex]));
}
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
//TODO 遍歷MethodMetadata中關於參數的所以以及對應名稱的配置信息
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
//TODO entry.getKey就是參數的索引
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
//TODO indexToExpander保存着將各種類型參數的值轉化爲String類型的Expander轉換器
if (indexToExpander.containsKey(i)) {
//TODO 將Value值爲String
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}
RequestTemplate template = resolve(argv, mutable, varBuilder);
//TODO 設置queryMap參數
if (metadata.queryMapIndex() != null) {
template = addQueryMapQueryParameters((Map<String, Object>) argv[metadata.queryMapIndex()], template);
}
//TODO 設置headersMap參數
if (metadata.headerMapIndex() != null) {
template = addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
}
return template;
}
3.10、feign.RequestTemplate#resolve()方法代碼:
protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
Map<String, Boolean> variableToEncoded = new LinkedHashMap();
Iterator var5 = this.metadata.indexToEncoded().entrySet().iterator();
while(var5.hasNext()) {
Entry<Integer, Boolean> entry = (Entry)var5.next();
Collection<String> names = (Collection)this.metadata.indexToName().get(entry.getKey());
Iterator var8 = names.iterator();
while(var8.hasNext()) {
String name = (String)var8.next();
variableToEncoded.put(name, entry.getValue());
}
}
return mutable.resolve(variables, variableToEncoded);
}
}
回調到feign-core-9.5.1.jar!\feign\RequestTemplate.resolve()方法:
RequestTemplate resolve(Map<String, ?> unencoded, Map<String, Boolean> alreadyEncoded) {
//替換query數值,將{queryVariable} 替換成實際的值
this.replaceQueryValues(unencoded, alreadyEncoded);
Map<String, String> encoded = new LinkedHashMap();
Iterator var4 = unencoded.entrySet().iterator();
while(var4.hasNext()) {
//把所有參數進行編碼
Entry<String, ?> entry = (Entry)var4.next();
String key = (String)entry.getKey();
Object objectValue = entry.getValue();
String encodedValue = this.encodeValueIfNotEncoded(key, objectValue, alreadyEncoded);
encoded.put(key, encodedValue);
}
String resolvedUrl = expand(this.url.toString(), encoded).replace("+", "%20");
if (this.decodeSlash) {
resolvedUrl = resolvedUrl.replace("%2F", "/");
}
this.url = new StringBuilder(resolvedUrl);
//把頭部信息進行傳串行化
Map<String, Collection<String>> resolvedHeaders = new LinkedHashMap();
Iterator var14 = this.headers.keySet().iterator();
while(var14.hasNext()) {
String field = (String)var14.next();
Collection<String> resolvedValues = new ArrayList();
Iterator var9 = Util.valuesOrEmpty(this.headers, field).iterator();
while(var9.hasNext()) {
String value = (String)var9.next();
String resolved = expand(value, unencoded);
resolvedValues.add(resolved);
}
resolvedHeaders.put(field, resolvedValues);
}
this.headers.clear();
this.headers.putAll(resolvedHeaders);
if (this.bodyTemplate != null) {
//處理body信息
this.body(urlDecode(expand(this.bodyTemplate, encoded)));
}
return this;
}
feign.SynchronousMethodHandler#invoke方法中的executeAndDecode()方法會根據RequestTemplate生成Request對象,然後交給Client實例發送網絡請求,最後返回對應的函數返回類型的實例。
這裏的client爲org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient
3.11、executeAndDecode()源碼分析
源碼位置:feign.SynchronousMethodHandler.executeAndDecode()
Object executeAndDecode(RequestTemplate template) throws Throwable {
//TODO 根據RequestTemplate生成Request
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
//TODO client發送網絡請求
response = client.execute(request, options);
//TODO 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));
}
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();
}
//TODO 如果是response返回類型就可以直接返回
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
//TODO 設置body
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();
}
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());
}
}
}