聊聊如何根据环境动态指定feign调用服务名

前言

前段时间和朋友聊天,他说他部门老大给他提了一个需求,这个需求的背景是这样,他们开发环境和测试环境共用一套eureka,服务提供方的serviceId加环境后缀作为区分,比如用户服务其开发环境serviceId为user_dev,测试环境为user_test。每次服务提供方发布的时候,会根据环境变量,自动变更serviceId。

消费方feign调用时,直接通过

@FeignClient(name = "user_dev")

来进行调用,因为他们是直接把feignClient的name直接写死在代码里,导致他们每次发版到测试环境时,要手动改name,比如把user_dev改成user_test,这种改法在服务比较少的情况下,还可以接受,一旦服务一多,就容易改漏,导致本来该调用测试环境的服务提供方,结果跑去调用开发环境的提供方。

他们的老大给他提的需求是,消费端调用需要自动根据环境调用到相应环境的服务提供方。

下面就介绍朋友通过百度搜索出来的几种方案,以及后面我帮朋友实现的另一种方案

方案一:通过feign拦截器+url改造

1、在API的URI上做一下特殊标记

@FeignClient(name = "feign-provider")
public interface FooFeignClient {

    @GetMapping(value = "//feign-provider-$env/foo/{username}")
    String foo(@PathVariable("username") String username);
}

这边指定的URI有两点需要注意的地方

  • 一是前面“//”,这个是由于feign
    template不允许URI有“http://"开头,所以我们用“//”标记为后面紧跟着服务名称,而不是普通的URI

  • 二是“$env”,这个是后面要替换成具体的环境

2、在RequestInterceptor中查找到特殊的变量标记,把
$env替换成具体环境

@Configuration
public class InterceptorConfig {

    @Autowired
    private Environment environment;

    @Bean
    public RequestInterceptor cloudContextInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                String url = template.url();
                if (url.contains("$env")) {
                    url = url.replace("$env", route(template));
                    System.out.println(url);
                    template.uri(url);
                }
                if (url.startsWith("//")) {
                    url = "http:" + url;
                    template.target(url);
                    template.uri("");
                }


            }


            private CharSequence route(RequestTemplate template) {
                // TODO 你的路由算法在这里
                return environment.getProperty("feign.env");
            }
        };
    }

}

这种方案是可以实现,但是朋友没有采纳,因为朋友的项目已经是上线的项目,通过改造url,成本比较大。就放弃了

该方案由博主无级程序员提供,下方链接是他实现该方案的链接

https://blog.csdn.net/weixin_45357522/article/details/104020061

方案二:重写RouteTargeter

1、API的URL中定义一个特殊的变量标记,形如下

@FeignClient(name = "feign-provider-env")
public interface FooFeignClient {

    @GetMapping(value = "/foo/{username}")
    String foo(@PathVariable("username") String username);
}

2、以HardCodedTarget为基础,实现Targeter

public class RouteTargeter implements Targeter {
    private Environment environment;
    public RouteTargeter(Environment environment){
       this.environment = environment;
    }   
    
    /**
     * 服务名以本字符串结尾的,会被置换为实现定位到环境
     */
    public static final String CLUSTER_ID_SUFFIX = "env";

    @Override
    public <T> T target(FeignClientFactoryBean factory, Builder feign, FeignContext context,
            HardCodedTarget<T> target) {

        return feign.target(new RouteTarget<>(target));
    }

    public static class RouteTarget<T> implements Target<T> {
        Logger log = LoggerFactory.getLogger(getClass());
        private Target<T> realTarget;

        public RouteTarget(Target<T> realTarget) {
            super();
            this.realTarget = realTarget;
        }

        @Override
        public Class<T> type() {
            return realTarget.type();
        }

        @Override
        public String name() {
            return realTarget.name();
        }

        @Override
        public String url() {
            String url = realTarget.url();
            if (url.endsWith(CLUSTER_ID_SUFFIX)) {
                url = url.replace(CLUSTER_ID_SUFFIX, locateCusterId());
                log.debug("url changed from {} to {}", realTarget.url(), url);
            }
            return url;
        }

        /**
         * @return 定位到的实际单元号
         */
        private String locateCusterId() {
            // TODO 你的路由算法在这里
            return environment.getProperty("feign.env");
        }

        @Override
        public Request apply(RequestTemplate input) {
            if (input.url().indexOf("http") != 0) {
                input.target(url());
            }
            return input.request();

        }

    }
}

3、 使用自定义的Targeter实现代替缺省的实现

    @Bean
    public RouteTargeter getRouteTargeter(Environment environment) {
        return new RouteTargeter(environment);
    }

该方案适用于spring-cloud-starter-openfeign为3.0版本以上,3.0版本以下得额外加

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

Targeter 这个接口在3.0之前的包是属于package范围,因此没法直接继承。朋友的springcloud版本相对比较低,后面基于系统稳定性的考虑,就没有贸然升级springcloud版本。因此这个方案朋友也没采纳

该方案仍然由博主无级程序员提供,下方链接是他实现该方案的链接

https://blog.csdn.net/weixin_45357522/article/details/106745468

方案三:使用FeignClientBuilder

这个类的作用如下

/**
 * A builder for creating Feign clients without using the {@link FeignClient} annotation.
 * <p>
 * This builder builds the Feign client exactly like it would be created by using the
 * {@link FeignClient} annotation.
 *
 * @author Sven Döring
 */

他的功效是和@FeignClient是一样的,因此就可以通过手动编码的方式

1、编写一个feignClient工厂类

@Component
public class DynamicFeignClientFactory<T> {

    private FeignClientBuilder feignClientBuilder;

    public DynamicFeignClientFactory(ApplicationContext appContext) {
        this.feignClientBuilder = new FeignClientBuilder(appContext);
    }

    public T getFeignClient(final Class<T> type, String serviceId) {
        return this.feignClientBuilder.forType(type, serviceId).build();
    }
}

2、编写API实现类

@Component
public class BarFeignClient {

    @Autowired
    private DynamicFeignClientFactory<BarService> dynamicFeignClientFactory;

    @Value("${feign.env}")
    private String env;

    public String bar(@PathVariable("username") String username){
        BarService barService = dynamicFeignClientFactory.getFeignClient(BarService.class,getBarServiceName());

        return barService.bar(username);
    }


    private String getBarServiceName(){
        return "feign-other-provider-" + env;
    }
}

本来朋友打算使用这种方案了,最后没采纳,原因后面会讲。

该方案由博主lotern提供,下方链接为他实现该方案的链接
https://my.oschina.net/kaster/blog/4694238

方案四:feignClient注入到spring之前,修改FeignClientFactoryBean

实现核心逻辑:在feignClient注入到spring容器之前,变更name

如果有看过spring-cloud-starter-openfeign的源码的朋友,应该就会知道openfeign通过FeignClientFactoryBean中的getObject()生成具体的客户端。因此我们在getObject托管给spring之前,把name换掉

1、在API定义一个特殊变量来占位

@FeignClient(name = "feign-provider-env",path = EchoService.INTERFACE_NAME)
public interface EchoFeignClient extends EchoService {
}

注: env为特殊变量占位符

2、通过spring后置器处理FeignClientFactoryBean的name

public class FeignClientsServiceNameAppendBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware , EnvironmentAware {

    private ApplicationContext applicationContext;

    private Environment environment;

    private AtomicInteger atomicInteger = new AtomicInteger();

    @SneakyThrows
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if(atomicInteger.getAndIncrement() == 0){
            String beanNameOfFeignClientFactoryBean = "org.springframework.cloud.openfeign.FeignClientFactoryBean";
            Class beanNameClz = Class.forName(beanNameOfFeignClientFactoryBean);

            applicationContext.getBeansOfType(beanNameClz).forEach((feignBeanName,beanOfFeignClientFactoryBean)->{
                try {
                    setField(beanNameClz,"name",beanOfFeignClientFactoryBean);
                    setField(beanNameClz,"url",beanOfFeignClientFactoryBean);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                System.out.println(feignBeanName + "-->" + beanOfFeignClientFactoryBean);
            });
        }


        return null;
    }

    private  void setField(Class clazz, String fieldName, Object obj) throws Exception{

        Field field = ReflectionUtils.findField(clazz, fieldName);
        if(Objects.nonNull(field)){
            ReflectionUtils.makeAccessible(field);
            Object value = field.get(obj);
            if(Objects.nonNull(value)){
                value = value.toString().replace("env",environment.getProperty("feign.env"));
                ReflectionUtils.setField(field, obj, value);
            }


        }



    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

注: 这边不能直接用FeignClientFactoryBean.class,因为FeignClientFactoryBean这个类的权限修饰符是default。因此得用反射。

其次只要是在bean注入到spring IOC之前提供的扩展点,都可以进行FeignClientFactoryBean的name替换,不一定得用BeanPostProcessor

3、使用import注入

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsServiceNameAppendEnvConfig.class)
public @interface EnableAppendEnv2FeignServiceName {


}

4、在启动类上加上@EnableAppendEnv2FeignServiceName

总结

后面朋友采用了第四种方案,主要这种方案相对其他三种方案改动比较小。

第四种方案朋友有个不解的地方,为啥要用import,直接在spring.factories配置自动装配,这样就不用在启动类上@EnableAppendEnv2FeignServiceName
不然启动类上一堆@Enable看着恶心,哈哈。

我给的答案是开了一个显眼的@Enable,是为了让你更快知道我是怎么实现,他的回答是那还不如你直接告诉我怎么实现就好。我竟然无言以对。

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-feign-servicename-route

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

推荐阅读更多精彩内容