在非 SpringBoot 工程中实现 Eureka 构建 Feign 服务
背景
最近有个需求需要嵌入至其他业务中,其中有个数据需要通过 Eureka 获取,正常情况下使用 @EnableDiscoveryClient
和 @EnableFeignClients
即可获取服务接口实例。可是由于我使用了 BeanPostProcessor 对注解进行了切片代理,并且目标项目也存在非 SpringCloud 项目,那么便只能通过底层 API 构建服务方实例了。
方案
EurekaClient 提供了服务发现注册功能,但是在 Feign 所接收的属性中并没有用于负载均衡的,其中最有可能的就是 Client 属性了,这是用于负责服务请求的模块,那么就需要调研下是否能够通过 EurekaClient 构建出一个带有负载均衡功能的客户端。
创建 LBClientFactory
Feign 的官方 Wiki 提供了构建服务实例的 方法,但 Feign 并不提供构建所需属性的实现,而是由其他框架适配 Feign 所提供的接口来实现每个功能。
在分析 Ribbon 的 Client 实现 RibbonClient
,其构造器接收一个 LBClientFactory
参数,负载均衡的功能就是由它提供。
通过搜索引擎发现了打通 Robbion 与 EurekaClient 的实现方式,需要引入依赖 <artifactId>ribbon-eureka</artifactId>
。
private Client newClient(String vipAddress, Provider<EurekaClient> provider) {
ServerList<DiscoveryEnabledServer> list = new DiscoveryEnabledNIWSServerList(vipAddress, provider);
ServerListFilter<DiscoveryEnabledServer> filter = new ZoneAffinityServerListFilter<DiscoveryEnabledServer>();
ZoneAwareLoadBalancer<DiscoveryEnabledServer> lb = LoadBalancerBuilder.<DiscoveryEnabledServer>newBuilder()
.withDynamicServerList(list)
.withRule(new AvailabilityFilteringRule())
.withServerListFilter(filter)
.buildDynamicServerListLoadBalancer();
return RibbonClient.builder().lbClientFactory(new LBClientFactory() {
@Override
public LBClient create(String clientName) {
return LBClient.create(lb, ClientFactory.getNamedConfig(clientName));
}
}).build();
}
其中需要使用 DiscoveryEnabledNIWSServerList
用于发现服务集合,它接收两个提供两个参数,一个是项目的 vipAddress,一个是 Provider
创建 EurekaClient
在 Eureka 的官方 Wiki 中提供了 EurekaClient 的
构造方式,需要传入 EurekaClientConfig
和 EurekaInstanceConfig
用来提供模块的配置信息。
在 netflix 提供的 eureka-client 模块下,提供了 DefaultEurekaClientConfig
和 MyDataCenterInstanceConfig
。
这两个类默认将会读取 eureka-client.properties 下的配置,可以通过设置 eureka.client.props
参数更改读取的配置文件,也可以使用 ConfigurationManager
的静态方法来指定读取配置文件。
除此之外,spring-cloud 也提供了相应的实现类 EurekaClientConfigBean
和 EurekaInstanceConfigBean
,通过 spring 的配置资源解析器将属性装配成对象使用。
入参已经拿到了,但是还未结束,在跟进 DiscoveryClient
代码实现中,发现他在构造方法中会对属性进行静态保存。
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
在这个项目中,有可能存在业务项目内部与嵌入模块使用了不同的 Eureka 服务,为了保持与业务项目的隔离,这里需要对官方提供的代码模版进行点改造。
private static volatile EurekaClient eurekaClient = null;
public EurekaClient initEurekaClient(EurekaInstanceConfig instanceConfig, EurekaClientConfig clientConfig) {
if (null != eurekaClient) {
return eurekaClient;
}
synchronized (DiscoveryManager.class) {
if (null == eurekaClient) {
DiscoveryClient discoveryClient = DiscoveryManager.getInstance().getDiscoveryClient();
EurekaClientConfig eurekaClientConfig = DiscoveryManager.getInstance().getEurekaClientConfig();
EurekaInstanceConfig eurekaInstanceConfig = DiscoveryManager.getInstance().getEurekaInstanceConfig();
// create the client
InstanceInfo instanceInfo = new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get();
ApplicationInfoManager infoManager = new ApplicationInfoManager(instanceConfig, instanceInfo);
infoManager.setInstanceStatus(InstanceInfo.InstanceStatus.STARTING);
EurekaClient client = new DiscoveryClient(infoManager, clientConfig);
infoManager.setInstanceStatus(InstanceInfo.InstanceStatus.UP);
eurekaClient = client;
DiscoveryManager.getInstance().setDiscoveryClient(discoveryClient);
DiscoveryManager.getInstance().setEurekaClientConfig(eurekaClientConfig);
DiscoveryManager.getInstance().setEurekaInstanceConfig(eurekaInstanceConfig);
}
}
return eurekaClient;
}
在实际开发中可通过 DiscoveryManager 的静态属性对其进行管理的,避免重复构建。但是在这个场景下需要保证与业务之间的隔离,则以这种方式构建。
创建 Provider
在创建 LBClientFactory 的过程中,我们需要构建 DiscoveryEnabledNIWSServerList 用于服务的发现,由于构造方法接收的参数为 Provider<EurekaClient>
,这里还需要对 EurekaClient 再经过包装后使用。
new Provider<EurekaClient>() {
@Override
public EurekaClient get() {
return eurekaClient;
}
};
构造服务实例
通过上述步骤基本完成了一个 EurekaClient 到 RobbionClient 的各个过程,最后代入 Feign 的创建方法中便可构造使用 Eureka 进行服务发现的实例。
public <T> T getInstance(Class<T> target, String namespace, Provider<EurekaClient> provider) {
return Feign.builder()
.client(newClient(namespace, provider))
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Request.Options(1000, 3000))
.logLevel(Logger.Level.FULL)
.target(target, "http://" + namespace);
}