【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)

目录

1.准备配套设施

2.配置

2.1 soul-admin 配置

2.2 soul-bootstrap 配置

3.启动服务

4.源码分析


1.准备配套设施

启动 MySQL、Nacos。

使用 docker 启动 Nacos(详情见官网github https://github.com/nacos-group/nacos-docker)。

Clone project

git clone --depth 1 https://github.com/nacos-group/nacos-docker.git
cd nacos-docker

Standalone Derby

docker-compose -f example/standalone-derby.yaml up

启动后,访问 http://localhost:8848/nacos/

用户名/密码 nacos/nacos

依次点击【命名空间】【新建命名空间】,手动创建一个 namespace,后面在配置文件中也要使用的,配置如下图所示:

这里要强调一下,同步时使用的是 ID,所以这个 ID 在下面配置文件中也需要配置一致(1c10d748-af86-43b9-8265-75f487d20c6c)。

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第1张图片

未启动服务时,全都是空的,如下图所示:

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第2张图片

 

2.配置

2.1 soul-admin 配置

soul:
  sync:
      nacos:
        url: localhost:8848
        namespace: 1c10d748-af86-43b9-8265-75f487d20c6c
  #      acm:
  #        enabled: false
  #        endpoint: acm.aliyun.com
  #        namespace:
  #        accessKey:
  #        secretKey:

这里使用的是源码自带的,如果没有使用ACM,注释可以不打开,只打开 url 和 namespace 即可,其中 namespace 要跟 nacos 中新建的保持一致。

2.2 soul-bootstrap 配置

soul:
    sync:
        nacos:
            url: localhost:8848
            namespace: 1c10d748-af86-43b9-8265-75f487d20c6c
#url: 配置成你的nacos地址,集群环境请使用(,)分隔。
# 其他参数配置,请参考naocs官网。

这里要注意的一点是,namespace 一定要跟 soul-admin 中的 namespace 保持一致。

还需要在 pom 文件中引入依赖:

        
        
            org.dromara
            soul-spring-boot-starter-sync-data-nacos
            ${project.version}
        

3.启动服务

在 soul-bootstrap 日志中,可以看到如下一条日志,就证明使用了 nacos 同步策略:

2021-01-29 02:22:28.839  INFO 22496 --- [           main] d.s.s.s.s.d.n.NacosSyncDataConfiguration : you use nacos sync soul data.......

那成功没成功呢?看下 nacos 管理页面【配置管理】【配置列表】

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第3张图片

 

4.源码分析

既然看到上面的日志了,直接搜一下,就找到了 NacosSyncDataConfiguration。

看所在目录结构,就知道这是 Spring start 机制加载的配置类,并且对应到我们上面 2.2 小节的配置文件和引入的依赖包。

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第4张图片

好的,那我们就以这里为起点,顺藤摸瓜,看看能摸到什么好东西。

// NacosSyncDataConfiguration.java
/**
 * Nacos sync data configuration for spring boot.
 *
 * @author xiaoyu(Myth)
 */
@Configuration
@ConditionalOnClass(NacosSyncDataService.class)
@ConditionalOnProperty(prefix = "soul.sync.nacos", name = "url")
@Slf4j
public class NacosSyncDataConfiguration {

    /**
     * Nacos sync data service.
     *
     * @param configService     the config service
     * @param pluginSubscriber the plugin subscriber
     * @param metaSubscribers   the meta subscribers
     * @param authSubscribers   the auth subscribers
     * @return the sync data service
     */
    @Bean
    public SyncDataService nacosSyncDataService(final ObjectProvider configService, final ObjectProvider pluginSubscriber,
                                           final ObjectProvider> metaSubscribers, final ObjectProvider> authSubscribers) {
        log.info("you use nacos sync soul data.......");
        return new NacosSyncDataService(configService.getIfAvailable(), pluginSubscriber.getIfAvailable(),
                metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
    }

    /**
     * Nacos config service config service.
     *
     * @param nacosConfig the nacos config
     * @return the config service
     * @throws Exception the exception
     */
    @Bean
    public ConfigService nacosConfigService(final NacosConfig nacosConfig) throws Exception {
        Properties properties = new Properties();
        if (nacosConfig.getAcm() != null && nacosConfig.getAcm().isEnabled()) {
            properties.put(PropertyKeyConst.ENDPOINT, nacosConfig.getAcm().getEndpoint());
            properties.put(PropertyKeyConst.NAMESPACE, nacosConfig.getAcm().getNamespace());
            properties.put(PropertyKeyConst.ACCESS_KEY, nacosConfig.getAcm().getAccessKey());
            properties.put(PropertyKeyConst.SECRET_KEY, nacosConfig.getAcm().getSecretKey());
        } else {
            properties.put(PropertyKeyConst.SERVER_ADDR, nacosConfig.getUrl());
            properties.put(PropertyKeyConst.NAMESPACE, nacosConfig.getNamespace());
        }
        return NacosFactory.createConfigService(properties);
    }

    /**
     * Http config http config.
     *
     * @return the http config
     */
    @Bean
    @ConfigurationProperties(prefix = "soul.sync.nacos")
    public NacosConfig nacosConfig() {
        return new NacosConfig();
    }
}

关键方法 nacosSyncDataService 调用了 NacosSyncDataService 的构造方法:

// NacosSyncDataService.java
    /**
     * Instantiates a new Nacos sync data service.
     *
     * @param configService         the config service
     * @param pluginDataSubscriber the plugin data subscriber
     * @param metaDataSubscribers   the meta data subscribers
     * @param authDataSubscribers   the auth data subscribers
     */
    public NacosSyncDataService(final ConfigService configService, final PluginDataSubscriber pluginDataSubscriber,
                                final List metaDataSubscribers, final List authDataSubscribers) {

        super(configService, pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
        start();
    }

    /**
     * Start.
     */
    public void start() {
        watcherData(PLUGIN_DATA_ID, this::updatePluginMap);
        watcherData(SELECTOR_DATA_ID, this::updateSelectorMap);
        watcherData(RULE_DATA_ID, this::updateRuleMap);
        watcherData(META_DATA_ID, this::updateMetaDataMap);
        watcherData(AUTH_DATA_ID, this::updateAuthMap);
    }

在 start 方法中调用的 watcherData 是其父类 NacosCacheHandler 的方法

// NacosCacheHandler.java
    protected void watcherData(final String dataId, final OnChange oc) {
        Listener listener = new Listener() {
            @Override
            public void receiveConfigInfo(final String configInfo) {
                oc.change(configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        };
        oc.change(getConfigAndSignListener(dataId, listener));
        LISTENERS.computeIfAbsent(dataId, key -> new ArrayList<>()).add(listener);
    }

    @SneakyThrows
    private String getConfigAndSignListener(final String dataId, final Listener listener) {
        // 这里根据 dataId 获取配置,并且注册监听器
        String config = configService.getConfigAndSignListener(dataId, GROUP, 6000, listener);
        if (config == null) {
            config = "{}";
        }
        return config;
    }

这里第2个入参使用了 OnChange 接口,而且里面只有一个方法 change,符合函数式接口的定义,我理解这里之所以这么设计,是为了方便前面在调用时可以使用方法引用。

// NacosCacheHandler.java
    protected interface OnChange {
        void change(String changeData);
    }

 这里的 oc.change 方法会分别根据 dataId 调用对应的方法,对应关系就是调用 watcherData 方法时的传参,即

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第5张图片

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第6张图片

// NacosCacheHandler.java
    protected void updatePluginMap(final String configInfo) {
        try {
            // Fix bug #656(https://github.com/dromara/soul/issues/656)
            List pluginDataList = new ArrayList<>(GsonUtils.getInstance().toObjectMap(configInfo, PluginData.class).values());
            pluginDataList.forEach(pluginData -> Optional.ofNullable(pluginDataSubscriber).ifPresent(subscriber -> {
                subscriber.unSubscribe(pluginData);
                subscriber.onSubscribe(pluginData);
            }));
        } catch (JsonParseException e) {
            log.error("sync plugin data have error:", e);
        }
    }

    protected void updateSelectorMap(final String configInfo) {
        try {
            List selectorDataList = GsonUtils.getInstance().toObjectMapList(configInfo, SelectorData.class).values().stream().flatMap(Collection::stream).collect(Collectors.toList());
            selectorDataList.forEach(selectorData -> Optional.ofNullable(pluginDataSubscriber).ifPresent(subscriber -> {
                subscriber.unSelectorSubscribe(selectorData);
                subscriber.onSelectorSubscribe(selectorData);
            }));
        } catch (JsonParseException e) {
            log.error("sync selector data have error:", e);
        }
    }

    protected void updateRuleMap(final String configInfo) {
        try {
            List ruleDataList = GsonUtils.getInstance().toObjectMapList(configInfo, RuleData.class).values()
                    .stream().flatMap(Collection::stream)
                    .collect(Collectors.toList());
            ruleDataList.forEach(ruleData -> Optional.ofNullable(pluginDataSubscriber).ifPresent(subscriber -> {
                subscriber.unRuleSubscribe(ruleData);
                subscriber.onRuleSubscribe(ruleData);
            }));
        } catch (JsonParseException e) {
            log.error("sync rule data have error:", e);
        }
    }

    protected void updateMetaDataMap(final String configInfo) {
        try {
            List metaDataList = new ArrayList<>(GsonUtils.getInstance().toObjectMap(configInfo, MetaData.class).values());
            metaDataList.forEach(metaData -> metaDataSubscribers.forEach(subscriber -> {
                subscriber.unSubscribe(metaData);
                subscriber.onSubscribe(metaData);
            }));
        } catch (JsonParseException e) {
            log.error("sync meta data have error:", e);
        }
    }

    protected void updateAuthMap(final String configInfo) {
        try {
            List appAuthDataList = new ArrayList<>(GsonUtils.getInstance().toObjectMap(configInfo, AppAuthData.class).values());
            appAuthDataList.forEach(appAuthData -> authDataSubscribers.forEach(subscriber -> {
                subscriber.unSubscribe(appAuthData);
                subscriber.onSubscribe(appAuthData);
            }));
        } catch (JsonParseException e) {
            log.error("sync auth data have error:", e);
        }
    }

这5个方法的逻辑一致,只是需要处理的类型不同而已。

里面最核心的方法

subscriber.unSubscribe(pluginData);
subscriber.onSubscribe(pluginData);
subscriber.unSelectorSubscribe(selectorData);
subscriber.onSelectorSubscribe(selectorData);
subscriber.unRuleSubscribe(ruleData);
subscriber.onRuleSubscribe(ruleData);
subscriber.unSubscribe(metaData);
subscriber.onSubscribe(metaData);
subscriber.unSubscribe(appAuthData);
subscriber.onSubscribe(appAuthData);

其中 pluginData、selectorData、ruleData 最终都会调用 CommonPluginDataSubscriber 的 subscribeDataHandler 方法,其中,解除订阅就是 DELETE,订阅就是 UPDATE:

// CommonPluginDataSubscriber.java
    private  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);
                    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));
                }
            }
        });
    }

这里之前也分析到了,但是对于不同类型的还需要再整体梳理一下,这个后面再梳理。

剩下的 metaData 会有5种不同的子类实现,分别是:

AlibabaDubboMetaDataSubscriber
ApacheDubboMetaDataSubscriber
MetaDataAllSubscriber
SofaMetaDataSubscriber
TarsMetaDataSubscriber

【Soul源码阅读】14.soul-admin 与 soul-bootstrap 同步机制之 nacos 解析(上)_第7张图片

appAuthData:

/**
 * The type Sign auth data subscriber.
 *
 * @author xiaoyu
 */
public class SignAuthDataSubscriber implements AuthDataSubscriber {
    
    @Override
    public void onSubscribe(final AppAuthData appAuthData) {
        SignAuthDataCache.getInstance().cacheAuthData(appAuthData);
    }
    
    @Override
    public void unSubscribe(final AppAuthData appAuthData) {
        SignAuthDataCache.getInstance().removeAuthData(appAuthData);
    }
}

把内存中的数据重新处理后,把数据缓存在了 LISTENERS 中。

OK,到这里,启动时处理逻辑就分析完了。

那如果我在 soul-admin web 页面修改 divide 插件内的 selector 或 rule 时,是如何同步的呢?我们明天再来分析,敬请期待。

 

 

你可能感兴趣的:(Soul网关,Java,Soul,网关,Java,源码阅读)