话不多说,上来就干
上一篇Soul网关—WebSocket同步数据—源码分析(六),我们通过源码,分析了soul网关的管理后台和soul网关之间的数据同步,这一篇,我们一起来分析一下通过ZooKeeper做数据同步的源码。
了解具体的数据同步之前,需要先了解同步配置和服务启动的时候,soul为了后续的同步数据都提前做了什么(提前剧透:就是提前准备好监听器,等进入使用状态时,网关可配置数据一旦有所更改,各司其职的监听器立刻开始处理自己的该做的事情)
环境配置
根据soul的官方文档,需要配置soul-admin模块的同步配置
soul:
sync: //同步策略配置
zookeeper: //同步数据采用zookeeper
url: localhost:2181 //zookeeper地址
sessionTimeout: 5000 //session超时时间
connectionTimeout: 2000 //连接超时时间
admin服务初始化
由于我也是第一次读soul的源码,我们来看看soul-admin模块哪里用到了这个配置项,通过idea搜索,我们找到这个类 org.dromara.soul.admin.config.DataSyncConfiguration
/**
* The type Data sync configuration.
*/
@Configuration
public class DataSyncConfiguration {
/**
* http long polling(default strategy).
*/
@Configuration
@ConditionalOnProperty(name = "soul.sync.http.enabled", havingValue = "true")
@EnableConfigurationProperties(HttpSyncProperties.class)
static class HttpLongPollingListener {...}
/**
* The type Zookeeper listener.
*/
@Configuration
@ConditionalOnProperty(prefix = "soul.sync.zookeeper", name = "url")
@Import(ZookeeperConfiguration.class)
static class ZookeeperListener {...}
/**
* The type Nacos listener.
*/
@Configuration
@ConditionalOnProperty(prefix = "soul.sync.nacos", name = "url")
@Import(NacosConfiguration.class)
static class NacosListener {...}
/**
* The WebsocketListener(default strategy).
*/
@Configuration
@ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(WebsocketSyncProperties.class)
static class WebsocketListener {...}
}
这是一个bean初始化配置类,我们可以看到,凡是在yml文件中soul.sync下可供配置的几种同步策略,都可以在这个类中找到使用的地方
类说明表示这是一个数据同步监听器初始化类,在soul-admin项目启动的时候,通过配置文件中不同的策略,将同步数据的初始化监听器注册到spring容器,这次我们关注的是zookeeper监听器初始化bean方法
/**
* The type Zookeeper listener.
*/
@Configuration
@ConditionalOnProperty(prefix = "soul.sync.zookeeper", name = "url")
@Import(ZookeeperConfiguration.class)
static class ZookeeperListener {
/**
* Config event listener data changed listener.
*
* @param zkClient the zk client
* @return the data changed listener
*/
@Bean
@ConditionalOnMissingBean(ZookeeperDataChangedListener.class)
public DataChangedListener zookeeperDataChangedListener(final ZkClient zkClient) {
return new ZookeeperDataChangedListener(zkClient);
}
/**
* Zookeeper data init zookeeper data init.
*
* @param zkClient the zk client
* @param syncDataService the sync data service
* @return the zookeeper data init
*/
@Bean
@ConditionalOnMissingBean(ZookeeperDataInit.class)
public ZookeeperDataInit zookeeperDataInit(final ZkClient zkClient, final SyncDataService syncDataService) {
return new ZookeeperDataInit(zkClient, syncDataService);
}
}
初始化了两个bean,一个是zookeeperDataInit,另一个是 zookeeperDataChangedListener
我们先来看zookeeperDataInit这个类
public class ZookeeperDataInit implements CommandLineRunner {
private final ZkClient zkClient;
private final SyncDataService syncDataService;
/**
* Instantiates a new Zookeeper data init.
*
* @param zkClient the zk client
* @param syncDataService the sync data service
*/
public ZookeeperDataInit(final ZkClient zkClient, final SyncDataService syncDataService) {
this.zkClient = zkClient;
this.syncDataService = syncDataService;
}
@Override
public void run(final String... args) {
String pluginPath = ZkPathConstants.PLUGIN_PARENT;
String authPath = ZkPathConstants.APP_AUTH_PARENT;
String metaDataPath = ZkPathConstants.META_DATA;
if (!zkClient.exists(pluginPath) && !zkClient.exists(authPath) && !zkClient.exists(metaDataPath)) {
syncDataService.syncAll(DataEventTypeEnum.REFRESH);
}
}
}
实现了CommandLineRunner这个类,重写了run()方法,接口CommandLineRunner是SpringBoot内置接口,表明该接口的用意就是,表明凡是实现了该接口的实现类,当被spring初始为bean放在spring容器中时,显示的申明调用run方法
说的简单的就是当 ZookeeperDataInit这个类被初始化为bean的时候,会有一个初始化调用run方法,我们可以在run方法中初始化我们想预装配的数据
@Override
public void run(final String... args) {
String pluginPath = ZkPathConstants.PLUGIN_PARENT;
String authPath = ZkPathConstants.APP_AUTH_PARENT;
String metaDataPath = ZkPathConstants.META_DATA;
// 当zookeeper检测 plugin/auth/metaData 三个节点是否都不存在,都不存在的时候,进行数据初始化同步
if (!zkClient.exists(pluginPath) && !zkClient.exists(authPath) && !zkClient.exists(metaDataPath)) {
syncDataService.syncAll(DataEventTypeEnum.REFRESH);
}
}
点进syncAll()方法,我们可以看到这个方法就是从数据库读取全量的数据,然后发布出去
@Override
public boolean syncAll(final DataEventTypeEnum type) {
appAuthService.syncData();
//从数据库捞插件的全量数据
List<PluginData> pluginDataList = pluginService.listAll();
//发布插件的初始化全量数据
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, type, pluginDataList));
//从数据库捞插件的全量数据
List<SelectorData> selectorDataList = selectorService.listAll();
//发布插件的初始化全量数据
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, type, selectorDataList));
//从数据库捞插件的全量数据
List<RuleData> ruleDataList = ruleService.listAll();
//发布插件的初始化全量数据
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, type, ruleDataList));
//发布从数据库捞出的全量元数据
metaDataService.syncData();
return true;
}
其实有兴趣的小伙伴可以一直跟着eventPublisher.publishEvent()这个方法点进去,这个应用事件发布器,是spring内置的类,一直点进去,其实最终还是调用的监听者的监听方法,所以各个监听事件器可以监听到。这不就形成了发布者和订阅者的消息闭环了吗。
这是spring的发布器直接调用监听器的代码,最重要的一行listener.onApplicationEvent(event),这里的监听器,开发者自己先实现类继承spring的应用事件监听器
@SuppressWarnings({"rawtypes", "unchecked"})
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
//调用监听器的监听事件方法,用户可以重写onApplicationEvent(event)方法,在里面实现用户自己监听到事件后的处理逻辑
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
// -> let's suppress the exception and just log a debug message.
Log logger = LogFactory.getLog(getClass());
if (logger.isTraceEnabled()) {
logger.trace("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}
我们再回来看ZookeeperDataChangedListener这个类,这个类在项目启动的时候,并没做啥具体的事情,但是这个接口是后面增量数据更新的核心监听器。这个监听类实现了接口DataChangedListener,而这个接口看名字就知道是数据更新监听接口,看了一下这个接口在soul中的实现类,一共有四种,正好对应官方文档中定义的四种数据同步通知策略:
- http长连接
- websocket
- zookeeper
- nacos
这个接口同样需要实现四个维度数据改变后,监听器需要执行的具体操作,分别是权限、插件、选择器还有元数据
public interface DataChangedListener {
/**
* invoke this method when AppAuth was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onAppAuthChanged(List<AppAuthData> changed, DataEventTypeEnum eventType) {
}
/**
* invoke this method when Plugin was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onPluginChanged(List<PluginData> changed, DataEventTypeEnum eventType) {
}
/**
* invoke this method when Selector was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onSelectorChanged(List<SelectorData> changed, DataEventTypeEnum eventType) {
}
/**
* On meta data changed.
*
* @param changed the changed
* @param eventType the event type
*/
default void onMetaDataChanged(List<MetaData> changed, DataEventTypeEnum eventType) {
}
/**
* invoke this method when Rule was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onRuleChanged(List<RuleData> changed, DataEventTypeEnum eventType) {
}
}
这个监听接口,应该是soul内部配置数据监听的核心顶层接口
已经有了具体策略和模块的监听器,那么一定有触发这个监听器的地方,从上一篇Soul网关—WebSocket同步数据—源码分析(六)文中可以知道,页面操作后,最终数据是通过发布者发布DataChangedEvent事件,有一个事件更新调度者DataChangedEventDispatcher通过监听DataChangedEvent,来进行更新事件调度分发,DataChangedEventDispatcher这个调度者实现了ApplicationListener<DataChangedEvent>, InitializingBean两个接口,在项目的开始就初始化完成
//事件更新调度者类
@Component
public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {
private ApplicationContext applicationContext;
private List<DataChangedListener> listeners;
public DataChangedEventDispatcher(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
//监听到具体时间,然后调度分发到具体的模块和监听器
@Override
@SuppressWarnings("unchecked")
public void onApplicationEvent(final DataChangedEvent event) {
for (DataChangedListener listener : listeners) {
switch (event.getGroupKey()) {
case APP_AUTH:
listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType());
break;
case PLUGIN:
listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType());
break;
case RULE:
listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType());
break;
case SELECTOR:
listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType());
break;
case META_DATA:
listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType());
break;
default:
throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
}
}
}
@Override
public void afterPropertiesSet() {
Collection<DataChangedListener> listenerBeans = applicationContext.getBeansOfType(DataChangedListener.class).values();
this.listeners = Collections.unmodifiableList(new ArrayList<>(listenerBeans));
}
}
以switch到rule模块,注入的是在启动时已经初始化好的bean:ZookeeperDataChangedListener
直接调用
ZookeeperDataChangedListener的onRuleChanged()方法
@Override
public void onRuleChanged(final List<RuleData> changed, final DataEventTypeEnum eventType) {
if (eventType == DataEventTypeEnum.REFRESH) {
final String selectorParentPath = ZkPathConstants.buildRuleParentPath(changed.get(0).getPluginName());
deleteZkPathRecursive(selectorParentPath);
}
for (RuleData data : changed) {
final String ruleRealPath = ZkPathConstants.buildRulePath(data.getPluginName(), data.getSelectorId(), data.getId());
//若是删除事件,直接删除节点
if (eventType == DataEventTypeEnum.DELETE) {
deleteZkPath(ruleRealPath);
continue;
}
final String ruleParentPath = ZkPathConstants.buildRuleParentPath(data.getPluginName());
createZkNode(ruleParentPath);
//create or update 更新zookeeper节点信息
upsertZkNode(ruleRealPath, data);
}
}
soul-bootstrap服务初始化
soul-bootstrap服务初始中有关网关数据的监听器和zookeeper策略和admin类似,订阅到相关数据进行相应的操作,订阅者CommonPluginDataSubscriber中的subscribeDataHandler()方法,就是进行插件热插拔等一些操作的方法
private <T> void subscribeDataHandler(final T classData, final DataEventTypeEnum dataType) {
Optional.ofNullable(classData).ifPresent(data -> {
if (data instanceof PluginData) {
PluginData pluginData = (PluginData) data;
// 插件更新事件
if (dataType == DataEventTypeEnum.UPDATE) {
BaseDataCache.getInstance().cachePluginData(pluginData);
//这里的hander实现PluginDataHandler接口,根据实现类不一样,进行区别化更新插件
Optional.ofNullable(handlerMap.get(pluginData.getName())).ifPresent(handler -> handler.handlerPlugin(pluginData));
//删除插件事件
} else if (dataType == DataEventTypeEnum.DELETE) {
BaseDataCache.getInstance().removePluginData(pluginData);
Optional.ofNullable(handlerMap.get(pluginData.getName())).ifPresent(handler -> handler.removePlugin(pluginData));
}
} else if (data instanceof SelectorData) {
SelectorData selectorData = (SelectorData) data;
if (dataType == DataEventTypeEnum.UPDATE) {
BaseDataCache.getInstance().cacheSelectData(selectorData);
Optional.ofNullable(handlerMap.get(selectorData.getPluginName())).ifPresent(handler -> handler.handlerSelector(selectorData));
} else if (dataType == DataEventTypeEnum.DELETE) {
BaseDataCache.getInstance().removeSelectData(selectorData);
Optional.ofNullable(handlerMap.get(selectorData.getPluginName())).ifPresent(handler -> handler.removeSelector(selectorData));
}
} else if (data instanceof RuleData) {
RuleData ruleData = (RuleData) data;
if (dataType == DataEventTypeEnum.UPDATE) {
BaseDataCache.getInstance().cacheRuleData(ruleData);
Optional.ofNullable(handlerMap.get(ruleData.getPluginName())).ifPresent(handler -> handler.handlerRule(ruleData));
} else if (dataType == DataEventTypeEnum.DELETE) {
BaseDataCache.getInstance().removeRuleData(ruleData);
Optional.ofNullable(handlerMap.get(ruleData.getPluginName())).ifPresent(handler -> handler.removeRule(ruleData));
}
}
});
}
同步数据
通过了解,我们可以清晰的明确了soul中数据同步的数据链,admin页面数据更新->数据更新事件发布者->调度器->不同监听器采取不同的数据处理逻辑->zookeeper维护节点数据->插件订阅者订阅消息->根据事件名称和插件名称不同进行热插拔负载均衡等操作