spring-cloud-starter-openfeign 源碼詳細講解


1.測試環境搭建:

1.1 架構圖:

 

 

 

 

  product服務提供一個接口:

 

 

 order服務通過feign的方式來調用product的接口:

 

 

 order服務需要引入依賴:

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

order服務對外的controller :

 

 

order服務主啟動類:

 2. 源碼分析

          2.1: spring 是如何找到@FeignClient標注的接口?
             我們在order服務啟動類中加了一個注解:@EnableFeignClients,點擊該注解進入看看

    

 

 

   由圖可知,該注解向容器導入了FeignClientsRegistrar.class類,然后我們跟蹤進入該類:

 

 

 該類實現了ImportBeanDefinitionRegistrar接口,該接口有個向spring容器注冊的組件的方法:

 debug調試:發現metadata就是主啟動類的信息

 先跟蹤第一個方法:registerDefaultConfiguration(metadata, registry);

 

 

 上圖:通過主啟動類獲取@EnableFeignClients注解的defaultConfiguration屬性,同時創建了一個名叫name的變量,值為:default.+主啟動類的全限定類名

 繼續跟進去:

 

 上圖:向sprin容器中注冊了一個FeignClientSpecification類,beanName為:default.com.yang.xiao.hui.order.OrderApplication.FeignClientSpecification

 

 @EnableFeignClients注解的defaultConfiguration屬性我並沒有配置信息,所以FeignClientSpecification的屬性值是一個空的數組

第一個方法分析完后,下面分析第二個:

 

 

 

public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner(); //創建一個掃描器
        scanner.setResourceLoader(this.resourceLoader);

        Set<String> basePackages;

        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());  //獲取EnableFeignClients注解的元素據,因為該注解可以包含你要掃描的路徑
                
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class); //注解過濾器,代表要過濾含有FeignClient.class的類
               final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) { //由於我沒有配置對應的屬性,所以會走這個分支
            scanner.addIncludeFilter(annotationTypeFilter); //掃描器將注解過濾器新增進來了,這個是重點了
            basePackages = getBasePackages(metadata); //EnableFeignClients注解的value,basePackages,basePackageClasses屬性有值,就掃描這些配置的包名,如果沒設置就掃描主啟動類的包名,此處我沒有配置,所以這里得到的是主啟動類的報名
        }
        else {
            final Set<String> clientClasses = new HashSet<>();
            basePackages = new HashSet<>();
            for (Class<?> clazz : clients) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }
            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                @Override
                protected boolean match(ClassMetadata metadata) {
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(
                    new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        }

//遍歷所有要掃描的包名
for (String basePackage : basePackages) { Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage); //通過掃描器,讀取包名下所有的類,然后看看哪些是有FeignClient.class注解的,將這些類轉成beanDefinition對象 for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { //這些BeanDefinition 都含有FeignClient.class注解 // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes( FeignClient.class.getCanonicalName()); //獲取FeignClient.class注解元信息 String name = getClientName(attributes);//獲取FeignClient.class的name值,我這里配置的是@FeignClient(name ="product" ),所以name就是product registerClientConfiguration(registry, name, attributes.get("configuration"));//這個跟之前分析的第一個方法邏輯是一樣的:向容器注入了一個name=product.feignClientSpecification 類型feignClientSpecification的bean registerFeignClient(registry, annotationMetadata, attributes);//注冊所有的帶有@FeignClient(name ="product" )注解的bean } } } }

 上面代碼總結:主要做了:

              1.創建了一個掃描器ClassPathScanningCandidateComponentProvider scanner,並將AnnotationTypeFilter過濾器傳入掃描器中(scanner.addIncludeFilter(annotationTypeFilter)),用於過濾掃描到的帶有@FeignClient注解的類

              2.獲取要掃描的包路徑:basePackages = getBasePackages(metadata);

 

 

      
             3.遍歷包名獲取候選的帶有FeignClient注解的類的信息 Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);

 

 

 因為掃描器之前加了一個注解過濾器,這里isCandidateComponent(metadataReader)的邏輯是使用注解過濾器來過濾出帶有FeignClien注解的類

 

 

 

 4.遍歷掃描到的BeanDefinition,向ioc容器中注冊對應的bean,這里每個帶有@FeignClient的類都會注冊2個bean

  registerClientConfiguration(registry, name,attributes.get("configuration")); 向容器中注冊了name=product.feignClientSpecification 類型feignClientSpecification的bean

重點是: registerFeignClient(registry, annotationMetadata, attributes);方法

 

 

 

 

 所以,該方法向spring容器注入的bean,名字為:com.yang.xiao.hui.order.controller.ProductService,類型為FeignClientFactoryBean,而FactoryBean都會提供一個getObject方法獲取對應的實例

 spring如何獲取帶有@FeignClient注解標准的接口,總結如下圖:

 

 3.springboot自動裝配機制:springboot啟動時會加載類路徑下/META-INF/spring.factories中key為org.springframework.boot.autoconfigure.EnableAutoConfiguration的類:

 

 

 

 

 因此有四個相關的配置類會被加載,我們主要關注2個FeignRibbonClientAutoConfiguration 和FeignAutoConfiguration

 

 

 //再看看DefaultFeignLoadBalancedConfiguration這個類:

 

 之后看看這個bean: FeignAutoConfiguration

 

 總結:自動裝配機制注入的bean:

 

 

 4.獲取第三方服務實例

                           4.1由前面分析,每個帶有@FeignClient注解的接口都會被注入到spring容器,名字為接口的權限定類名,類型為FeignClientFactoryBean

                   

 

         
                  4.2我們debug調試,從容器中根據beanName獲取對應的實例看看,我這里的beanName=com.yang.xiao.hui.order.controller.ProductService,order服務的controller方法先修改下:

      

 

                  跟進該方法:

                

 

              

 

           

 

          對於factoryBean,如果beanName前面帶上“&” 符號,獲取的bean就是原始的factoryBean,如果沒帶該符號,獲取的是factoryBean.getObject()方法返回的bean,我們這次調試是沒帶上該符合的

       

 

 

       

 

 

      

 

 

       至此,我們可以知道,所有帶有@FeignClient注解的接口獲取實例,最終都是調用FeignClientFactoryBean的getObject方法,該方法才是我們的重點

         4.3 FeignClientFactoryBean的getObject()方法的講解:

    

 

 

   

 

 分析: Feign.Builder builder = feign(context);

 

 

 //上面很多bean都是通過get(Context,xxx.class)方法獲取,而context就是FeignContext,在springBoot自動裝配時注入了spring容器,那么我們跟蹤第一個方法看看:

FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);

 

 
        

 

 

 這里有2個疑問:這個容器是怎么創建的,為何能通過該容器獲取對應的bean

 

 由於上面的獲取子容器的方法是在FeignContext中的,所以我們要來分析下FeignContext的創建過程:

 

 FeignContext創建時調用了父類的有參構造

 

 

此時們再回到,通過服務名稱獲取子容器的方法,獲取不到就創建一個子容器:

 

 由圖可知,子容器注入了一個FeignClientsConfiguration配置類,該類是由FeignContext初始化時,調用父類有參構造器傳入的,所以分析下該配置類:

@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Autowired(required = false)
    private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

    @Autowired(required = false)
    private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

    @Autowired(required = false)
    private Logger logger;

    @Autowired(required = false)
    private SpringDataWebProperties springDataWebProperties;

    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new OptionalDecoder(
                new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
    public Encoder feignEncoder() {
        return new SpringEncoder(this.messageConverters);
    }

    @Bean
    @ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
    @ConditionalOnMissingBean
    public Encoder feignEncoderPageable() {
        PageableSpringEncoder encoder = new PageableSpringEncoder(
                new SpringEncoder(this.messageConverters));
        if (springDataWebProperties != null) {
            encoder.setPageParameter(
                    springDataWebProperties.getPageable().getPageParameter());
            encoder.setSizeParameter(
                    springDataWebProperties.getPageable().getSizeParameter());
            encoder.setSortParameter(
                    springDataWebProperties.getSort().getSortParameter());
        }
        return encoder;
    }

    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        return new SpringMvcContract(this.parameterProcessors, feignConversionService);
    }

    @Bean
    public FormattingConversionService feignConversionService() {
        FormattingConversionService conversionService = new DefaultFormattingConversionService();
        for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
            feignFormatterRegistrar.registerFormatters(conversionService);
        }
        return conversionService;
    }

    @Bean
    @ConditionalOnMissingBean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    public Feign.Builder feignBuilder(Retryer retryer) {
        return Feign.builder().retryer(retryer);
    }

    @Bean
    @ConditionalOnMissingBean(FeignLoggerFactory.class)
    public FeignLoggerFactory feignLoggerFactory() {
        return new DefaultFeignLoggerFactory(this.logger);
    }

    @Bean
    @ConditionalOnClass(name = "org.springframework.data.domain.Page")
    public Module pageJacksonModule() {
        return new PageJacksonModule();
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
    protected static class HystrixFeignConfiguration {

        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled")  //這里可以發現,如果要想讓hystrix生效,需要配置一下屬性feign.hystrix.enabled
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign.builder();
        }

    }

}

通過該配置類,可以知道,子容器中注入了Decoder,Encoder,Contract,FormattingConversionService,Retryer,FeignLoggerFactory,Module

此時我們再看看父容器和子容器注入的主要類:

 

 有了上面的分析,我們回到之前的方法繼續分析:

 

 

我們看到該方法:configureUsingProperties(properties.getConfig().get(this.contextId),builder);,通過一個ContextId也就是服務名,我這里是product,獲取該服務的專有配置,說明每一個服務都可以擁有自己的特殊配置;
看看這個FeignClientProperties類:

 

 

 

 

我們再次回到FactoryBean的getObject()方法進行分析:

 

 

 

上圖,我們看到從容器中獲取了一個client和targeter,這2個bean是在父容器中獲取的,前面有分析過了:

 

 繼續跟進:

 

 

 

 可見是調用了ReflectiveFeign的newInstance(target);方法

 

 

 至此,我們可以發現,最終是調用了JDK動態代理機制來生成代理類,下面再來分析上面2個方法:

Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);和InvocationHandler handler = factory.create(target, methodToHandler);
先看第一個方法:targetToHandlersByName.apply(target)

 

 
        

 

 

 我這里只有一個方法:

 

 所以跟進去:

 

 可見,校驗是對RequestMaping注解的校驗

 

 

我們分析對方法上的處理:

 

 之后我們再分析InvocationHandler handler = factory.create(target, methodToHandler);方法:

 

 

 

 

 對上述所有分析做個總結:

 

 5.我們是如何通過feign調用到第三方服務的,在這里就是我們是如何通過order服務調用到product服務:

                5.1:調用任何服務都要知道ip和端口,因此,如何獲取ip和端口,如果服務提供者有多個,如何進行負載均衡,構建測試代碼如下,在order服務下:

 

 

 

 

 

 

 

 

 

 

 

 我們看看server是如何獲取到的,這里用到ribbon進行負載均衡,點擊command.submit

 

 

 

 

 

 上圖是ribbon獲取負載均衡獲取單個服務實例的過程,可以參考我之前的博客,有詳細源碼講解:https://www.cnblogs.com/yangxiaohui227/p/12614343.html

 

 繼續跟蹤,到達了:

 

 

 

 最終是使用HttpURLConnection發起請求:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
        

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

               

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

 















免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM