zoukankan      html  css  js  c++  java
  • 微服务通信之feign的注册、发现过程

    前言

    feign 是目前微服务间通信的主流方式,是springCloud中一个非常重要的组件。他涉及到了负载均衡、限流等组件,是服务之间的信使。系列文章都是以2.1.3版本作为学习依据。

    一、feign的使用

    feign 的使用和dubbo的使用本质上非常相似。dubbo的理念是:像调用本地方法一样调用远程方法。那么套在feign上同样适用:像调用本地接口一样调用远程接口。
    使用feign只需要2步:

    • 定义一个接口并用FeignClient注解说明接口所在服务和路径
    • 服务启动类上添加@EnableFeignClients。

    1.1,定义一个feign接口

    @FeignClient(contextId = "order", name = "order", path = "/app")
    public interface OrderApiFeignClient {
    
       /**
        * 获取订单列表
        * @return
        */
       @RequestMapping("order/list")
       BaseResponse<List<OrderVO>> obtaining(@PathVariable("userId") Long userId);
    }
    
    

    1.2,再启动类上添加注解

    
    @EnableSwagger2
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients("com.xxx.*")
    @ComponentScan(value={"com.xxx"})
    public class OrderApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderApplication .class, args);
        }
    }
    
    

    二、feign 接口如何被实例化到spring容器的?

    首先按照一般的思路,我们会猜测基于接口生成代理类,然后对接口的调用实际上调的是代理对象,那真的是这样么? 我们带着猜想往下看。

    2.1 @EnableFeignClients 注解都做了些什么?

    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
    }
    
    

    可以看到注解本身主要定义了要扫描的feign接口包路径以及配置,但是注解本身又有注解Import ,可以看到他引入了FeignClientsRegistrar到容器。从名字看这个类就应该是在将feign接口注册到容器中,接下来我们具体看一下这个类干了些什么。

    /**
     * @author Spencer Gibb
     * @author Jakub Narloch
     * @author Venil Noronha
     * @author Gang Li
     */
    class FeignClientsRegistrar
    		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    

    可以看到FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,但凡是实现了这个接口的类被注入到容器后,spring容器在启用过程中都会去调用它的void registerBeanDefinitions(AnnotationMetadata var1, BeanDefinitionRegistry var2)方法,可以确定的是FeignClientsRegistrar肯定重写了此方法,我们接下来看一下该方法的实现。

    
    public void registerBeanDefinitions(AnnotationMetadata metadata,
    			BeanDefinitionRegistry registry) {
    		registerDefaultConfiguration(metadata, registry);
    		registerFeignClients(metadata, registry);
    }
    
    

    可以看到在这个方法中做了两件事:

    • 注册feign配置。
    • 注册feign接口。

    我们这里抓一下重点,看一下feign接口是怎么注册的?

    
    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());
    		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
    				FeignClient.class);
    		final Class<?>[] clients = attrs == null ? null
    				: (Class<?>[]) attrs.get("clients");
    		if (clients == null || clients.length == 0) {
                            // 限定只扫描FeingClient注解
    			scanner.addIncludeFilter(annotationTypeFilter);
    			basePackages = getBasePackages(metadata);
    		}
    		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);
    			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");
    
    					Map<String, Object> attributes = annotationMetadata
    							.getAnnotationAttributes(
    									FeignClient.class.getCanonicalName());
    
    					String name = getClientName(attributes);
    					registerClientConfiguration(registry, name,
    							attributes.get("configuration"));
                                            // 这里生成bean并且注册到容器
    					registerFeignClient(registry, annotationMetadata, attributes);
    				}
    			}
    		}
    	}
    
    

    上面这段代码概括起来就是: 先找了包路径basePackages , 然后在从这些包路径中查找带有FeignClient注解的接口,最后将注解的信息解析出来作为属性手动构建beanDefine注入到容器中。(这里有一个类ClassPathScanningCandidateComponentProvider,它可以根据filter扫描指定包下面的class对象,十分好用,建议收藏)。包路径的获取以及扫描feign相对简单,这里不做阐述,我们看一下它生成bean的过程,关注上面代码中的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);
    		String contextId = getContextId(attributes);
    		definition.addPropertyValue("contextId", contextId);
    		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);
                    // 这里省略部分代码
    		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    	}
    

    代码中通过BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class)生成的BeanDefine(请记住这里设置得FeignClientFactoryBean.type就是feign接口对应得class对象)。那么所有的feign接口最终注册到容器中的都是FeignClientFactoryBean对应的一个实例(注意实际上注册到容器中压根就不是FeignClientFactoryBean对应的实例化对象,具体原因看下文),到此feign接口对应的实例注册过程已经完成。那么回到一开始的问题为什么我们调用接口的方法最终发起了请求? 是否有代理类的生成呢? 我们接下来看看FeignClientFactoryBean类的特殊之处

    2.2 FeignClientFactoryBean 类,feign接口代理生成类

    由上文知,每一个feign接口实际上最终都会生成FeignClientFactoryBean ,最终由FeignClientFactoryBean生成具体的bean实例注册到容器中。

    
    /**
     * @author Spencer Gibb
     * @author Venil Noronha
     * @author Eko Kurniawan Khannedy
     * @author Gregor Zurowski
     */
    class FeignClientFactoryBean
    		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware 
    
    

    可以看到该类实现了FactoryBean接口,这意味着当Spring注册该bean实例到容器中时,实际是调用其getObject方法,那么FeignClientFactoryBean一定是重写了getObject()方法,接下来我们看一下getObject()干了什么事情:

    	public Object getObject() throws Exception {
    		return getTarget();
    	}
    
    

    我们继续追踪getTarget()方法:

    	<T> T getTarget() {
    		FeignContext context = this.applicationContext.getBean(FeignContext.class);
    		Feign.Builder builder = feign(context);
      
                    // 省略部分代码...
    		 
    		Targeter targeter = get(context, Targeter.class);
    		return (T) targeter.target(this, builder, context,
    				new HardCodedTarget<>(this.type, this.name, url));
    	}
    

    显然最终的bean是通过target.target()方法生成,我们继续往下看:

    	@Override
    	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
    			FeignContext context, Target.HardCodedTarget<T> target) {
    		return feign.target(target);
    	}
    

    显然最终的bean是通过feign.target(target)生成。我们继续往下看:

        public <T> T target(Target<T> target) {
          return build().newInstance(target);
        }
    

    显然最终得bean是通过build().newInstance(target)生成。我们继续往下看:

      public <T> T newInstance(Target<T> target) {
        // 省略部分代码
        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;
      }
    

    可以看到Proxy.newProxyInstance这个熟悉得身影了,没错他就是基于JDK原生得动态代理生成了FeignClientFactoryBean.type属性对应得class对应得代理类。从前文我们知道FeignClientFactoryBean.type就是feign接口得class对象。所以最终我们调用feign接口得方法实际上调用得是InvocationHandler方法。

    三、小结

    总结起来,先通过@enableFeignClient上面的@import扫描feign接口,然后利用BeanDefinitionBuilder创建一个FeignClientFactoryBean,最后在FeignClientFactoryBean的getObject()方法中生成代理类。希望通过本文的阅读能让大家阅读源码的能力得到提升,也不在对feign有一种黑盒子的感觉。可能篇幅看起来较少,其实feign的注册过程牵涉到框架层面的知识还是蛮多的,包括springIoc、BeanDefine、动态代理等等,仔细看明白的话收获应该还是有蛮多的。哈哈,懂得都懂。顺手提一句:读源码一定要对SPI等等特别熟悉,要不然你会无从下手,没有方向,抓不到重点。后续会更新文章讲feign怎么实现负载均衡等。

  • 相关阅读:
    CSS書寫規範及CSS Hack
    C#中为什么不能再方法里定义静态变量(Static)
    本机操作Excel文件提示错误:未在本地计算机上注册“Microsoft.Jet.OLEDB.4.0”提供程序。
    C#中静态变量和 静态方法的作用
    C#静态构造函数和非静态构造函数
    C# 判断字符串为空的4种方法及效率
    ASP.NET反射
    C#排序1(冒泡排序、直接排序、快速排序)
    javascript、jquery 、C#、sqlserveer、mysql、oracle中字符串截取的区别和用法
    MySQL数据库不识别server=.而是识别localhost
  • 原文地址:https://www.cnblogs.com/enjoyall/p/13755252.html
Copyright © 2011-2022 走看看