Dubbo SPI机制(上):一个普通的扩展类是如何加载的

这一篇我们先不讲Dubbo中的具体业务逻辑,我们来打基础,聊一聊Dubbo中的SPI机制。

Dubbo SPI是干啥的

了解一个技术,得先知道它是为了解决什么问题而产生的。那么Dubbo SPI是干什么的呢?

按照官网的描述,Dubbo是一款高性能的RPC框架,是为了解决应用间的服务治理问题而诞生的。
服务治理会涉及到很多方面的内容,如网络连接、集群容错、服务路由、负载均衡等。这么多的内容,每个都有不同的解决方案。如网络连接,可以由netty实现,也可以由mina实现。

Dubbo并没有局限于某一种解决方案,而是博采众长。它通过一种机制,让用户可以自行扩展加载想要的功能实现。这种机制就是Dubbo SPI。

Dubbo SPI与Java SPI的渊源

你可能了解过Java SPI机制(如果不了解,建议了解一下),Dubbo SPI是在Java SPI的基础之上,又进行了一层功能的扩展得到的。相较于Java SPI方式,Dubbo SPI具有以下几个方面的优势:

  • 按需加载
    Java SPI无论你是否需要这个扩展类,都会将其加载到内存中。这样可能会造成资源浪费。Dubbo则不然,采用了一种按需加载扩展的方式,避免不必要的资源浪费
  • 扩展间的IOC和AOP
    Dubbo SPI机制还实现了一种扩展间的注入与切入功能。简单来说,一个扩展类可以注入另一个扩展类中,外层包装的扩展可以做更多的事,如:流量统计、监控等,好处不言而喻。

Dubbo SPI使用

Dubbo SPI的使用和Java SPI非常相像,也并不复杂,这里就不再赘述。如果你不太了解,建议去官网看一下。

一个普通扩展类是如何加载的

在Dubbo中,标注了@SPI的接口,即被认为是Dubbo SPI扩展类.。

接下来我们聊聊一个普通Dubbo SPI扩展类是如何加载的。首先从Dubbo中最长见到的一种扩展调用方式开始:

ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);

这行代码顾名思义,就是先查找Protocol这个类的加载器,然后利用加载器获取 Dubbo协议扩展类。
前半段的加载器获取逻辑我们暂时忽略,先从getExtension来说起。

不看源码实现,先想一想,如果要你实现一个getExtension方法,你会做哪些事情?

  • 扩展类加载完了之后,你是不是要将它缓存起来,方便下次直接获取?
  • 扩展类是从哪里获取的?对,配置文件。那么是不是要先读取文件,知道有哪些扩展类?
  • 上面说到扩展类支持IOC和AOP,那么在实例化扩展类的时候,是否应该有对扩展类中进行注入的逻辑?

带着上面几个猜测,我们来看一下getExtension的源码,看看一个普通扩展类是如何加载进来的。

public T getExtension(String name) {
	// $-- 空校验
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    // $-- 特殊处理,true则进行默认扩展类加载
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // $-- 尝试根据name从缓存中获取类实例,没有则创建
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // $-- double check
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // $-- 创建扩展类,并进行缓存
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

在getExtension方法里,Dubbo主要进行了空校验,并对一些特殊逻辑进行判断处理。接着就是一套“查缓存,缓存不存在创建并缓存”的套路了。在Dubbo的源码里存在大量这样的缓存套路使用,这样的缓存使用对提升效率是非常明显的。
注意:这里缓存的是扩展类的实例

接下来看一下createExtension创建扩展类的逻辑。

private T createExtension(String name) {
    // $-- 先获取该name对应的Class类
    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)
        injectExtension(instance);
        // $-- 对包装扩展类进行依赖注入(constructor)
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            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);
    }
}

createExtension先要根据name获取对应的扩展类,如果系统中都没有这个扩展类,那么生成扩展类实例就无从谈起了。
然后又是一套缓存的套路,通过反射创建了该扩展类的实例,放入缓存中。
接下来就是对扩展类进行IOC注入的逻辑了,主要对扩展类实例内部的setter方法进行注入。以上面DubboProtocol的加载为例,这里就是找DubboProtocol类中的setter方法,如果有扩展类可以注入,就进行注入。
随后是对包装扩展类进行依赖注入,使用的是构造器注入。这里与上述setter方法是不同的。同样以DubboProtocol为例,这里是对系统中以Protocol为构造函数的扩展类进行注入,注入的就是当前的DubboProtocol类。相当于给DubboProtocol外面装饰了一下再返回(装饰器模式)。举个例子,如:ProtocolListenerWrapper类

从文件中加载扩展类

关于扩展类注入的逻辑我们稍后再聊,先看一下Dubbo从文件中加载扩展类Class的实现逻辑。
方法getExtensionClasses会加载当前系统中所有的扩展点类

private Map<String, Class<?>> getExtensionClasses() {
    // $-- 缓存老套路
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // $-- 加载所有扩展类,生成包含所有扩展类Class的一个Map
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

这里没有啥逻辑,依然是一个缓存的老套路。不同的是这里缓存的是扩展类Class,而不是实例。
加载的逻辑还要往下看

private Map<String, Class<?>> loadExtensionClasses() {
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            // $-- @SPI注解只允许指定一个默认值
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
            }
            // $-- @SPI指定了一个默认值,缓存起来
            if (names.length == 1) cachedDefaultName = names[0];    
        }
    }

    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // $-- 加载扩展类资源目录
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

这里首先对扩展类@SPI注解的使用进行了一下校验。如果@SPI注解中配置了两个默认值,则抛异常。方法中的type就是我们要加载的Protocol接口,Protocol接口上使用了@SPI(“dubbo”)进行修饰,代表默认使用dubbo扩展类。
随后就是文件的读取与扩展类的加载了。这里可以看到,Dubbo默认的扩展类目录有以下三个:

  • META-INF/services/
  • META-INF/dubbo/
  • META-INF/dubbo/internal/

那么它是如何读取文件,加载扩展类资源的呢,且看loadDirectory方法

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    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("Exception when load extension class(interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

这里的主要逻辑实际上是获取类加载器,然后将文件的读取交给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为i扩展类的key,如 @SPI("dubbo")中的dubbo,line为类名
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // $-- 反射创建class并加载
                            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);
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class(interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

到这里,我们终于看到熟悉的文件读取操作了。
Dubbo扩展类文件的格式一般都是 “key=value” 样式的,并且支持 “#” 作为注释符的。

具体的加载类操作依然要往下看,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("Error when load extension class(interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + "is not subtype of interface.");
    }
    // $-- 该类上是否使用了Adaptive注解,有则进行缓存
    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 if (isWrapperClass(clazz)) {
        // $-- 是否为包装扩展类(构造函数注入),是则加入set缓存
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    } else {
        clazz.getConstructor();
        // $-- 此处应该是兼容旧方法(@Extension注解)的处理逻辑
        if (name == null || name.length() == 0) {
            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 (names != null && names.length > 0) {
            // $-- 是否为自激活扩展类,是则加入自动激活缓存
            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());
                }
            }
        }
    }
}

这里首先校验了扩展类类型。
随后对@Adaptive注解进行了判断处理。如果类上标注了@Adaptive,则代表其为默认实现,会进行缓存。关于@Adaptive注解,我们这里暂时不说,后续再聊。

接着判断了该扩展类是否存在包装扩展类,如果有的话,就加入到缓存。包装扩展类的逻辑,我们在注入的时候再统一讲,这里先知道有这么一层处理逻辑。

如果上述特殊场景都不满足,那么我们就直接进入默认的加载处理逻辑:首先判断了扩展类的name的取值,此处兼容了对旧的@Extension注解的处理逻辑。随后就自激活扩展类(@Activate注解修饰)和普通扩展类分别加入到相应的缓存中。

这样将三个目录都读取完成之后,扩展类就被直接加载到缓存中了。(o゜▽゜)o☆

扩展类注入

现在让我们将注意力转回到createExtension中。获取到扩展类后,直接进行实例化,然后就是重头戏注入了。
注入分为两种场景,分别是扩展类setter方法注入和包装扩展类构造器注入。

扩展类setter注入

setter方法注入由方法injectExtension实现

private T injectExtension(T instance) {
   try {
        if (objectFactory != null) {
            for (Method method : instance.getClass().getMethods()) {
                // $-- set方法注入
                if (method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers())) {
                    // $-- 方法上存在@DisableInject注解的,此处不进行注入
                    if (method.getAnnotation(DisableInject.class) != null) {
                        continue;
                    }
                    // $-- 获取要注入的类型
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        // $-- 获取要注入的扩展类名,如 setProtcol ==> protocol
                        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        // $-- 获取注入类实例
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // $-- 调用set方法进行注入
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("fail to inject via method " + method.getName()
                                + " of interface " + type.getName() + ": " + e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

objectFactory为扩展加载器工厂,它的主要作用是生成扩展加载器,下面会详细介绍,这里先忽略。
整个setter注入的逻辑还是非常清晰的,配合代码中的注释,相应应该不用我来解释什么了。

包装扩展类构造器注入

Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
    for (Class<?> wrapperClass : wrapperClasses) {
        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
    }
}

首先判断该扩展类是否有包装,这里是通过cachedWrapperClasses缓存来判断的。
对于包装扩展类,随后进行包装扩展类的实例化,以及该包装扩展类的setter注入。

整个逻辑还是非常简单的,唯一的问题在于cachedWrapperClass是如何赋值的呢?
回想读取文件加载类时,会进行该类是否有包装的判断。如果有包装,则加入到cachedWrapperClasses缓存中。

else if (isWrapperClass(clazz)) {
   // $-- 是否为包装扩展类(构造函数注入),是则加入set缓存
    Set<Class<?>> wrappers = cachedWrapperClasses;
    if (wrappers == null) {
        cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
        wrappers = cachedWrapperClasses;
    }
    wrappers.add(clazz);
}

isWrapperClass是其判断代码,主要是判断要加载的类是否有当前接口的构造函数

private boolean isWrapperClass(Class<?> clazz) {
    try {
        // $-- 判断Clazz是否有type类型的构造函数,没有则抛异常
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}

这里你可能有一点懵,我们举Protocol的例子来说明一下吧。

当进行文件读取,加载扩展类读取时,会加载到类ProtocolListenerWrapper。这个类里存在以Protocol为参数的构造方法,说明ProtocolListenerWrapper这个类可以进行包装扩展,因此就会在cachedWrapperClasses中记录下来。

当我们通过createExtension来创建dubbo类型的Protocol时,判断到其包装扩展类缓存cachedWrapperClasses中存在ProtocolListenerWrapper类型的包装类时,就会对DubboProtocol进行装饰,返回的是ProtocolListenerWrapper类。(DubboProtocol的包装类不只ProtocolListenerWrapper,这里仅仅是作为一个例子进行理解)

另外需要注意的是,代码中并没有对包装的顺序进行定义,所以理论上,文件读取时,加载到cachedWrapperClasses缓存中的顺序会直接影响到包装的结果。

ExtensionLoader的获取

上述就是整个普通扩展类加载的流程了,不知道你是否了然于胸了呢?

结合我个人的经历,当我初读这段代码时,对type这个字段很迷惑,特别是在包装扩展类缓存判断这一块儿。那么这个type到底是啥?

别急,让我们将视线转换回一切的起点

ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);

你可能已经猜到了,这里的Protocol.class就是type!
现在是时候分析一下这段代码的前半段了:如何获取某个扩展类的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!");
    }
    // $-- 是否有@SPI注解校验
    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) {
        // $-- 这里创建type类型的ExtensionLoader
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

获取扩展类加载器的这段代码里,主要先对要加载的类做了一些校验,然后是缓存的老套路。我们主要来看一下创建ExtensionLoader的方法,也就是ExtensionLoader的构造方法。

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

到这里可以看到,我们一直疑惑的type原来是在构造方法里被赋值的。type代表的实际上是我们将要获取的扩展类的类型定义。
objectFactory是扩展类加载器工厂,用来生成扩展类加载器ExtensionLoader的。这里先判断了一下我们要加载的类是否就是扩展类加载器工厂本身,如果是本身的话,则返回null,不用加载。否则,就再通过扩展类加载机制获取ExtensionLoader的扩展类。

ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());

像不像一个“套娃”?😂

Dubbo SPI中的ExtensionFactory体系

说到这里,我们顺便聊聊Dubbo 扩展类工厂的实现吧。
在Dubbo中,存在三种类型的扩展类加载器工厂。
Dubbo SPI中的ExtensionFactory体系

Dubbo中存在ExtensionFactory的三个实现类:SpiExtensionFactory、SpringExtensionFactory、AdaptiveExtensionFactory。其中AdaptiveExtensionFactory上有@Adaptive注解,代表这是ExtensionFactory的默认实现。

AdaptiveExtensionFactory的源码如下

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    // $-- 持有了所有的ExtensionFactory
    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory() {
        // $-- 通过SPI获取扩展类加载器工厂,然后放入factories中
        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);
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        // $-- 本质上还是调用了SPI和Spring两个ExtensionFactory来getExtension,顺序是SPI->Spring
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }
}

实际上AdaptiveExtensionFactory只是一层壳,内部也是调用其他两个ExtensionFactory的getExtension方法来实现的,且顺序为Spi->Spring

SpringExtensionFactory的中getExtension方法如下

public <T> T getExtension(Class<T> type, String name) {
    // $-- 遍历Spring容器,根据名称获取bean
    for (ApplicationContext context : contexts) {
        if (context.containsBean(name)) {
            Object bean = context.getBean(name);
            if (type.isInstance(bean)) {
                return (T) bean;
            }
        }
    }

    logger.warn("No spring extension (bean) named:" + name + ", try to find an extension (bean) of type " + type.getName());

    // $-- 如果要获取的bean类型为Object,返回null给它吧(毕竟所有类都是Object的子类)
    if (Object.class == type) {
        return null;
    }

    // $-- 遍历Spring容器,根据类型获取bean
    for (ApplicationContext context : contexts) {
        try {
            return context.getBean(type);
        } catch (NoUniqueBeanDefinitionException multiBeanExe) {
            logger.warn("Find more than 1 spring extensions (beans) of type " + type.getName() + ", will stop auto injection. Please make sure you have specified the concrete parameter type and there's only one extension of that type.");
        } catch (NoSuchBeanDefinitionException noBeanExe) {
            if (logger.isDebugEnabled()) {
                logger.debug("Error when get spring extension(bean) for type:" + type.getName(), noBeanExe);
            }
        }
    }

    logger.warn("No spring extension (bean) named:" + name + ", type:" + type.getName() + " found, stop get bean.");

    return null;
}

Spring扩展加载器工厂主要是从Spring上下文中获取对应的bean,先根据名称匹配,没匹配到再根据类型匹配,如果还没有匹配到,则返回null。

SpiExtensionFactory的getExtension方法如下

public <T> T getExtension(Class<T> type, String name) {
 if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
        // $-- 根据类型获取所有的扩展点加载器
        ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
        // $-- 如果缓存的扩展点加载器不为空,则直接返回Adaptive实例
        if (!loader.getSupportedExtensions().isEmpty()) {
            return loader.getAdaptiveExtension();
        }
    }
    return null;
}

可以看到,Spi扩展点加载器工厂的getExtension方法非常简单。默认返回的是Adaptive类型的实例。

总结

以上就是一个普通扩展类加载的大致内容了。
由于篇幅已经比较长了,自激活扩展(@Activate)和自适应扩展(@Adaptive)源码将在下一篇博文介绍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章