相关知识
Dubbo URL
看Dubbo源码时经常看到URL这个参数,所以先分享一下Dubbo URL
url即一个资源在互联网上的地址,在Dubbo中它被用做配置总线的功能,和服务相关的配置都放在这个总线上。一个标准格式的URL如下
protocol://username:password@host:port/path?key=value&key=value
Dubbo中URL对象的构造函数如下
public URL(String protocol, String username, String password, String host, int port, String path, Map<String, String> parameters) {
}
Dubbo中URL对象的解析参数如下
参数 | 解释 |
---|---|
protocol | dubbo中的各种协议,dubbo,thrift,http |
username/password | 用户名/密码 |
host/port | 主机/端口 |
path | 路径,默认为接口名称,可以设置 |
parameters | 参数键值对 |
一些典型的Dubbo URL如下
// 描述一个 dubbo 协议的服务
dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000
// 描述一个 zookeeper 注册中心
zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=1214&qos.port=33333×tamp=1545721981946
// 描述一个消费者
consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer×tamp=1545721827784
在后续的介绍中,可以看到在Dubbo运行过程中,会频繁的对这个URL进行修改,增加等操作
为什么要有URL这个对象呢?其实主要就是为了方便传参的过程。将所有和服务相关的参数都放在URL中。
Invoker
在Dubbo中你会频繁看到Invoker,你可以把Invoker理解为一个可执行体
服务消费方用来执行远程调用(即代理类,封装了网络通信等细节)
服务提供方用来调用方法
先来看一下服务导出的整体流程,对服务导出的流程有个大概的了解
XML配置解析的过程
DubboNamespaceHandler来对dubbo的scame进行解析,并转为对应的对象
我们可以理解成application节点会被解析成ApplicationConfig对象,service节点会被解析成ServiceBean对象(实现了ApplicationListener接口),即每个导出的服务对应成一个ServiceBean对象
ServiceBean有2个重要的方法
afterPropertiesSet:初始化一些service的属性
onApplicationEvent:监听spring容器刷新事件,在这个方法进行服务导出
根据Bean的初始化顺序可以知道,先执行afterPropertiesSet方法,后执行onApplicationEvent方法
开始服务导出
ServiceBean实现了ApplicationListener接口
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
ApplicationEventPublisherAware {
所以当监听到ContextRefreshedEvent事件发生时开始导出服务
// ServiceBean.java
// 监听事件开始服务导出,观察者模式
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 是否已经导出 && 是否已被取消导出
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
// 服务导出的入口方法
export();
}
}
接着调用到
// ServiceConfig.java
public synchronized void export() {
// 检查及更新配置
checkAndUpdateSubConfigs();
// 有类似如下配置的时候,服务则不会暴露出去,例如本地调试等
// <dubbo:provider export="false" />
if (!shouldExport()) {
return;
}
// 延时导出服务
if (shouldDelay()) {
delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
} else {
// 立即导出服务
doExport();
}
}
在doExport中进行导出服务,会调用到doExportUrls()方法
可以看到一个服务可以以多种形式进行导出,并且注册到多个注册中心
// ServiceConfig.java
// 多协议多注册中心导出服务
// 配置文件中配了多个<dubbo:protocol/>
// 配置文件中配了多个<dubbo:config-center>
private void doExportUrls() {
// 加载注册中心链接
List<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
// 应用模型
// 服务名 对应的实现类 对应的接口
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
ApplicationModel.initProviderModel(pathKey, providerModel);
// 在每个协议下导出服务,并且注册到每个注册中心
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
doExportUrlsFor1Protocol方法比较长,因此截成几端来分析
// ServiceConfig.java
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
// 默认协议为dubbo
name = Constants.DUBBO;
}
// 省略部分代码
String scope = url.getParameter(Constants.SCOPE_KEY);
从第一行到取scope属性,这一大块主要就是构造url参数,看一下最终构建成的url对象长啥样
string内容如下
dubbo://192.168.97.70:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.97.70&bind.port=20880&default.deprecated=false&default.dynamic=false&default.register=true&deprecated=false&dubbo=2.0.2&dynamic=false&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=3752&qos.port=22222®ister=true&release=&side=provider×tamp=1599285134651
下面就正式开始服务暴露的过程,我们可以根据配置导出如下类型的三种服务
<!-- 导出本地服务 -->
<dubbo:service scope="local" />
<!-- 导出远程服务 -->
<dubbo:service scope="remote" />
<!-- 不导出服务 -->
<dubbo:service scope="none" />
导出本地服务
// ServiceConfig.java
private void exportLocal(URL url) {
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
// 显示指定injvm协议进行暴露
URL local = URLBuilder.from(url)
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
// 这里会调用InjvmProtocol#export
// 返回 InjvmExporter
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
}
}
看protocol,proxyFactory成员变量的定义,都是获取自适应扩展类。
即框架帮我们生成代理类,代理类在执行过程中从url获取对应的值,然后返回相应的实现类
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
老规矩用Arthas看一下生成的代理类,典型的Dubbo SPI代码
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
# 根据前面的序号选择进入的进程,然后执行下面的命令
jad *Adaptive
jad org.apache.dubbo.rpc.Protocol$Adaptive
生成的代码如下,后续环节我就不看生成的代理类了,都是一个套路
public class Protocol$Adaptive implements Protocol {
public Exporter export(Invoker invoker) throws RpcException {
String string;
if (invoker == null) {
throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
}
if (invoker.getUrl() == null) {
throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
}
URL uRL = invoker.getUrl();
String string2 = string = uRL.getProtocol() == null ? "dubbo" : uRL.getProtocol();
if (string == null) {
throw new IllegalStateException(new StringBuffer().append("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (").append(uRL.toString()).append(") use keys([protocol])").toString());
}
Protocol protocol = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(string);
return protocol.export(invoker);
}
public Invoker refer(Class class_, URL uRL) throws RpcException {
String string;
if (uRL == null) {
throw new IllegalArgumentException("url == null");
}
URL uRL2 = uRL;
String string2 = string = uRL2.getProtocol() == null ? "dubbo" : uRL2.getProtocol();
if (string == null) {
throw new IllegalStateException(new StringBuffer().append("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (").append(uRL2.toString()).append(") use keys([protocol])").toString());
}
Protocol protocol = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(string);
return protocol.refer(class_, uRL);
}
public void destroy() {
throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort() {
throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
}
从代码中可以看出
当URL中的protocol为registry时,protocol的实现类为RegistryProtocol
当URL中的protocol为injvm时,protocol的实现类为InjvmProtocol
仔细分析一个这行代码的执行过程
// ServiceConfig.java
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
JavassistProxyFactory将服务对象包装为AbstractProxyInvoker,然后被InjvmProtocol#export导出为InjvmExporter
将AbstractProxyInvoker导出为InjvmExporter
将Invoker包装为Exporter主要是为了方便对Invoker的生命周期进行管理
追JavassistProxyFactory的getInvoker方法
public class JavassistProxyFactory extends AbstractProxyFactory {
/**
* 针对provider端,将服务对象包装成一个Invoker对象
*/
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
// 重写类AbstractProxyInvoker类的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);
}
};
}
}
当有服务调用的时候最终会执行AbstractProxyInvoker#doInvoke(后续文章中会介绍服务调用的过程),而在这个方法中又会执行wrapper#invokeMethod,wrapper是框架用Javassist帮我们生成的类,包装了服务的实现类,主要是为了减少反射调用。
你可以看一下JdkProxyFactory#getInvoker(生成Invoker的另一种方式)方法,直接根据调用信息反射执行方法,效率比较低
总结一下导出本地服务的过程。
导出远程服务
在看后面的代码的时候还是要提一下,导出远程服务时有两种类型的URL
- 应用类型,如dubbo://192.168.97.70:20880/org.apache.dubbo.demo.DemoService
- 注册中心类型,如registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService,zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService
我们主要看一下从Invoker到Exporter过程即可,其他过程和导出本地服务的过程差不多
// ServiceConfig.java
// 从这里开始导出服务
// 调用了 RegistryProtocol
Exporter<?> exporter = protocol.export(wrapperInvoker);
传入的是wrapperInvoker,我们追一下url中的内容,就能看到最后的生成类
protocol为registry,所以protocol此时选择的类为RegistryProtocol,接着追RegistryProtocol
这个方法执行完毕,整个服务导出的过程就全完了,重点关注图中标红的三个部分
URL registryUrl = getRegistryUrl(originInvoker);
这行代码做的事情很简单
- 从parameters中key为registry中取值为zookeeper,将protocol改为zookeeper
- 将parameters中的registry移除
和上上个图对照一下就知道了
因为registryUrl的protocol=zookeeper,所以后续创建的Registry为ZookeeperRegistry
导出服务的过程如下,
// RegistryProtocol.java
// 导出服务,服务已经启动
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
服务启动的链路比较长,单开一个小节来分析
// 向注册中心注册服务
register(registryUrl, registeredProviderUrl);
注册完毕,方法返回DestroyableExporter,导出完毕。
画图总结一下
服务启动的过程
// RegistryProtocol.java
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
// 获取服务缓存的key
String key = getCacheKey(originInvoker);
// java8新特性,判断指定key是否存在,不存在就会调用函数接口,计算value并放入map
return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
Invoker<?> invokerDelegete = new InvokerDelegate<>(originInvoker, providerUrl);
return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
});
}
因为protocol.export(invokerDelegete)最后的实现类是由providerUrl中的protocol决定的,看一下到底是啥?
protocol=dubbo,所以会执行DubboProtocol#export方法来返回一个Exporter
这个部分有个重要的部分
protocol.export(invokerDelegete)
用SPI获取具体协议的时候,会被2个wrapper类包装(Dubbo SPI说过这个特性了哈)
ProtocolListenerWrapper和ProtocolFilterWrapper
所以调用各种协议的实现时,调用链路如下
ProtocolListenerWrapper:协议监听
ProtocolFilterWrapper:执行各种Filter
QosProtocolWrapper:在线运维服务
// DubboProtocol.java
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
// export service.
// 获取服务的key
// 例如 org.apache.demo.DemoService:20880
// 将Invoker转为Exporter,并且保存在 exporterMap 中
String key = serviceKey(url);
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
exporterMap.put(key, exporter);
// 省略部分代码
// 同一个机器的不同服务导出只会开启一个NettyServer
openServer(url);
optimizeSerialization(url);
return exporter;
}
// DubboProtocol.java
private void openServer(URL url) {
// find server.
String key = url.getAddress();
//client can export a service which's only for server to invoke
// 只有服务提供方才会启动监听
boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
if (isServer) {
ExchangeServer server = serverMap.get(key);
if (server == null) {
synchronized (this) {
server = serverMap.get(key);
if (server == null) {
// 创建服务器实例
serverMap.put(key, createServer(url));
}
}
} else {
// server supports reset, use together with override
server.reset(url);
}
}
}
这里就是创建server,并绑定端口的部分。不追了,画个流程图了,你们自己追一下把,不会晕的。
// DubboProtocol.java
private ExchangeServer createServer(URL url) {
// 省略部分代码
ExchangeServer server;
try {
// 传输默认选择的是
server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}
// 省略部分代码
return server;
}
画图总结一下服务启动的过程
因为最后默认启动的是NettyServer,因为Netty处理业务逻辑是通过ChannelHandler来处理,我们就来看看NettyServer中加入了哪些ChannelHandler,用了编解码的handler,IdleStateHandler,还有NettyServerHandler。
难道所有的逻辑用NettyServerHandler来处理?当然不是,在后面请求处理的部分,我们接着这部分继续追。
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// FIXME: should we use getTimeout()?
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
.addLast("decoder", adapter.getDecoder()) // 解码器handler
.addLast("encoder", adapter.getEncoder()) // 编码器handler
// 心跳检查handler
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
.addLast("handler", nettyServerHandler);
}
});
欢迎关注
参考博客
Dubbo 中的 URL 统一模型
[1]https://dubbo.apache.org/zh-cn/blog/introduction-to-dubbo-url.html
dubbo token
[2]https://www.jianshu.com/p/1a97f62ae663
挺好的一个系列文章
[3]https://www.bookstack.cn/read/apache-dubbo-2.7-source_code_guide/5e7559bb72a4ec11.md
[4]https://blog.csdn.net/qq_35190492/article/details/108345229