美文网首页
[Soul 源码之旅] 1.8 Soul插件初体验 (Divid

[Soul 源码之旅] 1.8 Soul插件初体验 (Divid

作者: AndyWei123 | 来源:发表于2021-01-23 17:06 被阅读0次

    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的注册流程,后面我们有了数据就是转发流程怎么根据这些数据进行流转了,敬请期待。

    相关文章

      网友评论

          本文标题:[Soul 源码之旅] 1.8 Soul插件初体验 (Divid

          本文链接:https://www.haomeiwen.com/subject/bfluzktx.html