转自:https://www.yuque.com/docs/share/10960cb9-449b-4177-94b6-493b8f4ff9b9?
一、引言
服务引入是rpc调用不可或缺的一部分,本文会围绕以下几个问题对服务引入进行讲解。
- 什么是服务引入?服务引入需要做什么事情?
- spring作为当前java项目几乎必备的框架,如何将服务引入切入spring?
- 服务引入如何做到降低对业务代码的侵入性?
- 不同的提高方可能支持不同的通信协议,作为一个rpc框架怎么做到让调用端可以针对不同提高端使用不同的通信协议?甚至支持配置自定义协议?
- 服务提供者可能出现部分宕机的情况,如何保证引入服务的可用性?
本文以Dubbo的实现进行分析,因为日常项目中常常是配合Spring使用的,所以本文会以Dubbo结合Spring的使用进行分析。
二、解析
2.1 概况
2.1.1 什么是服务引入?
RPC(Remote Procedure Call) 即远程过程调用,包含了两个最基本的角色,服务提供者和服务消费者。
既然是远程过程调用,那么消费者在调用提供者之前就得先拿到服务提供者的服务信息(比如最基本的提供者的服务地址),并对调用信息进行封装准备好,我们才能对提供者发起调用请求。
而服务引入就是引入提供者服务信息,并封装调用类的一个过程。
2.1.2 服务引入类型
服务引用****有三种方式
(1) 本地引入
一个服务即可以提供者,同时也可以是消费者。
所以会存在消费者消费的服务,同时也是当前服务提供的服务,如图所示:
图 2.1
针对这种情况,RPC框架应该避免发起网络请求,直接本地调用。封装的调用类应当通过本地导出的服务发起调用。
(2) 直接服务
直接服务指的是直接在消费端指定提供端的服务地址。
如图直接将提供者url由消费端直接指定,发起调用时直接根据配置好的url发起调用。
图 2.2
优点:方便测试,直接连接服务,不依赖第三方。
缺点:存在服务可用性问题,也不能动态伸缩服务,不建议在线上使用。
(3) 基于注册中心引入
为了避免提高可用性,引入了注册中心。服务提供者将服务信息注册到注册中心,消费者订阅注册中心获取提供者url、提供者配置等服务信息,根据获取到的服务信息封装调用类发起调用。如图所示:
图 2.3
优点:提供者服务不可用时自动删除提供者信息,重启时自动恢复,可以动态伸缩服务。
缺点:相比直接引用,需要依赖服务中心,且需要保证注册中心的可用性。
2.2 切入spring
spring框架作为当前java项目几乎必不可少的框架,如何接入spring也是一个RPC框架需要考虑的点。
那么Dubbo是怎么接入的spring的呢?
在spring项目中引入Dubbo服务,只需要配置<****dubbo****:reference/>标签,然后依赖注入提供者就可以实现RPC调用。
那么spring项目启动的时候,dubbo是如何做到让<****dubbo****:reference/>标签被spring识别并解析呢?
2.2.1 切入入口
Spring启动时ClassPathXmlApplicationContext会对引入的配置文件进行解析,并将bean注入到spring容器中。
但是spring并不认识第三方自定义的标签,为了支持解析外部自定义的标签,Spring提供了扩展点,会通过查找classPath下所有 spring.handlers 文件,从该文件中获取所有扩展的命名空间处理器。也就是获取外部的标签处理器。
文件中的内容需要以键值对的方式表示,NamespaceUrl为key,value为解析器。spring在解析到某个外部标签时,会以外部标签的NamespaceUrl为key,获取对应的解析器,解析该xml标签。
图 2.4
而Dubbo就是通过这种方式去扩展,如图2.4所示,我们可以看到在Dubbo包下在 META-INF/spring.handlers 文件中,以dubbo标签的NamespaceUrl为key,解析处理器全限定名为value存储。Spring解析到dubbo标签时会通过dubbo提供的命名空间处理器DubboNamespaceHandler进行解析。
在Spring中spring.handlers文件最终由spring的DefaultNamespaceHandlerResolver加载并保存到标签处理器****集合handlerMappings中。获取handlerMappings源码如下:
图 2.5
如图2.5所示getHandllerMapping将DubboNamespaceHandler加载进了handlerMappings 中。
spring加载spring.handlers调用时序图如下:
图 2.6
由图2.6可知,Spring最终获取到解析xml标签的处理器之后,调用处理器的init方法、paser方法,最终获取到一个BeanDefinition注册到spring容器中。
(spring要求自定义命名空间处理器要实现NamespaceHandler接口,因此都会init方法和parse方法)
2.2.2 切入细节
我们已经的得知切入Spring的入口,那么作为一个RPC框架,应该如何去实现这个命名空间解析器,如何将标签转换成spring的BeanDefinition呢?
针对Dubbo的分析,我们已经得知dubbo标签最终会由DubboNamespaceHandler进行解析,并且最终Spring会调用命名空间解析器的init方法和parse方法,最终转换成spring的BeanDefinition。那么DubboNamespaceHandler这两步做了什么事情?
DubboNamespaceHandler类图如下:
(1)init 方法
一个完整的RPC服务不止是包含服务引入,同时也是还有协议定义、服务导出、注册中心等等模块,而RPC框架就得针对不同的模块定义不同的声明标签。那么在正式解析之前,就得先将不同模块的解析区分开,Spring也考虑了这一点,提供了NamespaceHandler接口的实现抽象类NamespaceHandlerSupport,该类提供了针对不同模块的标签进行注册的方法,实际上就是以模块名为key,对应的解析器为value存储在一个Map集合。
init顾名思义就是做一些初始化操作的,Dubbo就选择在init方法中注册不同模块的标签解析器。
DubboNamespaceHandler#init 源码如下
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}
public void init() {
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
// 服务引入标签解析 指定BeanDefinition BeanClass 为 ReferenceBean
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}
}
代码块 2.1
如代码块2.2.1所示,DubboNamespaceHandler 实现了NamespaceHandler 的init方法,将各类型标签的对应的解析器注册到解析器集合Map中。并且可以看出dubbo每个标签都是通过DubboBeanDefinitionParser进行解析,只是指定解析后BeanDefinition的beanClass不同。
(2) parse方法
parse也就是解析标签的方法,已经分析过Dubbo会注册不同标签的解析器,那么可以猜想parse方法会根据不同标签取出对应的解析器,再进行解析。
NamespaceHandlerSuppor****t#parse 源码如下:
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 根据标签名从解析器map拿出解析bean,根据解析bean的parse方法进行解析
return findParserForElement(element, parserContext)
.parse(element, parserContext);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 根据标签名从解析器map拿出解析bean
String localName = parserContext.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
代码块 2.2
如代码块2.2所示,根据标签名,取出对应的解析器,再通过解析器DubboBeanDefinitionParser进行解析。
已经拿到对应的解析器后,下一步就是将标签解析处理成BeanDefinition并注册到spring容器。
解析标签创建BeanDefinition,需要指定实际引用类beanClass, 保存标签类的各种配置信息,再指定beanName将BeanDefinition注册到spring容器。
DubboBeanDefinitionParser 通过parse方法进行解析xml标签,部分源码如下:
public BeanDefinition parse(Element element, ParserContext parserContext) {
// beanClass 即为Dubbo在init创建DubboNamespaceHandler时指定beanClass.
return parse(element, parserContext, beanClass, required);
}
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
// 配置RootBeanDefinition bean类型
// 比如服务引用类型 则为ReferenceBean,id属性即为引用的bean名
beanDefinition.setBeanClass(beanClass);
String id = element.getAttribute("id");
...
if (id != null && id.length() > 0) {
if (parserContext.getRegistry().containsBeanDefinition(id)) {
throw new IllegalStateException("Duplicate spring bean id " + id);
}
// 以id作为bean名注册beanDefinition 到spring容器
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
beanDefinition.getPropertyValues().addPropertyValue("id", id);
}
... 一系列配置bean过程
... 将标签上配置的参数存到beandefinition的PropertyValues属性中
return beanDefinition;
}
代码块 2.3
以服务引用标签为例子,beanClass为ReferenceBean。
根据代码块2.3,DubboBeanDefinitionParser 通过 parse方法进行解析获取到beanClass为ReferenceBean的BeanDefinition,注册到spring容器并返回解析结果。
ps:
从该源码还可以看到,在解析的时候,发现spring容器中已经包含这个bean名,那么会报错重复的bean id,因此****dubbo****:reference标签指定的id不能是已经存在spring容器中的bean名
2.2.3 结论
基于Dubbo在服务引入切入Spring的方式,我们可以得到一下结论:
- RPC框架服务引入切入spring可以通过spring提供的xml解析扩展点spring.handlers,对xml解析进行了扩展。
- 框架定义的解析器,可以基于NamespaceHandlerSupport在init针对不同标签注册不同的自定义解析器,NamespaceHandlerSupport#****parse方法会去根据不同标签获取对应的自定义解析器。
- 自定义解析器parse方法需要创建BeanDefinition,保存标签配置信息,注册BeanDefinition到Spring容器。
2.3 服务引入实现分析
服务引入需要根据配置信息封装服务调用类,作为一个RPC框架还需要考虑如何支持区分多种服务引入类型,如何避免服务引入对业务代码的侵入,如何提高服务引入的可扩展性,支持用户自己指定引入服务调用使用的协议。
本节通过分析Dubbo的实现,解析Dubbo是怎么处理的。
根据2.2节的分析,xml解析只是将引入的bean转化成BeanDefinition,保存了配置一些信息,并没有去封装一个服务调用类。Dubbo的服务引用标签<reference>标签最终会被spring解析成ReferenceBean类型的BeanDefinition,并将标签上配置的参数注入到BeanDefinition(比如引用服务的权限定名),并加载到bean容器中。
我们根据以下几点做下分析
- ReferenceBean是什么时候真正去封装服务调用类的
- 我们依赖注入的是引用类, 为什么beanClass是ReferenceBean
- 为什么我们直接注入引用类就可以关联上提供者类,并发起rpc调用
ReferenceBean类图如下:
图 2.7
2.3.1 封装调用类时机
(1)懒汉式
懒汉式即用到ReferenceBean这个引入服务被用到才去封装。
由图2.7可以看到ReferenceBean实现了FactoryBean,因此当我们依赖ReferenceBean的时候,spring会调用getObject()方法去获取真实的bean。
ReferenceBean#getObject部分源码如下:
public Object getObject() throws Exception {
return get();
}
public synchronized T get() {
if (destroyed){
throw new IllegalStateException("Already destroyed!");
}
// ReferenceBean 全局变量ref 代表这个引用bean真实的业务bean
if (ref == null) {
init();
}
return ref;
}
private void init() {
// 做一些创建代理类前的校验和配置操作(比如校验接口合法性、封装URL参数到map(比如interface=com.xxService))
...
// 根据参数map调用封装调用类
// 根据调用类创建代理类,将代理类赋予ref变量
ref = createProxy(map);
...
}
代码块 2.4
可以看到这个方法就是调用init()方法初始化,并将引用ref返回, 也就是当引入服务被依赖到的时候,会去封装调用类。
(2)****饿汉式
饿汉式即引用类没有被依赖也会
ReferenceBean实现了InitializingBean,因此初始化ReferenceBean时,Spring容器会调用 ReferenceBean的afterPropertiesSet方法。
ReferenceBean部分源码如下:
public void afterPropertiesSet() throws Exception {
····
装载 监控器、注册中心信息、应用配置信息、消费端配置信息等等
····
Boolean b = isInit();
if (b == null && getConsumer() != null) {
b = getConsumer().isInit();
}
if (b != null && b.booleanValue()) {
getObject();
}
}
代码块 2.5
通过代码块2.5 我们可以看到,afterPropertiesSet 方法主要做了一些初始化操作,最后判断是否初始化bean, 如果需要则会调用init()方法初始化bean,赋予ref变量。
ps:
默认是关闭状态,即不会开启。需要初始化可通过配置<dubbo:reference>
的 init 属性开启。
2.3.2 调用类封装细节
服务引入需要封装调用类,需要做哪些事情?
- 获取提供端地址。封装调用类,首先要知道提供者的地址,并且因为有多种类型的服务引入,得区分多种服务引入方式的服务地址。
- 获取传输协议。不同的提供者可能支持的传输协议不一致,因此需要获取传输协议类型。
- 根据获取到的配置信息创建调用类。
- 封装代理类。为了避免造成代码侵入,不能让业务代码直接依赖框架封装的调用类,所以需要支持让业务代码可以直接依赖提供端。那么RPC框架就需要根据提供端和调用类封装一个代理类。
2.3.2.1 获取提供端地址
前面我们讲过,服务引入分为三种类型(本地引入、直接引入、基于注册中心引入),三种引入类型获取的提供端地址也不同。
- 本地引入
判断是否为本地调用,如果是本地调用则根据提供端信息拼接URL,格式为injvm:127.0.0.1:0/com.service?param。
判断是否为本地调用流程图如下:
图 2.8
通过流程图我们可以看到,如果ReferenceBean指定的inJvm=ture或者scope=local则认为是本地调用(通过标签配置)。
否则如果作用域没有指定remote、并且不是泛化调用、并且本地暴露的服务包含该服务才认为是本地引用。
- 直接引用
如果判断不是本地调用,则判断是否存在直接引用地址(通过标签的url指定)。
如果是存在直接引用URL,假设配置的URL是dubbo协议的,则url的格式为 dubbo://service-host/com.service?param。
因为直接引用也可能是配置注册中心地址
因此Dubbo判断是直接引用是registry前缀的地址,则会加上refer参数,标示实际调用哪个提供者,如下。
- 基于注册中心引用
如果没有指定引用URL,则会通过加载注册中心地址,获取到注册中心的地址集合,URL的格式为
registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode("consumer://host/com.Service?version=1.0.0")
即地址为注册中心地址,refer参数为实际引用的提供者
需要注意的是,直接引用都是可能配置多个地址的,而通过注册中心获取也可能会获取到多个提供端地址,因此获取到的地址可能是多个的。
2.3.2.2 获取传输协议
Dubbo针对不同协议都封装了对应的Protocol类,因此本节分析Dubbo如何根据当前传输协议获取对应的Protocol类。
截取ReferenceBean Protocol的获取如下,
private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
我们可以看到ReferenceBean中并没有固定创建某一个ReferenceBean的实现类,那么Dubbo是怎么做到根据不同传输协议获取对应Protocol的呢?
分析getAdaptiveExtension()的实现,该方法最终创建了一个Protocol的代理对象,由该代理对象来根据当前传输URL获取对应的Protocol。
该方法内部根据代理的对象类型(比如:Protocol)动态拼接java代码,动态拼接code生成自适应扩展对象,并动态编译,通过类加载器加载到jvm中,返回代理对象。
流程图如下:
图 2.9
以Protocol为例子,拼接后的java代码如下:
package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
// 不被代理的方法 如果被调用直接报错
public void destroy() {throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort() {throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
// 代理导出方法
public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
public void destroyServer() {throw new UnsupportedOperationException("method public default void com.alibaba.dubbo.rpc.Protocol.destroyServer() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
// 代理引入方法
public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
if (arg1 == null) throw new IllegalArgumentException("url == null");
// 获取url参数
com.alibaba.dubbo.common.URL url = arg1;
// 获取url上配置的协议
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
// 根据协议名称 从dubbo容器中获取对应的协议类
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
// 根据协议类创建调用对应的服务引入方法
return extension.refer(arg0, arg1);
}
}
dubbo就是通过动态拼接java code,在运行时生成自适应扩展bean,由这个bean来获取ReferenceBean配置的protocol和cluster对应的实现类.
动态拼接code逻辑如下:
图 2.10
主要思想:
通过@Adaptive注解表明哪些方法需要被代理,被代理的方法都要能提供URL参数,代理对象会根据URL以被代理类为key,获取对应参数值,从而返回对应的实现类的bean名,再通过bean名从Dubbo容器中获取对应的实现累。如果URL上没有指明用哪个实现类,则用@SPI注解上的值为key获取对应的实现类。
以Protocol为例,源码如下:
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
void destroyServer();
}
代码块 2.6
export和refer方法都被@Adaptive修饰,因此这两个方法。生成的代理类从URL上获取以protocol为名的参数值作为key,从dubbo容器中取出对应的实现类去执行。如果没有获取到则以@SPI上的值dubbo作为key取出对应的protocol。即默认得到了DubboProtocol。
ps
同样通过这个机制,也进而得以支持Spi扩展,可以在运行时才确认使用哪个实现类,方便外部扩展。比如我新增一个自定义协议MyProtocol,配置指定服务引入的协议为myProtocol, 并将这个协议对应的实现类基于dubbo的spi扩展注入到Dubbo容器中。那么代理类就可以根据URL上的协议名,基于spi获取对应的协议实现类,再根据调用类调用refer方法。
2.3.2.3 封装调用类
获取到提供者的url和传输的协议对象后,就开始封装调用对象了。
封装调用对象需要考虑做几个事情
- 先前基于注册中心引入服务的地址,并非最终发起调用的协议和地址,而是以注册中心地址为路径,提供端地址为参数组合。因此需要区分开来,封装真正的调用地址。并且registry协议并非真正传输协议,只是标识是注册中心引入,封装调用类还得替换成真正的传输协议,比如dubbo协议。
- 由于提供者有可能有多个提供者,因此需要考虑如何将多个提供者封装成一个调用者,发起调用时如何处理。
- 为了便于知道消费端消费情况,消费端也需要将消费的服务注册到注册中心。并且为了在提供者发生变动时收到通知,还需要订阅提供者的节点数据。
截取ReferenceConfig#createProxy封装调用类invoke源码如下:
// 截取部分注册中心和直接引用获取到URL集合后的代码
if (urls.size() == 1) {
invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
....
// 遍历多个url 生成invoker集合
for (URL url : urls) {
invokers.add(refprotocol.refer(interfaceClass, url));
}
if (registryURL != null) {
// 指定Cluster为AvailableCluster 选择任意可用的服务
// 如果是注册中心则说明当前遍历的是注册中心地址,所以使用AvailableCluster封装invoke
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
// 基于Cluster合并多个invoker 基于集群容错策略调用
invoker = cluster.join(new StaticDirectory(u, invokers));
} else {
invoker = cluster.join(new StaticDirectory(invokers));
}
}
代码块 2.7
如代码块2.7所示,只有一个URL时直接通过协议类封装一个invoker对象,如果有多个URL(即多个服务提供者)则通过Cluster封装多个,后续基于集群容错策略做调用(关于Cluster相关本文不做讲解,属于负载均衡处理模块的范畴)。
以Protocol为例,调用该代理对象refer方法,代理对象会解析refer方法传入的URL不同的协议获取到不同的Protocol实现类,通过对应协议类****Protocol创建对应invoke实现类,不同协议类会创建不同的invoke类。
关于refer方法的实现,本地引入和直接引入都是直接根据URL、interfaceClass创建Invoke实现类, 重点讲一下RegistryProtocol。
RegistryProtocol的refer方法,截取关键源码如下:
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 修改url的协议内容 根据参数配置的协议进行修改,如果没有配置默认为dubbo
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
// 连接注册中心
Registry registry = registryFactory.getRegistry(url);
...
return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// 生成消费服务地址 如consumer:consumer-host/com.xxService?param
// com.xxService表示消费的提供者全限制定名
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
...
// 在注册中心消费者目录注册消费端地址
// 例如:在zk注册消费端地址的目录为 /分组名/服务权限定名/consumers/subscribeUrl
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
// 订阅注册中心节点数据 providers、routers、configurators
// 订阅的时候会
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY +
"," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY));
// 这里使用cluster是因为注册中心可能会有多个提供者,因此返回的invoke是具备选择提供者能力的invoke
return cluster.join(directory);
}
代码块 2.8
如代码块2.8所示,RegistryProtocol的refer方法主要是做了几个事情
- 将原本url为registry协议,修改成真正发起调用使用的协议,默认为dubbo。
- 连接注册中心,创建注册中心实例。
- 注册消费端地址。
- 订阅注册中心提供端和配置数据。
- 基于RegistryDirectory和cluster创建invoke对象返回。
思考:
- 当多注册中心和多提供者时,怎么选择注册中心和怎么选提供者流程是怎么设计?
- 注册中心最大的作用是可以提高服务调用的可用性,某个提供者服务挂了之后,自动下线该提供者,避免调用到该提供者,或者流量增大服务无法应对,动态扩容提供者。那么如何在提供者变动时去更新invoke.
图 2.10
针对第一点,dubbo基于多个注册中心url生成invoke集合,再通过StaticDirectory包装,cluster固定使用****AvailableCluster进行选择任意可用的节点(代码块2.7)。获取到clusterInvoke之后,再根据cluster策略(服务引入配置)选择一个提供者。
针对第二点,dubbo处理某个注册中心url时,返回的是通过RegistryDirectory与cluster创建的invoke,RegistryDirectory会监听注册中心的通知,动态更新提供者集合。
dubbo基于RegistryDirectory订阅注册中心,订阅的时候会将当前RegistryDirectory作为监听器,当订阅的节点发生变动的时候就会通知RegistryDirectory更新invoke集合。notity方法源码截取如下
public synchronized void notify(List<URL> urls) {
// 1. 根据URL的协议类型封装各种订阅URL集合
// 2. 更新configurators URL
// 3. 更新routers URL
...
// 4. 更新providers URL
refreshInvoker(invokerUrls);
}
private void refreshInvoker(List<URL> invokerUrls){
...
// 将URL列表转成Invoker列表
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
...
this.urlInvokerMap = newUrlInvokerMap;
// 关闭未使用的Invoker
destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap);
}
代码块 2.10
2.3.2.4 创建代理类
根据引用的服务类和invoke对象生成代理对象返回。
// ReferenceConfig源码 代理工厂代理类,根据url获取对应代理类
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
proxyFactory.getProxy(invoker);
代码块 2.9
默认代理方式支持Javassist和Jdk代理,可以通过引用标签配置proxy指定。
与上面讲述的Protocol一样,是一个动态生成的类,会根据url上的参数获取对应的动态代理实现类。
为什么要创建代理类?
假设我们不创建代理类,那么生成的就是invoke对象,客户端就不能通过直接注入提供服务类方式,而是要依赖invoke对象,造成代码入侵。
有了代理类我们就可以直接注入提供者,实际上调用的时候就是通过invoke发起调用了。
2.4 拓展思考回顾
- 本地通过<dubbo:provider>暴露了dubbo服务,那么我们调用本地dubbo服务时是否会发起网络请求?
- 通过手动创建T****estService注入spring容器,又通过<dubbo:reference>引用服务T****estService,依赖注入获取到的是哪个bean?
- 使用懒汉式时,只通过Spring注入引用类就不会立即创建调用类,而是实际用到才创建吗?
问题解答:
- 不会,使用的是本地服务调用
- 以注入beanName为主,beanName无法对应则随机取一个。
- 会取封装调用类。
针对第三个问题进行解析,在依赖注入的时候,注入的bean就会被加载了,因此ReferenceBean实现的getObject方法就会被调用,调用类也会被封装创建。
ReferenceBean****懒加载常规情况下,只能保证当你服务中没有依赖引入的服务时,保证getObject不会被执行。
这时有人可能想问了,ReferenceBean的BeanDefinition不是都加入到Spring容器中了吗,Spring容器不是会对进行所有BeanDefinition进行初始化创建吗?
其实Spring加载所有BeanDefinition去创建时,BeanDefinition因为本身实际上是ReferenceBean,会先以 &beanName 创建ReferenceBean本身。然后再判断要不要是否需要早期初始化,如果需要才会去创建真实的bean。
所以如果在没有被依赖的情况下,也就不会以beanName去创建bean,所以也就不会去调用getObject。
public void preInstantiateSingletons() throws BeansException {
...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 是否为懒加载
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 判断是否为FactoryBean
if (isFactoryBean(beanName)) {
// FACTORY_BEAN_PREFIX = &
// &beanName 表示获取实现FactoryBean的类本身
final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
// 判断是否是SmartFactoryBean
// 如果只是实现FactoryBean,则默认不会去创建真实的object
boolean isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
// 不需要早期初始化 因此不会去调用getObject
if (isEagerInit) {
// 获取beanName对应的bean 真正创建调用getObject创建bean
getBean(beanName);
}
}
....
}
}
}
怎么实现懒汉式加载ReferenceBean****,但是又要依赖引用类?
可以搭配@Lazy使用,让依赖的bean被懒加载,这时获取到的是懒加载bean代理类,只有真正发起调用时才会去获取bean,这样就可以实现在真正发起调用才调用getObject创建服务引用调用类。
三、总结
服务引入最基本的实现就是根据提供者信息封装成一个调用类,但是作为一个优秀的RPC框架,得考虑方方面面的问题。
- 避免代码侵入。为引入服务生成代理类。
- 提高可用性。引入了注册中心处理机制。
- 提高扩展性。引入了自定义适应类,根据url参数自动选择对应的实现类。同时也牵扯到Dubbo实现了自己的IOC容器。
- 提高启动性能,避免加载无效引入。引入了懒加载机制。
- Spring作为广泛使用的框架如何接入启动。基于Spring的扩展机制,实现了一套加载机制。