0.背景
这两天在debug Dubbo的源码的时候,发现明明同一个对象,每次调用它的方法的时候居然跳到了不同的实现类中,属实让我晕了好久。最后去Dubbo的官网才发现,这个是Dubbo SPI的自适应机制。Dubbo根据Dubbo URL中的parameter中的指定的key的value,去自动使用对应类型的对象,来处理逻辑(变相的一种策略模式?)。
1.简介
SPI全称是Service Provider Interface,是一种服务发现机制。SPI的本质是将接口的实现类的全限定名配置在文件中。应用在运行的过程中,读取配置文件,加载指定的实现类。所以SPI机制至少给我们提供了两种功能:
- 应用运行时,动态替换接口的实现类。
- 对于第三方用户,赋予应用可扩展的能力。JDK
因为SPI机制的这些优点,很多第三方框架都使用了SPI机制。比如Springboot,它的无需配置,开箱即用的特点,是基于它的Auto-configuration机制实现的,而Auto-configuration机制是依赖于SPI机制的,Springboot通过SPI机制从Auto-configuration包中读取配置好的Auto-Configuration类,并自动加载他们,实现自动化配置。处此之外,本文的主角Dubbo,也是使用了SPI机制,来让自己变得更容易扩展,同时在SPI的基础上增加了自适应机制,让Dubbo变得更加的灵活。
2.Dubbo SPI加载过程分析
Dubbo没有直接使用Java提供的SPI,而是自己实现了一套功能更强的SPI。Java的SPI加载类是java.util.ServiceLoader,Dubbo的SPI加载类是org.apache.dubbo.common.extension.ExtensionLoader。
下面我们就以Dubbo的ExtensionLoader为入口,来看下Dubbo是如何实现自己的SPI机制。
2.1.Dubbo SPI 例子
先准备一个待实现的接口:
package com.huang.dubboprovider.spi;
import org.apache.dubbo.common.extension.SPI;
// Dubbo的SPI接口必须要加上@SPI注解,否则ExtesionLoader会无法加载
@SPI
public interface Database {
String getDatabaseName();
}
然后基于这个接口实现3个实现类:
package com.huang.dubboprovider.spi;
public class MySQLDatabase implements Database{
private static final String MYSQL_NAME = "MySQL";
@Override
public String getDatabaseName() {
return MYSQL_NAME;
}
}
package com.huang.dubboprovider.spi;
public class PostgreSQLDatabase implements Database {
public static final String PSQL_NAME = "PostgreSQL";
@Override
public String getDatabaseName() {
return PSQL_NAME;
}
}
package com.huang.dubboprovider.spi;
public class SQLServerDatabase implements Database {
public static final String SQLSERVER_NAME = "SQLServer";
@Override
public String getDatabaseName() {
return SQLSERVER_NAME;
}
}
在项目的resource目录下创建目录META-INF.services,在META-INF.services下创建文件,文件名为Database接口的全限定名:com.huang.dubboprovider.spi.Database,如下图所示:
com.huang.dubboprovider.spi.Database文件内容如下,在文件中我们给Database接口提供了三个实现类:
mysql=com.huang.dubboprovider.spi.MySQLDatabase
psql=com.huang.dubboprovider.spi.PostgreSQLDatabase
sqlserver=com.huang.dubboprovider.spi.SQLServerDatabase
接下来我们使用ExtensionLoader来加载Database的实现类,代码如下:
package com.huang.dubboprovider.spi;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SPITest {
public static void main(String[] args) {
// 获取Database接口的ExtensionLoader
ExtensionLoader<Database> extensionLoader = ExtensionLoader.getExtensionLoader(Database.class);
// 使用Database的ExtensionLoader获取指定的实现类
Database mysql = extensionLoader.getExtension("mysql");
System.out.println(mysql.getDatabaseName());
Database psql = extensionLoader.getExtension("psql");
System.out.println(psql.getDatabaseName());
Database sqlServer = extensionLoader.getExtension("sqlserver");
System.out.println(sqlServer.getDatabaseName());
}
}
执行SPITest.main方法输出如下:
MySQL
PostgreSQL
SQLServer
如输出结果所示,SPI机制确实生效了。接下来我们就分析下,上文中调用到的ExtensionLoader的两个方法。
- org.apache.dubbo.common.extension.ExtensionLoader#getExtensionLoader
- org.apache.dubbo.common.extension.ExtensionLoader#getExtension
2.2.ExtensionLoader#getExtensionLoader分析
ExtensionLoader#getExtensionLoader的代码很简单,这是一个静态方法,入参是一个指定的接口,返回指定的接口的ExtensionLoader实例。
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
// 必须时接口,否则抛出异常
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
// withExtensionAnnotation方法中,校验入参指定的接口是否有Dubbo的@SPI注解。如果没有的话就抛出异常。所以Dubbo的SPI接口必须要加上@SPI注解才能使用。
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// 获取Database.class的ExtensionLoader时,先去EXTENSION_LOADERS中获取,EXTENSION_LOADERS是ExtensionLoader持有的一个静态的ConcurrentMap,key是SPI接口的class对象,value是SPI接口对应的ExtensionLoader。如果EXTENSION_LOADERS中没有指定的ExtensionLoader,则创建并放入EXTENSION_LOADERS中。
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
2.3.ExtensionLoader#getExtension分析
public T getExtension(String name) {
// 实现类的别名为空,就报错
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
// 如果name为true就返回接口的默认实现,具体逻辑我们稍后分析。
if ("true".equals(name)) {
return getDefaultExtension();
}
// 每个ExtensionLoader都持有一个叫cachedInstances的ConcurrentHasnMap实例,这个map用来缓存对应的SPI接口实现类,如果对应的实现类还未创建,在创建了之后缓存到这个map中,等下次要用的时候直接获取,map的key是实现类的别名,value是一个Holder实例,Holder实例中保存着对应SPI接口的一个实现类。
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
// 通过double-check,来保证SPI实现类的创建过程是线程安全的。自然而然,我们也能明白,之所以用Holder来包装SPI实例,是为了拿holder对象来作为SPI实例的锁(因为SPI实例可能没被创建,是null,无法被拿来用作锁)。
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
特别指出下,Holder对象中用value来保存被封装的实际对象,且value是被volatile所修饰的,volatile保证了对象在线程之间的可见性。
package org.apache.dubbo.common.utils;
/**
* Helper Class for hold a value.
*/
public class Holder<T> {
private volatile T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
下面来继续看下ExtensionLoader#getDefaultExtension和ExtensionLoader#createExtension的具体实现。
2.3.1.ExtensionLoader#getDefaultExtension
getDefaultExtension接口时用来获取对应的SPI接口的默认实现。逻辑很简单,先执行getExtensionClasses方法,执行完毕之后,根据cachedDefaultName来返回对应的默认实现,可以看出,应该是在getExtensionClasses方法中会设置cachedDefaultName。
public T getDefaultExtension() {
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
return null;
}
// 如果SPI接口的默认实现有配置,再调用getExtension方法,getExtension方法里再通过cachedInstances判断默认实现类的实例是否已经缓存,如果还没缓存,就调用createExtension方法,来创建实现类的实例。
return getExtension(cachedDefaultName);
}
接下来我们查看getExtensionClasses()方法
// getExtensionClasses方法会从指定的配置文件下,加载指定的SPI接口的所有实现类的Class对象
private Map<String, Class<?>> getExtensionClasses() {
// private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// cachedClasses用来保存指定的SPI接口的所有实现类的Class对象
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
// 如果还没有加载指定的SPI接口的所有实现类的Class,就去开始加载。
synchronized (cachedClasses) {
// 拿holder对象来"上锁"(怪不得要封装一层Holder对象呢,原来是拿来当工具人加锁用的啊),线程安全。
classes = cachedClasses.get();
// 依旧是熟悉的double-check,来保证线程安全
if (classes == null) {
// 加载指定的SPI接口的所有实现类的Class对象
classes = loadExtensionClasses();
// 将指定的SPI接口的所有实现类的Class对象保存到cachedClasses中
cachedClasses.set(classes);
}
}
}
return classes;
}
// synchronized in getExtensionClasses
private Map<String, Class<?>> loadExtensionClasses() {
// 获取并保存默认的SPI接口实现
cacheDefaultExtensionName();
// 从"META-INF/services/"、"META-INF/dubbo/"、"META-INF/services/"internal/"目录下加载指定的SPI接口的所有Class对象。
Map<String, Class<?>> extensionClasses = new HashMap<>();
// loadDirectory的逻辑和Dubbo的自适应机制相关,后文讲自适应机制的时候再分析这个方法。
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
// 从指定的SPI接口获取默认的实现类配置,并保存到ExtensionLoader的cachedDefaultName字段上。
private void cacheDefaultExtensionName() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
// 默认实现类通过@SPI注解来配置。
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
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];
}
}
}
}
2.3.2.ExtensionLoader#createExtension
private T createExtension(String name) {
// 如果指定的实现类不存在,则报错
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
// 创建指定的实现类的实例,并保存到EXTENSION_INSTANCES中
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// Dubbo的SPI机制,还提供了IOC的功能,会给被创建的实现类实例,进行依赖注入(DI),具体的逻辑我们稍后分析。
injectExtension(instance);
// 如果指定的SPI接口,有配置”包装类“,则将要创建的实现类包装进"包装类"中(装饰者模式),并对包装类进行依赖注入。"包装类"和SPI接口其他的实现类的配置方式全都一样,唯一不同的是,它有一个以对应的SPI接口为入参的构造函数,且包装类不能作为SPI接口的扩展实例,它只能去包装SPI接口下的其他扩展实例:"包装类"不会被保存到ExtensionLoader实例的cachedInstances中,而是保存在cachedWrapperClasses。后文将会专门演示列子。
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
// 将要创建的实现类包装进"包装类"中,并对包装类进行依赖注入
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
2.3.3.ExtensionLoader#injectExtension
在分析injectExtension方法前,先说明下ExtensionLoader实例的objectFactory字段(类型为ExtensionFactory),这个字段是Dubbo SPI依赖注入的核心,我们看下这个字段的值是如何创建的:
private ExtensionLoader(Class<?> type) {
this.type = type;
// 入参type就是SPI接口的Class对象。如果type是ExtensionFactory.class,则后续创建对应的实现类实例的时候不会执行依赖注入的逻辑。如果不是ExtensionFactory.class,则objectFactory指向ExtensionFactory.class的@Adaptive类实例,即AdaptiveExtensionFactory.class实例。关于@Adaptive即Dubbo的自适应机制,将会在后文分析,这里就先不深入讨论。
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
AdaptiveExtensionFactory类如下:
package org.apache.dubbo.common.extension.factory;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.ExtensionFactory;
import org.apache.dubbo.common.extension.ExtensionLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* AdaptiveExtensionFactory
*/
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
private final List<ExtensionFactory> factories;
// AdaptiveExtensionFactory唯一的构造方法,会将ExtensionFactory.class接口的所有实现类(除了被@Adaptive注解的类,即AdaptiveExtensionFactory)的实例保存到自己的factories字段中,即factories中保存着SpiExtensionFactory和SpringExtensionFactory
public AdaptiveExtensionFactory() {
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
}
// 依次遍历SpiExtensionFactory和SpringExtensionFactory,直到获取到指定类型和名字的对象。
// SpiExtensionFactory是通过Dubbo的SPI机制去获取指定SPI接口的Adaptive实例,详见SpiExtensionFactory的源码。
// SpringExtensionFactory从Spring上下文中(通过ApplicationContextAware获取Spring上下文),获取指定类型和名字的bean,详见SpringExtensionFactory的源码。
@Override
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}
}
下面来分析Dubbo的SPI是如何进行依赖注入的。
private T injectExtension(T instance) {
try {
// injectExtension 依赖注入的核心是objectFactory
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
// 判断方法是否是以"set"开头,且方法的可见性为public,且入参有且仅有1个
if (isSetter(method)) {
// 如果方法上加了@DisableInject,则自动略过,不进行依赖注入。
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// 获取set方法唯一入参的类型。
Class<?> pt = method.getParameterTypes()[0];
// 判断是否是原始数据类型(short、int、long等等)以及它们对应的包装类
// 或者是否是String类型
// 或者是否是Number或者Date的实现类
// 如果是,则忽略,不进行依赖注入。
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
// 获取set方法对应的property的名字,比如方法名为setVersion,则property的名字为version。
String property = getSetterProperty(method);
// 这里的objectFactory我们上文已经分析过了,是AdaptiveExtensionFactory.class实例,依次从Dubbo的SPI和Spring的容器中尝试获取指定的对象。
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通过set方法完成注入。
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
2.3.4.Dubbo SPI"包装类"与""依赖注入"功能测试
上文在分析ExtensionLoader#createExtension的过程中,发现Dubbo的SPI机制在创建实现类实例的时候,会对实现类实例进行依赖注入,依赖注入完毕之后,会用包装类封装实现类,并再对包装类进行依赖注入。现在我们就来测试下这两个功能。
以上文"2.1.例子"章节中的代码为基础。
在Database接口下添加包装类DatabaseNameUpperCaseWrapper:
package com.huang.dubboprovider.spi;
import org.springframework.util.StringUtils;
/**
* Database的包装类
* 将getDatabaseName()方法返回的字符串进行装饰,将小写字母全都转成大写字母。
*
*/
public class DatabaseNameUpperCaseWrapper implements Database {
public DatabaseNameUpperCaseWrapper(Database database) {
this.delegate = database;
}
private final Database delegate;
private UsingDatabaseRecord databaseRecord;
public void setUsingDatabaseRecord(UsingDatabaseRecord databaseRecord) {
this.databaseRecord = databaseRecord;
}
@Override
public String getDatabaseName() {
String databaseName = delegate.getDatabaseName();
databaseName = databaseName == null ? null : databaseName.trim().toUpperCase();
if (databaseName != null && databaseName.length() > 0 && databaseRecord != null) {
databaseRecord.usingDatabaseName.add(databaseName);
}
return databaseName;
}
}
在 META-INF/services/com.huang.dubboprovider.spi.Database 文件中配置DatabaseNameUpperCaseWrapper:
mysql=com.huang.dubboprovider.spi.MySQLDatabase
psql=com.huang.dubboprovider.spi.PostgreSQLDatabase
sqlserver=com.huang.dubboprovider.spi.SQLServerDatabase
databaseNameUpperCaseWrapper=com.huang.dubboprovider.spi.DatabaseNameUpperCaseWrapper
在Spring容器中添加bean,UsingDatabaseRecord:
package com.huang.dubboprovider.spi;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Component("databaseRecord")
public class UsingDatabaseRecord {
// 记录正在被使用的 Database 的名字。
public final List<String> usingDatabaseName = new ArrayList<>();
}
用单元测试验证Dubbo SPI的“包装类”和“依赖注入”功能,详见每个单元测试方法上的注释:
package com.huang.dubboprovider.spi;
import org.apache.dubbo.common.extension.ExtensionLoader;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@SpringBootTest
public class DubboSPITest implements ApplicationContextAware {
private ApplicationContext springContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.springContext = applicationContext;
}
/**
* 测试 Dubbo SPI 的包装类机制
*
* {@link Database} 接口存在包装类 {@link DatabaseNameUpperCaseWrapper},
* 会包装{@link Database} 接口所有的实现类,将getDatabaseName()方法返回的字符串进行装饰,将小写字母全都转成大写字母。
*/
@Test
public void testWrapperClass() {
Database mysql = ExtensionLoader.getExtensionLoader(Database.class).getExtension("mysql");
assertEquals(mysql.getDatabaseName(), "MYSQL");
}
/**
* 测试 无法直接获取Dubbo SPI的包装类
*/
@Test
public void testCantGetWrapperClassDirectly() {
IllegalStateException exception = assertThrows(IllegalStateException.class, () ->
ExtensionLoader.getExtensionLoader(Database.class)
.getExtension("databaseNameUpperCaseWrapper"));
assertTrue(exception.getMessage().contains("No such extension com.huang.dubboprovider.spi.Database by name"));
}
/**
* 测试 Dubbo SPI 的 从Spring上下文中获取依赖并注入的功能
*
* {@link DatabaseNameUpperCaseWrapper#databaseRecord} 通过Dubbo SPI 的依赖注入
* 从Spring上下文中获取UsingDatabaseRecord的bean,
* 每次通过{@link DatabaseNameUpperCaseWrapper}调用{@link Database#getDatabaseName()}方法时,
* 只要{@link Database#getDatabaseName()}返回值trim之后不会null且不为空,就会保存到
* {@link DatabaseNameUpperCaseWrapper#databaseRecord}中。
*
*/
@Test
public void testDIFromSpringContext() {
ExtensionLoader.getExtensionLoader(Database.class).getExtension("mysql").getDatabaseName();
ExtensionLoader.getExtensionLoader(Database.class).getExtension("psql").getDatabaseName();
ExtensionLoader.getExtensionLoader(Database.class).getExtension("sqlserver").getDatabaseName();
UsingDatabaseRecord bean = springContext.getBean(UsingDatabaseRecord.class);
assertEquals(bean.usingDatabaseName.size(), 3);
assertTrue(bean.usingDatabaseName.contains("MYSQL"));
assertTrue(bean.usingDatabaseName.contains("POSTGRESQL"));
assertTrue(bean.usingDatabaseName.contains("SQLSERVER"));
}
}
3.Dubbo SPI自适应机制分析
自适应机制,就是调用Dubbo SPI接口方法的时候,能够根据接口的URL类型入参,或者入参中的URL成员变量(Dubbo服务调用全流程,所有的配置都聚合在当前调用的URL上,所以URL是Dubbo服务调用全流程的核心(相当于是"门面模式"了)),去获取URL中指定的parameter的value,根据这个value去自动使用相应的接口扩展实现。
举个具体的例子(引用自SPI 自适应拓展),大家就很好理解了:
为了让大家对自适应拓展有一个感性的认识,下面我们通过一个示例进行演示。这是一个与汽车相关的例子,我们有一个车轮制造厂接口 WheelMaker:
public interface WheelMaker { Wheel makeWheel(URL url); }WheelMaker 接口的自适应实现类如下:
public class AdaptiveWheelMaker implements WheelMaker { public Wheel makeWheel(URL url) { if (url == null) { throw new IllegalArgumentException("url == null"); } // 1.从 URL 中获取 WheelMaker 名称 String wheelMakerName = url.getParameter("Wheel.maker"); if (wheelMakerName == null) { throw new IllegalArgumentException("wheelMakerName == null"); } // 2.通过 SPI 加载具体的 WheelMaker WheelMaker wheelMaker = ExtensionLoader .getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName); // 3.调用目标方法 return wheelMaker.makeWheel(url); } }AdaptiveWheelMaker 是一个代理类,与传统的代理逻辑不同,AdaptiveWheelMaker 所代理的对象是在 makeWheel 方法中通过 SPI 加载得到的。makeWheel 方法主要做了三件事情:
- 从 URL 中获取 WheelMaker 名称
- 通过 SPI 加载具体的 WheelMaker 实现类
- 调用目标方法
接下来,我们来看看汽车制造厂 CarMaker 接口与其实现类。
public interface CarMaker { Car makeCar(URL url); } public class RaceCarMaker implements CarMaker { WheelMaker wheelMaker; // 通过 setter 注入 AdaptiveWheelMaker public setWheelMaker(WheelMaker wheelMaker) { this.wheelMaker = wheelMaker; } public Car makeCar(URL url) { Wheel wheel = wheelMaker.makeWheel(url); return new RaceCar(wheel, ...); } }
上面的例子是手动实现自适应机制的方式。如果每一个Dubbo SPI都要手动实现的话,那可太麻烦了。所以Dubbo提供了@Adaptive注解。
3.1.@Adaptive
@Adaptive注解可以注解在Dubbo SPI接口的任意一个需要实现自适应机制的方法上,也可以注解在Dubbo SPI接口的实现类上。前者表示,由Dubbo通过java字节码技术(默认使用javassist)为对应的接口自动生成adaptive实例。后者表示,被注解的实现类的实例为对应Dubbo SPI接口的adaptive实例,具体的自适应逻辑,由用户自己编码实现。
下面是Adaptive注解的代码说明:
package org.apache.dubbo.common.extension;
import org.apache.dubbo.common.URL;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
/**
* value表示dubbo URL中指定的parameter的名字。
* 可以指定多个,比如:
* {"key1","key2"}
* 当URL中存在key1时,就以key1的value值为准,去加载url.getParameter("key1")对应的Dubbo SPI接口实现类。
* 当URL不存在key1时,就去找key2。
* 如果key1和key2都没有,就以找默认key,默认key的生成规则如下:
* 假如Dubbo SPI接口的类名(class的simpleName)叫 : YyyInvokerWrapper
* 则默认key的值为: yyy.invoker.wrapper。
* 如果最终这个没有找到目标的key的value,这个时候存在两种情况:
* 1.@SPI的value中配置了默认使用的扩展类,直接用默认的扩展类。
* 2.@SPI的value没配置,抛出异常IllegalStateException
*
*
* 也可以不指定value,会默认去找默认key,如果默认key对应的值最终没有找到,也会抛出IllegalStateException
*/
String[] value() default {};
}
3.2.自适应机制例子
先准备代码。
创建一个新的SPI接口 Song
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.SPI;
/**
* 歌曲
*/
@SPI
public interface Song {
/**
* 歌词
*
* @return
*/
String getLyric(URL url);
default void notAdaptiveMethod() {};
}
再给Song接口创建两个实现类:
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
public class HappySong implements Song {
private static final String LYRIC =
"新年好啊,新年好啊。祝贺大家新年好,我们唱歌我们跳舞,祝贺大家新年好";
@Override
public String getLyric(URL url) {
return LYRIC;
}
}
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
public class SadSong implements Song {
private static final String LYRIC = "我爱的人,不是我的爱人。";
@Override
public String getLyric(URL url) {
return LYRIC;
}
}
创建配置文件”META-INF/services/com.huang.dubboprovider.spi.adaptive.Song“:
happy=com.huang.dubboprovider.spi.adaptive.HappySong
sad=com.huang.dubboprovider.spi.adaptive.SadSong
下面来分别展示下@Adaptive自适应类实现的两种方式,分别是将@Adaptive注解放在SPI接口内部的方法上,由Dubbo帮我们自动生成自适应类。以及将@Adaptive放在我们自己写的SPI接口的实现类上面,我们在编写实现类中实现自SPI接口的方法时,自己在方法内部编写自适应的逻辑。
3.2.1.@Adaptive放到SPI接口的方法上
我们在Song接口的getLyric方法上添加@Adaptive注解,@Adaptive的value没有设置,根据规则,value默认为”song“
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
/**
* 歌曲
*/
@SPI
public interface Song {
/**
* 歌词
*
* @return
*/
@Adaptive
String getLyric(URL url);
default void notAdaptiveMethod() {};
}
然后我们写一个main方法测试下:
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SPIAdaptiveTest {
public static void main(String[] args) {
final String happySongURL = "http://xxxxxx?song=happy";
final String sadSongURL = "http://xxxxxx?song=sad";
final String noSongTypeURL = "http://xxxxxx";
ExtensionLoader<Song> extensionLoader = ExtensionLoader.getExtensionLoader(Song.class);
Song adaptiveExtension = extensionLoader.getAdaptiveExtension();
System.out.println(happySongURL + " : " + adaptiveExtension.getLyric(URL.valueOf(happySongURL)));
System.out.println(sadSongURL + " : " + adaptiveExtension.getLyric(URL.valueOf(sadSongURL)));
System.out.println(noSongTypeURL + " : " + adaptiveExtension.getLyric(URL.valueOf(noSongTypeURL)));
}
}
执行结果:
http://xxxxxx?song=happy : 新年好啊,新年好啊。祝贺大家新年好,我们唱歌我们跳舞,祝贺大家新年好
http://xxxxxx?song=sad : 我爱的人,不是我的爱人。
Exception in thread "main" java.lang.IllegalStateException: Failed to get extension (com.huang.dubboprovider.spi.adaptive.Song) name from url (http://xxxxxx) use keys([song])
at com.huang.dubboprovider.spi.adaptive.Song$Adaptive.getLyric(Song$Adaptive.java)
at com.huang.dubboprovider.spi.adaptive.SPIAdaptiveTest.main(SPIAdaptiveTest.java:18)
```
@SPI("happy")
public interface Song {
......
}
http://xxxxxx?song=happy : 新年好啊,新年好啊。祝贺大家新年好,我们唱歌我们跳舞,祝贺大家新年好
http://xxxxxx?song=sad : 我爱的人,不是我的爱人。
http://xxxxxx : 新年好啊,新年好啊。祝贺大家新年好,我们唱歌我们跳舞,祝贺大家新年好
可以看到,当自适应机制无法找到指定的扩展类时,就会使用@SPI注解中给目标接口配置的默认扩展类。
下面我们来看下,如果用自适应类实例执行没有被添加@Apdative的接口方法时,会怎么样:
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SPIAdaptiveTest {
public static void main(String[] args) {
ExtensionLoader<Song> extensionLoader = ExtensionLoader.getExtensionLoader(Song.class);
Song adaptiveExtension = extensionLoader.getAdaptiveExtension();
adaptiveExtension.notAdaptiveMethod();
}
}
Exception in thread "main" java.lang.UnsupportedOperationException: The method public default void com.huang.dubboprovider.spi.adaptive.Song.notAdaptiveMethod() of interface com.huang.dubboprovider.spi.adaptive.Song is not adaptive method!
at com.huang.dubboprovider.spi.adaptive.Song$Adaptive.notAdaptiveMethod(Song$Adaptive.java)
at com.huang.dubboprovider.spi.adaptive.SPIAdaptiveTest.main(SPIAdaptiveTest.java:16)
直接抛出UnsupportedOperationException,提示Song接口的notAdaptiveMethod方法不是自适应方法。
3.2.2.@Adaptive放到SPI接口的实现类上
将Song接口上的getLyric方法上的@Adaptive注解删除。然后创建我们自定义的自适应类,实现Song接口,如下:
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.extension.SPI;
import org.apache.dubbo.common.utils.StringUtils;
@Adaptive
public class AdaptiveSong implements Song {
private static final String PARAM_KEY = "song";
@Override
public String getLyric(URL url) {
if (url == null) {
throw new IllegalStateException("URL shouldn't be null");
}
// 先从URL中获取key为“song”的parameter的value
String songType = url.getParameter(PARAM_KEY);
if (StringUtils.isEmpty(songType)) {
// 如果url中没有设置song,则去获取Song接口上,在@SPI注解中设置的默认扩展类
SPI spi = Song.class.getAnnotation(SPI.class);
songType = spi.value();
}
if (StringUtils.isEmpty(songType)) {
// 自适应扩展最终没有获取到要使用的扩展类的名字,就抛出异常。
throw new IllegalStateException(
String.format("Failed to get extension (%s) name from url (" + url.toString() + ") use keys(%s)",
Song.class.getName(), PARAM_KEY));
}
// 自适应扩展最终获取到要使用的扩展类的名字,通过ExtensionLoader中获取扩展类实例,并执行目标方法。
Song extension = ExtensionLoader.getExtensionLoader(Song.class).getExtension(songType);
return "使用了手动编写的自适应类:" + extension.getLyric(url);
}
}
在”META-INF/services/com.huang.dubboprovider.spi.adaptive.Song“配置文件中配置我们刚刚编写的自适应类:
happy=com.huang.dubboprovider.spi.adaptive.HappySong
sad=com.huang.dubboprovider.spi.adaptive.SadSong
adaptive=com.huang.dubboprovider.spi.adaptive.AdaptiveSong
编写一个main方法,测试下我们手动编码的自适应类:
package com.huang.dubboprovider.spi.adaptive;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SPIAdaptiveTest {
public static void main(String[] args) {
ExtensionLoader<Song> extensionLoader = ExtensionLoader.getExtensionLoader(Song.class);
Song adaptiveExtension = extensionLoader.getAdaptiveExtension();
System.out.println(adaptiveExtension.getLyric(URL.valueOf("http://xxxxxx?song=sad")));
System.out.println(adaptiveExtension.getLyric(URL.valueOf("http://xxxxxx")));
}
}
执行结果(因为Song接口上的@SPI注解中指定了默认的扩展类为"happy",所以当在URL中找不到指定的扩展类的时候,就使用默认的扩展类HappySong去执行getLyric方法):
使用了手动编写的自适应类:我爱的人,不是我的爱人。
使用了手动编写的自适应类:新年好啊,新年好啊。祝贺大家新年好,我们唱歌我们跳舞,祝贺大家新年好
3.2.3.两种@Adaptive配置方式的优先级
如果我们在Song接口的getLyric方法上添加@Adaptive注解,同时配置了自定义的@Adaptive类,那么最终使用自适应机制的时候,使用的时Dubbo为我们动态生成的自适应类还是我们自己手动编写的自适应类?,下面我们来测试下。在上文3.2.2章节的代码基础上,在Song接口的getLyric接口上,重新加上@Adaptive注解,然后执行3.2.2章节中的main方法,输出结果如下:
使用了手动编写的自适应类:我爱的人,不是我的爱人。
使用了手动编写的自适应类:新年好啊,新年好啊。祝贺大家新年好,我们唱歌我们跳舞,祝贺大家新年好
由上可知,当配置了我们手动编写的自适应类,以我们手动编写的自适应类为准。
3.2.4.自适应机制如何获取URL
上文3.2.1章节中,我们演示Dubbo使用动态创建的自适应类例子中,Song接口中被@Adaptive注解的getLyric
方法的入参是URL,但是在Dubbo的代码中,@Adaptive方法也是可以加到其他入参不是URL类型的方法上,比如Protocol接口:
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
/**
* Protocol. (API/SPI, Singleton, ThreadSafe)
*/
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
Protocol的exprot方法上有@Adaptive注解,但是它的入参中是没有URL参数的。那Dubbo给Protocol动态创建自适应类的方法的时候该如何处理呢?可以看下org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateUrlAssignmentIndirectly方法:
private String generateUrlAssignmentIndirectly(Method method) {
Class<?>[] pts = method.getParameterTypes();
// find URL getter method
for (int i = 0; i < pts.length; ++i) {
for (Method m : pts[i].getMethods()) {
String name = m.getName();
if ((name.startsWith("get") || name.length() > 3)
&& Modifier.isPublic(m.getModifiers())
&& !Modifier.isStatic(m.getModifiers())
&& m.getParameterTypes().length == 0
&& m.getReturnType() == URL.class) {
// 添加了@Adaptive的方法中,只要存在这样的入参:入参对应的类中存在public、非static、get开头、没有入参、返回类型是URL的方法就行。Dubbo会记录这个入参的index,以及从这个入参对象中获取URL的get方法,在动态创建被@Adaptive注解的方法时,直接对入参调用被记录的get方法来获取URL。
return generateGetUrlNullCheck(i, pts[i], name);
}
}
}
// getter method not found, throw
throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
+ ": not found url parameter or url attribute in parameters of method " + method.getName());
}
3.3.ExtensionLoader.getAdaptiveExtension()加载自适应类过程源码分析
ExtensionLoader.getAdaptiveExtension():
// 获取指定@SPI接口的自适应类
public T getAdaptiveExtension() {
// 自适应类被创建后会缓存在ExtensionLoader实例的cachedAdaptiveInstance字段中
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
// 如果自适应类之前尝试创建过且创建失败了,会把失败的信息保存在createAdaptiveInstanceError中
if (createAdaptiveInstanceError == null) {
// 如果自适应类不存在,且没有曾尝试创建且失败了,则去创建自适应类实例。
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
// 老规矩,为了线程安全,使用double-check,内层check时需持有cachedAdaptiveInstance即Hodler实例的对象锁。
try {
// 创建自适应对象
instance = createAdaptiveExtension();
// 将创建的自适应对象保存到cachedAdaptiveInstance中。
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
下面来看看创建自适应实例的具体方法 ExtensionLoader#createAdaptiveExtension:
private T createAdaptiveExtension() {
try {
// getAdaptiveExtensionClass()获取自适应类的Class对象,下面分析这个方法。
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}
ExtensionLoader#getAdaptiveExtensionClass
// 获取自适应类的Class对象
private Class<?> getAdaptiveExtensionClass() {
// getExtensionClasses方法我们在2.3.1章节中已经讲了一部分了,从getExtensionClasses方法到调用loadExtensionClasses,再到调用loadDirectory方法,这个过程之间的逻辑都已经讲了,loadDirectory方法内部的代码还没分析。所以我们接下来分析getExtensionClasses方法的时候,就从loadDirectory方法开始。
getExtensionClasses();
// cachedAdaptiveClass用来保存被@Adaptive方法注解的类的Class对象,即我们自己手动编码实现的自适应类的Class对象。
if (cachedAdaptiveClass != null) {
//如果在当前的SPI接口下,我们有手动编码实现自适应类,则使用我们自己编写的自适应类,优先级高于Dubbo帮我们自动生成的自适应类,我们在3.2.3章节中已经验证测试过这个逻辑。
return cachedAdaptiveClass;
}
// 如果在当前的SPI接口下,我们没有手动编码实现自适应类,则由Dubbo通过字节码技术帮我们自动生成自适应类,具体逻辑在createAdaptiveExtensionClass方法内实现。
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
ExtensionLoader.loadDirectory:
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
String fileName = dir + type;
try {
// 保存指定的SPI接口的配置文件,配置文件可能有多个。
Enumeration<java.net.URL> urls;
// 获取当前的ClassLoader
ClassLoader classLoader = findClassLoader();
// 加载指定的SPI接口的配置文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
// 加载SPI接口的配置文件
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
ExtensionLoader.loadResource:
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
// 一行行的读取指定SPI接口配置文件
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) {
// 每读一行,加载一个配置在SPI配置文件中的扩展类。
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
ExtensionLoader#loadClass:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
// 在SPI配置文件中配置的扩展类必须时class不能是其他类型,你如说enum或者interface
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.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {
// 如果当前类被添加了@Adaptive,则保存到ExtensionLoader实例的cachedAdaptiveClass字段上,一个SPI接口下,不能有多个类上面被添加了@Adaptive注解,否则会抛出IllegalStateException异常。
cacheAdaptiveClass(clazz);
} else if (isWrapperClass(clazz)) {
// 如果是包装类,则保存到ExtensionLoader实例的cachedWrapperClasses字段中
cacheWrapperClass(clazz);
} else {
// 检查有没有默认的构造方法,且是public的
clazz.getConstructor();
// 设置扩展类的名字
if (StringUtils.isEmpty(name)) {
// 加入SPI配置文件中扩展类的名字为空,就去对应的Class上面的@Extension中去找
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注解,就将其保存到ExtensionLoader的cachedActivates字段中
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 将每个扩展类,和他的名字保存到ExtensionLoader的cachedNames中,如果这个扩展类有多个名字,只保存第一个,详见cacheName方法。
cacheName(clazz, n);
// 将解析到的扩展类放入extensionClasses,最后extensionClasses会在getExtensionClasses()方法中被设置到ExtensionLoader实例的cachedClasses字段中,注意cachedClasses字段中保存的扩展类,不包括自适应类和包装类。
saveInExtensionClass(extensionClasses, clazz, n);
}
}
}
}
如果在当前的SPI接口下,我们没有手动编码实现自适应类,则由Dubbo通过字节码技术帮我们自动生成自适应类,具体逻辑在createAdaptiveExtensionClass方法内实现。
ExtensionLoader#createAdaptiveExtensionClass
private Class<?> createAdaptiveExtensionClass() {
// dubbo中很好的遵循了面向对象原则以及单一职责原则,将创建动态的自适应的逻辑都封装在了AdaptiveClassCodeGenerator的generate方法中。generate方法返回自动生成的自适应类代码。
// generate方法的代码就不分析了,感兴趣的好兄弟们可以自己去看看(我太懒了,呜呜呜~)
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
// 通过SPI机制获取Compile接口的手动编写的自适应类AdaptiveCompiler,AdaptiveCompiler判断用户有无指定java代码编译器,如果没有,则使用默认的javassist来编译dubbo动态生成的自适应类的源码。
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 将源码编译成class,并返回。
return compiler.compile(code, classLoader);
}
4.总结
通过分析Dubbo SPI机制的源码,除了对Dubbo SPI机制的细节更加的熟悉,同时也学习到了很多对编码有用的思想以及编码手段:
- 使用SPI + 微内核的方式,可以构建易于扩展的应用。
- 加载资源的时候,可以使用 懒加载 & 缓存 的方式。
- 多线程的情况下创建一个对象,可以通过 "double-check & volatile关键字"来保证线程安全。
- 多线程的情况下创建一个对象,可以通过给被创建对象包装一层Holder对象,由Holder对象来作为对象锁,来保证线程安全。
- 多考虑"约定大于配置"原则,对于每个配置点,都给出一个默认的配置,方便第三方的使用,Springboot的Auto-configuration就很好的秉持了"约定大于配置的原则",所以才能做到开箱即用。
- 编码的时候牢记"面向接口编码"的原则,明确划分类的职责,保证类职责分明,高内聚低耦合,这样的代码才好维护和扩展。
- 使用”模板方法模式“、”命令模式“等设计方式给代码逻辑中预留”钩子“,来达到易于扩展的目的。