Dubbo良好的扩展性与两个方面是密不可分的,一是Dubbo整体架构中,在合适的场景中巧妙的使用了设计模式,二是使用Dubbo SPI机制,使Dubbo的接口与实现完全解耦。
在本次分享中,您可以了解如下知识点
- Java SPI机制
- Java SPI机制的缺点
- Dubbo SPI配置规范
- Dubbo SPI的分类与缓存
- Dubbo SPI的特点
- Dubbo SPI源码分析
- Dubbo对IOC的支持
1、定义一个接口和对应的方法。
package com.wb.service;
public interface OrderService {
void doOrder();
}
2、编写该接口的实现类,可以有多种实现。
package com.wb.service.impl;
import com.wb.service.OrderService;
public class OrderServiceImpl01 implements OrderService {
@Override
public void doOrder() {
System.out.println("OrderServiceImpl01 run");
}
public class OrderServiceImpl02 implements OrderService {
@Override
public void doOrder() {
System.out.println("OrderServiceImpl02 run");
}
}
3、在META-INF/services目录下新建一个文件,文件名是接口的全路径,如com.xxx.OrderService。
4、文件的具体内容是接口的实现类的全路径名称,如果接口有多个实现类,则换行编写。
com.wb.service.impl.OrderServiceImpl01
com.wb.service.impl.OrderServiceImpl02
5、在业务代码中使用java.util.ServiceLoader加载具体的实现类。
import com.wb.service.OrderService;
import java.util.ServiceLoader;
public class Test {
public static void main(String[] args){
ServiceLoader loader = ServiceLoader.load(OrderService.class);
for (OrderService orderService : loader) {
orderService.doOrder();
}
}
}
输出结果:
OrderServiceImpl01 run
OrderServiceImpl02 run
Java SPI机制的缺点
Dubbo为什么不使用Java提供的SPI机制,而使用自己的SP?相对于Java SPI,Dubbo SPI做了一定的优化和改进。
1、Java SPI会一次性加载所有的扩展类,如果扩展类没有被使用到也会加载,很耗时。
2、如果扩展类加载失败,则连扩展的名称都获取不到,排查错误困难。
3、增加了对IOC和AOP的支持,一个扩展类可以通过setter方法注入到其他扩展类中。
Dubbo SPI机制 Dubbo SPI配置规范
Dubbo SPI和Java SPI类似,需要在META-INF/dubbo目录下放置相应的SPI配置文件,文件名是接口的全路径名,文件内容为key=扩展点实现类的全路径名,如果有多个实现类,则用换行符分割。其中,key是Dubbo SPI注解中传入的参数。另外,Dubbo SPI兼容了Java SPI,Dubbo在启动时会扫描META-INF/services/,META-INF/dubbo/、META-INF/dubbo/internal/三个路径下的SPI配置。详情可查看org.apache.dubbo.remoting.Transporter接口的实现类org.apache.dubbo.remoting.transport.netty4.NettyTransporter的配置,此处就不一一展开。
Dubbo SPI的分类与缓存
Dubbo SPI根据缓存类型可以分为以下两类:
- Class缓存
Dubbo SPI获取扩展类配置时,先从缓存中获取,如果缓存中没有,则加载配置文件,根据配置把Class缓存起来,但不会全部初始化。
- 实例缓存
基于性能考虑,Dubbo SPI不仅会缓存Class,也会缓存Class的实例对象。每次获取时先从缓存中获取,如果缓存中没有,则重新加载并缓存。这也是Dubbo SPI比Java SPI性能高的原因之一,按需加载。
- 【Java|Apache Dubbo系列(增强SPI)】普通扩展类
最基础的扩展类,在SPI中配置的扩展类。
- 包装扩展类(Wrapper类)
如果该类的构造方法里有普通扩展类作为参数,Dubbo会判定为扩展点 Wrapper 类。
- 自适应扩展类(Adaptive类)
这样的扩展接口有多种实现,具体使用哪个类不写死在配置中,而是通过URL参数动态确定(此处不展开解释)。
- 自动包装特性
如果该类的构造方法里有普通扩展类作为参数,Dubbo会判定为扩展点 Wrapper 类。
- 自动加载特性
如果该扩展类的成员变量有其他扩展类作为属性,并且拥有setter方法,那么Dubbo也会注入对应的扩展点实例(类似于Spring的依赖注入)。
- 自适应
在Dubbo中使用@Adaptive注解,我们可以在URL中动态的传入参数来确定具体使用哪个实现类。
与Java SPI代码类似,Dubbo官网中有这么一段示例
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
所以我们阅读源码的切入点是org.apache.dubbo.common.extension.ExtensionLoader#getExtension方法。
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
// 获取默认的拓展实现类
if ("true".equals(name)) {
return getDefaultExtension();
}
// Holder,顾名思义,用于持有目标对象。通过name从缓存中获取
final Holder
上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建扩展对象的过程是怎样的。
org.apache.dubbo.common.extension.ExtensionLoader#createExtension
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);
}
// 通过反射获取setter方法,向实例中注入依赖,体现了Dubbo SPI的自动加载特性
injectExtension(instance);
Set> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
// 循环创建 Wrapper 实例,体现出Dubbo SPI的自动包装特性
for (Class> wrapperClass : wrapperClasses) {
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:
- 通过 getExtensionClasses 获取所有的拓展类
- 通过反射创建拓展对象
- 向拓展对象中注入依赖
- 将拓展对象包裹在相应的 Wrapper 对象中
org.apache.dubbo.common.extension.ExtensionLoader#getExtensionClasses
private Map> getExtensionClasses() {
// 从缓存中获取
Map> classes = cachedClasses.get();
// 缓存中没有
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 从SPI配置文件中加载
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
这段代码逻辑也很简单,下面分析 loadExtensionClasses 方法的逻辑
org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
private Map> loadExtensionClasses() {
// 获取SPI注解
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
String value = https://www.it610.com/article/defaultAnnotation.value();
if ((value = value.trim()).length()> 0) {
// 检测 SPI 注解内容是否合法,不合法则抛出异常
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
Map> extensionClasses = new HashMap<>();
// 加载指定文件夹下的配置文件
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.excludedPackages());
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.excludedPackages());
}
return extensionClasses;
}
loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map> extensionClasses, String dir, String type,
boolean extensionLoaderClassLoaderFirst, String... excludedPackages) {
// 拼接文件名
String fileName = dir + type;
try {
Enumeration urls = null;
ClassLoader classLoader = findClassLoader();
// 根据文件名加载所有的同名文件
if(urls == null || !urls.hasMoreElements()) {
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, excludedPackages);
}
}
} catch (Throwable t) {
logger.error("...");
}
}
loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现
private void loadResource(Map> extensionClasses, ClassLoader classLoader,
java.net.URL resourceURL, String... excludedPackages) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
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;
// 通过=分割key和value
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
// 加载类,并通过 loadClass 方法对类进行缓存
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("...");
exceptions.put(line, e);
}
}
}
}
} catch (Throwable t) {
logger.error("...");
}
}
loadResource方法用于读取配置文件,最后通过loadClass方法对结果进行缓存,loadClass方法代码如下:
private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + " is not subtype of interface.");
}
// 检查类是否有@Adaptive注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz);
} else if (isWrapperClass(clazz)) {
// 检查类是否是包装类
cacheWrapperClass(clazz);
} else {
// 走到这一分支,是普通扩展剋
clazz.getConstructor();
if (StringUtils.isEmpty(name)) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
// 存储 name 到 Activate 注解对象的映射关系
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 存储 Class 到名称的映射关系
cacheName(clazz, n);
// 存储名称到 Class 的映射关系
saveInExtensionClass(extensionClasses, clazz, n);
}
}
}
}
Dubbo对IOC的支持
Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中
org.apache.dubbo.common.extension.ExtensionLoader#injectExtension
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
// 获取所有setter方法
for (Method method : instance.getClass().getMethods()) {
if (!isSetter(method)) {
continue;
}
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// 获取 setter 方法参数类型
Class> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = getSetterProperty(method);
// 从 ObjectFactory 中获取依赖对象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通过反射调用 setter 方法设置依赖
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("...");
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
好了,这是我读Dubbo官网所理解的知识点,今天的分享就到这里,下期再见。
扫描二维码,加作者微信,更多精彩
文章图片
推荐阅读
- Java|Java基础——数组
- 人工智能|干货!人体姿态估计与运动预测
- java简介|Java是什么(Java能用来干什么?)
- Java|规范的打印日志
- Linux|109 个实用 shell 脚本
- 程序员|【高级Java架构师系统学习】毕业一年萌新的Java大厂面经,最新整理
- Spring注解驱动第十讲--@Autowired使用
- SqlServer|sql server的UPDLOCK、HOLDLOCK试验
- jvm|【JVM】JVM08(java内存模型解析[JMM])
- 技术|为参加2021年蓝桥杯Java软件开发大学B组细心整理常见基础知识、搜索和常用算法解析例题(持续更新...)