[Soul 源碼之旅] 1.8 Soul插件初體驗 (Divide-> Upstream 同步)

Divide 插件是 Soul 中最基礎的插件之一,主要負責Spring MVC 項目的請求轉發,我們這次從這里開始一步步探索 Soul 處理 Spring MVC 轉發的整體流程。

1.8.1 插件數據注冊流程

1.8.1.1 Spring MVC 項目注冊數據

在使用Divide 插件,我們只需要在項目中引入 如下依賴:

        <dependency>
            <groupId>org.dromara</groupId>
            <artifactId>soul-spring-boot-starter-client-springmvc</artifactId>
            <version>${soul.version}</version>
        </dependency>

和以下 soul 配置信息, 這些功能我們接下來一一解析。

soul:
  http:
    adminUrl: http://localhost:9095
    port: 8188
    contextPath: /http
    appName: http
    full: false

我們還是按照之前的流程,從 soul-spring-boot-starter-client-springmvc 開始,這里只引入了配置類 SoulSpringMvcClientConfiguration ,這個類只引入了三個 Bean ,我們接下來一個個看他們的作用。首先是 soulHttpConfig 這個類主要是讀取 soul 的配置信息,然后給接下來兩個 bean 使用。
我們先看第一個 bean SpringMvcClientBeanPostProcessor 這里先校驗數據是否正確,然后生成一個線程池,主要是為后面注冊服務使用。

    public SpringMvcClientBeanPostProcessor(final SoulSpringMvcConfig soulSpringMvcConfig) {
        ValidateUtils.validate(soulSpringMvcConfig);
        this.soulSpringMvcConfig = soulSpringMvcConfig;
        url = soulSpringMvcConfig.getAdminUrl() + "/soul-client/springmvc-register";
        executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
    }

它實現了 BeanPostProcessor 接口,按照套路,他就是在 postProcessAfterInitialization 方法做信息初始化流程。他會先檢測該bean 是否有 Controller RestController 或者 RequestMapping 三個注解,假如有 再看這個類是否有SoulSpringMvcClient 這個注解,然后檢測該注解的 path 屬性是否有 /* 的屬性,有則注冊整個路徑 如 /test/** ,然后在 buildJsonParams 中還將會拼接上 上面配置的 context path ,然后向線程池里面建立一個任務,就是向 admin 注冊服務信息。假如類上沒有 SoulSpringMvcClient 注解,則遍歷該類上的所有方法,同樣走一遍上面的注冊流程。

    @Override
    public Object postProcessAfterInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException {
        if (soulSpringMvcConfig.isFull()) {
            return bean;
        }
        Controller controller = AnnotationUtils.findAnnotation(bean.getClass(), Controller.class);
        RestController restController = AnnotationUtils.findAnnotation(bean.getClass(), RestController.class);
        RequestMapping requestMapping = AnnotationUtils.findAnnotation(bean.getClass(), RequestMapping.class);
        if (controller != null || restController != null || requestMapping != null) {
            SoulSpringMvcClient clazzAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), SoulSpringMvcClient.class);
            String prePath = "";
            if (Objects.nonNull(clazzAnnotation)) {
                if (clazzAnnotation.path().indexOf("*") > 1) {
                    String finalPrePath = prePath;
                    executorService.execute(() -> RegisterUtils.doRegister(buildJsonParams(clazzAnnotation, finalPrePath), url,
                            RpcTypeEnum.HTTP));
                    return bean;
                }
                prePath = clazzAnnotation.path();
            }
            final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(bean.getClass());
            for (Method method : methods) {
                SoulSpringMvcClient soulSpringMvcClient = AnnotationUtils.findAnnotation(method, SoulSpringMvcClient.class);
                if (Objects.nonNull(soulSpringMvcClient)) {
                    String finalPrePath = prePath;
                    executorService.execute(() -> RegisterUtils.doRegister(buildJsonParams(soulSpringMvcClient, finalPrePath), url,
                            RpcTypeEnum.HTTP));
                }
            }
        }
        return bean;
    }

我們接著看一下 ContextRegisterListener

    public ContextRegisterListener(final SoulSpringMvcConfig soulSpringMvcConfig) {
        ValidateUtils.validate(soulSpringMvcConfig);
        this.soulSpringMvcConfig = soulSpringMvcConfig;
        url = soulSpringMvcConfig.getAdminUrl() + "/soul-client/springmvc-register";
    }

同時它也實現了接口 ApplicationListener<ContextRefreshedEvent> ,ContextRefreshedEvent 是Spring 內置的事件,但所有Spring bean 分發完成后就會觸發 onApplicationEvent 方法。onApplicationEvent 方法是先判斷Spring 的配置 isfull 但為true 時執行注冊流程。 這里有個關鍵點,這里會用一個AtomicBoolean 來設置是否已經完成注冊,這是因為在 web應用會出現父子容器,這個事件會觸發兩次, 這里通過這個狀態看是否已經注冊過該信息。

    @Override
    public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) {
        if (!registered.compareAndSet(false, true)) {
            return;
        }
        if (soulSpringMvcConfig.isFull()) {
            RegisterUtils.doRegister(buildJsonParams(), url, RpcTypeEnum.HTTP);
        }
    }

那么 isfull 屬性是干什么但呢,我們看一下它但 buildJsonParam 有點不一樣。這里注冊路徑是直接注冊成 contextPath + /** 這就意味著該服務代理 /contextPath/** 下所有轉發請求,我們看到上面 postProcessAfterInitialization 中也做了判斷,就是假如是 full ,那么就不掃描各個 bean了, 因為他已經代理了contextPath 下但所有請求,沒必要后面一個個注冊。

private String buildJsonParams() {
        String contextPath = soulSpringMvcConfig.getContextPath();
        String appName = soulSpringMvcConfig.getAppName();
        Integer port = soulSpringMvcConfig.getPort();
        String path = contextPath + "/**";
        String configHost = soulSpringMvcConfig.getHost();
        String host = StringUtils.isBlank(configHost) ? IpUtils.getHost() : configHost;
        SpringMvcRegisterDTO registerDTO = SpringMvcRegisterDTO.builder()
                .context(contextPath)
                .host(host)
                .port(port)
                .appName(appName)
                .path(path)
                .rpcType(RpcTypeEnum.HTTP.getName())
                .enabled(true)
                .ruleName(path)
                .build();
        return OkHttpTools.getInstance().getGson().toJson(registerDTO);
    }

1.8.1.2 Soul admin

我們可以看到 客戶端都是通過調用 /soul-client/springmvc-register 這個 admin 的 Rest 接口進行請求的。我們看看它做了什么。它調用了 soulClientRegisterService 進行信息注冊。這里先判斷是否是注冊元信息,假如是則調用 saveSpringMvcMetaData 進行注冊。

    @Override
    @Transactional
    public String registerSpringMvc(final SpringMvcRegisterDTO dto) {
        if (dto.isRegisterMetaData()) {
            MetaDataDO exist = metaDataMapper.findByPath(dto.getPath());
            if (Objects.isNull(exist)) {
                saveSpringMvcMetaData(dto);
            }
        }
        String selectorId = handlerSpringMvcSelector(dto);
        handlerSpringMvcRule(selectorId, dto);
        return SoulResultMessage.SUCCESS;
    }

這里 根據selector 的id 更新 rule。

    private void handlerSpringMvcRule(final String selectorId, final SpringMvcRegisterDTO dto) {
        RuleDO ruleDO = ruleMapper.findByName(dto.getRuleName());
        if (Objects.isNull(ruleDO)) {
            registerRule(selectorId, dto.getPath(), dto.getRpcType(), dto.getRuleName());
        }
    }

registe流程如下, 先拼接規則,假如是有 * 則使用 match 規則,假如沒有則是 =規則。最終調用 ruleService.register 進行注冊信息。

    private void registerRule(final String selectorId, final String path, final String rpcType, final String ruleName) {
        RuleHandle ruleHandle = RuleHandleFactory.ruleHandle(RpcTypeEnum.acquireByName(rpcType), path);
        RuleDTO ruleDTO = RuleDTO.builder()
                .selectorId(selectorId)
                .name(ruleName)
                .matchMode(MatchModeEnum.AND.getCode())
                .enabled(Boolean.TRUE)
                .loged(Boolean.TRUE)
                .sort(1)
                .handle(ruleHandle.toJson())
                .build();
        RuleConditionDTO ruleConditionDTO = RuleConditionDTO.builder()
                .paramType(ParamTypeEnum.URI.getName())
                .paramName("/")
                .paramValue(path)
                .build();
        if (path.indexOf("*") > 1) {
            ruleConditionDTO.setOperator(OperatorEnum.MATCH.getAlias());
        } else {
            ruleConditionDTO.setOperator(OperatorEnum.EQ.getAlias());
        }
        ruleDTO.setRuleConditions(Collections.singletonList(ruleConditionDTO));
        ruleService.register(ruleDTO);
    }

最終調用了 publishEvent 方法

    private void publishEvent(final RuleDO ruleDO, final List<RuleConditionDTO> ruleConditions) {
        SelectorDO selectorDO = selectorMapper.selectById(ruleDO.getSelectorId());
        PluginDO pluginDO = pluginMapper.selectById(selectorDO.getPluginId());

        List<ConditionData> conditionDataList =
                ruleConditions.stream().map(ConditionTransfer.INSTANCE::mapToRuleDTO).collect(Collectors.toList());
        // publish change event.
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, DataEventTypeEnum.UPDATE,
                Collections.singletonList(RuleDO.transFrom(ruleDO, pluginDO.getName(), conditionDataList))));
    }

這里就又回到了我們之前說的 DataChangedEventDispatcher 方法,和我們之前的流程串起來了。

1.8.1.3 總結

今天主要介紹了 SpringMVC 客戶端Divide的注冊流程,后面我們有了數據就是轉發流程怎么根據這些數據進行流轉了,敬請期待。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容