zoukankan      html  css  js  c++  java
  • 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发起请求:

     

     
    
    
    
    
    
    
    
    
    
    
    
    
    
    

     

                   

     

     

      

     














  • 相关阅读:
    【Android】给Android Studio设置代理
    Android studio如何使用SVN进行版本控制?
    Android studio
    nohup java -jar 启动java项目
    Linux命令发送Http GET/POST请求
    Java 读取配置文件的几种方式
    java -jar 报错 Error: A JNI error has occurred, please check your installation and try again
    java.net 发送http请求
    textarea高度自适应
    webrtc切换媒体设备
  • 原文地址:https://www.cnblogs.com/yangxiaohui227/p/12965340.html
Copyright © 2011-2022 走看看