Soul网关源码学习(三)——Dubbo插件详解

Soul网关源码学习(三)——Dubbo插件详解

概述

前一篇我们介绍了基于http的divide插件的使用方法,以及soul网关的使用流程。这一篇我们介绍一下基于Dubbo插件的使用及原理,同样的,在开始之前,我们还是先来思考几个设计问题,抱着疑问去学习会事半功倍。

还是简单来梳理一下调用流程:
Soul网关源码学习(三)——Dubbo插件详解_第1张图片
和divide插件的功能类似,我们的dubbo插件也是负责soul网关与后端服务的通信,只不过将之前的http协议换成dubbo协议。

上一篇我们了解到,soul网关与后端服务的通信,是通过后端服务注册到admin然后再同步到网关来实现的。基于以上假设,我们在实现dubbo协议的时候,如果是自己来设计,需要考虑些什么问题呢?

  1. 后端服务将提供的服务注册到zookeeper。
  2. 在admin配置zookeeper的地址,并且及时从zookeeper监听、同步最新的服务列表。
  3. soul网关同步admin推送来的服务列表,缓存到本地。
  4. 至于从网关到后端的转发流程,跟http是一致:选择器->规则->条件,它是一个通用的流程。

以上是我们如果要实现一个dubbo插件,所需要考虑的技术点。下面我们通过实践来验证我们的设计是否与作者的设计一致,有哪些差异。

实战练习

  1. 本地启动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>
    
  2. admin后台打开dubbo插件, 启用状态。
    Soul网关源码学习(三)——Dubbo插件详解_第2张图片

  3. 启动soul-examples/soul-examples-dubbo/soul-examples-alibaba-dubbo-service的后端服务。

    如果启动成功可以看到向admin注册成功的信息:
    Soul网关源码学习(三)——Dubbo插件详解_第3张图片

  4. 打开admin——dubbo插件,查看刚刚后端服务注册上来的数据信息。
    Soul网关源码学习(三)——Dubbo插件详解_第4张图片

  5. 使用postman调用网关服务,进行转发测试。

    可以发现在bootstrap并没有直接连接zookeeper的情况下,也只直接进行服务转发,说明admin将后端服务的数据同步给了bootstrap网关服务,验证了我们之前的设计猜想。
    Soul网关源码学习(三)——Dubbo插件详解_第5张图片

源码分析

源码分析我们从在概述中提到的4个设计问题入手,一一进行调试,分析源码:

1. 后端服务将提供的服务注册到zookeeper

找到项目soul-examples/soul-examples-dubbo/soul-examples-alibaba-dubbo-service,这是我们以dubbo协议调用的后端服务。

项目结构如下:
Soul网关源码学习(三)——Dubbo插件详解_第6张图片
先查看配置文件:

可以看到和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,可以看到注册上去的两个服务。
Soul网关源码学习(三)——Dubbo插件详解_第7张图片
从项目结构上很好理解该服务有两个实现类,实现方法通过@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的逻辑。

2. 在admin配置zookeeper的地址,并且及时从zookeeper监听、同步最新的服务列表

我们在实践练习步骤3中打开了插件dubbo的状态,并配置了zookeeper的地址,说明这里admin服务是可连接zookeeper的。

查看admin项目,查找zookeeper相关的监听逻辑,我们可以很容易找到以下目录结构中的两个类:
Soul网关源码学习(三)——Dubbo插件详解_第8张图片
重点来看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,包括:

  • HttpLongPollingListener
  • ZookeeperListener
  • NacosListener
  • WebsocketListener
@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的设计。

3. soul网关同步admin推送来的服务列表,缓存到本地。

从上一个步骤中,我们可以知道,每当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网关源码学习(三)——Dubbo插件详解_第9张图片

如此我们只需要在soul网关把数据缓存下来,就可以同步到admin的数据了。

4. 至于从网关到后端的转发流程,跟http是一致:选择器->规则->条件,它是一个通用的流程。

这个过程和http插件调用时一致的,上一篇中已经介绍过,不再赘述。

思考总结

经过上一篇的梳理,我们大致知道了soul网关的整个架构逻辑,基于soul网关与后端服务完全无感知,而是通过admin服务进行数据同步的基础假设,我们设想了如果是自己来设计dubbo插件,应该需要考虑的设计点,从这个角度出发,我们通过实践并追踪源码的方式,很快的梳理出了作者的设计思路和实现细节。其中大部分设计思路和我们设想的一致,当然了,具体代码实现可以看出作者运用了很多设计模式,巧妙的将复杂的业务逻辑抽象封装在一个个易于理解的概念中。其中数据同步部分,DataChangedEventDispatcher类的实现尤其印象深刻,通过中介者、策略模式,将原本需要编写多个复杂的listener逻辑,统一交给了DataChangedEventDispatcher来委派,提高了编码效率的同时,更易于以后的扩展。

你可能感兴趣的:(soul,后端)