ShenYu网关源码解析(二十四)ShenYuDubboClient注解

1,080 阅读7分钟

简介

    本篇文章来探索下Soul网关Apache Dubbo客户端的注册处理流程

概览

    有了上一篇的基础:Soul网关源码解析(二十三)SoulSpringMvcClient注解

    我们直接在Soul-Client模块下定位到:soul-client-apache-dubbo,找到进行具体处理的类:ApacheDubboServiceBeanPostProcessor

    我们看下是如何获取到Bean的?如果判断是Soul-Client-Apache-Dubbo的?注册了那些信息?

    下面开始分析:

源码Debug

如何获取到Bean

    直接定位到处理类如下,onApplicationEvent方法是入口,那应该是从这里开始像HTTP那样获取Bean的

    首先看下继承的类,可以看到和HTTP不同,这里是:ApplicationListener

    由于不熟悉,我们搜索下它是干嘛的,参考链接是:ApplicationListener和ContextRefreshedEvent

IOC的容器的启动过程,当所有的bean都已经处理完成之后,spring ioc容器会有一个发布事件的动作AbstractApplicationContext 的源码中就可以看出
当ioc容器加载处理完相应的bean之后,也给我们提供了一个机会(先有InitializingBean,后有ApplicationListener<ContextRefreshedEvent>),可以去做一些自己想做的事其实这也就是spring ioc容器给提供的一个扩展的地方我们可以这样使用这个扩展机制

作者:AmeeLove
链接:https://www.jianshu.com/p/4bd3f68cb179
来源:简书
著作权归作者所有商业转载请联系作者获得授权,非商业转载请注明出处

    大致就是在所有容器都初始化完成以后,会触发执行

    我们在下面的代码中打断点进行调试,发现:contextRefreshedEvent.getApplicationContext().getBeansOfType(ServiceBean.class),执行完以后就可以得到我们示例中的两个Impl服务,感觉挺便利的。这个感觉需要对Dubbo有一些熟悉,现在又学到了一手

    获取以后遍历开启线程去进行注册,获取Bean的过程大致就是这样了

public class ApacheDubboServiceBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {

	// DubboConfig(adminUrl=http://localhost:9095, contextPath=/dubbo, appName=dubbo)
    private DubboConfig dubboConfig;

    private ExecutorService executorService;

	// http://localhost:9095/soul-client/dubbo-register
    private final String url;

    public ApacheDubboServiceBeanPostProcessor(final DubboConfig dubboConfig) {
        String contextPath = dubboConfig.getContextPath();
        String adminUrl = dubboConfig.getAdminUrl();
        if (StringUtils.isEmpty(contextPath)
                || StringUtils.isEmpty(adminUrl)) {
            throw new RuntimeException("apache dubbo client must config the contextPath, adminUrl");
        }
        this.dubboConfig = dubboConfig;
        url = dubboConfig.getAdminUrl() + "/soul-client/dubbo-register";
        executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
    }

    @Override
    public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) {
        if (Objects.nonNull(contextRefreshedEvent.getApplicationContext().getParent())) {
            return;
        }
        // Fix bug(https://github.com/dromara/soul/issues/415), upload dubbo metadata on ContextRefreshedEvent
        Map<String, ServiceBean> serviceBean = contextRefreshedEvent.getApplicationContext().getBeansOfType(ServiceBean.class);
        for (Map.Entry<String, ServiceBean> entry : serviceBean.entrySet()) {
            executorService.execute(() -> handler(entry.getValue()));
        }
    }
}

判断是否需要注册

    我们直接来到具体处理的handler函数

    可以看到其直接从bean获取到类信息,而且如果是发射代理类的话,要获取到原来被代理的类(这里有些奇怪,为啥不能直接使用代理类,是方法生成后有什么不同吗?后面去探索一下)

    这里没有看到像HTTP那样的通配符了,Dubbo好像不能这样,这个感觉是个值得注意的点,感觉通配的话在从URL转Dubbo的路径的时候不好转,所有没有提供这方面的机制

    最后遍历类中的所有方法,如果有SoulDubboClient注解,将其接口注入到Admin中

public class ApacheDubboServiceBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {

    private void handler(final ServiceBean serviceBean) {
		// clazz == org.dromara.soul.examples.apache.dubbo.service.impl.DubboTestServiceImpl
        Class<?> clazz = serviceBean.getRef().getClass();
		// 这应该是得到反射代理生成的原本的类吧(为啥不能直接使用代理类呢?)
        if (ClassUtils.isCglibProxyClass(clazz)) {
            String superClassName = clazz.getGenericSuperclass().getTypeName();
            try {
                clazz = Class.forName(superClassName);
            } catch (ClassNotFoundException e) {
                log.error(String.format("class not found: %s", superClassName));
                return;
            }
        }
		// 遍历所有的方法,如果有相关的注解,则注册到Admin中
        final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(clazz);
        for (Method method : methods) {
			// public org.dromara.soul.examples.dubbo.api.entity.DubboTest org.dromara.soul.examples.apache.dubbo.service.impl.DubboTestServiceImpl.insert(org.dromara.soul.examples.dubbo.api.entity.DubboTest)
            SoulDubboClient soulDubboClient = method.getAnnotation(SoulDubboClient.class);
            if (Objects.nonNull(soulDubboClient)) {
                RegisterUtils.doRegister(buildJsonParams(serviceBean, soulDubboClient, method), url, RpcTypeEnum.DUBBO);
            }
        }
    }
}

注册信息

    下面是构造请求相关的函数,从其中可以看出,注册了选择器和规则相关的信息。在其中的Ext中还有分组、版本、负载均衡等信息

    我们没有看到像HTTP的那些IP和端口之类的信息,但联想到RPC的相关知识,大致知道Dubbo已经将其注册到Zookeeper中了

public class ApacheDubboServiceBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {

	// DubboConfig(adminUrl=http://localhost:9095, contextPath=/dubbo, appName=dubbo)
    private DubboConfig dubboConfig;

    private ExecutorService executorService;

	// http://localhost:9095/soul-client/dubbo-register
    private final String url;

    private String buildJsonParams(final ServiceBean serviceBean, final SoulDubboClient soulDubboClient, final Method method) {
        String appName = dubboConfig.getAppName();
        if (StringUtils.isEmpty(appName)) {
            appName = serviceBean.getApplication().getName();
        }
        String path = dubboConfig.getContextPath() + soulDubboClient.path();
        String desc = soulDubboClient.desc();
		// org.dromara.soul.examples.dubbo.api.service.DubboTestService
        String serviceName = serviceBean.getInterface();
        String configRuleName = soulDubboClient.ruleName();
		// /dubbo/findAll
        String ruleName = ("".equals(configRuleName)) ? path : configRuleName;
        String methodName = method.getName();
        Class<?>[] parameterTypesClazz = method.getParameterTypes();
        String parameterTypes = Arrays.stream(parameterTypesClazz).map(Class::getName)
                .collect(Collectors.joining(","));
        MetaDataDTO metaDataDTO = MetaDataDTO.builder()
                .appName(appName)
                .serviceName(serviceName)
                .methodName(methodName)
                .contextPath(dubboConfig.getContextPath())
                .path(path)
                .ruleName(ruleName)
                .pathDesc(desc)
                .parameterTypes(parameterTypes)
                .rpcExt(buildRpcExt(serviceBean))
                .rpcType("dubbo")
                .enabled(soulDubboClient.enabled())
                .build();
        return OkHttpTools.getInstance().getGson().toJson(metaDataDTO);

    }

    private String buildRpcExt(final ServiceBean serviceBean) {
        MetaDataDTO.RpcExt build = MetaDataDTO.RpcExt.builder()
                .group(StringUtils.isNotEmpty(serviceBean.getGroup()) ? serviceBean.getGroup() : "")
                .version(StringUtils.isNotEmpty(serviceBean.getVersion()) ? serviceBean.getVersion() : "")
                .loadbalance(StringUtils.isNotEmpty(serviceBean.getLoadbalance()) ? serviceBean.getLoadbalance() : Constants.DEFAULT_LOADBALANCE)
                .retries(Objects.isNull(serviceBean.getRetries()) ? Constants.DEFAULT_RETRIES : serviceBean.getRetries())
                .timeout(Objects.isNull(serviceBean.getTimeout()) ? Constants.DEFAULT_CONNECT_TIMEOUT : serviceBean.getTimeout())
                .url("")
                .build();
        return OkHttpTools.getInstance().getGson().toJson(build);
    }
}

Apache Dubbo 注册调用梳理

    下面是最终发送的请求信息,不像HTTP那样直观,如果没有接触过RPC,可以感觉有点懵,这里梳理一下RPC相关的注册调用流程

{
	"appName": "dubbo",
	"contextPath": "/dubbo",
	"path": "/dubbo/insert",
	"pathDesc": "Insert a row of data",
	"rpcType": "dubbo",
	"serviceName": "org.dromara.soul.examples.dubbo.api.service.DubboTestService",
	"methodName": "insert",
	"ruleName": "/dubbo/insert",
	"parameterTypes": "org.dromara.soul.examples.dubbo.api.entity.DubboTest",
	"rpcExt": "{\"group\":\"\",\"version\":\"\",\"loadbalance\":\"random\",\"retries\":2,\"timeout\":10000,\"url\":\"\"}",
	"enabled": true
}

    一个Dubbo的注册和调用流程图如下:

    注册和调用都有可能同时发生,也就是路由Soul网关的路由时可以动态变化的

    当有新的服务进行注册或服务更新时,就会将数据发给Zookeeper

    Boostrap那边有一个Dubbo的客户端之类的东西,会受到Zookeeper变化的服务信息,实时进行更新

    当请求进行访问时,Soul网关直接调用Dubbo的客户端,让它去访问后台的Dubbo服务,拿到结果后返回即可

总结

    本篇对Soul-Client的Apache Dubbo模块进行探索,发现和HTTP的注册流程大致相同:

  • 1.获取对应的Bean(类):

    • HTTP是通过在每个Bean构造完成后进行操作,这样比Dubbo可能要费时
    • Dubbo在所有Bean构造完成后进行操作,直接能获取Dubbo相关的Bean
  • 2.注册判断:两者都是通过判断是否满足相应的注解

  • 3.注册信息:

    • HTTP没有中间层,所有必须要有额外的IP和端口信息
    • Dubbo有Zookeeper中间层,Boostrap有个Dubbo客户端,通过Zookeeper能得到后台信息,就没有传入额外的IP和端口信息

    最后留一个疑问:为啥不能使用发射代理进行注册?

Soul网关源码解析文章列表

掘金

了解与初步运行

请求处理流程解析

数据同步解析

Soul-Client模块

番外