1 前言
微服务之间的调用,OpenFeign 是一种选择,并且还提供了很多功能,比如我们有多个节点,它能负载均衡,当服务发生异常时,它还能提供熔断机制。所以它是怎么实现的,因为我们平时只需要写 @FeignClient 是个接口,所以它势必会走代理,所以是不是要从我们的 @FeignClient 下手。那么这节我们就先简单看下OpenFeign 中 @FeignClient 代理的创建过程,当代理的创建过程知道了,然后我们就可以深入它的增强逻辑,继而能看到它的负载均衡、熔断又是如何做的对吧。
如果你之前看过我的 【Mybatis】【二】源码分析-Mapper 接口都是怎么注入到 Spring容器中的? 以及【Mybatis】【三】源码分析- MapperFactoryBean 的创建过程以及 Mapper 接口代理的生成过程详解 那么这节你就能很好的理解,因为他俩的注入过程太像了,基本都差不多。大体都是先扫描包、然后注入FactoryBean 形式的 BeanDefinition 来实现的。
那么废话不多说,我们直接看。
2 源码分析
2.1 入口 EnableFeignClients
起点在哪里,是不是我们的启动类上,是不是会配置基本的扫描包,@EnableFeignClients 它就是入口。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients {
可以看到它引入了 FeignClientsRegistrar,从名称上就可以看出,它就是 FeignClient 的注册器。
2.2 注册器 FeignClientsRegistrar
那我们看看注册器里都干了什么:
// 可以看到它实现了 ImportBeanDefinitionRegistrar class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {// 注册 BeanDefinitionpublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 注册默认配置 @FeignClient 注解有个属性是 defaultConfiguration 自定义全局默认配置比如可以配置你自己的重试机制、编码器、解码器等也就是控制 @FeignClient 的行为的(通常不需要配)this.registerDefaultConfiguration(metadata, registry);// 注册 @FeignClientthis.registerFeignClients(metadata, registry);} }
如上可以看到大概两部分,一部分是默认的全局配置、一部分是注册我们的 @FeignClient。
2.2.1 全局默认配置类 registerDefaultConfiguration
这个我们就先草草略过:
// @see FeignClientsRegistrar#registerDefaultConfiguration private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {String name;if (metadata.hasEnclosingClass()) {name = "default." + metadata.getEnclosingClassName();} else {name = "default." + metadata.getClassName();}this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));} } // @see FeignClientsRegistrar#registerClientConfiguration private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {// FeignClientSpecificationBeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);builder.addConstructorArgValue(name);builder.addConstructorArgValue(configuration);registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); }
2.2.2 注册 registerFeignClients
我们瞅瞅具体都做了哪些内容:
// 开始注册 FeignClient public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 存放候选的 FeignClient 的 BeanDefinitionLinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();// 获取注解信息Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());// @EnableFeignClients 可以直接配置 Client 我们平时基本不配这个,因为当存在大量的 Client 的话那不是费劲么,每新加一个还要配一个多麻烦 所以我们平时配一个或者多个包名 Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));// 当配置了 Client 属性值的话,用 Annotated 的 BeanDefinition 包起来if (clients != null && clients.length != 0) {Class[] var12 = clients;int var14 = clients.length;for(int var16 = 0; var16 < var14; ++var16) {Class<?> clazz = var12[var16];candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));}} else {// 扫描器ClassPathScanningCandidateComponentProvider scanner = this.getScanner();scanner.setResourceLoader(this.resourceLoader);// 指定扫描的筛子 也就是过滤出 @FeignClient 注解的类scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));// 获取注解上配的包名Set<String> basePackages = this.getBasePackages(metadata);// 逐个循环扫描包下的类Iterator var8 = basePackages.iterator();while(var8.hasNext()) {String basePackage = (String)var8.next();// 都加入到集合中 candidateComponents.addAll(scanner.findCandidateComponents(basePackage));}}// 对集合中的每个类进行注入Iterator var13 = candidateComponents.iterator();while(var13.hasNext()) {BeanDefinition candidateComponent = (BeanDefinition)var13.next();// 对每一个进行基础检查 然后进行注入if (candidateComponent instanceof AnnotatedBeanDefinition) {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 = this.getClientName(attributes);// 每个 @FeignClient 也可以设置自己的配置this.registerClientConfiguration(registry, name, attributes.get("configuration"));// 注册 FeignClientthis.registerFeignClient(registry, annotationMetadata, attributes);}} }private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);builder.addConstructorArgValue(name);builder.addConstructorArgValue(configuration);registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); }
可以看到会从我们配置的包下扫描出 @FeignClient 的接口,然后进行注入,并且每个 Client 也都会注入一个自己的 ClientConfiguration,即使没有配置也会注入一个这样的 BeanDefinition 哈。
那我们继续看下 FeignClient 的具体注入:
// annotationMetadata 就是 @FeignClient 注解信息 // attributes 就是每个属性的值 private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {// 我们的 @FeignClient 下的接口名String className = annotationMetadata.getClassName();Class clazz = ClassUtils.resolveClassName(className, (ClassLoader)null);ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory)registry : null;// 我们常设置的 contextId 和 name contextId没有设置的会拿 name 属性的值 name 属性的值又会先获取 serviceId 的值,没有的话拿 name 再没有的话会拿 value 并且他俩都可以设置表达式 ${}这种写法 会根据环境变量等配置属性解析的String contextId = this.getContextId(beanFactory, attributes);String name = this.getName(attributes);// 嘿嘿, 外壳是用 FeignClientFactoryBean 来包装的FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();factoryBean.setBeanFactory(beanFactory);factoryBean.setName(name);factoryBean.setContextId(contextId);factoryBean.setType(clazz);// 是否开启配置动态刷新这个跟 feign.client.refresh-enabled 有关默认是 falsefactoryBean.setRefreshableClient(this.isClientRefreshEnabled());BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {factoryBean.setUrl(this.getUrl(beanFactory, attributes));factoryBean.setPath(this.getPath(beanFactory, attributes));factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));Object fallback = attributes.get("fallback");// 两个异常钩子if (fallback != null) {factoryBean.setFallback(fallback instanceof Class ? (Class)fallback : ClassUtils.resolveClassName(fallback.toString(), (ClassLoader)null));}Object fallbackFactory = attributes.get("fallbackFactory");if (fallbackFactory != null) {factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class)fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), (ClassLoader)null));}return factoryBean.getObject();});// 装配模式=2 也就是根据类型装配 和 mabatis 里的 MapperFactoryBean 异曲同工definition.setAutowireMode(2);// 延迟加载的 也就是没有人依赖这个 FeignClient 它就不会创建 很合理definition.setLazyInit(true);// 校验属性 主要是检查两个失败回调的设置this.validate(attributes);AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();beanDefinition.setAttribute("factoryBeanObjectType", className);beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);// 哟 还能设置主的boolean primary = (Boolean)attributes.get("primary");beanDefinition.setPrimary(primary);String[] qualifiers = this.getQualifiers(attributes);if (ObjectUtils.isEmpty(qualifiers)) {qualifiers = new String[]{contextId + "FeignClient"};}BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);// 如果开启了动态配置刷新的话 会更改 BeanDefinition 的 scope 来进行增强this.registerOptionsBeanDefinition(registry, contextId); }
别看这么二三十行代码,它东西还挺全乎,我们看看它取了哪些重要属性哈:
(1)contextId
取的就是注解里的 contextId 属性值,没有的话就等于 name 的属性值,并且它配有 BeanFactory 的 resolve,也就是说支持 spring 的表达式 类似我们平时 @Value 或者配置文件里的表达式写法
private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {String contextId = (String)attributes.get("contextId");// 没有 contextId 那么 contextId 就等于 name 属性的值if (!StringUtils.hasText(contextId)) {return this.getName(attributes);} else {// 设置了的话,通过 getName 检查一下 检查的方式主要是 加上协议前缀然后 URI.host 一下 这是为什么的 我不得其解contextId = this.resolve(beanFactory, contextId);return getName(contextId);} } static String getName(String name) {if (!StringUtils.hasText(name)) {return "";} else {String host = null;try {String url;if (!name.startsWith("http://") && !name.startsWith("https://")) {url = "http://" + name;} else {url = name;}host = (new URI(url)).getHost();} catch (URISyntaxException var3) {}Assert.state(host != null, "Service id not legal hostname (" + name + ")");return name;} }
(2)name
我们继续看看 name 的获取,可以看到顺序获取的优先级:serviceId > name > value:
String getName(Map<String, Object> attributes) {return this.getName((ConfigurableBeanFactory)null, attributes); } // 重载 String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {// 先获取 serviceId 的属性值String name = (String)attributes.get("serviceId");// 没有的话 再获取 name的值if (!StringUtils.hasText(name)) {name = (String)attributes.get("name");}// 还没有的话 获取 value的值if (!StringUtils.hasText(name)) {name = (String)attributes.get("value");}// 表达式解析 虽然beanFactory为空,它有 environment 进行解析name = this.resolve(beanFactory, name);// 一样还是要检查一下 URI.host 还是不得其解return getName(name); } static String getName(String name) {if (!StringUtils.hasText(name)) {return "";} else {String host = null;try {String url;if (!name.startsWith("http://") && !name.startsWith("https://")) {url = "http://" + name;} else {url = name;}host = (new URI(url)).getHost();} catch (URISyntaxException var3) {}Assert.state(host != null, "Service id not legal hostname (" + name + ")");return name;} }
(3)url 的获取
private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {// 一样解析表达式String url = this.resolve(beanFactory, (String)attributes.get("url"));// url 通过 URL 解析 这个我能理解 上边那俩我就不太理解为什么要用 URL.hostreturn getUrl(url); } static String getUrl(String url) {if (StringUtils.hasText(url) && (!url.startsWith("#{") || !url.contains("}"))) {// 会补 http:// if (!url.contains("://")) {url = "http://" + url;}try {new URL(url);} catch (MalformedURLException var2) {throw new IllegalArgumentException(url + " is malformed", var2);}}return url; }
(4)path 路径的获取
private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {String path = this.resolve(beanFactory, (String)attributes.get("path"));return getPath(path); } static String getPath(String path) {if (StringUtils.hasText(path)) {// 去除多余空格path = path.trim();// 贴心自动补 / 前缀if (!path.startsWith("/")) {path = "/" + path;}// 去掉最后的 /if (path.endsWith("/")) {path = path.substring(0, path.length() - 1);}}return path; }
BeanDefinition 是 FactoryBean 方式的,类型为FeignClientFactoryBean,还有 lazyInit 为 true 表示懒加载开启,装配模式=2表示根据类型自动装配,当多个冲突的时候还可以设置 Primary,最后还有一个重要的就是配置热加载的,这个就涉及到 spring 中Scope 的原理哈。这里就不详细解释了,后序单独讲这个 Scope。
那么最后我们画个图小小的捋一下:
大家看源码一定要画图,真的,不画图的话你后续再回忆的时候,又要一点点再看起,这样当你看完画个思路图,下次看的时候直接看图就大概能想起来怎么个过程,是不是。