Spring Cloud Feign 分析(二)之FeignClient注解实现版本兼容

使用过Spring Cloud Netflix组件的同学都知道,Netflix组件的版本兼容性几乎等于零,特别是大版本变化简直就是噩梦,所以本节主要讲解如何实现Feign的版本兼容,如何兼容SpringBoot1.x、SpringBoot2.x版本中的Feign使用!这样我们在SpringBoot1.x版本使用@FeignClient在后续升级到SpringBoot2.x之后也不需要我们进行单独修改,毕竟现在微服务众多,全部重新使用SpringBoot2.x版本的FeignClient也是一件不小的事情,毕竟你改了代码那就可能出现问题。所以这一节我们主要提供注解版本的兼容方式(基本零修改),顺带分析下FeignClientsRegistrar部分原理!


@EnableFeignClients注解

@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 {};
}

这里我们先看看FeignClient默认实现,通过在启动类上面注解这个类即可开启FeignClient客户端,那么这里我们看看原始FeignClientsRegistrar做了什么事情,为什么就不能兼容以前的版本呢?


FeignClientsRegistrar#registerDefaultConfiguration

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    ......
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
    //根据@EnableFeignClients中参数defaultConfiguration注册FeignClient的默认配置(FeignClientsConfiguration)
    private void registerDefaultConfiguration(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        //注意这里的获取的EnableFeignClients.class.getName()这个属性
        //我们后面要做自定义注解的映射
        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();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }
    ......
}

这里我们简单的讲解下,从@EnableFeignClients注解中获取defaultConfiguration参数并生产默认配置,其中这个地方关注点metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true),后面我们的自定义注解会和这个形成映射关系


FeignClientsRegistrar#registerFeignClients

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    ......
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
    //注册@FeignClient类到IOC容器中
    public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {

        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            //获取扫描器,
            ClassPathScanningCandidateComponentProvider scanner = getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            //扫描org.springframework.cloud.openfeign.FeignClient注解类
            scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            Set<String> basePackages = getBasePackages(metadata);
            for (String basePackage : basePackages) {
                //添加满足条件的BeanDefinition
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }
        else {
            for (Class<?> clazz : clients) {
                candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
            }
        }
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(),
                        "@FeignClient can only be specified on an interface");
                //获取org.springframework.cloud.openfeign.FeignClient对应的属性
                Map<String, Object> attributes = annotationMetadata
                        .getAnnotationAttributes(FeignClient.class.getCanonicalName());

                String name = getClientName(attributes);
                registerClientConfiguration(registry, name,
                        attributes.get("configuration"));
                //注册到IOC容器
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

这个地方我们也简单的解释下具体做了哪些事情

  1. 扫描标注了org.springframework.cloud.openfeign.FeignClient注解类
  2. 通过basePackages路径添加添加满足条件的BeanDefinition
  3. 通过BeanDefinition集合获取org.springframework.cloud.openfeign.FeignClient注解对应的属性
  4. 注册Bean到IOC容器中

FeignClient注解实现版本兼容

package org.springframework.cloud.netflix.feign;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@org.springframework.cloud.openfeign.FeignClient
public @interface FeignClient {

    /**
     * The name of the service with optional protocol prefix. Synonym for {@link #name()
     * name}. A name must be specified for all clients, whether or not a url is provided.
     * Can be specified as property key, eg: ${propertyKey}.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "name")
    String value() default "";

    /**
     * The service id with optional protocol prefix. Synonym for {@link #value() value}.
     *
     * @deprecated use {@link #name() name} instead
     */
    @Deprecated
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "serviceId")
    String serviceId() default "";

    /**
     * The service id with optional protocol prefix. Synonym for {@link #value() value}.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "value")
    String name() default "";

    /**
     * Sets the <code>@Qualifier</code> value for the feign client.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "qualifier")
    String qualifier() default "";

    /**
     * An absolute URL or resolvable hostname (the protocol is optional).
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "url")
    String url() default "";

    /**
     * Whether 404s should be decoded instead of throwing FeignExceptions
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "decode404")
    boolean decode404() default false;

    /**
     * A custom <code>@Configuration</code> for the feign client. Can contain override
     * <code>@Bean</code> definition for the pieces that make up the client, for instance
     * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
     *
     * @see FeignClientsConfiguration for the defaults
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "configuration")
    Class<?>[] configuration() default {};

    /**
     * Fallback class for the specified Feign client interface. The fallback class must
     * implement the interface annotated by this annotation and be a valid spring bean.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "fallback")
    Class<?> fallback() default void.class;

    /**
     * Define a fallback factory for the specified Feign client interface. The fallback
     * factory must produce instances of fallback classes that implement the interface
     * annotated by {@link FeignClient}. The fallback factory must be a valid spring
     * bean.
     *
     * @see feign.hystrix.FallbackFactory for details.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "fallbackFactory")
    Class<?> fallbackFactory() default void.class;

    /**
     * Path prefix to be used by all method-level mappings. Can be used with or without
     * <code>@RibbonClient</code>.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "path")
    String path() default "";

    /**
     * Whether to mark the feign proxy as a primary bean. Defaults to false.
     */
    @AliasFor(annotation = org.springframework.cloud.openfeign.FeignClient.class, attribute = "primary")
    boolean primary() default true;
}

这里我们采用路径覆盖大法,我们重新定义一个org.springframework.cloud.netflix.feign这个包名,然后定义一个FeignClient注解类,然后我们在这个类上在引入一个注解@org.springframework.cloud.openfeign.FeignClient,把SpringBoot2.x版本的FeignClient引入进来,这样我们就实现了版本兼容,我们以前的SpringBoot1.x版本的可以不用修改就可以实现版本兼容。然后在启动类上面使用标准的@EnableFeignClients注解

注意事项

spring:
  main:
    allow-bean-definition-overriding: true

在SpringBoot2.1之前,这个开关默认是打开的,及可以重复定义Bean,但是在SpringBoot2.1之后这个配置默认是false,所以如果我们的SpringBoot版本为2.1之后的,那么这个参数需要设置为true,及允许后面的Bean可以覆盖之前相同名称的Bean,因为这个地方registerClientConfiguration会重复定义Bean,建议根据情况配置,笔者这里的业务默认都打开了的,毕竟我们的FeignClient配置是一样的,所以允许重复定义。


已经讲解到这里了,这一节我们通过路径覆盖大法,重写老版本的netflix.FeignClient注解在其之上加上新版本的openfeign.FeignClient注解来实现兼容,下一节我们将通过继承FeignClientsRegistrar来实现的方式,这种方式的实现能让我们更加清楚的了解到@FeignClient的注册方式,如果觉得总结不错就点赞关注吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容