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

16次阅读

共计 7227 个字符,预计需要花费 19 分钟才能阅读完成。

前言

前段时间和敌人聊天,他说他部门老大给他提了一个需要,这个需要的背景是这样,他们开发环境和测试环境共用一套 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

正文完
 0