一起学RPC(零)

作者: MR丿VINCENT | 来源:发表于2018-08-02 10:17 被阅读82次

    最近又重新开始看jupiter的源码。这个开源项目是阿里的一位大神写的,比起现在较为流行的dubbo、motan等生产上的开源软件来说轻量很多,也比较容易入门学习。本来想看看dubbo的源码的,无奈第一步都没卖出去,被extension机制给难住了。虽说目前dubbo已经成为apache的孵化项目了,对于研究源码的渣渣我来说还是有一定的难度的。于是退而求其次,jupiter就是一个比较容易入手的选择。为什么说这个jupiter比较容易入门呢?首先代码比较少,不是很多,对阅读来说不会有很多绕的地方。其次这个项目有很多热心的网友也在一起读,可以有很多交流的地方,有一个专门讨论jupiter的交流群,可以很方便的和各路大神交流学习。

    因为jupiter源码我没有完全读完,只能看一点写一点。说不定等看完源码后再重新整理一下行文结构呢,也说不定放弃了呢,谁知道呢?

    按照常规思路来说肯定是从一个demo来入门,但是我不决定这么做,因为如果对rpc熟悉的伙计一定知道怎么去玩,不知道怎么去玩的现在可以关掉浏览器打lol或者吃鸡去了,因为你不配。没错,就是这么傲娇。

    看了这么多java rpc的框架比如motan、dubbo和jupiter,都有一个共同的地方,他们都使用spring作为容器来集成。这样也是情有可原,我相信java应用中没有不使用spring的吧。因此都选择这样去做大概是因为这样很容易去集成到自己的项目中。当然,这类rpc框架并不是一定得和spring集成。把他们称为“框架”其实并不是很准确。更准确的应该称为“中间件”。我的理解是因为他们虽然是集成到自己的项目代码中,但是他们却占用独立的端口。

    spring目前在java开发中的地位很高,使用spring来管理bean是非常流行的做法。更重要的是非常方便。对于中间件来说,通过寥寥几行xml的描述就能将一个复杂的bean实例化出来,而且耦合度很低,何乐而不为呢?看一个sonsumer的配置:

        <bean id="globalInterceptor1" class="org.jupiter.example.spring.interceptor.consumer.MyGlobalConsumerInterceptor1" />
        <bean id="globalInterceptor2" class="org.jupiter.example.spring.interceptor.consumer.MyGlobalConsumerInterceptor2" />
    
        <jupiter:client id="jupiterClient" registryType="default">
            <jupiter:property registryServerAddresses="127.0.0.1:20001" />
            <jupiter:property globalConsumerInterceptors="globalInterceptor1,globalInterceptor2" />
            <!-- 可选配置 -->
            <!--
                String registryServerAddresses                          // 注册中心地址 [host1:port1,host2:port2....]
                String providerServerAddresses                          // IP直连到providers [host1:port1,host2:port2....]
                ConsumerInterceptor[] globalConsumerInterceptors;       // 全局拦截器
            -->
    
            <!-- 网络层配置选项 -->
            <jupiter:netOptions>
                <jupiter:childOption SO_RCVBUF="8192" />
                <jupiter:childOption SO_SNDBUF="8192" />
                <jupiter:childOption ALLOW_HALF_CLOSURE="false" />
            </jupiter:netOptions>
        </jupiter:client>
    
        <bean id="interceptor1" class="org.jupiter.example.spring.interceptor.consumer.MyConsumerInterceptor1" />
        <bean id="interceptor2" class="org.jupiter.example.spring.interceptor.consumer.MyConsumerInterceptor2" />
    
        <!-- consumer -->
        <jupiter:consumer id="serviceTest" client="jupiterClient" interfaceClass="org.jupiter.example.ServiceTest">
            <!-- 以下都选项可不填 -->
            <!-- 服务版本号, 通常在接口不兼容时版本号才需要升级 -->
            <jupiter:property version="1.0.0.daily" />
            <!-- 序列化/反序列化类型: (proto_stuff, hessian, kryo, java)可选, 默认proto_stuff -->
            <jupiter:property serializerType="proto_stuff" />
            <!-- 软负载均衡类型[random, round_robin] -->
            <jupiter:property loadBalancerType="round_robin" />
            <!-- 派发方式: (round, broadcast)可选, 默认round(单播) -->
            <jupiter:property dispatchType="round" />
            <!-- 调用方式: (sync, async)可选, 默认sync(同步调用) -->
            <jupiter:property invokeType="sync" />
            <!-- 集群容错策略: (fail_fast, fail_over, fail_safe)可选, 默认fail_fast(快速失败) -->
            <jupiter:property clusterStrategy="fail_over" />
            <!-- 在fail_over策略下的失败重试次数 -->
            <jupiter:property failoverRetries="2" />
            <!-- 超时时间设置 -->
            <jupiter:property timeoutMillis="3000" />
            <jupiter:methodSpecials>
                <!-- 方法的单独配置 -->
                <jupiter:methodSpecial methodName="sayHello" timeoutMillis="5000" clusterStrategy="fail_fast" />
            </jupiter:methodSpecials>
            <jupiter:property consumerInterceptors="interceptor1,interceptor2" />
            <!-- 可选配置 -->
            <!--
                SerializerType serializerType                   // 序列化/反序列化方式
                LoadBalancerType loadBalancerType               // 软负载均衡类型[random, round_robin]
                long waitForAvailableTimeoutMillis = -1         // 如果大于0, 表示阻塞等待直到连接可用并且该值为等待时间
                InvokeType invokeType                           // 调用方式 [同步, 异步]
                DispatchType dispatchType                       // 派发方式 [单播, 广播]
                long timeoutMillis                              // 调用超时时间设置
                List<MethodSpecialConfig> methodSpecialConfigs; // 指定方法的单独配置, 方法参数类型不做区别对待
                ConsumerInterceptor[] consumerInterceptors      // 消费者端拦截器
                String providerAddresses                        // provider地址列表, 逗号分隔(IP直连)
                ClusterInvoker.Strategy clusterStrategy;        // 集群容错策略
                int failoverRetries                             // fail_over的重试次数
            -->
        </jupiter:consumer>
    

    对于一个相对比较成熟的rpc中间件来说,核心的bean配置是比较复杂的。你看看其中的参数就知道。通过spring的这种xml描述文件起码能够稍微容易地理解到一个bean需要哪些参数,哪些可以不要,同时根据xsd的约束能够让开发者更清楚的知道自己的配置有什么问题。如果不给api文档的情况下干巴巴的给你一个类,让你去实例化这个复杂的class,我相信很多人都会抓狂。在这个配置文件中很容易的看出要有2个节点:client和consumer。子节点的内容就是参数。consumer会去引用client去执行一个请求。而我们的业务中直接去调用consumer就完事了。如此而已,简单直观。

    这里的spring xml配置使用的是自定义的标签,算是对spring的拓展。不仅是jupiter,基本上大多数rpc中间件都实现了自己的一套标签,似乎不去自己实现一套自定义标签都不好意思开源。比如dubbo的自定义标签就是<dubbo:xxx>,motan类似如此。然而实际上也不是必须得实现自定义标签,使用spring的bean也是可以的,只不过显得很臃肿,不是那么直观罢了。

    对于一个新手来讲,这些东西显得格外的高大上。其实里面没有什么黑魔法,在spring的reference中对自定义标签有介绍。感兴趣的去看看这个官方文档:spring xml extension.

    要实现一个自定义的spring xml标签需要做一下几个步骤:

    • 定义一个约束文件,用来规范xml的内容。现在都流行使用xsd去编写约束文件,dtd已经成为老古董了。xsd了解一下.
    • 自定义一个NamespaceHandler的实现。实际上是去实现这个接口。非常容易,复制粘贴一把梭。
    • 写一个或者多个BeanDefinitionParser的实现。也是去实现接口,当然继承抽象类也是ok的。这个是最核心的内容。
    • 将上面所定义的全部注册到spring中,让spring知道有这些玩意儿。也就是在META-INF文件夹下新增两个配置文件:spring.handlersspring.schemas

    下面就结合jupiter中自定义的spring标签来谈谈他是如何实现的。

    首先得定义xsd约束文件,完整的定义在这里.这个没什么好说的,枯燥的xml定义罢了。无非就是定义有哪些元素,哪些元素下有哪些属性,其中有没有子元素,属性类型是什么,是不是必填的等等。

    接下来就是配置一个handler。这个handler用来解析自定义的标签。用过spring都知道,除了最常见的bean标签还有很多其他的标签,比如<context:component-scan><aop:aspectj-autoproxy proxy-target-class="true" />以及<mvc:annotation-driven/>等。这些标签和bean标签的不同之处在于都有一个前缀。我们称这个叫做命名空间。然而自定义的当然也得加上命名空间。虽说不能和bean平起平坐,但是和aop、context这样的标签还是可以一视同仁的。

    基于这种思路,那就很容易来自定义自己的标签了。难怪文档中对这个步骤加了一个说明:

    Coding a custom NamespaceHandler implementation (this is an easy step, don’t worry).

    的确如此,常人的思路就是照着spring的实现抄一把。如此简单!

    而比较复杂的就是对BeanDefinitionParser的实现了。这个是最核心的步骤。根据文档中的描述,这个可以有一个或者多个。但是在jupiter中只定义了一个实现。

    public class JupiterNamespaceHandler extends NamespaceHandlerSupport {
    
        @Override
        public void init() {
            registerBeanDefinitionParser("server", new JupiterBeanDefinitionParser(JupiterSpringServer.class));
            registerBeanDefinitionParser("client", new JupiterBeanDefinitionParser(JupiterSpringClient.class));
            registerBeanDefinitionParser("provider", new JupiterBeanDefinitionParser(JupiterSpringProviderBean.class));
            registerBeanDefinitionParser("consumer", new JupiterBeanDefinitionParser(JupiterSpringConsumerBean.class));
        }
    }
    

    spring的TaskNamespaceHandler中就使用了多个paser:

    public class TaskNamespaceHandler extends NamespaceHandlerSupport {
    
        @Override
        public void init() {
            this.registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
            this.registerBeanDefinitionParser("executor", new ExecutorBeanDefinitionParser());
            this.registerBeanDefinitionParser("scheduled-tasks", new ScheduledTasksBeanDefinitionParser());
            this.registerBeanDefinitionParser("scheduler", new SchedulerBeanDefinitionParser());
        }
    
    }
    

    这个paser用通俗的话来解释就是将在xml的配置参数给set到相应的实例中去。举个栗子:

    public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { 
    
        protected Class getBeanClass(Element element) {
            return SimpleDateFormat.class; 
        }
    
        protected void doParse(Element element, BeanDefinitionBuilder bean) {
            // this will never be null since the schema explicitly requires that a value be supplied
            String pattern = element.getAttribute("pattern");
            bean.addConstructorArg(pattern);
    
            // this however is an optional property
            String lenient = element.getAttribute("lenient");
            if (StringUtils.hasText(lenient)) {
                bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
            }
        }
    
    }
    

    这个栗子是继承自AbstractSingleBeanDefinitionParser并没有去实现BeanDefinitionParser接口。道理都知道,没有必要去实现一个要啥没啥的接口,吃现成的就好。重写父类的getBeanClass方法,将需要纳入spring管理的对象返回掉。这里不仅仅可以重写这个方法,还有其他例如getBeanClassName也行。值得注意的是如果采用继承抽象类的方式,这两个方法必须选择一个来重写。这个也非常容易理解,因为这个方法返回的class实例或者类的全路径名就是用来实例化的对象。如果通过实现接口的方式来定义paser就不需要考虑这个规则了,只需要创建出BeanDefinition的实例即可。jupiter中就是采用实现接口的方式,因为继承抽象类有一定的局限性,实现接口会有更多的灵活性。

    有了要煮饭的锅,就差下锅的米了。这个栗子中重写父类的doParser方法。从代码的表现上来看实际上就是将xml配置文件中的属性获取到,然后做一下检查放到实例化的对象中去。当然这里没有那么直接,这里使用的是BeanDefinitionBuilder来操作的。这只是最简单的实现。

    复杂的parser都是自己去实现接口的。比如jupiter:

    public class JupiterBeanDefinitionParser implements BeanDefinitionParser {
    
        private final Class<?> beanClass;
    
        public JupiterBeanDefinitionParser(Class<?> beanClass) {
            this.beanClass = beanClass;
        }
    
        @Override
        public BeanDefinition parse(Element element, ParserContext parserContext) {
            if (beanClass == JupiterSpringServer.class) {
                return parseJupiterServer(element, parserContext);
            } else if (beanClass == JupiterSpringClient.class) {
                return parseJupiterClient(element, parserContext);
            } else if (beanClass == JupiterSpringProviderBean.class) {
                return parseJupiterProvider(element, parserContext);
            } else if (beanClass == JupiterSpringConsumerBean.class) {
                return parseJupiterConsumer(element, parserContext);
            } else {
                throw new BeanDefinitionValidationException("Unknown class to definition: " + beanClass.getName());
            }
        }
    }
    

    jupiter的自定义parser中需要纳入spring管理的bean class对象是通过构造器传进来的。根据不同的class来作不同的处理。其中具体的逻辑很枯燥无味,就不再细细探讨了。不过我在看源码的过程中发现了一个细节的地方,也是值得注意的地方。

    JupiterSpringConsumerBean不仅仅和其他(如JupiterSpringServer等)实现InitializingBean,还实现了一个叫做FactoryBean的接口。这说明了一个问题,这个bean不是普通的bean,而是一个factory bean。相信很多人都会疑惑factory bean 和bean factory有什么区别。要我说两者都没有直接的联系,如果在面试的时候有人问我这个问题,我一定直接怼回去:雷锋和雷峰塔有什么区别?言归正传,这个factory bean本质上也是bean,但是与其他bean不同的是这个bean在spring容器中获取的方式和别的不一样。通常在spring中获取一个bean采用ctx.getBean(xxx.class)方法。通过这个方法获取的factory bean并不是他自己,而是它的某个成员。可以看看这个接口的定义:

    public interface FactoryBean<T> {
        T getObject() throws Exception;
    
        Class<?> getObjectType();
    
        boolean isSingleton();
    }
    
    

    也就是说返回的对象是getObject()返回值。那么这个接口存在的意义是什么呢?我也不复制粘贴了,觉着这篇文章写得很不错,浅显易懂。那么如何获取这个bean本身呢?干嘛想着获取它本身,简直是无聊!也有方法,加个前缀"&"就行了(ctx.getBean("&sb"))。

    最后呢,就是照着spring的官方文档抄一下配置文件。依葫芦画瓢,非常简单。

    完成了以上的几个步骤,自定义的spring xml标签就大功告成了。接下来要做的就是去使用自定义的标签。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:myns="http://www.mycompany.com/schema/myns"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.mycompany.com/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">
    
        <!-- as a top-level bean -->
        <myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
    
        <bean id="jobDetailTemplate" abstract="true">
            <property name="dateFormat">
                <!-- as an inner bean -->
                <myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
            </property>
        </bean>
    
    </beans>
    

    这里是抄的官方文档的栗子。标签myns:dateformat实际上定义了一个SimpleDateFormat的bean实例。在spring容器加载的时候这个实例就回被初始化。在使用自定义的标签的时候,需要注意的是得声明好命名空间和指定location,不然会报无法找到这个标签的错误。其实这些东西照着抄就行了,只是不要忘记了或者抄错了。

    自定义spring xml标签如此简单。无非就是照着文档抄一把,自己再改吧改吧万事就大吉了。对于其中核心的东西实际上还是一知半解,比方说BeanDefinition的具体实现原理等。上层的封装太抽象了,留给开发者的仅仅只是一个需要实现的方法。要想知道为什么要这样做,还得去研究spring的源码。

    rpc中的最简单的一个可选模块就这样简单的实现了。这是一小步,也是一大步。接下来会继续探索稍微核心一点的jupiter实现。

    相关文章

      网友评论

      本文标题:一起学RPC(零)

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