一、引言
對于 Java 開發者而言,關于 dubbo ,我們一般當做黑盒來進行使用,不需要去打開這個黑盒。
但随着目前程式員行業的發展,我們有必要打開這個黑盒,去探索其中的奧妙。
本期 dubbo 源碼解析系列文章,将帶你領略 dubbo 源碼的奧秘
本期源碼文章吸收了之前 Spring、Kakfa、JUC源碼文章的教訓,将不再一行一行的帶大家分析源碼,我們将一些不重要的部分當做黑盒處理,以便我們更快、更有效的閱讀源碼。
雖然現在是網際網路寒冬,但乾坤未定,你我皆是黑馬!
廢話不多說,發車!
二、環境配置
本篇文章适合對 dubbo 有興趣 & 日常工作中有使用的人
環境配置:
- dubbo版本:3.1.8
- maven版本:3.5.4
- JDK版本:JDK8
- Zookeeper版本:3.4.9
因為服務資料是注冊在 Zookeeper 上的,是以需要一個 Zookeeper 的可視化界面:ZooInspector
當然,就算你上述環境配置不全,也不影響你本篇文章的閱讀體驗。
三、服務暴露
上一篇文章《從源碼全面解析dubbo服務注冊的來龍去脈》 我們分析了我們的 Dubbo 是如何解析 @EnableDubboConfig 和 @DubboComponentScan 這兩個注解的
我們留了一個 服務暴露 的過程沒有講。因為 服務暴露 在 Dubbo 中屬于比較重要的知識點,是以單獨一篇來進行講解
我們上一篇文章講到在 DubboDeployApplicationListener 裡面實作了 服務暴露 的過程
我們主要講一下 DubboDeployApplicationListener 的實作:
java複制代碼public class DubboDeployApplicationListener implements ApplicationListener<ApplicationContextEvent>, ApplicationContextAware, Ordered {
@Override
public void onApplicationEvent(ApplicationContextEvent event) {
if (nullSafeEquals(applicationContext, event.getSource())) {
// 判斷目前的事件
if (event instanceof ContextRefreshedEvent) {
// 重新整理事件
onContextRefreshedEvent((ContextRefreshedEvent) event);
} else if (event instanceof ContextClosedEvent) {
// 關閉事件
onContextClosedEvent((ContextClosedEvent) event);
}
}
}
}
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
ModuleDeployer deployer = moduleModel.getDeployer();
Future future = deployer.start();
}
public Future start() throws IllegalStateException {
// 初始化子產品
applicationDeployer.initialize();
return startSync();
}
private synchronized Future startSync() throws IllegalStateException {
// 服務暴露
exportServices();
}
1、判斷注冊方式
由于我們這個是 Dubbo3 的源碼,Dubbo3 新增了一種注冊方式:應用級注冊,是以在這裡會判斷目前的注冊方式是哪一種
- dubbo2:接口級注冊
- dubbo3:應用級注冊
我們看一下 exportServices 是如何對其進行處理的
直接來到 org.apache.dubbo.config.ServiceConfig 的 doExport 方法
java複制代碼protected synchronized void doExport() {
// 将一個服務暴露成多個URL
doExportUrls();
exported();
}
1.1 擷取注冊的URL
java複制代碼private void doExportUrls() {
// 擷取目前
List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
// 省略部分代碼
}
public static List<URL> loadRegistries(AbstractInterfaceConfig interfaceConfig, boolean provider) {
// 擷取配置裡面的位址
String address = config.getAddress();
if (StringUtils.isEmpty(address)) {
address = ANYHOST_VALUE;
}
// 若配置多個URL位址,在這裡切割
List<URL> urls = UrlUtils.parseURLs(address, map);
// 填充URL多個位址
for (URL url : urls) {
// 填充URL位址
url = URLBuilder.from(url)
.addParameter(REGISTRY_KEY, url.getProtocol())
.setProtocol(extractRegistryType(url))
.setScopeModel(interfaceConfig.getScopeModel())
.build();
// 判斷目前是不是服務端,将URL添加至registryList
if (provider || url.getParameter(SUBSCRIBE_KEY, true)) {
registryList.add(url);
}
// 這裡的URL示例:
// registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873
}
// 根據注冊類型拼接URL配置
return genCompatibleRegistries(interfaceConfig.getScopeModel(), registryList, provider);
}
1.2 應用注冊
- 目前 registerMode(注冊類型) 是 instance 或者 all 時,走應用注冊
java複制代碼if ((DEFAULT_REGISTER_MODE_INSTANCE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode))
&& registryNotExists(registryURL, registryList, SERVICE_REGISTRY_PROTOCOL)) {
URL serviceDiscoveryRegistryURL = URLBuilder.from(registryURL)
.setProtocol(SERVICE_REGISTRY_PROTOCOL)
.removeParameter(REGISTRY_TYPE_KEY)
.build();
result.add(serviceDiscoveryRegistryURL);
}
- URL:
- http複制代碼
- service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873
1.3 接口注冊
- 目前 registerMode(注冊類型) 是 interface 或者 all 時,走接口注冊
- java複制代碼
- if (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) { result.add(registryURL); }
- URL
- http複制代碼
- registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873
2、服務導出
java複制代碼// 周遊目前的傳輸協定(dubbo、rest、tri)
for (ProtocolConfig protocolConfig : protocols) {
// 組裝目前的接口URL:com.common.service.IUserService:1.0.0.dev
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
// 正式導出服務
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
// 擷取資料配置
Map<String, String> map = buildAttributes(protocolConfig);
serviceMetadata.getAttachments().putAll(map);
// 組裝URL
URL url = buildUrl(protocolConfig, map);
// 服務暴露
exportUrl(url, registryURLs);
}
- 組裝的URL:
- http複制代碼
- dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&bind.ip=192.168.0.103&bind.port=20883&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=1168&qos.enable=true®ister-mode=instance&release=3.1.8&revision=1.0.0.dev&side=provider&timeout=100×tamp=1685377125239&version=1.0.0.dev
2.1 服務暴露
java複制代碼private void exportUrl(URL url, List<URL> registryURLs) {
exportLocal(url);
}
private void exportLocal(URL url) {
// 組裝配置
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
local = local.setScopeModel(getScopeModel())
.setServiceModel(providerModel);
// 服務暴露
doExportUrl(local, false);
}
private void doExportUrl(URL url, boolean withMetaData) {
// 生成動态代理
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
if (withMetaData) {
invoker = new DelegateProviderMetaDataInvoker(invoker, this);
}
// 服務導出
Exporter<?> exporter = protocolSPI.export(invoker);
exporters.add(exporter);
}
2.2 動态代理生成
這個裡面的 getIUnvoker 很經典的動态代理模式,當我們用戶端調用時,會調用 doInvoke 裡面的方法
java複制代碼public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('#39;) < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) {
// proxy:UserServicelmpl
// 直接調用實作類
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
2.3 服務導出
java複制代碼Exporter<?> exporter = protocolSPI.export(invoker);
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// 暴露服務
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
// 擷取注冊的Registry
final Registry registry = getRegistry(registryUrl);
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// 注冊方式的選擇
boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
if (register) {
// 這裡有兩種注冊情況:
// 1、接口注冊:直接将注冊資料注冊到Zookeeper即可
// 2、應用注冊:将注冊資料轉換成中繼資料等後面釋出中繼資料
register(registry, registeredProviderUrl);
}
}
2.3.1 服務啟動
我們都知道 dubbo 都是自定義的端口,比如上面的我們的 20883,這個端口哪裡來的呢?
相信有部分同學可能猜到答案了,沒錯,就是 Netty 啟動的
java複制代碼final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
我們直接跳轉到 DubboProtocol 的 export 方法
java複制代碼public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
// 啟動服務
openServer(url);
optimizeSerialization(url);
return exporter;
}
private void openServer(URL url) {
checkDestroyed();
String key = url.getAddress();
boolean isServer = url.getParameter(IS_SERVER_KEY, true);
if (isServer) {
ProtocolServer server = serverMap.get(key);
// 典型的雙端檢鎖機制
if (server == null) {
synchronized (this) {
server = serverMap.get(key);
if (server == null) {
// createServer:啟動服務
serverMap.put(key, createServer(url));
return;
}
}
}
// 出現問題重置服務配置
server.reset(url);
}
}
建立服務端:
java複制代碼private ProtocolServer createServer(URL url) {
// 資料組裝
url = URLBuilder.from(url)
.addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
.addParameter(CODEC_KEY, DubboCodec.NAME)
.build();
String transporter = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);
ExchangeServer server = Exchangers.bind(url, requestHandler);
}
直接跳到 NettyServer 的 doOpen 方法
java複制代碼protected void doOpen() throws Throwable {
bootstrap = new ServerBootstrap();
bossGroup = createBossGroup();
workerGroup = createWorkerGroup();
final NettyServerHandler nettyServerHandler = createNettyServerHandler();
channels = nettyServerHandler.getChannels();
initServerBootstrap(nettyServerHandler);
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
}
2.3.2 注冊Zookeeper
我們這裡直接跳到 ZookeeperRegistry 的 doRegister 方法
java複制代碼public void doRegister(URL url) {
// 校驗
checkDestroyed();
// 将接口資訊注冊至Zookeeper
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), false);
}
// URL:dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752®ister-mode=interface&release=3.1.8&side=provider&timeout=100×tamp=1685542170287
我們看下 Zookeeper 的前後對比:
前:
後:
這時候拿出我們的UnCode轉換器将亂碼進行轉換:
http複制代碼dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752®ister-mode=interface&release=3.1.8&side=provider&timeout=100×tamp=1685542170287
發現其完全正确,這樣我們的資訊就被注冊到了 Zookeeper 上。
3、疑惑解答
我們可以看到,我們第一次擷取注冊 Zookeeper 的 URL 是:
這個 127.0.0.1:2181 是我們目前 Zookeeper 的位址,
java複制代碼service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873
通過該 URL 我們可以和我們的 Zookeeper 進行一些互相
第二次拿到的 URL:
這個 192.168.0.103:20883 是我們 Netty 伺服器暴露的位址,将該位址注冊至 Zookeeper,便于消費者的通路
java複制代碼dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752®ister-mode=interface&release=3.1.8&side=provider&timeout=100×tamp=1685542170287
四、總結
魯迅先生曾說:獨行難,衆行易,和志同道合的人一起進步。彼此毫無保留的分享經驗,才是對抗網際網路寒冬的最佳選擇。
其實很多時候,并不是我們不夠努力,很可能就是自己努力的方向不對,如果有一個人能稍微指點你一下,你真的可能會少走幾年彎路。
作者:愛敲代碼的小黃
連結:https://juejin.cn/post/7240636320762413114