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处理之后,我们就可以得到一个完整的接口实现类对象(外层可能是个包装类)。

相关推荐
超级大福宝1 分钟前
用买火车票的例子讲解Java反射的作用
java·开发语言·后端
程序员爱钓鱼1 分钟前
Go高性能缓冲IO详解: bufio包深度指南
后端·面试·go
熙胤13 分钟前
Spring Boot 3.x 引入springdoc-openapi (内置Swagger UI、webmvc-api)
spring boot·后端·ui
tumeng071121 分钟前
springboot项目架构
spring boot·后端·架构
LES000LIE24 分钟前
Spring Cloud
后端·spring·spring cloud
mldlds1 小时前
Spring Boot应用关闭分析
java·spring boot·后端
zjjsctcdl1 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
tuyanfei1 小时前
Spring 简介
java·后端·spring
代码探秘者1 小时前
【大模型应用】2.RAG详细流程
java·开发语言·人工智能·后端·python