JSF 源码分析(一)

作者:京东零售 李孟冬
架构设计


1.7.4-HOTFIX-T4 版本包布局及简要含义

看过了全包的简要,那么其核心的功能模块,就从常用的项目 xml 配置出发,便于我们的理解。如下:
jsf-provider.xml 配置
以我们地址服务的 jsf-provider.xml 文件为例,即:

可以看到,在 JSF 的配置文件中,我们并没有看到任何关于注册中心的内容。说到底,作为(集团自主研发的高效)RPC 调用框架,其高可用的注册中心重中之重,所以带着这份疑惑,继续往下探究,没有注册中心地址,这些标签是怎么完成服务的注册,订阅的。
配置解析
在 Spring 的体系中,Spring 提供了可扩展 Schema 的支持,即自定义的标签解析。
1、首先我们发现配置文件中自定义的 xsd 文件,在标签名称上找到 NamespaceUri 链接http://jsf.jd.com/schema/jsf/jsf.xsd
2、然后根据 SPI 加载,在 META-INF 中找到定义好 Spring.handlers 文件和 Spring.schemas 文件,一个是具体的解析器的配置,一个是 jsf.xsd 的具体路径
3、由此我们进一步查询继承 NameSpaceHanderSupport 或者实现 NameSpaceHandler 对应的接口类,在我们的 jsf 框架中 JSFNamespaceHandler 是采用继承前者(NameSpaceHanderSupport)去实现的,即:
com.jd.jsf.gd.config.spring.JSFNamespaceHandler

【补充】NamespaceHandler 的功能就是解析我们自定义的 JSF 命名空间的,为了方便起见,我们实现 NamespaceHandlerSupport,其内部通过 BeanDefinitionParser 对具体的标签进行处理,即对我们定义的标签进行具体处理。
com.jd.jsf.gd.config.spring.JSFBeanDefinitionParser#parse

4、最终这些配置(也就是我们在 xml 中配置的标签值)会解析成为 ServerConfig 和 ProviderConfig,并且会依据所配置的属性,对相应的类进行属性的赋值。
com.jd.jsf.gd.config.ServerConfig

com.jd.jsf.gd.config.ProviderConfig

初始化
OK,我们回到 JSFNamespaceHandler 看一下服务是如何暴露的。众所周知,在 Spring 容器中的 bean 一定会经历一个初始化的过程。所以通过 com.jd.jsf.gd.config.spring.JSFBeanDefinitionParser 实现 org.springframework.beans.factory.xml.BeanDefinitionParser 来进行 xml 的解析,另外通过 ParserContext 中封装了 BeanDefinitionRegistry 对象,用于 BeanDefinition 的注册,用来初始化各个 bean,即(com.jd.jsf.gd.config.spring.JSFBeanDefinitionParser#parse)。
如下:bean 类 ProviderBean 会监听上下文事件,并且当整个容器初始化完毕之后会调用 export()方法进行服务的暴露。
com.jd.jsf.gd.config.spring.ProviderBean

服务暴露
我们回到源码中,发现其核心代码逻辑如下图:
com.jd.jsf.gd.config.ProviderConfig#doExport

com.jd.jsf.gd.config.ProviderConfig#doExport 该方法的整体逻辑如下:
1、首先进行各种基本的校验和拦截,如:
[JSF-21200]provider 的 alias 不能为空;
[JSF-21202]providerconfig 的 server 为空;
[JSF-21203]同一接口+alias 的 provider 配置了多个;
...
2、其次获取所有的 RegistryConfig,如果获取不到注册的地址,那么就会走默认的注册中心地址:“i.jsf.jd.com”。

com.jd.jsf.gd.config.RegistryConfig

3、然后获取 provider 中配置的 server,如果 provider 中存在 server 相关的配置,即是 com.jd.jsf.gd.config.ServerConfig,此时会启动 server(serverConfig.start()),并且采用默认对应的序列化方式(serverConfig.getSerialization(),默认 msgpack)进行注册服务编码。
com.jd.jsf.gd.config.ServerConfig#start(服务启动)

4、start 方法中会调用 ServerFactory 中的方法产生相对应的对象,然后会调用 Server 中的方法去启动 Server,而在这个过程中,最终会对应到 ServerTransportFactory 产生相应的传输层,即是定位到 JSFServerTransport 的 start()方法,在这里我们可以看到,该部分实现了 netty 框架的 transport 层,在进入这个方法的时候,会根据是否配置使用 epoll 模型来选择所生成的对象是 EpollServerSocketChannel 或者 NioServerSocketChannel,然后在 ServerBootStrap 中初始化相关的参数,直到最后绑定好端口号。
com.jd.jsf.gd.server.ServerFactory#initServer

com.jd.jsf.gd.transport.JSFServerTransport#start

5、最后通过(this.register();)服务注册并且暴露出去,其中 JsfRegistry 这个类的对象,在该类的构造函数中会连接 jsf 的注册中心,如果注册中心不可用的话,会生成并使用本地的文件并且开始守护线程,并使用两个线程池去发送心跳检测以及重试机制,另外一个线程池去检测连接是否成功(com.jd.jsf.gd.registry.JSFRegistry#addRegistryStatListeners)。
com.jd.jsf.gd.registry.JSFRegistry

6、服务注册(com.jd.jsf.gd.registry.JSFRegistry#register)的过程会将对应的 ProviderConfig 转换为 JsfUrl 类,这里 JsfUrl 是整个框架的核心,他保存了一系列的配置,并且和其同样重要的还有订阅 Url 类 SubscribeUrl,这里 JsfUrl 属于服务 Url,服务 Url 中保存了协议,端口号,ip 地址等相关重要的信息,并且回到上层 JsfContext 会将配置信息维护起来(JSFContext.cacheProviderConfig(this);)。到这里(this.exported = true;)provider 的服务从配置装配到服务暴露就完成了。
com.jd.jsf.gd.registry.JSFRegistry#register

com.jd.jsf.vo.JsfUrl

jsf-consumer.xml 配置
以上是完成了 provider 服务的暴露,那么我们回到 consumer 中,看一下,如下:

我们在上方的配置文件中发现到了注册中心地址i.jsf.jd.com,也就是说服务注册相关的配置没有写到 jsf-provider.xml 端,只是配置到了 jsf-consumer.xml 中而已。
配置解析 &初始化
配置解析过程是同上,就不多做赘述了,最终这些配置会解析成为 ConsumerConfig 和 RegistryConfig,并且会依据所配置的属性,对相应的类进行属性的赋值。
com.jd.jsf.gd.config.ConsumerConfig->AbstractConsumerConfig

最终初始化映射到 ConsumerBean 类。
com.jd.jsf.gd.config.spring.ConsumerBean

服务订阅
不过我们发现其实现了 FactoryBean,如我们所了解的,如果一个 bean 实现了该接口(FactoryBean),它被用作一个对象的工厂来暴露,而不是直接作为一个将自己暴露的 bean 实例。这也就意味着需要调用 getObject()来获取真正的实例化对象(可能是共享的或独立的)。之所以这样使用的原因在于我们的 Consumer 端只能调用接口,接口是无法直接使用的,它需要被动态代理封装,产生代理对象,再把它放入 Spring 容器中。因此使用 FactoryBean 其实只是为了方便创建代理对象而已。
在 getObject()方法中 ConsumerBean 会调用子类 consumerConfig 的 refer()方法,从而开始了客户端的初始化的过程。在 refer()过程中,consumer 会去订阅相关的 provider 的服务。核心代码如下:

com.jd.jsf.gd.config.ConsumerConfig#refer
该方法的整体逻辑为,如下:
1、首先进行各种基本的校验和拦截,如:
consumer 的 alias 不能为空,请检查配置
同一个接口+alias+protocol 本地配置的超过三次,抛出启动异常
...
2、一些不同配置的逻辑,如是否泛化调用,是否走 injvm 调用等
3、通过工厂模式生成一个 Client 的实例,由于上面 ConsumerConfig->AbstractConsumerConfig 默认的集群策略 failover,所以在没有配置的情况下会生成 FailoverClient,然后进行相关的 Invoke 操作(this.proxyInvoker = new ClientProxyInvoker(this);)。
com.jd.jsf.gd.client.ClientFactory#getClient

【补充】目前 JSF 支持的集群策略有,failover:失败重试(默认);failfast:失败忽略;pinpoint:定点调用;
4、在 client 中,先定义其负载均衡,然后判断是否需延迟建立长链接。否的话,会直接进行初始化连接。其次如果未定义路由规则,或者不存在连接时,Client 会先初始化相关的路由以及初始化连接,如下:

5、在初始化连接(initConnections)中会进行调用 ConsumerConfig 中的 subscribe()进行服务的订阅,并且在初始化的过程中,consumer 会连接相应的 Providers。
com.jd.jsf.gd.client.Client#initConnections

6、我们看到在获取连接过程(connectToProviders())中,如果连接池中已经存在则直接返回,不存在则需要重新建立连接。如下:会初始化一个名为 JSF-CLI-CONN-#interfaceId 的线程池,在线程池中会执行对应任务,即获取到一个 ClientTransport 对象。
com.jd.jsf.gd.client.Client#connectToProviders

如果连接(com.jd.jsf.gd.transport.ClientTransportFactory#initTransport),则:1)首先判断相关协议;
com.jd.jsf.gd.transport.ClientTransportFactory#initTransport

2)然后 BuildChannel 建立连接(com.jd.jsf.gd.transport.ClientTransportFactory#BuildChannel),在这里会对应到 Server 端启动代码的相关参数,即是该处会监听服务器端的端口号,绑定到相应的 ip,并根据对应的通道做数据传输。
com.jd.jsf.gd.transport.ClientTransportFactory#BuildChannel

3)并且,对于 transport 层,我们应该关注他注入了哪些 pipeline:可以看到 JSFEncoder/JSFDecoder 用来解码,编码具体的协议。handler 则是具体的 channel 处理器,用于处理心跳包,客户端请求,和消息路由。最后回到最上层(com.jd.jsf.gd.config.ConsumerConfig#refer)将配置文件维护起来(JSFContext.cacheConsumerConfig(this);)。
com.jd.jsf.gd.transport.ClientChannelInitializer#initChannel

综上简单的过了一遍各自功能的初始化加载方式等简单阐述了一下,不过阅读起来肯定是偏零散的,需要我们根据以上步骤和源码包进行进一步探索。另外我们也可以用服务器启动日志上,看到 JSF 核心流程的日志记录,部分截图如下(感兴趣可以在预发机器的启动日志中翻阅):
服务器启动日志(JSF)


总的来说,其提供者 Provider,消费者 Consumer,注册 Registry 等关系应该如下图所示:
Architecture

第一部分:Provider 端启动服务向注册中心 Session 注册自己的服务,注册服务的形式如:[jsf://192.168.124.73:22000/?safVersion=210&jsfVersion=1691&interface=com.jd.wxt.material.service.DspTaskQueueService&alias=chengzhi36_42459] 此时在注册服务,暴露服务的过程中,此时 JsfRegistry 会进行初始化连接。在此过程中,如果有配置对应的 Server 端,那么还会对 Server 进行初始化.
第二部分:Consumer 在第一次获取实体信息时,由于其是 FactoryBean,故必须调用 getObject()方法去获取实体,在此过程会调 ConsumerConfig 的 refer()方法,即可将 Client 端注册到 jsf 注册中心并订阅对应 alias 下 Provider 的变化。在这个过程中会初始化 Consumer 以及 Provider 端的监听器,对相关的 Consumer 和 Provider 做事件监听。此过程会产生 Client 对象,并且在初始化该对象时,会默认的去使用随机的负载均衡策略,并且会初始化路由,初始化路由后,会调用 ConsumerConfig 的 subscribe 方法,然后会初始化客户端对之前启动的 Server 进行连接。
第三部分:注册中心会异步通知 Consumer 是否需要重新订阅 Provider。
第四部分:就是直接调用方法 invoke。
第五部分:Monitor 对服务进行监控,治理或者降级容灾。
流程图

最终调用,如下:
com.jd.jsf.gd.filter.FilterChain#invoke

参考资料
SPI: http://jnews.jd.com/circle-info-detail?postId=215333
NamespaceHandler:https://www.yisu.com/zixun/447867.html
jsf:https://cf.jd.com/pages/viewpage.action?pageId=132108361
netty:https://www.cnblogs.com/jing99/p/12515149.html
本次就先写到这,文章中如有问题,欢迎留言斧正。也希望能和更多志同道合的伙伴沟通交流。后续会再更新关于 JSF 心跳检测、服务治理、服务反注册、钩子工具等模块细化的分析以及当前我们平台系统初步接入 wormhole 平台(中间件 mesh 化)的一些经验分享。欢迎大家点赞关注。
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/1ce4dcc0aa5b6f565581fa72c】。文章转载请联系作者。
评论