写点什么

Dubbo—SPI 及自适应扩展原理,劲爆

发布于: 1 小时前

正文

一、什么是 SPI?

SPI(Service Provider Interface)是一种服务发现机制,它的作用是解耦接口和其具体实现,让各厂商可以自定义自己的扩展实现,并使得程序可以自动使用引入的组件。 什么意思呢?举个例子就清楚了,Java 原生就提供 SPI 机制,比如数据库连接驱动的实现就是 SPI 很好的一个应用,在 Java sql 下提供了 Driver 接口,而具体的驱动程序是由各个数据库厂商实现的,平时我们要连接哪个数据库只需要引入对应的驱动 jar 包就可以了,非常方便,即使数据库变更也一样,我们不需要改变代码。而 Dubbo 的 SPI 机制则是在此基础上提供了更强大的功能,因此,学习了解 Java SPI 更益于深入了解 Dubbo,下面就先来看看 Java SPI 的使用吧。

1. Java SPI 的实现

  • 首先定义一个服务接口和两个自定义实现类(一般实现类会由第三方提供):



public interface SPI {
void sayHello(String s);
}
public class SPIImpl1 implements SPI {
@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.SPIImpl1cn.dark.SPIImpl2
复制代码


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




ServiceLoader<SPI> load = ServiceLoader.load(SPI.class);for (SPI spi : load) { spi.sayHello("SPI");}
复制代码


输出:




Hello, SPI! I'm oneHello, 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.RegistryProtocolfilter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapperlistener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrappermock=com.alibaba.dubbo.rpc.support.MockProtocolinjvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocoldubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocolrmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocolhessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocolcom.alibaba.dubbo.rpc.protocol.http.HttpProtocolcom.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocolthrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocolmemcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocolredis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
复制代码


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

SPI 源码

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


// loader缓存private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, 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 interface!");    }    if(!withExtensionAnnotation(type)) {        throw new IllegalArgumentException("Extension type(" + type +                 ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");    }
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;}
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<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();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);  }  // 缓存中没有该扩展,说明还没有加载扩展类,就去配置文件中加载并创建对应的扩展对象  // 这里通过双重校验锁的方式保证线程安全,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<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();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, (T) clazz.newInstance());            instance = (T) EXTENSION_INSTANCES.get(clazz);        }        // 依赖注入,这里及后面都和当前流程无关,可以先略过,有个印象就好        injectExtension(instance);        // 获取包装类并实例化,最后注入依赖对象        Set<Class<?>> wrapperClasses = cachedWrapperClasses;        if (wrapperClasses != null && wrapperClasses.size() > 0) {            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 + ")  could not be instantiated: " + t.getMessage(), t);    }}
复制代码


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


private Map<String, Class<?>> 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<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); 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<Class<?>> wrappers = cachedWrapperClasses;            if (wrappers == null) {                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();                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<Object> cachedAdaptiveInstance = new Holder<Object>();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编译


复制代码


用户头像

VX:Lzzzzzz63 领取资料 2021.07.07 加入

还未添加个人简介

评论

发布
暂无评论
Dubbo—SPI及自适应扩展原理,劲爆