聊聊如何根據(jù)環(huán)境動態(tài)指定feign調用服務名

前言

前段時間和朋友聊天,他說他部門老大給他提了一個需求信姓,這個需求的背景是這樣,他們開發(fā)環(huán)境和測試環(huán)境共用一套eureka桐愉,服務提供方的serviceId加環(huán)境后綴作為區(qū)分财破,比如用戶服務其開發(fā)環(huán)境serviceId為user_dev,測試環(huán)境為user_test。每次服務提供方發(fā)布的時候从诲,會根據(jù)環(huán)境變量左痢,自動變更serviceId。

消費方feign調用時系洛,直接通過

@FeignClient(name = "user_dev")

來進行調用俊性,因為他們是直接把feignClient的name直接寫死在代碼里,導致他們每次發(fā)版到測試環(huán)境時描扯,要手動改name定页,比如把user_dev改成user_test,這種改法在服務比較少的情況下绽诚,還可以接受典徊,一旦服務一多,就容易改漏恩够,導致本來該調用測試環(huán)境的服務提供方卒落,結果跑去調用開發(fā)環(huán)境的提供方。

他們的老大給他提的需求是蜂桶,消費端調用需要自動根據(jù)環(huán)境調用到相應環(huán)境的服務提供方儡毕。

下面就介紹朋友通過百度搜索出來的幾種方案,以及后面我?guī)团笥褜崿F(xiàn)的另一種方案

方案一:通過feign攔截器+url改造

1扑媚、在API的URI上做一下特殊標記

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

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

這邊指定的URI有兩點需要注意的地方

  • 一是前面“//”腰湾,這個是由于feign
    template不允許URI有“http://"開頭雷恃,所以我們用“//”標記為后面緊跟著服務名稱,而不是普通的URI

  • 二是“$env”费坊,這個是后面要替換成具體的環(huán)境

2倒槐、在RequestInterceptor中查找到特殊的變量標記,把
$env替換成具體環(huán)境

@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("http://")) {
                    url = "http:" + url;
                    template.target(url);
                    template.uri("");
                }


            }


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

}

這種方案是可以實現(xiàn)葵萎,但是朋友沒有采納导犹,因為朋友的項目已經(jīng)是上線的項目,通過改造url羡忘,成本比較大。就放棄了

該方案由博主無級程序員提供磕昼,下方鏈接是他實現(xiàn)該方案的鏈接

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為基礎漫雕,實現(xiàn)Targeter

public class RouteTargeter implements Targeter {
    private Environment environment;
    public RouteTargeter(Environment environment){
       this.environment = environment;
    }   
    
    /**
     * 服務名以本字符串結尾的,會被置換為實現(xiàn)定位到環(huán)境
     */
    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實現(xiàn)代替缺省的實現(xiàn)

    @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版本相對比較低魁蒜,后面基于系統(tǒng)穩(wěn)定性的考慮,就沒有貿然升級springcloud版本吩翻。因此這個方案朋友也沒采納

該方案仍然由博主無級程序員提供兜看,下方鏈接是他實現(xiàn)該方案的鏈接

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實現(xiàn)類

@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提供,下方鏈接為他實現(xiàn)該方案的鏈接
https://my.oschina.net/kaster/blog/4694238

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

實現(xiàn)核心邏輯:在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,是為了讓你更快知道我是怎么實現(xiàn)你虹,他的回答是那還不如你直接告訴我怎么實現(xiàn)就好绘搞。我竟然無言以對。

demo鏈接

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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末傅物,一起剝皮案震驚了整個濱河市夯辖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌董饰,老刑警劉巖蒿褂,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異尖阔,居然都是意外死亡贮缅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門介却,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谴供,“玉大人,你說我怎么就攤上這事齿坷」鸺。” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵永淌,是天一觀的道長崎场。 經(jīng)常有香客問我,道長遂蛀,這世上最難降的妖魔是什么谭跨? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上螃宙,老公的妹妹穿的比我還像新娘蛮瞄。我一直安慰自己,他們只是感情好谆扎,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布挂捅。 她就那樣靜靜地躺著,像睡著了一般堂湖。 火紅的嫁衣襯著肌膚如雪闲先。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天无蜂,我揣著相機與錄音伺糠,去河邊找鬼。 笑死酱讶,一個胖子當著我的面吹牛退盯,可吹牛的內容都是我干的。 我是一名探鬼主播泻肯,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼慰照!你這毒婦竟也來了灶挟?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤毒租,失蹤者是張志新(化名)和其女友劉穎稚铣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體墅垮,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡惕医,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了算色。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抬伺。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡喷众,死狀恐怖蔓彩,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情夭委,我是刑警寧澤若河,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布能岩,位于F島的核電站,受9級特大地震影響萧福,放射性物質發(fā)生泄漏拉鹃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望膏燕。 院中可真熱鬧钥屈,春花似錦、人聲如沸煌寇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阀溶。三九已至腻脏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間银锻,已是汗流浹背永品。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留击纬,地道東北人鼎姐。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像更振,于是被迫代替她去往敵國和親炕桨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內容