待Dubbo3.0发版之际,出于好奇心和学习需求。又一次回归dubbo的源码细节
Dubbo源码构建
1.拉取源码
git clone https://github.com/apache/dubbo.git dubbo
Dubbo 使用 maven 作为构建工具。
- Java 1.8 以上的版本
- Maven 2.2.1 或者以上的版本
2.配置源码参数,构建之前需要配置以下的 MAVEN_OPTS 防止本地编译时运行内存不足
export MAVEN_OPTS=-Xmx1024m -XX:MaxPermSize=512m
3.编译源码
mvn clean install -Dmaven.test.skip
4.构建源代码jar包
mvn clean source:jar install -Dmaven.test.skip
dubbo3 概念预览
dubbo作为一个微服务框架,Dubbo SDK与应用服务绑定在同一个进程内,随着应用服务被部署在集群的各个位置。 为了能使得各个微服务之间协同工作,Dubbo定义了一些中心化组件,包括:
- 注册中心: 协调Consumer与Provider之间的地址注册与发现
- 配置中心:
- 存储Dubbo启动阶段的全局配置,保证配置的跨环境共享与全局一致性
- 负责服务治理规则(路由规则,动态配置等)的存储与推送
- 元数据中心
-
接受Provider上报的服务接口元数据,为Admin等控制台提供运维能力(如服务测试,接口文档等)
-
作为服务发现机制的补充,提供额外的接口/方法级别的配置信息同步能力,相当于注册中心的额外拓展
-
从xml看dubbo的启动流程
Dubbo是一个远程调用的RPC框架,对于每个我们封装的微服务都会以Invoker以及exporter的形式引用和暴露服务 源码分析参考: dubbo.apache.org/zh/docs/v2.…
provider以及consumer的xml配置请见: dubbo.apache.org/zh/docs/v2.…
下面以dubbo3的xml文件配置到启用服务引用做一个总结
dubbo xml完整demo : github.com/apache/dubb…
dubbo-provider-demo.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-provider" metadata-type="remote">
</dubbo:application>
<dubbo:config-center address="zookeeper://127.0.0.1:2181"/>
<dubbo:metadata-report address="zookeeper://127.0.0.1:2181"/>
<dubbo:registry id="registry1" address="zookeeper://127.0.0.1:2181?registry-type=service"/>
<dubbo:protocol name="dubbo" port="-1"/>
<dubbo:protocol name="rest" port="8080"/>
<bean id="echoService" class="org.apache.dubbo.demo.provider.EchoServiceImpl"/>
<bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<dubbo:service delay="5000" interface="org.apache.dubbo.demo.EchoService" timeout="3000" ref="echoService" registry="registry1" protocol="dubbo"/>
<dubbo:service delay="5000" version="1.0.0" group="greeting" timeout="5000" interface="org.apache.dubbo.demo.EchoService"
ref="echoService" protocol="dubbo"/>
<dubbo:service delay="5000" version="1.0.0" timeout="5000" interface="org.apache.dubbo.demo.UserService"
ref="userService" protocol="rest"/>
</beans>
上述xml文件会被转化为如下java代码
public class ZookeeperDubboServiceProviderBootstrap {
public static void main(String[] args) {
DubboBootstrap.getInstance()
.application("zookeeper-dubbo-provider", app -> app.metadata(COMPOSITE_METADATA_STORAGE_TYPE))
.registry(builder -> builder.address("yuluo-15:2181").protocol("zookeeper")
.parameter(REGISTRY_TYPE_KEY, SERVICE_REGISTRY_TYPE))
.protocol("dubbo", builder -> builder.port(-1).name("dubbo"))
.protocol("rest", builder -> builder.port(8081).name("rest"))
.service("echo", builder -> builder.interfaceClass(EchoService.class).ref(new EchoServiceImpl()).protocolIds("dubbo"))
.service("user", builder -> builder.interfaceClass(UserService.class).ref(new UserServiceImpl()).protocolIds("rest"))
.start()
.await();
}
}
究竟是如何转换的?首先需要谈谈dubbo的类加载机制
基于dubbo SPI的简单拓展点加载
从dubbo2.7开始,不同于dubbo2.5单纯从xml当中读取配置属性进行解析,而是基于类加载机制的ExtensionLoader进行加载。基于extensionLoader可以为以后配置文件中新的配置属性定义多个拓展点
dubbo SPI源码工程: github.com/apache/dubb…
代码位置: dubbo-common/src/test 在其下新建一个文件夹添加样例如下
package org.apache.dubbo.test;
import org.apache.dubbo.common.extension.SPI;
@SPI
public interface Robot {
void sayHello(String str);
}
package org.apache.dubbo.test;
public class FirstObject implements Robot{
@Override
public void sayHello(String str) {
System.out.println(str);
}
}
package org.apache.dubbo.test;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.junit.jupiter.api.Test;
import java.util.ServiceLoader;
public class JavaSPITest {
@Test
public void sayHello() throws Exception{
ExtensionLoader<Robot> serviceLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot robot = serviceLoader.getExtension("firstObject");
robot.sayHello("nice first object");
}
}
效果如下
dubbo的启动流程中拓展点的加载
那么dubbo的xml中的配置属性是如何通过拓展点加载的? 在Dubbo启动时期,需要加载FrameWorkModel,ApplicationModel,ConfigCenter,MetadataCenter等拓展点。在加载拓展点的过程当中会根据dubbo部署的配置文件 加载对应应用下的配置中心,元数据中心。并且逐个加载对应的接口实现类
根据拓展点类型的不同,分为三类拓展点
-
dubbo.internal内置的组件比如 配置文件,环境参数,以及服务消费者和服务提供者的拓展点,过滤器链的优先级顺序注解@Order,分组注解Activate,以及负载均衡的拓展点AdaptiveClassCodeGenerator -
dubbo.external外置的组件比如一些参数转换类型的String2BooleanConverter以及测试类 -
dubbo.service指定加载哪些拓展点 内置组件或者内置
看到DubboBootstrap的启动流程当中来
/**
* Start the bootstrap
*/
public DubboBootstrap start() {
if (started.compareAndSet(false, true)) {
destroyed.set(false);
ready.set(false);
// 初始化各类拓展点之后,对外暴露服务
initialize();
if (logger.isInfoEnabled()) {
logger.info(NAME + " is starting...");
}
// 1. export Dubbo Services
exportServices();
.....
.....
}
public void initialize() {
if (!initialized.compareAndSet(false, true)) {
return;
}
//初始化ConfigManager 所有的外部依赖在这里进行初始化
ApplicationModel.initFrameworkExts();
startConfigCenter();
loadRemoteConfigs();
checkGlobalConfigs();
// @since 2.7.8
startMetadataCenter();
initMetadataService();
if (logger.isInfoEnabled()) {
logger.info(NAME + " has been initialized!");
}
}
点开initFrameWorkExts 方法,发现ExtensionLoader#getExtensionClasses通过双重锁的方式加载缓存
DubboBootstrap.java
public static void initFrameworkExts() {
Set<FrameworkExt> exts = ExtensionLoader.getExtensionLoader(FrameworkExt.class).getSupportedExtensionInstances();
for (FrameworkExt ext : exts) {
ext.initialize();
}
}
ExtensionLoader.java
public Set<String> getSupportedExtensions() {
Map<String, Class<?>> clazzes = getExtensionClasses();
return Collections.unmodifiableSet(new TreeSet<>(clazzes.keySet()));
}
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。下面从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 获取默认的拓展实现类
return getDefaultExtension();
}
// Holder,顾名思义,用于持有目标对象
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// 双重检查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建拓展实例
instance = createExtension(name);
// 设置实例到 holder 中
holder.set(instance);
}
}
}
return (T) instance;
}
上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。
private T createExtension(String name) {
// 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通过反射创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向实例中注入依赖
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循环创建 Wrapper 实例
for (Class<?> wrapperClass : wrapperClasses) {
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}
createExtension 方法的逻辑,包含了如下的步骤:
- 通过
getExtensionClasses获取所有的拓展类 - 通过反射创建拓展对象
- 向拓展对象中注入依赖
- 将拓展对象包裹在相应的
Wrapper对象中
以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中,将会重点分析 getExtensionClasses 方法的逻辑,以及简单介绍 Dubbo IOC 的具体实现。
获取所有的拓展类
在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码分析如下:
private Map<String, Class<?>> getExtensionClasses() {
// 从缓存中获取已加载的拓展类
Map<String, Class<?>> classes = cachedClasses.get();
// 双重检查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加载拓展类
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。
private Map<String, Class<?>> loadExtensionClasses() {
// 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
// 对 SPI 注解内容进行切分
String[] names = NAME_SEPARATOR.split(value);
// 检测 SPI 注解内容是否合法,不合法则抛出异常
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension...");
}
// 设置默认名称,参考 getDefaultExtension 方法
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 加载指定文件夹下的配置文件 上述提及的拓展点dubbo.internal dubbo dubbo.service
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
// fileName = 文件夹路径 + type 全限定名
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 根据文件名加载所有的同名文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
// 加载资源
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("...");
}
}
loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。
private void loadResource(Map<String, Class<?>> extensionClasses,
ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
// 按行读取配置内容
while ((line = reader.readLine()) != null) {
// 定位 # 字符
final int ci = line.indexOf('#');
if (ci >= 0) {
// 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
// 以等于号 = 为界,截取键与值
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 加载类,并通过 loadClass 方法对类进行缓存
loadClass(extensionClasses, resourceURL,
Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class...");
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class...");
}
}
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
Class<?> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("...");
}
// 检测目标类上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
// 设置 cachedAdaptiveClass缓存
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("...");
}
// 检测 clazz 是否是 Wrapper 类型
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
// 存储 clazz 到 cachedWrapperClasses 缓存中
wrappers.add(clazz);
// 程序进入此分支,表明 clazz 是一个普通的拓展类
} else {
// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
clazz.getConstructor();
if (name == null || name.length() == 0) {
// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("...");
}
}
// 切分 name
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
// 存储 name 到 Activate 注解对象的映射关系
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
// 存储 Class 到名称的映射关系
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
// 存储名称到 Class 的映射关系
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("...");
}
}
}
}
}
综上所述,ExtensionLoader通过调用loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapper 改写了双亲委派机制需要从最顶级的类加载器加载的机制