✏️ Pic by Alibaba Tech on Facebook
技术实践的作用在于:除了用于构建业务,也是为了验证某项技术或框架是否值得大规模推广。
这是《RSocket 从入门到落地》系列文章的第二篇,来一起实践一下如何基于RSocket来实现Spring Config Server的功能。该系列文章作者是阿里巴巴资深技术专家雷卷,第一篇回顾「传送门」。第二篇
阅读本系列文章,需要大家对Java有了解,其中可能会涉及到Kotlin,有少部分C++和Python(不做要求),如果了解Spring Boot则最好。
?
配置推送一直都是应用部署的必要环节,不同的环境都会有不同的配置信息,如本地开发连接到本地127的数据库,线上环境则需要不一样的配置,这里我们不一一列举不同配置项,一句话,软件运行需要加载一定的配置项。
对Spring Boot应用来说,使用Config Server非常简单,如Spring Cloud Config Server(基于git),Spring Cloud Consul等都支持配置推送,所以整合也非常简单。 今天我们看看如何基于RSocket来实现Spring Config Server的功能。声明一下,接下来的是Demo,只是实现基础功能,不是说其他的产品都没有技术难度,只是实现通讯这一块,配置的一致性,版本控制等功能,还是要花功夫的。
配置推送的核心
配置推送的功能主要是两个:
主动获取配置: request/response,这个是非常有必要的,当有一些错误,需要去主动刷新配置。
配置更新推送:当配置有变化时,将新的配置推送下来,也就是通常所说的 request/stream
RSocket 支持大部分通讯模型的,所以我们写一个最基本的RSocket Handler就可以了。出于接下来讲解的需要,我将50行不到的实现代码都贴出来(我应该使用Kotlin的,这样可能就只有25行啦 :) ),后面我们再进行分析一下:
public class RSocketConfigHandlerImpl extends AbstractRSocket implements RSocketConfigHandler { private static String CONFIG_TYPE = "text/x-java-properties"; private Map> configProcessorStore = new ConcurrentHashMap<>(); private Map configSnapshot = new ConcurrentHashMap<>(); public RSocketConfigHandlerImpl() { refresh("app1", "name=leijuan"); } public Mono requestResponse(Payload payload) { String appName = "app1"; //payload.getDataUtf8(); if (!configSnapshot.containsKey(appName)) { initApp(appName); } return Mono.just(DefaultPayload.create(configSnapshot.get(appName), CONFIG_TYPE)); } public Flux requestStream(Payload payload) { String appName = "app1"; //payload.getDataUtf8(); return Flux.create(sink -> { configProcessorStore.get(appName).subscribe(config -> { sink.next(DefaultPayload.create(config, CONFIG_TYPE)); }); }); } public String getLastConfig(String appName) { return configSnapshot.get(appName); } public void refresh(String appName, String config) { if (!configSnapshot.containsKey(appName)) { initApp(appName); } configSnapshot.put(appName, config); configProcessorStore.get(appName).onNext(config); } private void initApp(String appName) { synchronized (this) { configSnapshot.put(appName, ""); configProcessorStore.put(appName, ReplayProcessor.cacheLast()); } }}
上述的代码非常简单,我们稍微解释一下:
配置项基于properties文本文件格式,也是spring boot通用的;
requestResponse返回该应用最新的配置项值,从configSnapshot这个Map数据结构获取;
requestStream的处理稍微有一点技巧,就是要选择Reactor的ReplayProcessor,然后使用ReplayProcessor.cacheLast()创建该ReplayProcessor,表示缓存配置流中的最新的一个值,这样应用启动后,接收到的配置更新就是最新的。一个应用名称对应一个ReplayProcessor就可以了;
接下来我们做一个接口,叫做refresh,负责刷新配置;
通过RSocket和Reactor的配合,就是这么简单,接下来就是启动RSocket Server,这在第一篇文章中介绍过,这里贴一下代码,主要就是和Spring Boot整合的。
@Configurationpublic class RSocketAutoConfiguration { private Logger log = LoggerFactory.getLogger(RSocketAutoConfiguration.class); @Bean public RSocketConfigHandlerImpl rsocketHandler() { return new RSocketConfigHandlerImpl(); } @Bean(destroyMethod = "dispose") public Disposable rsocketResponder(RSocketConfigHandlerImpl rsocketHandler) { Disposable responder = RSocketFactory.receive() .acceptor((setup, sendingSocket) -> Mono.just(rsocketHandler)) .transport(TcpServerTransport.create("0.0.0.0", 42252)) .start() .subscribe(); log.info("RSocket Config Server started on 42252."); return responder; }}
就是创建RSocket Handler Bean,然后将该bean传给acceptor函数即可。
Spring Boot 应用如何继承配置推送
这个也是非常简单,spring-cloud-context已经支持自定义配置项加载,所以我们只需要实现一个基于RSocket的PropertySourceLocator就可以啦,代码如下:
public class RSocketConfigPropertySourceLocator implements PropertySourceLocator { private static Logger log = LoggerFactory.getLogger(RSocketConfigPropertySourceLocator.class); public static RSocket CONFIG_RSOCKET; public static Properties CONFIG_PROPERTIES = new Properties(); public static String LAST_CONFIG = ""; @Override public PropertySource> locate(Environment environment) { String applicationName = environment.getProperty("spring.application.name"); if (applicationName != null) { String cloudConfigUri = environment.getProperty("spring.cloud.config.rsocket.uri"); if (cloudConfigUri == null) { cloudConfigUri = "tcp://127.0.0.1:42252"; } if (CONFIG_RSOCKET == null) { initRsocket(cloudConfigUri); } CONFIG_RSOCKET.requestResponse(DefaultPayload.create(applicationName)) .subscribe(configPayload -> { refresh(configPayload.getDataUtf8()); }); } else { log.error("Please setup spring.application.name in application.properties"); } return new PropertiesPropertySource("rsocket", CONFIG_PROPERTIES); } public void initRsocket(String cloudConfigUri) { CONFIG_RSOCKET = RSocketFactory.connect() .transport(UriTransportRegistry.clientForUri(cloudConfigUri)) .start() .timeout(Duration.ofSeconds(3)) .block(); } public static void refresh(String config) { try { if (!LAST_CONFIG.equals(config)) { CONFIG_PROPERTIES.load(new ByteArrayInputStream(config.getBytes())); LAST_CONFIG = config; } } catch (Exception e) { log.error("Failed to refresh config", e); } }
这个类的核心就是从environment拿到RSocket Config Server的地址和应用名,然后再调用一次requestResponse拿到最新的配置项。
接下来,我们会再创建一个config listener,负责监听配置项变化,说监听是便于理解,其实就是对Flux的subscribe。代码如下:
public class RSocketConfigListener { private Logger log = LoggerFactory.getLogger(RSocketConfigListener.class); private ContextRefresher contextRefresher; private String applicationName; public RSocketConfigListener(ContextRefresher contextRefresher, String applicationName) { this.contextRefresher = contextRefresher; this.applicationName = applicationName; } @PostConstruct public void init() { CONFIG_RSOCKET.requestStream(DefaultPayload.create(applicationName)) .subscribe(payload -> { String config = payload.getDataUtf8(); log.info("Config refresh: " + config); refresh(payload.getDataUtf8()); contextRefresher.refresh(); }); }}
调用requestStream获取配置流,然后进行subscribe,收到新的配置项后,调用一下ContextRefresher bean的refresh()方法,就完成了。
应用集成配置刷新,这个非常简单,只需要@RefreshScope 即可。
服务注册和服务发现
服务注册和发现是另外一个非常重要的特性,但是在上述的config server代码中,你会发现如果应用在连接到配置服务器的时候,能够带上一些信息,如应用的原信息,提供的服务列表信息,加上还是长连接(包括心跳检测),那么实现这样的一个服务注册就非常简单了,代码如下:
CONFIG_RSOCKET = RSocketFactory.connect() .setupPayload(DefaultPayload.create("service=accout.svc.alibaba.net,ip=192.168.0.22,port=8080","application/json")) .transport(UriTransportRegistry.clientForUri(cloudConfigUri)) .start() .timeout(Duration.ofSeconds(3)) .block();
通过setupPayload()方法,我们向config server发送应用的信息,在server的acceptor()方法中可以将这些信息进行保存。 如果在创建连接时不能获取服务列表,可以在容器所有的ready后通过调用fireAndForget来通知注册中。 注册中心的实现中,当连接被关闭或者非法断开后,从服务注册中心进行删除。
这样一个服务注册就实现了,样例代码如下:
Disposable responder = RSocketFactory.receive() .acceptor((setupPayload, peerRSocket) -> { //进行 setup payload解析,将应用的信息注册 peerRSocket.onClose().subscribe(t -> { //连接关闭后的处理逻辑 }); return Mono.just(rsocketHandler); }) .transport(TcpServerTransport.create("0.0.0.0", 42252)) .start() .subscribe();
当然服务注册还有其他非常多的逻辑,但是我们解决了核心的注册、健康度检查和下线处理。其他的优雅上下线,可能还会涉及一些细节,以及要实现fireAndForgot()进行事件通知。
服务注册和服务发现
通过此文,我们介绍了通过RSocket的两个通讯模型来实现配置推送这个场景,并利用同一个连接和RSocket的connection setup特性,将服务注册也进行了简单实现。有兴趣的同学可以自己看一下,代码量非常小的。
本文作者:雷卷,GitHub ID linux-china,Java程序员,阿里巴巴资深技术专家。
/ 推荐另一篇值得细品的「云原生经验分享」 /
Photo by David Heslop on Unsplash
/ 推荐一个值得参与的「开发者活动」 /
©每周一推
第一时间获得下期分享
☟☟☟
Tips:
# 点下“好看”❤️
# 然后,公众号对话框内发送“雨伞”,试试手气??
# 本期奖品由采购不设限的「淘宝企业服务」赞助 ?