文章目录
一、前言
本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。
系列文章地址:Dubbo源码分析:全集整理
本文衍生篇:
在 Dubbo笔记③ : 服务发布流程 - ServiceConfig#export 中我们了解到服务发布时,首先会解析出来所有的注册中心 registryURLs,再针对不同的协议类型进行服务发布。如下:
// org.apache.dubbo.config.ServiceConfig#doExportUrls
private void doExportUrls() {
// 加载所有注册中心 URL
List<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
// 遍历所有协议,进行服务发布
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
我们在之前的文章中已经分析了 ServiceConfig#loadRegistries 方法的实现,下面我们来看一下 ServiceConfig#doExportUrlsFor1Protocol 的实现:
二、ServiceConfig#doExportUrlsFor1Protocol
在加载出所有的注册中心URL后,此时的URL并非终点,还需要继续根据不同的协议进行解析 而 ServiceConfig#doExportUrlsFor1Protocol 对不同注册中心 和 不同协议的进行了服务发布,下面我们来详细看一下其实现过程。
// org.apache.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
=========================1. 下面开始解析配置参数 ===============
// 解析各种配置参数,组成URL
// 获取当前协议的协议名,默认 dubbo,用于服务暴露
String name = protocolConfig.getName();
if (name == null || name.length() == 0) {
name = Constants.DUBBO;
}
// 追加参数到map中
Map<String, String> map = new HashMap<String, String>();
map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);
appendRuntimeParameters(map);
appendParameters(map, application);
appendParameters(map, module);
appendParameters(map, provider, Constants.DEFAULT_KEY);
appendParameters(map, protocolConfig);
// 这里将 ServiceConfig 也进行的参数追加
appendParameters(map, this);
// 1.1 对 MethodConfig 的解析。MethodConfig 保存了针对服务暴露方法的配置,可以映射到 <dubbo:method />
// 对 methods 配置的解析,即是对参数的解析并保存到 map 中
if (methods != null && !methods.isEmpty()) {
for (MethodConfig method : methods) {
// 添加 MethodConfig 对象的字段信息到 map 中,键 = 方法名.属性名。
// 比如存储 <dubbo:method name="sayHello" retries="2"> 对应的 MethodConfig,
// 键 = sayHello.retries,map = {"sayHello.retries": 2, "xxx": "yyy"}
appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
// 检测 MethodConfig retry 是否为 false,若是,则设置重试次数为0
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
// 获取 ArgumentConfig 列表
List<ArgumentConfig> arguments = method.getArguments();
if (arguments != null && !arguments.isEmpty()) {
for (ArgumentConfig argument : arguments) {
// convert argument type
// 检测 type 属性是否为空,或者空串,如果指定了type 则按照 type进行解析
if (argument.getType() != null && argument.getType().length() > 0) {
// 获取接口方法
Method[] methods = interfaceClass.getMethods();
// visit all methods
if (methods != null && methods.length > 0) {
for (int i = 0; i < methods.length; i++) {
String methodName = methods[i].getName();
// target the method, and get its signature
// 比对方法名,查找 MethodConfig 配置的方法
if (methodName.equals(method.getName())) {
Class<?>[] argtypes = methods[i].getParameterTypes();
// one callback in the method
// argument.getIndex() 默认值是 -1 ,如果这里不为-1,说明用户不仅设置了type还设置了index
// 则需要校验index索引的参数类型是否是 type类型
if (argument.getIndex() != -1) {
// 检测 ArgumentConfig 中的 type 属性与方法参数列表
// 中的参数名称是否一致,不一致则抛出异常
if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
// 添加 ArgumentConfig 字段信息到 map 中,
// 键前缀 = 方法名.index,比如:
// map = {"sayHello.3": true}
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
} else {
// multiple callbacks in the method
// index =-1 则需要遍历所有参数列表,获取到指定type 类型的参数
for (int j = 0; j < argtypes.length; j++) {
Class<?> argclazz = argtypes[j];
// 从参数类型列表中查找类型名称为 argument.type 的参数
if (argclazz.getName().equals(argument.getType())) {
appendParameters(map, argument, method.getName() + "." + j);
if (argument.getIndex() != -1 && argument.getIndex() != j) {
throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
}
}
}
}
}
}
} else if (argument.getIndex() != -1) {
// 用户未配置 type 属性,但配置了 index 属性,且 index != -1
// 添加 ArgumentConfig 字段信息到 map 中
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
}
}
}
} // end of methods for
}
// 1.2 针对 泛化调用的 参数,设置泛型类型( true、bean 和 nativejava)
if (ProtocolUtils.isGeneric(generic)) {
map.put(Constants.GENERIC_KEY, generic);
map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
} else {
// 获取服务版本信息
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put("revision", revision);
}
// 获取 将要暴露接口 的所有方法,并使用逗号拼接在map中
String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
logger.warn("NO method found in service interface " + interfaceClass.getName());
map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
} else {
map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}
// 1.3 如果接口使用了token验证,则对token处理
if (!ConfigUtils.isEmpty(token)) {
if (ConfigUtils.isDefault(token)) {
map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(Constants.TOKEN_KEY, token);
}
}
// 1.4 本地导出属性设置,本地导出,不开端口,不发起远程调用,仅与JVM 内直接关联
// LOCAL_PROTOCOL 值为 injvm
if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
protocolConfig.setRegister(false);
map.put("notify", "false");
}
// export service
// 获取全局配置路径
String contextPath = protocolConfig.getContextpath();
if ((contextPath == null || contextPath.length() == 0) && provider != null) {
contextPath = provider.getContextpath();
}
// 这里获取的host,port是dubbo暴露的地址端口
String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = this.findConfigedPorts(protocolConfig, name, map);
// 拼接URL对象
URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);
// 1.5 确定是否存在当前协议的扩展的ConfiguratorFactory可以用来设计自己的URL组成规则
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
=========================2. 下面开始服务导出 ===============
// 2. 服务导出, 针对 本地导出和远程导出
String scope = url.getParameter(Constants.SCOPE_KEY);
// don't export when none is configured
// scope 为SCOPE_NONE 则不导出服务
if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {
// export to local if the config is not remote (export to remote only when config is remote)
// scope 不为SCOPE_NONE 则导出本地服务
if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
// 2.1 本地服务导出
exportLocal(url);
}
// export to remote if the config is not local (export to local only when config is local)
//scope 不为SCOPE_LOCAL则导出远程服务
if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
// ... 日志打印
// 如果存在服务注册中心
if (registryURLs != null && !registryURLs.isEmpty()) {
for (URL registryURL : registryURLs) {
// 是否动态,该字段标识是有自动管理服务提供者的上线和下线,若为false 则人工管理
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
// 加载 Dubbo 监控中心配置
URL monitorUrl = loadMonitor(registryURL);
// 如果监控中心配置存在,则添加到url中
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
// 代理配置解析
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}
// 2.2 远程服务导出
// 将registryURL拼接export=providerUrl参数。 为服务提供类(ref)生成 Invoker
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
// DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 导出服务,并生成 Exporter
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
// 如果没有注册中心,则采用直连方式,即没有服务注册和监听的过程
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
/**
* @since 2.7.0
* ServiceData Store
*/
// 元数据存储
MetadataReportService metadataReportService = null;
if ((metadataReportService = getMetadataReportService()) != null) {
metadataReportService.publishProvider(url);
}
}
}
this.urls.add(url);
}
ServiceConfig#doExportUrlsFor1Protocol 的代码比较长,其中较大篇幅的内容都用于配置参数解析。我们将上面的代码分为两部分
- 解析配置参数:这一部分解析的URL 是待暴露服务的URL信息。包括 方法配置解析、泛化、token 等。
- 服务导出 : 将 服务 URL 解析完成后,则开始准备暴露服务。
关于这两步的具体内容,我们下面详细分析:
三. 解析配置参数
ServiceConfig#doExportUrlsFor1Protocol 方法将大部分篇幅都花在了解析配置参数的过程中,我们关注其中部分参数的解析。
关于Dubbo的配置参数意义,详情请参考 Dubbo – 系统学习 笔记 – 配置参考手册。
1. MethodConfig的解析
这部分内容比较冗长,这里并没有放出来,职责就是对 MethodConfig 配置的解析,我们以 <dubbo:method>
标签为例 。<dubbo:method>
标签为<dubbo:service>
或<dubbo:reference>
的子标签,用于方法级别的控制:
如下:
<dubbo:reference interface="com.xxx.XxxService">
<!-- 指定方法, 超时时间为3s,重试次数为2 -->
<dubbo:method name="findXxx" timeout="3000" retries="2">
<!-- 指定第一个参数(index=0,index 默认-1) 类型为callback -->
<dubbo:argument index="0" callback="true" />
<!-- 指定参数XXX 类型的(index=0) 类型为callback, index 和 type 二选一 -->
<dubbo:argument type="com.kingfish.XXX" callback="true"/>
<dubbo:method>
</dubbo:reference>
Dubbo method 的配置有两种方式,指定类型 type 或者指定 索引 index。在 上面解析的中针对这两种情况进行了解析, 上面的注释写的比较详细,这里就不再赘述。
2. 泛化实现的解析
这里判断一下是否是泛化实现,如果是则添加 generic 参数。泛化调用的参数我们在 Dubbo笔记③ : 服务发布流程 - ServiceConfig#export ServiceConfig#checkAndUpdateSubConfigs 中提到过是如何解析。
// 判断是否是泛化调用,判断逻辑就是 generic的值是否与 true、bean、nativejava相等
if (ProtocolUtils.isGeneric(generic)) {
// 标记泛化的规则
map.put(Constants.GENERIC_KEY, generic);
// 设置匹配所有的方法 。Constants.ANY_VALUE 为 *
map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
}
3. token属性的解析
token 作为 令牌验证,为空表示不开启,如果为true,表示随机生成动态令牌,否则使用静态令牌,令牌的作用是防止消费者绕过注册中心直接访问,保证注册中心的授权功能有效,如果使用点对点调用,需关闭令牌功能
<dubbo:service interface="" token="123"/>
代码也很简单,如下:
if (!ConfigUtils.isEmpty(token)) {
// 如果未 true 或者 default,则使用UUID
if (ConfigUtils.isDefault(token)) {
map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(Constants.TOKEN_KEY, token);
}
}
token 功能的实现依赖于 Dubbo的 TokenFilter 过滤器,如对其实现感兴趣,详参:
Dubbo笔记 ⑰ :Dubbo Filter 详解 中对 TokenFilter 解析的部分。
4. 本地服务的解析
Dubbo服务导出分为本地导出和远程导出,本地导出使用了injvm协议,这是一个伪协议,它不开启端口,不发起远程调用,只在JVM 内直接关联,但执行Dubbo的 Filter 链。
在默认情况下,Dubbo同时支持本地导出和远程协议导出,我们可以通过ServiceConfig 的setScope 方法进行配置,为none表示不导出服务,为 remote 表示只导出远程服务,为local表示只导出本地服务。
这里判断如果协议类型是 injvm 则直接设置为 本地导出服务。
// LOCAL_PROTOCOL 值为 injvm
if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
// 设置当前服务不注册到注册中心
protocolConfig.setRegister(false);
// 设置不通知
map.put("notify", "false");
}
5. ConfiguratorFactory 的解析
这里是根据当前协议类型来获取到 SPI 接口 ConfiguratorFactory 的实现类,这里是Dubbo预留的一个扩展点,通过 ConfiguratorFactory 可以用来设计自己的URL生成策略。
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
而默认的实现为:
override=org.apache.dubbo.rpc.cluster.configurator.override.OverrideConfiguratorFactory
absent=org.apache.dubbo.rpc.cluster.configurator.absent.AbsentConfiguratorFactory
我们可以通过定义 org.apache.dubbo.rpc.cluster.ConfiguratorFactory
文件来自定义策略,如下(由于默认策略为dubbo,所以我们指定dubbo协议我们定制的ConfiguratorFactory ):
CustomConfiguratorFactory 的实现参考OverrideConfiguratorFactory 的实现,我们这里通过自定义的CustomConfiguratorFactory 策略给URL 添加了 custom 参数 value 为 demo。
public class CustomConfiguratorFactory extends AbstractConfigurator implements ConfiguratorFactory {
public CustomConfiguratorFactory(URL url) {
super(url);
}
// 返回当前定制下的 AbstractConfigurator 实现类
@Override
public Configurator getConfigurator(URL url) {
return new CustomConfiguratorFactory(url);
}
// url 添加 custom 参数
@Override
protected URL doConfigure(URL currentUrl, URL configUrl) {
return currentUrl.addParameter("custom", "demo");
}
}
四、 服务导出
参数解析并非是重头戏,在上面一系列的解析结束后,Dubbo 开始进行服务导出的操作。我们这里假设使用zk作为注册中心,协议为dubbo。
在默认情况下,Dubbo同时支持本地导出和远程协议导出,我们可以通过ServiceConfig 的setScope 方法进行配置,scope 的取值有四种情况:
- none :表示不导出服务,
- remote :表示只导出远程服务,
- local :表示只导出本地服务。
- null :在默认情况下为null,会同时导出本地服务和远程服务
对于远程服务来说,提供者和调用者一般不在同一个环境中,通过网络进行调用,如下为远程服务调用图:
除此之外,Dubbo还提供了一种本地服务暴露和引用的方式,在同一个JVM 进行中发布和调用同一个服务时,这种方式可以避免一次远程调用,而直接在JVM 内进行通信。
由于我们这里没有设置 Scope, 所以本地服务和远程服务都会导出,下面我们一一来看:
1. 本地服务导出
本地服务导出并不需要与注册中心交互,也不需要开启远程服务,所以其实现比较简单,将某些参数限制,指定协议类型为injvm。
private void exportLocal(URL url) {
// 如果 URL 的协议头等于 injvm,说明已经导出到本地了,无需再次导出
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
// 设置协议为 injvm
.setProtocol(Constants.LOCAL_PROTOCOL)
// 设置host为127.0.0.1
.setHost(LOCALHOST)
// 设置端口为0
.setPort(0);
// 创建 Invoker,并导出服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
// 添加到暴露的服务集合中
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
}
}
因为Dubbo SPI 的原因,这里协议设置为 injvm,会使用 InjvmProtocol 来进行服务暴露。
关于 InJvmProtocol 的内容,我们在 Dubbo笔记 ⑤ : 服务发布流程 - Protocol#export 中进行了分析。
2 远程服务导出
相较于本地服务导出,远程服务导出需要考虑到协议类型,notify 通知等方面,所以逻辑就显得复杂许多。这里整个暴露流程简化流程如下:
下面我们来看具体实现:
// 如果存在服务注册中心
if (registryURLs != null && !registryURLs.isEmpty()) {
for (URL registryURL : registryURLs) {
// 是否动态,该字段标识是有自动管理服务提供者的上线和下线,若为false 则人工管理
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
// 1. 加载 Dubbo 监控中心配置
URL monitorUrl = loadMonitor(registryURL);
// 如果监控中心配置存在,则添加注册中心的 URL。key 为 monitor
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
// 代理配置解析
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}
// 2. 远程服务导出
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
// 将暴露的服务保存到 exporters中,exporters 保存了本机暴露出的服务
exporters.add(exporter);
}
} else {
// 3. 直连方式,不存在服务注册中心,直连方式是和上面的区别就是不经过注册中心注册,并不订阅注册中心节点。
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 由于没有注册中心,这里会直接根据服务指定的协议,如Dubbo,则会直接使用 DubboProtocol 来暴露服务。
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
这里需要注意Dubbo在这里区分了存在注册中心和不存在注册中心的情况,因为 Dubbo允许不使用注册中心而通过服务之间直接连接的方式来调用服务。其区别就是直连方式不会经过 Dubbo的 各种容错机制。
我们这里仍以上一篇的 URL 为例,这里为了方便描述,URL 做了简化,registryURL 和 URL 如下:
// 注册中心 URl
registryURL = registry://localhost:2181/org.apache.dubbo.registry.RegistryService®istry=zookeeper
// 要暴露的服务 URL
url = dubbo://localhost:9999/com.kingfish.service.DemoService
2.1 注册监控中心
对监控中心的处理并不复杂,通过loadMonitor 生成一个关于监控中心的 URL ,并追加到 url 上。
loadMonitor 方法是完成监控URL的生成过程,并不复杂,这里篇幅问题不再展开。
// 1. 加载 Dubbo 监控中心配置
URL monitorUrl = loadMonitor(registryURL);
// 如果监控中心配置存在,则添加
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
我们假设为存在 localhost:8080 的监控中心,则此时 URL 变为(URL 有简化) :
dubbo://localhost:9999/com.kingfish.service.DemoService
&monitor=http://localhost:8080?interface=org.apache.dubbo.monitor.MonitorService
2.2 服务的暴露
服务提供者导出服务具体使用的是下列代码:
// 1. 生成代理类
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 2. 暴露服务
Exporter<?> exporter = protocol.export(wrapperInvoker);
// 将暴露的服务保存到 exporters中,当消费者调用时,提供者可以从中获取已经暴露的服务
exporters.add(exporter);
这里需要需要注意的是,服务最终暴露出来的是 Exporter,其内部结构如下:
这里的 Invoker 是 通过JavassistProxyFactory#getInvoker 方法生成的匿名 AbstractProxyInvoker 类。从这里开始,慢慢进入了服务导出的核心内容,下面我们来继续具体分析这个过程。
2.2.1 生成代理对象
该部分代码如下,其中Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠拢,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
这里需要注意:
-
proxyFactory.getInvoker
的入参URL 为registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())
。这里将 url 添加到了 registryURL 中,所以proxyFactory.getInvoker 的入参 URL 如下:registry://localhost:2181/org.apache.dubbo.registry.RegistryService®istry=zookeeper &export=dubbo://localhost:9999/com.kingfish.service.DemoService &monitor=http://localhost:8080?interface=org.apache.dubbo.monitor.MonitorService
此时我们看一下 URL的结构:
registry://... : 保存了注册中心的信息, registry=zookeeper 表明使用zk作为注册中心 export=dubbo://... : 保存了要导出服务的信息 monitor=http://... : 保存了监控中心的信息
在后面我们会介绍,由于
registry://
所以 Dubbo会选择 RegistryProtocol 来处理本地服务暴露,而 RegistryProtocol 中会将 export 的参数取出识别出dubbo://
选择 DubboProtocol 来进行服务暴露。 -
proxyFactory 和 protocol 定义如下:
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
可以看出 proxyFactory 和 protocol 都是 SPI 扩展接口的适配器类型。所以这里的 proxyFactory实际上是
ProxyFactory$Adaptive
类型,所以这里首先执行的是ProxyFactory$Adaptive#getInvoker()
方法,而对于 ProxyFactory SPI 接口来说,默认的协议类型为 javassist,所以调用的是JavassistProxyFactory#getInvoker
方法获取了代理类。而JavassistProxyFactory#getInvoker
代码如下 :@Override public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) { // TODO Wrapper cannot handle this scenario correctly: the classname contains '$' // 将服务实现类转换成Wrapper 类,以减少反射的调用 final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); // 创建匿名 Invoker 类对象,并实现 doInvoke 方法。 return new AbstractProxyInvoker<T>(proxy, type, url) { @Override protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable { return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); } }; }
可以看到,在
JavassistProxyFactory#getInvoker
中将SPI 实现类动态转换成了 Wrapper,并封装成Invoker类型返回。到这里,便完成了 Dubbo笔记 ② : 架构概述 中描述的 服务提供实现类Ref 到 Invoker 的转换
所以这里我们可以总结出来 ProxyFactory#getInvoker 生成代理类的流程如下:
其中Wrapper 是在 JavassistProxyFactory#getInvoker
中动态生成的,如对于DemoService 接口来说,
public interface DemoService {
void sayMsg(String msg);
String sayHello(String name);
}
其生成的Wrapper 类如下,可以看到这里的 invokeMethod 方法通过方法名称来匹配调用的方法,从而调用ref 对应的方法。:
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* com.kingfish.service.impl.DemoServiceImpl
*/
package org.apache.dubbo.common.bytecode;
import com.kingfish.service.impl.DemoServiceImpl;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.bytecode.NoSuchMethodException;
import org.apache.dubbo.common.bytecode.NoSuchPropertyException;
import org.apache.dubbo.common.bytecode.Wrapper;
public class Wrapper1
extends Wrapper
implements ClassGenerator.DC {
public static String[] pns;
public static Map pts;
public static String[] mns;
public static String[] dmns;
public static Class[] mts0;
public static Class[] mts1;
public static Class[] mts2;
public static Class[] mts3;
@Override
public String[] getPropertyNames() {
return pns;
}
@Override
public boolean hasProperty(String string) {
return pts.containsKey(string);
}
public Class getPropertyType(String string) {
return (Class)pts.get(string);
}
@Override
public String[] getMethodNames() {
return mns;
}
@Override
public String[] getDeclaredMethodNames() {
return dmns;
}
@Override
public void setPropertyValue(Object object, String string, Object object2) {
try {
DemoServiceImpl demoServiceImpl = (DemoServiceImpl)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class com.kingfish.service.impl.DemoServiceImpl.").toString());
}
@Override
public Object getPropertyValue(Object object, String string) {
try {
DemoServiceImpl demoServiceImpl = (DemoServiceImpl)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class com.kingfish.service.impl.DemoServiceImpl.").toString());
}
// 这里会根据方法名匹配到对应实例到的具体方法
public Object invokeMethod(Object object, String string, Class[] classArray, Object[] objectArray) throws InvocationTargetException {
DemoServiceImpl demoServiceImpl;
try {
demoServiceImpl = (DemoServiceImpl)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
try {
if ("sayMsg".equals(string) && classArray.length == 1) {
demoServiceImpl.sayMsg((String)objectArray[0]);
return null;
}
if ("sayHello".equals(string) && classArray.length == 1) {
return demoServiceImpl.sayHello((String)objectArray[0]);
}
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class com.kingfish.service.impl.DemoServiceImpl.").toString());
}
}
这里就可以知道 ProxyFactory#getInvoker
返回的是由 JavassistProxyFactory#getInvoker
生成的匿名代理类 Invoker。
我们这里再来梳理一下:
-
当服务提供者暴露服务接口时,会调用 ProxyFactory#getInvoker 来生成一个 Invoker,此处由于 Dubbo SPI 的存在实际调用的是
JavassistProxyFactory#getInvoker
。我们这里为了方便描述,把JavassistProxyFactory#getInvoker
方法返回的 Invoker 称为 Javassist Invoker。 -
JavassistProxyFactory#getInvoker
方法中 第一步会生成一个 Wrapper 类来包装暴露的接口实例。如果需要调用实例的方法,则通过 Wrapper#invokeMethod 方法来调用,在 Wrapper#invokeMethod 中会根据调用的methodName 来直接调用实例的方法。如下代码:final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type)
-
JavassistProxyFactory#getInvoker
方法中 第二步会创建一个AbstractProxyInvoker 的匿名实现类并返回,即我们上面提到的 Javassist Invoker。而 Javassist Invoker 的doInvoke 方法具体实现会委托给 Wrapper#invokeMethod 方法来处理。@Override protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable { return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); }
-
当消费者调用提供者服务时,提供者会调用 Javassist Invoker 的 doInvoke 方法,该方法会委托给 Wrapper#invokeMethod 来处理,而 Wrapper#invokeMethod 中会根据 methodName 来调用实例的具体方法。即整个流程简化如下:
Invoker#invoke -> AbstractProxyInvoker#doInvoke -> Wrapper#invokeMethod -> 调用 Ref 具体方法
注: Wrapper 的存在是为了减少调用 接口实例的反射。如果没有Wrapper,在 Javassist Invoker 的 doInvoke 方法中,则会根据 methodName 直接反射调用 proxy 对象。而 Wrapper 中动态生成了针对当前对象的方法,会根据方法名是否相同来直接调用实例方法,免去了反射调用 proxy 对象的过程。
2.2.2 服务暴露
经过了上面的过程, 暴露的接口对象的代理对象已经创建,这一步开始对该代理对象进行暴露。
该部分代码如下:
// 使用 DelegateProviderMetaDataInvoker 包装了一下 代理invoker。DelegateProviderMetaDataInvoker 中除了 Invoker 还保存了服务配置的数据
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 进行服务暴露
Exporter<?> exporter = protocol.export(wrapperInvoker);
当执行到 protocol.export(wrapperInvoker);
时,同样由于Dubbo SPI 机制,实际调用的是 Protocol$Adaptive#export()
方法。
并且由于Dubbo SPI 的扩展点使用了Wrapper自动增强,对于 Protocol 来说,存在三个Wrapper 增强,如下:
我们在 Dubbo笔记衍生篇②:Dubbo SPI 原理 四、扩展点的自动包装
章节中介绍过 扩展点的自动包装,得知SPI 接口的包装类 会先于 SPI 接口执行,并且由于 Wrapper在加载时的Class是通过 ExtensionLoader#cachedWrapperClasses
保存,由于Set 无序,所以包装类的实际加载顺序是无序的,如果再人工添加一个ProtocolWrapper,顺序并非是直接追加。
而这里由于只有已知的这三个Wrapper,并且加载顺序不影响功能,为了方便描述,这里忽略这三个 Wrapper的排序问题。所以整个调用过程是 Protocol$Adaptive#export() => QosProtocolWrapper#export() => ProtocolListenerWrapper #export() => ProtocolFilterWrapper#export() => XxxProtocol#export()
。
其流程如图:
关于三个包装类的作用并不影响服务发布主流程,如有需要详细解读请阅:Dubbo笔记衍生篇③:ProtocolWrapper
在 Dubbo笔记 ③ : 服务发布流程 - ServiceConfig#export 一文中我们知道,对于 远程导出 的URL 来说:
-
存在注册中心:会使用 RegistryProtocol 来处理服务。这就导致 URL 变更为了
registry://localhost:2181/org.apache.dubbo.registry.RegistryService
,但是这个URL中并没有包含暴露的接口的信息,所以URL 在暴露服务时会添加一个参数 export来记录需要暴露的服务信息(即本文中2.2.1 生成代理类
中所提到的)。此时 URL 会变成registry://localhost:2181/org.apache.dubbo.registry.RegistryService&export=URL.encode("dubbo://localhost:9999/com.kingfish.service.DemoService?version=1.0.0")
。 而之后基于 Dubbo SPI的 自适应机制,根据 URL registry 协议会选择RegistryProtocol 来暴露服务,而 RegistryProtocol 只负责处理注册中心相关的内容,额外的暴露服务,会根据 export 参数指定的 URL 信息选择。这里URL 协议为 dubbo,则说明服务的暴露需要使用 Dubbo协议,则会使用 DubboProtocol 来进行服务暴露。 -
不存在注册中心 :不存在注册中心时,最终暴露服务的URL 为
dubbo://localhost:9999/com.kingfish.service.DemoService?version=1.0.0
,此时会根据 Dubbo SPI选择 DubboProtocol中的export方法进行暴露服务端口。这里 URL 也可能是别的协议,此时会寻找对应的Protocol 来处理,我们这里还是以 Dubbo 为例。
所以这里对于 XxxProtocol,存在三种情况:
- 本地服务导出:这种情况我们在上面说了,其内部根据URL 中Protocol类型为 injvm,会选择InjvmProtocol
- 远程服务导出 & 有注册中心:其内部根据URL 中 Protocol 类型为 registry,会选择RegistryProtocol
- 远程服务导出 & 没有注册中心:根据服务协议头类型判断,我们这里假设是 dubbo ,则会选择 DubboProtocol
关于服务暴露的具体内容,由于篇幅所限,所以另开新篇 : Dubbo笔记⑤ : 服务发布流程 - Protocol#export
以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
https://www.cnblogs.com/ClassNotFoundException/p/6973958.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正