[Soul 源码之旅] 1.8 Soul插件初体验 (Divide-> Upstream 同步)

Divide 插件是 Soul 中最基础的插件之一,主要负责Spring MVC 项目的请求转发,我们这次从这里开始一步步探索 Soul 处理 Spring MVC 转发的整体流程。

1.8.1 插件数据注册流程

1.8.1.1 Spring MVC 项目注册数据

在使用Divide 插件,我们只需要在项目中引入 如下依赖:

        
            org.dromara
            soul-spring-boot-starter-client-springmvc
            ${soul.version}
        

和以下 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 是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 ruleConditions) {
        SelectorDO selectorDO = selectorMapper.selectById(ruleDO.getSelectorId());
        PluginDO pluginDO = pluginMapper.selectById(selectorDO.getPluginId());

        List 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插件初体验 (Divide-> Upstream 同步))