一起学RPC(零)

最近又重新开始看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实现。

    原文作者:MR丿VINCENT
    原文地址: https://www.jianshu.com/p/2df868381b9a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞