一起学RPC(零)

2018-08-06
一起读源码

最近又重新开始看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的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<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中只定义了一个实现。

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
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到相应的实例中去。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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并不是他自己,而是它的某个成员。可以看看这个接口的定义:

1
2
3
4
5
6
7
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<?> getObjectType();
boolean isSingleton();
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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实现。


留言: