前言

前段时间和敌人聊天,他说他部门老大给他提了一个需要,这个需要的背景是这样,他们开发环境和测试环境共用一套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替换成具体环境
@Configurationpublic 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工厂类
@Componentpublic 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实现类
@Componentpublic 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