Soul网关(13) - divide插件分析

352 阅读3分钟

话不多说,上来就干

注册路由路径

soul中的divide插件,个人理解是分流插件,即作为流量打进来的负载均衡插件,在使用插件前,需要在controller层的路径上绑定接口的请求具体url,通过加入注解进行切面处理,而soul中通过监听器模式,ContextRegisterListener类监听应用上下文,实现接口ApplicationListener<ContextRefreshedEvent> ,这个监听器通过泛型,进行上下文感应监听,在项目启动时,上下文构建的过程中,会执行实现方法中的onApplicationEvent()

    @Override
    public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) {
        //通过注册自旋状态码 registered 获取注册锁,防止并发
        if (!registered.compareAndSet(false, true)) {
            return;
        }
        // 判断是否全部注册,全部注册就在admin上注册全部
        if (soulSpringMvcConfig.isFull()) {
            RegisterUtils.doRegister(buildJsonParams(), url, RpcTypeEnum.HTTP);
        }
    }

在admin上注册全部路径

    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);
    }

上面是任意路径注册,如果soulSpringMvcConfig.isFull()==false怎么办,既只有部分url想注册到网关上,在soul-client-springmvc包下,有一个类SpringMvcClientBeanPostProcessor,这个类实现了spring的BeanPostProcessor接口

    // bean初始化前作切面操作
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
 	
    // bean初始化后作切面操作
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

soul的SpringMvcClientBeanPostProcessor重写postProcessAfterInitialization()方法

    public Object postProcessAfterInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException {
        if (soulSpringMvcConfig.isFull()) {
            return bean;
        }
        // 通过 @RestController @Controller @RequestMapping @RequestMapping 获取controller层每个方法对应的请求路径
        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)) {
                // 如果是controller类路径是全匹配,直接将该路径匹配到注册admin上
                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;
    }

我们再来看一下这个bean是在哪里被注入容器的


package org.dromara.soul.springboot.starter.client.springmvc;

import org.dromara.soul.client.springmvc.config.SoulSpringMvcConfig;
import org.dromara.soul.client.springmvc.init.ContextRegisterListener;
import org.dromara.soul.client.springmvc.init.SpringMvcClientBeanPostProcessor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * The type Soul client http configuration.
 *
 * @author xiaoyu
 */
@Configuration
public class SoulSpringMvcClientConfiguration {
    
    /**
     * Spring http client bean post processor spring http client bean post processor.
     *
     * @param soulSpringMvcConfig the soul http config
     * @return the spring http client bean post processor
     */
    @Bean
    public SpringMvcClientBeanPostProcessor springHttpClientBeanPostProcessor(final SoulSpringMvcConfig soulSpringMvcConfig) {
        return new SpringMvcClientBeanPostProcessor(soulSpringMvcConfig);
    }
    
    /**
     * Context register listener context register listener.
     *
     * @param soulSpringMvcConfig the soul spring mvc config
     * @return the context register listener
     */
    @Bean
    public ContextRegisterListener contextRegisterListener(final SoulSpringMvcConfig soulSpringMvcConfig) {
        return new ContextRegisterListener(soulSpringMvcConfig);
    }
    
    /**
     * Soul http config soul http config.
     *
     * @return the soul http config
     */
    @Bean
    @ConfigurationProperties(prefix = "soul.http")
    public SoulSpringMvcConfig soulHttpConfig() {
        return new SoulSpringMvcConfig();
    }
}

在soul-spring-bootstrap-starter-client-springmvc这个包下,被注入了容器,到这里,所有divide插件需要的基本路由信息都已经加载完成

divide插件

divide插件类实际执行的方法是在DividePlugin这个类中,这个类继承了AbstractSoulPlugin抽象类,实现模板设计模式,具体的插件类,重写doExecute()方法

    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        assert soulContext != null;
        final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
        final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
        if (CollectionUtils.isEmpty(upstreamList)) {
            log.error("divide upstream configuration error: {}", rule.toString());
            Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
        // 加载插件选择器策略
        DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
        if (Objects.isNull(divideUpstream)) {
            log.error("divide has no upstream");
            Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        // set the http url
        String domain = buildDomain(divideUpstream);
        String realURL = buildRealURL(domain, soulContext, exchange);
        exchange.getAttributes().put(Constants.HTTP_URL, realURL);
        // set the http timeout
        exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
        exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
        return chain.execute(exchange);
    }

这其中加载策略方法,是通过spi机制动态加载

    public static DivideUpstream selector(final List<DivideUpstream> upstreamList, final String algorithm, final String ip) {
    // 通过算法标记值 algorithm ,拓展类加载器动态加载负载均衡策略
        LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm);
        // 具体的负载均衡算法
        // 实现方式三种
        // HashLoadBalance 哈希值
        // RandomLoadBalance 随机
        // RoundRobinLoadBalance 轮询
        return loadBalance.select(upstreamList, ip);
    }

三种负载均衡算法

哈希值

随机

轮询