Dubbo-SPI机制深度解析

SPI机制简介

spi是一种服务发现机制,其目的是,在调用一个spi时,动态的选择不同的实现方式。这和api有很大区别,api是定死的,调用哪个api,其实现类的实现方式是一样的。而spi可以动态选择不同实现类作为实现。

其本质,是将接口实现类的全限定名配置在文件中,并由服务加载器读取,加载实现类,这样,就可以实现在运行时,动态为接口替换实现类。

常见的SPI机制

Java SPI、Spring SPI、Dubbo SPI

Java SPI

spi机制的配置文件保存在resources/META-INF.services下

文件名是interface的全限定名

例如:com.youlong.spi.ITestService

文件内容是接口实现类的全限定名列表

例如:

com.youlong.spi.impl.TestServiceImpl1 com.youlong.spi.impl.TestServiceImpl2

调用时使用ServiceLoader进行加载,假设该接口中定义doTest方法

例如:

java 复制代码
ServiceLoader<ITestService> serviceLoader = ServiceLoader.load(ITestService.class);
serviceLoader.forEach(ITestService::doTest);

Java SPI并不能按需加载,容易造成资源浪费,并且只能通过iterator获取,不能通过某个参数来获取对应实现类。

Spring SPI

spi机制的配置文件保存在spring.factories文件,相比于java spi,spring的spi只要一个文件即可,所有spi实现对应关系都保存在一个文件,而java spi一个接口对应一个文件。

spring spi配置文件中,以kv形式保存

例如:

com.youlong.spi.ITestService =

com.youlong.spi.impl.TestServiceImpl1,

com.youlong.spi.impl.TestServiceImpl2

调用时使用SpringFactoriesLoader.loadFactories来加载实现类列表

例如:

java 复制代码
List<ITestService> testServices = SpringFactoriesLoader.loadFactories(ITestService.class, Thread.currentThread().getContextClassLoader())
testServices.forEach(ITestService::doTest);

spring spi和java spi类似,还是无法获取固定的某个实现类,只能按顺序获取所有实现。

Dubbo SPI

Dubbo SPI所需配置文件放在META-INF/dubbo,命名按照接口的全限定名来命名

与Spring SPI和Java SPI配置不同,Dubbo SPI通过键值对的方式进行配置,K是实现方式命名,V是实现类全限定名

例子:

文件名:com.youlong.spi.ITestService

内容:

ini 复制代码
testServiceImpl1 = com.youlong.spi.impl.TestServiceImpl1
testServiceImpl2 = com.youlong.spi.impl.TestServiceImpl2
com.youlong.spi.impl.TestServiceWrapper  注意:这个没有name的类,是AOP,dubbo的切面类

Dubbo SPI相关逻辑被封装在ExtensionLoader类中,通过此类可以按需加载不同实现类。另外,在要实现SPI机制的接口上,要加上@SPI注解,表示这个接口需要被扩展

获取所需实现类实例举例:

假设我们需要TestServiceImpl1实现类

java 复制代码
ExtensionLoader<ITestService> loader = ExtensionLoader.getExtensionLoader(ITestService);
ITestService testServiceImpl1 = loader.getExtension("testServiceImpl1");
testServiceImpl1.doTest();

Dubbo SPI除了支持按需加载接口实现类,还增加了IOC和AOP特性。

下面,我们来深入探究以下Dubbo SPI的实现原理

Dubbo SPI实现原理

上面提到了Dubbo SPI使用ExtensionLoader来获取扩展点加载器

下面让我们看一下源码中getExtensionLoader是如何实现的

java 复制代码
@Override
@SuppressWarnings("unchecked")
public <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    checkDestroyed();
    // 为空则抛异常
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    // 不是接口,则抛异常
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
    }
    // 接口没加@SPI注解,则抛异常
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type (" + type +
            ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }

    // 从本地缓存去获取对应的扩展点加载器
    ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoadersMap.get(type);

    ExtensionScope scope = extensionScopeMap.get(type);
    if (scope == null) {
        SPI annotation = type.getAnnotation(SPI.class);
        scope = annotation.scope();
        extensionScopeMap.put(type, scope);
    }

    if (loader == null && scope == ExtensionScope.SELF) {
        // create an instance in self scope
        loader = createExtensionLoader0(type);
    }

    // 2. find in parent
    if (loader == null) {
        if (this.parent != null) {
            loader = this.parent.getExtensionLoader(type);
        }
    }

    // 3. create it
    if (loader == null) {
        loader = createExtensionLoader(type);
    }

    return loader;
}

这里特殊说明一下,扩展点加载器的本地缓存是ConsurrentMap类型的,也就是说它是线程安全的(注意:JavaSPI机制并不是线程安全的)

java 复制代码
private final ConcurrentMap<Class<?>, ExtensionLoader<?>> extensionLoadersMap = new ConcurrentHashMap<>(64);

继续说getExtensionLoader方法,会从本地缓存获取ExtensionLoader,若获取不到,则回去new一个ExtensionLoader并放入到本地缓存,最后将其返回

下面继续看下getExtension()方法

java 复制代码
public T getExtension(String name, boolean wrap) {
    checkDestroyed();
    // 名称为空则报错
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 名称为true,则加载默认扩展点,默认扩展点下面会讲
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    String cacheKey = name;
    if (!wrap) {
        cacheKey += "_origin";
    }
    // 这里用对象持有帮助对象Holder去辅助实现双检索单例
    final Holder<Object> holder = getOrCreateHolder(cacheKey);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 创建扩展点实例
                instance = createExtension(name, wrap);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

默认扩展点:就是在接口的@SPI注解上定义的扩展点

例如:@SPI("testServiceImpl1")

默认扩展点就是testServiceImpl1 上述过程是去从Holder中获取一个扩展点实例(Holder也是一个ConcurrentHashMap,key是实例name),获取不到,则双检索单例去创建,并setHolder

下面看一下createExtension()是如何创建的(可以猜一下哎,肯定用了反射)

java 复制代码
private T createExtension(String name, boolean wrap) {
    // 先得到一个类
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null || unacceptableExceptions.contains(name)) {
        throw findException(name);
    }
    try {
        T instance = (T) extensionInstances.get(clazz);
        if (instance == null) {
            // 根据实现类得到一个实例
            extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
            instance = (T) extensionInstances.get(clazz);
            // 在初始化前的处理
            instance = postProcessBeforeInitialization(instance, name);
            // 依赖注入IOC
            injectExtension(instance);
            // 后置处理
            instance = postProcessAfterInitialization(instance, name);
        }

        if (wrap) {
            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }
            // aop
            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    boolean match = (wrapper == null) ||
                        ((ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) &&
                            !ArrayUtils.contains(wrapper.mismatches(), name));
                    if (match) {
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                        instance = postProcessAfterInitialization(instance, name);
                    }
                }
            }
        }

        // 检查instance是否实现了Lifecycle接口,若实现了,则调用initialize方法
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
            type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

这个方法里,会先获得实现类,然后根据实现类去创建实例

从配置文件获取实现类Map

我们首先来看下,是如何根据名字找到对应的实现类的 getExtensionClasses()

java 复制代码
private Map<String, Class<?>> getExtensionClasses() {
    // 获取所有的class
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                try {
                    // 加载扩展类
                    classes = loadExtensionClasses();
                } catch (InterruptedException e) {
                    logger.error(COMMON_ERROR_LOAD_EXTENSION, "", "", "Exception occurred when loading extension class (interface: " + type + ")", e);
                    throw new IllegalStateException("Exception occurred when loading extension class (interface: " + type + ")", e);
                }
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

这个方法会获得一个包含了所有name的对应的Class

这里,提前猜测一下,首先会获取文件中所有实现类的URL资源,然后根据类全限定名加载这些实现类,从而获得所有实现类的Map<name,Class>

接下来,看下加载扩展类

java 复制代码
/**
 * synchronized in getExtensionClasses
 */
@SuppressWarnings("deprecation")
private Map<String, Class<?>> loadExtensionClasses() throws InterruptedException {
    checkDestroyed();
    // 默认扩展器缓存(SPI注解里的name)
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 加载不同目录下的extensionClass
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy, type.getName());

        // compatible with old ExtensionFactory
        if (this.type == ExtensionInjector.class) {
            loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
        }
    }

    return extensionClasses;
}

此方法首先会对default扩展器进行处理,然后加载不同目录下的扩展器实现类。strategies是不同目录的列表,是通过loadLoadingStrategies方法加载获得的。代码如下

java 复制代码
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();

private static LoadingStrategy[] loadLoadingStrategies() {
    return stream(load(LoadingStrategy.class).spliterator(), false)
        .sorted()
        .toArray(LoadingStrategy[]::new);
}

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

看代码能知道,这下面用到Java SPI机制了,这里直接放配置文件看下

文件名:org.apache.dubbo.common.extension.LoadingStrategy

dart 复制代码
org.apache.dubbo.common.extension.DubboInternalLoadingStrategy
org.apache.dubbo.common.extension.DubboLoadingStrategy
org.apache.dubbo.common.extension.ServicesLoadingStrategy

随便找一个实现类看下

java 复制代码
public class DubboLoadingStrategy implements LoadingStrategy {

    @Override
    public String directory() {
        return "META-INF/dubbo/";
    }

其实就是一个文件目录path

我们回去继续看下loadDirectory方法

java 复制代码
private void loadDirectory(Map<String, Class<?>> extensionClasses, LoadingStrategy strategy, String type) throws InterruptedException {
    loadDirectoryInternal(extensionClasses, strategy, type);
    try {
        // 这里是dubbo中自己定义的SPI接口用到了这里,我们自己定义的不需要管
        String oldType = type.replace("org.apache", "com.alibaba");
        if (oldType.equals(type)) {
            return;
        }
        //if class not found,skip try to load resources
        ClassUtils.forName(oldType);
        loadDirectoryInternal(extensionClasses, strategy, oldType);
    } catch (ClassNotFoundException classNotFoundException) {

    }
}

上述方法去执行加载扩展实现类,后续是为了兼容老版本(先用此版本找一遍,再替换type前缀,去找一遍),替换的type名称,此处为dubbo内部的SPI,我们自己定义的,不用管。

loadDirectoryInternal方法中会根据文件path来利用ClassLoader获取对应URL loadDirectoryInternal方法关键代码如下

java 复制代码
Set<ClassLoader> classLoaders = scopeModel.getClassLoaders();

if (CollectionUtils.isEmpty(classLoaders)) {
    Enumeration<java.net.URL> resources = ClassLoader.getSystemResources(fileName);
    if (resources != null) {
        while (resources.hasMoreElements()) {
            loadResource(extensionClasses, null, resources.nextElement(), loadingStrategy.overridden(),
                loadingStrategy.includedPackages(),
                loadingStrategy.excludedPackages(),
                loadingStrategy.onlyExtensionClassLoaderPackages());
        }
    }
} else {
    classLoadersToLoad.addAll(classLoaders);
}

ClassLoader通过fileName获取URL列表resources后,执行loadResource方法,如下

java 复制代码
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
    try {
        List<String> newContentList = getResourceContent(resourceURL);
        String clazz;
        for (String line : newContentList) {
            try {
                String name = null;
                int i = line.indexOf('=');
                if (i > 0) {
                    name = line.substring(0, i).trim();
                    clazz = line.substring(i + 1).trim();
                } else {
                    clazz = line;
                }
                if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages) && isIncluded(clazz, includedPackages)
                    && !isExcludedByClassLoader(clazz, classLoader, onlyExtensionClassLoaderPackages)) {
                    // 将反射获得的Class加载进去
                    loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
                }
            } 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(COMMON_ERROR_LOAD_EXTENSION, "", "", "Exception occurred when loading extension class (interface: " +
            type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

这个方法,就是将文件中的内容封装成一个newContentList,之后分割,获取类的全限定名clazz,之后,执行loadClass方法,其中,会传入class.forName获取到的Class

loadClass代码如下

java 复制代码
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) {
    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)) {
        cacheAdaptiveClass(clazz, overridden);
        // 判断是不是包装类,aop就是依据此模式实现的,判断条件是:若有构造方法,参数是这个接口的type,则是包装类。可以到这个方法源码中具体看一下。
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        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)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

这个方法会对clazz进行一系列校验,其中,isWrapperClass用来判断是否是包装类,

若为包装类,则放到 Set<Class<?>> cachedWrapperClasses本地缓存中,这也是Dubbo中AOP的实现方式。目的是给我们这个接口的所有实现类增加一个切面。

到最后其实就是填充了extensionClasses

根据配置文件进行类加载过程已经结束,下面该进行实例化。

实现类的实例化

看下createExtensionInstance下的instantiate方法

java 复制代码
public <T> T instantiate(Class<T> type) throws ReflectiveOperationException {

    // should not use default constructor directly, maybe also has another constructor matched scope model arguments
    // 1. try to get default constructor
    Constructor<T> defaultConstructor = null;
    try {
        defaultConstructor = type.getConstructor();
    } catch (NoSuchMethodException e) {
        // ignore no default constructor
    }

    // 2. use matched constructor if found
    List<Constructor<?>> matchedConstructors = new ArrayList<>();
    Constructor<?>[] declaredConstructors = type.getConstructors();
    for (Constructor<?> constructor : declaredConstructors) {
        if (isMatched(constructor)) {
            matchedConstructors.add(constructor);
        }
    }
    // remove default constructor from matchedConstructors
    if (defaultConstructor != null) {
        matchedConstructors.remove(defaultConstructor);
    }

    // match order:
    // 1. the only matched constructor with parameters
    // 2. default constructor if absent

    Constructor<?> targetConstructor;
    if (matchedConstructors.size() > 1) {
        throw new IllegalArgumentException("Expect only one but found " +
            matchedConstructors.size() + " matched constructors for type: " + type.getName() +
            ", matched constructors: " + matchedConstructors);
    } else if (matchedConstructors.size() == 1) {
        targetConstructor = matchedConstructors.get(0);
    } else if (defaultConstructor != null) {
        targetConstructor = defaultConstructor;
    } else {
        throw new IllegalArgumentException("None matched constructor was found for type: " + type.getName());
    }

    // create instance with arguments
    Class<?>[] parameterTypes = targetConstructor.getParameterTypes();
    Object[] args = new Object[parameterTypes.length];
    for (int i = 0; i < parameterTypes.length; i++) {
        args[i] = getArgumentValueForType(parameterTypes[i]);
    }
    return (T) targetConstructor.newInstance(args);
}

这里会有一个获取targetConstructor的过程,需要实现类中有无参构造方法,否则会报错。最后通过反射创建一个实例。

回到前面,创建完实例之后,有一个前置处理器(创建完实例之后,初始化之前),之后会进行依赖注入,然后进行AOP切面处理,再然后进行后置处理器(和spring bean的生命周期很像)

AOP切面的实现

我们来看下AOP切面处理的实现,createExtension方法中有下面一行代码

java 复制代码
// aop,这里得到的instance是Wrapper类的实例
instance = injectExtension(
        (T) wrapperClass.getConstructor(type).newInstance(instance));

此代码表示,再进行依赖注入时,传入的是一个实现该接口的Wrapper类的实例(若配置了Wrapper类,则会走到这一步,若没有配置,则不会去执行这一步)

依赖注入的实现

下面看一下依赖注入的过程,Dubbo的依赖注入方式是setter方式

java 复制代码
private T injectExtension(T instance) {
    if (injector == null) {
        return instance;
    }

    try {
        for (Method method : instance.getClass().getMethods()) {
            // 不是set方法,则跳过
            if (!isSetter(method)) {
                continue;
            }
            /**
             * Check {@link DisableInject} to see if we need auto-injection for this property
             */
            // 若标记了不可注入注解,则跳过
            if (method.isAnnotationPresent(DisableInject.class)) {
                continue;
            }

            // When spiXXX implements ScopeModelAware, ExtensionAccessorAware,
            // the setXXX of ScopeModelAware and ExtensionAccessorAware does not need to be injected
            // 属于ScopeModelAware和ExtensionAccessorAware的set方法,则跳过
            if (method.getDeclaringClass() == ScopeModelAware.class) {
                continue;
            }
            if (instance instanceof ScopeModelAware || instance instanceof ExtensionAccessorAware) {
                if (ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method))) {
                    continue;
                }
            }

            Class<?> pt = method.getParameterTypes()[0];
            // 若set方法的参数是基本类型,则跳过
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
                String property = getSetterProperty(method);
                Object object = injector.getInstance(pt, property);
                if (object != null) {
                    // 把所需的对象object注入到目标实例instance
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
           
            }
        }
    } catch (Exception e) {
        logger.error(COMMON_ERROR_LOAD_EXTENSION, "", "", e.getMessage(), e);
    }
    return instance;
}

这个方法完成了向instance中依赖注入的工作,具体实现方式就是调用set方法,往instanceset一个依赖实例

createExtension()方法的最后,回去检验此instance是否实现了Lifecycle接口,若是,则执行初始化方法。

至此,Dubbo SPI底层的实现原理解析就结束了。

总结

Dubbo SPI机制中,配置文件中会保存实现类和Wrapper类相关信息,要按需获取某一个扩展点时,首先,通过接口的type去获取ExtensionLoader,获取到之后,再根据name获取Extension扩展点实例,其底层会去读取配置文件,获取对应的实现类的全限定类名和Wrapper类的全限定类名,再根据反射获取对应的ClassWrapperClass(aop切面类。以拿到nameClassMap。之后,会通过反射,将Class进行实例化,得到一个个的空对象实例。 再依靠setter方法进行依赖注入,依赖注入之后,会检查是否有Wrapper包装类,若有,则创建包装类的实例,并将instance当做参数传入到包装类中,再对此包装类实例进行依赖注入,完成这个工作,在调用扩展点的方法时,就会自动去调用包装类的方法,实现aop切面的功能。在完成aop处理之后,我们就可以得到一个完整的接口实现类对象(外层可能是个包装类)。

相关推荐
声声codeGrandMaster3 小时前
Django项目入门
后端·mysql·django
千里码aicood3 小时前
【2025】基于springboot+vue的医院在线问诊系统设计与实现(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·后端
yang_love10114 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
Pandaconda4 小时前
【后端开发面试题】每日 3 题(二十)
开发语言·分布式·后端·面试·消息队列·熔断·服务限流
鱼樱前端5 小时前
mysql事务、行锁、jdbc事务、数据库连接池
java·后端
Adellle5 小时前
MySQL
数据库·后端·mysql
JavaGuide6 小时前
Kafka 4.0 正式发布,彻底抛弃 Zookeeper,队列功能来袭!
后端·kafka
轻松Ai享生活6 小时前
2030年的大模型将会是什么样的?机械可解释性又是什么?
人工智能·后端·面试
uhakadotcom7 小时前
解锁网页解析的秘密:BeautifulSoup4 入门指南
后端·面试·github
Hello.Reader7 小时前
初探 Dubbo Rust SDK打造现代微服务的新可能
微服务·rust·dubbo