前一篇我们介绍了基于http的divide插件的使用方法,以及soul网关的使用流程。这一篇我们介绍一下基于Dubbo插件的使用及原理,同样的,在开始之前,我们还是先来思考几个设计问题,抱着疑问去学习会事半功倍。
还是简单来梳理一下调用流程:
和divide插件的功能类似,我们的dubbo插件也是负责soul网关与后端服务的通信,只不过将之前的http协议换成dubbo协议。
上一篇我们了解到,soul网关与后端服务的通信,是通过后端服务注册到admin然后再同步到网关来实现的。基于以上假设,我们在实现dubbo协议的时候,如果是自己来设计,需要考虑些什么问题呢?
以上是我们如果要实现一个dubbo插件,所需要考虑的技术点。下面我们通过实践来验证我们的设计是否与作者的设计一致,有哪些差异。
本地启动admin服务和bootstrap网关服务,启动zookeeper(端口2181)。
这里我们bootstrap没有注解连接zookeeeper,只与admin进行通信,是否能转发到后端服务呢?
soul :
file:
enabled: true
corss:
enabled: true
dubbo :
parameter: multi
sync:
websocket :
urls: ws://localhost:9095/websocket
# zookeeper:
# url: localhost:2181
# sessionTimeout: 5000
# connectionTimeout: 2000
# http:
添加bootstrap网关服务dubbo相关依赖
<dependency>
<groupId>org.dromaragroupId>
<artifactId>soul-spring-boot-starter-plugin-alibaba-dubboartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubboartifactId>
<version>${alibaba.dubbo.version}version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-clientartifactId>
<version>${curator.version}version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>${curator.version}version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>${curator.version}version>
dependency>
启动soul-examples/soul-examples-dubbo/soul-examples-alibaba-dubbo-service的后端服务。
使用postman调用网关服务,进行转发测试。
可以发现在bootstrap并没有直接连接zookeeper的情况下,也只直接进行服务转发,说明admin将后端服务的数据同步给了bootstrap网关服务,验证了我们之前的设计猜想。
源码分析我们从在概述中提到的4个设计问题入手,一一进行调试,分析源码:
找到项目soul-examples/soul-examples-dubbo/soul-examples-alibaba-dubbo-service,这是我们以dubbo协议调用的后端服务。
可以看到和http一致,我们的后端服务连接了admin,它直接与admin进行交互,并且将context-path设置为了dubbo。
server:
port: 8011
address: 0.0.0.0
servlet:
context-path: /
spring:
main:
allow-bean-definition-overriding: true
soul:
dubbo:
adminUrl: http://localhost:9095
contextPath: /dubbo
appName: dubbo
dubbo的配置文件,连接了zookeeper,跟我们的猜想一致,它将自身提供的两个provider注册到了zookeeper。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="test-dubbo-service"/>
<dubbo:registry address="zookeeper://localhost:2181"/>
<dubbo:protocol name="dubbo" port="20888"/>
<dubbo:service timeout="10000" interface="org.dromara.soul.examples.dubbo.api.service.DubboTestService" ref="dubboTestService"/>
<dubbo:service timeout="10000" interface="org.dromara.soul.examples.dubbo.api.service.DubboMultiParamService" ref="dubboMultiParamService"/>
beans>
查看ZooInspector,可以看到注册上去的两个服务。
从项目结构上很好理解该服务有两个实现类,实现方法通过@SoulDubboClient注解将接口信息注册到admin。搜索SoulDubboClient的引用,发现类ApacheDubboServiceBeanPostProcessor是起作用的关键。
这里ApacheDubboServiceBeanPostProcessor实现了ApplicationListener,说明是一个监听器,接收 ContextRefreshedEvent事件。
其核心逻辑是从事件的上下文中获取ServiceBean,然后调用handle方法将其注册到admin服务。
@Override
public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) {
if (Objects.nonNull(contextRefreshedEvent.getApplicationContext().getParent())) {
return;
}
// Fix bug(https://github.com/dromara/soul/issues/415), upload dubbo metadata on ContextRefreshedEvent
Map<String, ServiceBean> serviceBean = contextRefreshedEvent.getApplicationContext().getBeansOfType(ServiceBean.class);
for (Map.Entry<String, ServiceBean> entry : serviceBean.entrySet()) {
executorService.execute(() -> handler(entry.getValue()));
}
}
/***
* 注册到admin
* @param serviceBean
*/
private void handler(final ServiceBean serviceBean) {
Class<?> clazz = serviceBean.getRef().getClass();
if (ClassUtils.isCglibProxyClass(clazz)) {
String superClassName = clazz.getGenericSuperclass().getTypeName();
try {
clazz = Class.forName(superClassName);
} catch (ClassNotFoundException e) {
log.error(String.format("class not found: %s", superClassName));
return;
}
}
final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(clazz);
for (Method method : methods) {
SoulDubboClient soulDubboClient = method.getAnnotation(SoulDubboClient.class);
if (Objects.nonNull(soulDubboClient)) {
RegisterUtils.doRegister(buildJsonParams(serviceBean, soulDubboClient, method), url, RpcTypeEnum.DUBBO);
}
}
}
ContextRefreshedEvent 事件会在Spring容器初始化完成会触发该事件,这里相当于我们在容器初始化完成后,自动执行向admin注册服务api的逻辑。
我们在实践练习步骤3中打开了插件dubbo的状态,并配置了zookeeper的地址,说明这里admin服务是可连接zookeeper的。
查看admin项目,查找zookeeper相关的监听逻辑,我们可以很容易找到以下目录结构中的两个类:
重点来看ZookeeperDataChangedListener, 它是一个listener,核心逻辑就是接受某个类型的事件,然后执行响应的业务逻辑,并创建或者更新zk的节点,如下所示。
@Override
public void onPluginChanged(final List<PluginData> changed, final DataEventTypeEnum eventType) {
for (PluginData data : changed) {
final String pluginPath = ZkPathConstants.buildPluginPath(data.getName());
// delete
if (eventType == DataEventTypeEnum.DELETE) {
deleteZkPathRecursive(pluginPath);
final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(data.getName());
deleteZkPathRecursive(selectorParentPath);
final String ruleParentPath = ZkPathConstants.buildRuleParentPath(data.getName());
deleteZkPathRecursive(ruleParentPath);
continue;
}
//create or update
upsertZkNode(pluginPath, data);
}
}
/**
* create or update zookeeper node.
* @param path node path
* @param data node data
*/
private void upsertZkNode(final String path, final Object data) {
if (!zkClient.exists(path)) {
zkClient.createPersistent(path, true);
}
zkClient.writeData(path, data);
}
可以预测我们在admin对插件进行的操作,会生成一个Event事件,最终在这里触发响应的数据变更。这里ZookeeperDataChangedListener并没有直接实现ApplicationListener,所以他并不会直接触发监听回调。
跟踪对ZookeeperDataChangedListener的引用,发现在一个DataSyncConfiguration的配置类中,zookeeperDataChangedListener被注册成了一个bean,还有其他网络通信的几个类型相关的事件也都被注册成了bean,包括:
@Configuration
public class DataSyncConfiguration {
/**
* http long polling(default strategy).
*/
@Configuration
@ConditionalOnProperty(name = "soul.sync.http.enabled", havingValue = "true")
@EnableConfigurationProperties(HttpSyncProperties.class)
static class HttpLongPollingListener {
@Bean
@ConditionalOnMissingBean(HttpLongPollingDataChangedListener.class)
public HttpLongPollingDataChangedListener httpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) {
return new HttpLongPollingDataChangedListener(httpSyncProperties);
}
}
@Bean
@ConditionalOnMissingBean(ZookeeperDataChangedListener.class)
public DataChangedListener zookeeperDataChangedListener(final ZkClient zkClient) {
return new ZookeeperDataChangedListener(zkClient);
}
//...
}
但是他们都仅仅是个bean,还不能直接响应事件,这里他们的返回类型都是DataChangedListener。我们查找对DataChangedListener的引用,发现了最终的Listener是DataChangedEventDispatcher,它实现了ApplicationListener接口,并监听DataChangedEvent事件。
@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));
}
}
在Bean初始化完成后,通过IOC容器拿到所有DataChangedListener类型的Bean,然后初始化给DataChangedEventDispatcher持有。然后DataChangedEventDispatcher通过监听不同的事件类型,分发给不同的监听器来执行具体的业务逻辑,很像是DispatcherServlet的设计。
从上一个步骤中,我们可以知道,每当admin发生数据变更,都会触发一个事件。由于我们在soul和admin之间采用的websocket进行通行,这里WebsocketDataChangedListener也监听了数据变更的事件,会将变更的值同步给soul网关。
public class WebsocketDataChangedListener implements DataChangedListener {
@Override
public void onPluginChanged(final List<PluginData> pluginDataList, final DataEventTypeEnum eventType) {
WebsocketData<PluginData> websocketData =
new WebsocketData<>(ConfigGroupEnum.PLUGIN.name(), eventType.name(), pluginDataList);
WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
}
//...
}
WebsocketCollector核心的send方法,负责具体的数据推送逻辑,如下图:
如此我们只需要在soul网关把数据缓存下来,就可以同步到admin的数据了。
这个过程和http插件调用时一致的,上一篇中已经介绍过,不再赘述。
经过上一篇的梳理,我们大致知道了soul网关的整个架构逻辑,基于soul网关与后端服务完全无感知,而是通过admin服务进行数据同步的基础假设,我们设想了如果是自己来设计dubbo插件,应该需要考虑的设计点,从这个角度出发,我们通过实践并追踪源码的方式,很快的梳理出了作者的设计思路和实现细节。其中大部分设计思路和我们设想的一致,当然了,具体代码实现可以看出作者运用了很多设计模式,巧妙的将复杂的业务逻辑抽象封装在一个个易于理解的概念中。其中数据同步部分,DataChangedEventDispatcher类的实现尤其印象深刻,通过中介者、策略模式,将原本需要编写多个复杂的listener逻辑,统一交给了DataChangedEventDispatcher来委派,提高了编码效率的同时,更易于以后的扩展。