第四章 Feign接口服務及源碼解析筆記

前面已經學習了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());
        }
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章