- 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
- 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
- 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
- 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀、
文章目录
- 一、引言
- 二、环境配置
- 三、服务暴露
- 1、判断注册方式
- 1.1 获取注册的URL
- 1.2 应用注册
- 1.3 接口注册
- 2、服务导出
- 2.1 服务暴露
- 2.2 动态代理生成
- 2.3 服务导出
- 2.3.1 服务启动
- 2.3.2 注册Zookeeper
- 3、疑惑解答
- 四、总结
一、引言
对于 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
的实现:
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
方法
protected synchronized void doExport() {
// 将一个服务暴露成多个URL
doExportUrls();
exported();
}
1.1 获取注册的URL
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
时,走应用注册
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:
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
时,走接口注册if (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) { result.add(registryURL); }
-
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
2、服务导出
// 遍历当前的传输协议(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:
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 服务暴露
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
里面的方法
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 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 服务导出
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
启动的
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
我们直接跳转到 DubboProtocol
的 export
方法
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);
}
}
创建服务端:
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
方法
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
方法
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转换器
将乱码进行转换:
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
的地址,
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
,便于消费者的访问
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
四、总结
鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。
其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。
如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长
我是爱敲代码的小黄,独角兽企业的 Java
开发工程师,CSDN
博客专家,喜欢后端架构和中间件源码。
我们下期再见。
我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。
往期文章推荐:
- 不亏是阿里三面,ConcurrentHashMap多线程扩容机制被面试官装到了
- 美团二面:聊聊ConcurrentHashMap的存储流程
- 从源码全面解析Java 线程池的来龙去脉
- 从源码全面解析LinkedBlockingQueue的来龙去脉
- 从源码全面解析 ArrayBlockingQueue 的来龙去脉
- 从源码全面解析ReentrantLock的来龙去脉
- 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
- 从源码全面解析 ThreadLocal 关键字的来龙去脉
- 从源码全面解析 synchronized 关键字的来龙去脉
- 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试