Dubbo—SPI及自适应扩展原理,Java技术面试常见问题

@Override
public void sayHello(String s) {
    System.out.println("Hello, " + s + "! I'm one");
}

}

public class SPIImpl2 implements SPI {

@Override
public void sayHello(String s) {
    System.out.println("Hello, " + s + "! I'm two");
}

}



*   然后在resources/META-INF/services创建一个以接口全类名为名称的文件cn.dark.SPI,并在文件中填入自定义实现类的全类名

cn.dark.SPIImpl1
cn.dark.SPIImpl2



*   最后通过ServiceLoader类发现并调用服务:

ServiceLoader load = ServiceLoader.load(SPI.class);
for (SPI spi : load) {
spi.sayHello(“SPI”);
}



输出:

Hello, SPI! I’m one
Hello, SPI! I’m two



如果需要扩展新的实现,只需要将实现类配置到资源文件中,并引入对应的Jar即可。Java SPI机制就这么简单,其实现原理也很简单,读者们可以自行阅读源码,这里就不再详细分析了,那Dubbo的SPI有何异同呢?

## 2\. Dubbo SPI实现原理

## 由配置文件得到的猜想

Dubbo SPI是基于Java原生SPI思想重新实现的一套更加强大的SPI机制,类似的你可以在Dubbo的META-INF.dubbo.internal(不止这一个路径,后面在源码中会看到)路径下看到很多以接口全类名命名的配置文件,但是文件内容和JAVA SPI有点不一样,以Protocol扩展为例:

registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol



看到这么多扩展类(每一个配置文件中都有很多),我们首先应该思考一个问题:Dubbo一启动,就加载所有的扩展类么?作为一个优秀的RPC框架,肯定不会耗时耗力做这样的无用功,所以肯定会通过一种方式拿到指定的扩展才对。我们可以看到大多是以键值对方式(表示为extName-value)配置的扩展,那么不难猜测,这里的extName就是用来实现上面所说的功能的。 那到底是不是呢?以上纯属猜测,下面就到源码中去验证。

## SPI源码

Dubbo中实现SPI的核心类是ExtensionLoader,该类并未提供公共的构造方法来初始化,而是通过getExtensionLoader方法获取一个loader对象:

// loader缓存
private static final ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap, ExtensionLoader>();
public static ExtensionLoader getExtensionLoader(Class type) {
if (type == null)
throw new IllegalArgumentException(“Extension type == null”);
if(!type.isInterface()) {
throw new IllegalArgumentException(“Extension type(” + type + “) is not interface!”);
}
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException(“Extension type(” + type +
“) is not extension, because WITHOUT @” + SPI.class.getSimpleName() + " Annotation!");
}

ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
if (loader == null) {
    EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
    loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
}
return loader;

}

private final ExtensionFactory objectFactory;
private ExtensionLoader(Class type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}



这里的class参数就是扩展点的接口类型,每一个loader都需要绑定一个扩展点类型。然后首先从缓存中获取loader,未获取到就初始化一个loader并放入缓存。而在私有构造器初始化的时候我们需要注意objectFactory这个变量,先大概有个映像,后面会用到。 拿到loader之后,就可以调用getExtension方法去获取指定的扩展点了,该方法传入了一个name参数,不难猜测这个就是配置文件中的键,可以debugger验证一下:

private final ConcurrentMap cachedInstances = new ConcurrentHashMap();
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 holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder());
holder = cachedInstances.get(name);
}
// 缓存中没有该扩展,说明还没有加载扩展类,就去配置文件中加载并创建对应的扩展对象
// 这里通过双重校验锁的方式保证线程安全,Dubbo中大量运用了该技巧
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}



同样的也是先从缓存拿,缓存没有就创建并添加到缓存,因此主要看createExtension方法:

// 扩展类实例缓存对象
private static final ConcurrentMap, Object> EXTENSION_INSTANCES = new ConcurrentHashMap



关键点代码就在getExtensionClasses方法中,怎么从配置文件中加载扩展类的。而该方法主要是调用了loadExtensionClasses方法:

private Map> loadExtensionClasses() {
// 判断接口上是否标注有 @SPI注解,该注解的值就是默认使用的扩展类,
// 赋值给cachedDefaultName变量缓存起来
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if(defaultAnnotation != null) {
String value = defaultAnnotation.value();
if(value != null && (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];
}
}

// 真正读取配置文件的方法
Map> extensionClasses = new HashMap>();
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadFile(extensionClasses, DUBBO_DIRECTORY);
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;

}


该方法主要是缓存当前扩展接口指定的默认扩展实现类(@SPI注解指定),并调用loadFile读取配置文件,从这里我们可以看到Dubbo默认是读取以下3个文件夹中的配置文件:

private static final String SERVICES_DIRECTORY = “META-INF/services/”;
private static final String DUBBO_DIRECTORY = “META-INF/dubbo/”;
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + “internal/”;



然后是loadFile,该方法很长,不全部放上来了,这里提取关键的代码:

String fileName = dir + type.getName();


首先通过文件全路径找到对应的文件,并用BufferedReader一行行读取文件内容:

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) {
// 加载class,如果有些类的依赖jar包未导入,这里就会抛出异常(比如WebserviceProtocol)
Class clazz = Class.forName(line, true, classLoader);
// 验证当前类型是否是扩展类的父类型
if (! type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + “is not subtype of interface.”);
}
// 扩展类是否标注了 @Adaptive 注解,表示为一个自定义的自适应扩展类
// 如果是将其缓存到cachedAdaptiveClass
if (clazz.isAnnotationPresent(Adaptive.class)) {
if(cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (! cachedAdaptiveClass.equals(clazz)) {
// 超过一个自定义的自适应扩展类就抛出异常
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else {
try {
// 进入到该分支表示为Wrapper装饰扩展类,该类都有一个特征:包含
// 一个有参的构造器,如果没有,就抛出异常进入到另一个分支,
// Wrapper类的作用我们后面再分析
clazz.getConstructor(type);
// 缓存Wrapper到cachedWrapperClasses中
Set> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} catch (NoSuchMethodException e) {
// 进入此分支表示为一个普通的扩展类
clazz.getConstructor();
if (name == null || name.length() == 0) {
// 由于历史原因,Dubbo最开始配置文件中并不是以K-V来配置的
// 扩展点,而是会通过@Extension注解指定,所以这里会通过
// 该注解去获取到name
name = findAnnotationName(clazz);
// 由于@Extension废弃使用,但配置文件中仍存在非K-V的配置,
// 所以这里是直接通过类名获取简单的name
if (name == null || name.length() == 0) {
if (clazz.getSimpleName().length() > type.getSimpleName().length()
&& clazz.getSimpleName().endsWith(type.getSimpleName())) {
name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
} else {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url);
}
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
// 判断当前扩展点是否标注有@Activate注解,该注解表示
// 该扩展点自动激活
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (! cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
}


至此,我们就看到了Dubbo SPI的实现全过程,我们也了解了Dubbo强大的扩展性是如何实现的,但是这么多扩展,Dubbo在运行中是如何决定调用哪一个扩展点的方法呢?这就是Dubbo另一强大的机制:自适应扩展。(PS:这里需要留意cachedAdaptiveClass和cachedWrapperClasses两个变量的赋值,后面会用到。)

## 二、自适应扩展机制

什么是自适应扩展?上文刚刚也说了,Dubbo中存在很多的扩展类,这些扩展类不可能一开始就全部初始化,那样非常的耗费资源,所以我们应该在使用到该类的时候再进行初始化,也就是懒加载。但是这是比较矛盾的,拓展未被加载,那么拓展方法就无法被调用(静态方法除外)。拓展方法未被调用,拓展就无法被加载(官网原话)。所以也就有了自适应扩展机制,那么这个原理是怎样的呢? 首先需要了解@Adaptive注解,该注解可以标注在类和方法上:

*   标注在类上,表明该类为自定义的适配类

*   标注在方法上,表明需要动态的为该方法创建适配类

当有地方调用扩展类的方法时,首先会调用适配类的方法,然后适配类再根据扩展名称调用getExtension方法拿到对应的扩展类对象,最后调用该对象的方法即可。流程就这么简单,下面看看代码怎么实现的。 首先我们回到ExtensionLoader的构造方法中:

objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());


其中调用了getAdaptiveExtension方法,从方法名不难看出就是去获取一个适配类对象:

private final Holder cachedAdaptiveInstance = new Holder();
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if(createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
}
else {
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}

return (T) instance;

}


该方法很简单,就是从缓存中获取适配类对象,未获取到就调用createAdaptiveExtension方法加载适配类并通过反射创建对象:

private T createAdaptiveExtension() {
try {
// 这里又注入了些东西,先略过
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
}
}



调用getAdaptiveExtensionClass加载适配类:

private Class getAdaptiveExtensionClass() {
// 这里刚刚分析过了,从配置文件中加载配置类
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}



cachedAdaptiveClass这个变量应该还没忘,在loadFile里赋值的,即我们自定义的适配扩展类,若没有则调用createAdaptiveExtensionClass动态创建:

private Class createAdaptiveExtensionClass() {
// 生成适配类的Java代码,主要实现标注了@Adaptive的方法逻辑
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
// 调用compiler编译
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}


该方法就是生成适配类的字节码,你一定好奇适配类的代码是怎样的,只需要打断点就可以看到了,这里我们以Protocol类的适配类为例:

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException(“method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!”);
}

public int getDefaultPort() {
    throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
    if (arg1 == null) throw new IllegalArgumentException("url == null");
    com.alibaba.dubbo.common.URL url = arg1;
    String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
    if (extName == null)
        throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
    com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
    return extension.refer(arg0, arg1);
}

public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) {
    if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
    if (arg0.getUrl() == null)
        throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
    com.alibaba.dubbo.common.URL url = arg0.getUrl();
    String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
    if (extName == null)
        throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
    com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
    return extension.export(arg0);
}

}


后面会讲到Protocol扩展类都是通过export方法暴露服务,refer方法引用服务,而这两个方法在接口中都标注了@Adaptive注解,所以Dubbo为其生成了动态的适配类(这个和Java的动态代理的原理有点像。同时我们看到这两个方法中都通过getExtension方法去获取指定的扩展类的实例(这个扩展类名称来自于Invoker(后面会讲)中的url,因为dubbo是基于url驱动的,所有的配置都在url中)。 这就是Dubbo强大的自适应扩展机制的实现原理,我们可以将其运用到我们的项目中去,这就是看源码的好处。不过还有个问题,刚刚在createAdaptiveExtensionClass方法中你一定疑惑compiler是什么,它也是调用的getAdaptiveExtension获取适配类,这不就进入了死循环么? 当然不会,首先我们可以去配置文件看看Compiler的扩展类都有哪些:

adaptive=com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler
jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler
javassist=com.alibaba.dubbo.common.compiler.support.JavassistCompiler


有一个AdaptiveCompiler类,从名字上我们就能猜到它是一个自定义的适配类了,然后在其类上可以看到@Adaptive注解验证我们的猜想,那么上文也说了在loadFile方法中会将该类赋值给cachedAdaptiveClass变量缓存,然后在createAdaptiveExtension -> getAdaptiveExtensionClass方法中获取并实例化对象,所以并不会死循环,那么在该类中做了什么呢?

public class AdaptiveCompiler implements Compiler {

// 这个是在哪赋值的?
private static volatile String DEFAULT_COMPILER;

public static void setDefaultCompiler(String compiler) {
    DEFAULT_COMPILER = compiler;
}

public Class compile(String code, ClassLoader classLoader) {
    Compiler compiler;
    ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Compiler.class);
    String name = DEFAULT_COMPILER; // copy reference
    if (name != null && name.length() > 0) {
    	// 根据 DEFAULT_COMPILER 名称获取
        compiler = loader.getExtension(name);
    } else {
		// 获取@SPI注解值指定的默认扩展
        compiler = loader.getDefaultExtension();
    }
    return compiler.compile(code, classLoader);
}

}



该适配类会从两个地方获取扩展类,先来看看getDefaultExtension:

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】

compiler = loader.getDefaultExtension();
}
return compiler.compile(code, classLoader);
}

}



该适配类会从两个地方获取扩展类,先来看看getDefaultExtension:

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】

你可能感兴趣的:(程序员,java,memcached,面试,后端)