说一说 Java、Spring、Dubbo 三者 SPI 机制的原理和区别

149 阅读7分钟

说一说 Java、Spring、Dubbo 三者 SPI 机制的原理和区别

什么是SPI机制?

SPI全称为Service Provider Interface,是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。

简单来说SPI是一种非常优秀的设计思想,它的核心就是解耦、方便扩展。

Java SPI机制--ServiceLoader

ServiceLoader是Java提供的一种简单的SPI机制的实现,Java的SPI实现约定了以下两件事:

  • 文件必须放在META-INF/services/目录底下
  • 文件名必须为接口的全限定名,内容为接口实现的全限定名

这样就能够通过ServiceLoader加载到文件中接口的实现。

接下来我们来简单实现一个基于ServiceLoader的案例.

//1.创建一个接口
public interface LoaderBanlance {
    public void load();
}

//2.创建该接口的实现类
public class LoaderBanlanceImpl1 implements LoaderBanlance {
    @Override
    public void load() {
        System.out.println("LoaderBanlanceImpl 已加载");
    }
}
public class LoaderBanlanceImpl2 implements LoaderBanlance {
    @Override
    public void load() {
        System.out.println("LoaderBanlanceImpl2 已加载");
    }
}

这里,基础的代码就已经有了,接下来我们去创建文件.

img

//1.文件的内容为实现类的全类名
com.itheima.service.impl.LoaderBanlanceImpl1
com.itheima.service.impl.LoaderBanlanceImpl2

到这里我们的前置代码就准备完成了,接下来编写测试类.

public class LoaderDemo {
    public static void main(String[] args) {
        ServiceLoader<LoaderBanlance> loader = ServiceLoader.load(LoaderBanlance.class);
        Iterator<LoaderBanlance> iterator = loader.iterator();
        while (iterator.hasNext()) {
            //这里迭代器会遍历配置文件内容,去生成对应的实现类
            LoaderBanlance loaderBanlance = iterator.next();
            loaderBanlance.load();
            System.out.println("获取到对象"+loaderBanlance);
        }
    }
}

运行结果如下:

img

这里我们可以看到,不止实现类对象被我们获取到了,我们还可以使用实现类的方法.

那么接下来就有一个问题了,ServiceLoader是如何实现的呢?(这里以JDK17的源码为例,JDK版本不同可能导致源码不一样)

private Class<?> nextProviderClass() {
    if (configs == null) {
        try {
            //PREFIX = "META-INF/ services/"
            //这里拼接了一个配置文件路径
            //(META-INF/services/com.itheima.service.LoaderBanlance)
            String fullName = PREFIX + service.getName();
            //下面这堆代码就是利用不同的类加载器获取资源
            if (loader == null) {
                configs = ClassLoader.getSystemResources(fullName);
            } else if (loader == ClassLoaders.platformClassLoader()) {
                // The platform classloader doesn't have a class path,
                // but the boot loader might.
                if (BootLoader.hasClassPath()) {
                    configs = BootLoader.findResources(fullName);
                } else {
                    configs = Collections.emptyEnumeration();
                }
            } else {
                configs = loader.getResources(fullName);
            }
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
     // 解析配置文件内容,生成实现类名迭代器
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return null;
        }
        // parse()会解析ClassLoader获取的资源
        pending = parse(configs.nextElement());
    }
    //解析下一个全类名
    String cn = pending.next();
      try {
        return Class.forName(cn, false, loader);  // 通过类加载器加载类(不初始化类)
    } catch (ClassNotFoundException x) {  // 类未找到(可能因模块隔离或路径错误)
        fail(service, "Provider " + cn + " not found");  // 记录错误并终止加载
        return null;
    }
}

结合源码,我们可以了解到,JAVA SPI就是通过去获取META-INF/services/目录底下的以**接口全类名命名的文件,然后通过ClassLoader获取到资源,其实就是接口的全限定名文件对应的资源,然后交给****parse**方法解析资源.

所以其实不难发现ServiceLoader实现原理比较简单,总结起来就是通过IO流读取META-INF/services/接口的全限定名文件的内容,然后反射实例化对象。

Spring SPI机制--SpringFactoriesLoader

Spring的SPI机制的约定如下:

  • 配置文件必须在META-INF/目录下,文件名必须为spring.factories
  • 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。

所以也可以看出,Spring的SPI机制跟Java的不论是文件名还是内容约定都不一样。

同样,我们也通过一个案例来实现:

//1.创建一个接口
public interface LoadBanlance {
    void load();
}
//2.创建接口实现类
public class LoadBlanceImpl implements LoadBanlance {
    @Override
    public void load() {
        System.out.println("LoadBlanceImpl 已经加载.....");
    }
}

接下来我们去创建文件:

img

img

基础构建完成,接下来创建一个测试类:

public class Test {
    public static void main(String[] args) {
        List<LoadBanlance> loadBanlances = SpringFactoriesLoader
                .loadFactories(LoadBanlance.class, LoadBanlance.class.getClassLoader());
        for (LoadBanlance loadBanlance : loadBanlances) {
            loadBanlance.load();
            System.out.println("获取到:"+loadBanlance);
        }
    }
}

运行结果如下:

img

我们依旧结合源码解析一下:

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // 1. 检查缓存中是否已存在该 ClassLoader 的解析结果
    Map<String, List<String>> result = (Map)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        Map<String, List<String>> result = new HashMap();
        //2.依旧是通过类加载器获取META-INF/spring.factories下的
        try {
            Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
            //3.遍历配置文件的url
            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                //将配置文件解析为 Properties 对象
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();
                //遍历 Properties 中的每个键值对,
                while(var6.hasNext()) {
                    //......
                     //将实现类名添加到对应接口的列表中
                    for(int var12 = 0; var12 < var11; ++var12) {
                        String factoryImplementationName = var10[var12];
                        ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                            return new ArrayList();
                        })).add(factoryImplementationName.trim());
                    }
                }
            }
            //将接口列表去重后转为一个不可变集合
            result.replaceAll((factoryType, implementations) -> {
                return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
            });
            //添加到缓存,避免重复解析配置文件,提升性能。
            cache.put(classLoader, result);
            return result;
        } catch (IOException var14) {
            IOException ex = var14;
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);
        }
    }
}

tip : Spring Boot 的自动装配其实就是Spring SPI的应用场景

Spring SPI也支持加载多个实现类,全类名用逗号隔开:

img

img

与Java SPI机制对比

首先Spring的SPI机制对Java的SPI机制对进行了一些简化,Java的SPI每个接口都需要对应的文件,而Spring的SPI机制只需要一个spring.factories文件。

其次是内容,Java的SPI机制文件内容必须为接口的实现类,而Spring的SPI并不要求键值对必须有什么关系,更加灵活。

第三点就是Spring的SPI机制提供了获取类限定名的方法loadFactoryNames,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射。

但是Spring的SPI也同样没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计。

Dubbo SPI机制--ExtensionLoader

ExtensionLoader是dubbo的SPI机制的实现类。每一个接口都会有一个自己的ExtensionLoader实例对象,这点跟Java的SPI机制是一样的。

同样地,Dubbo的SPI机制也做了以下几点约定:

  • 接口必须要加@SPI注解
  • 配置文件可以放在META-INF/services/META-INF/dubbo/internal/ META-INF/dubbo/META-INF/dubbo/external/这四个目录底下,文件名也是接口的全限定名
  • 内容为键值对,键为短名称(可以理解为spring中Bean的名称),值为实现类的全限定名

我们在接口上添加注解:

@SPI
public interface LoadBanlance {
    void load();
}

这里我们选择META-INF/dubbo/ 的方式

img

在文件中,也是采用键值对的形式

img

public class Test {
    public static void main(String[] args) {
        ExtensionLoader<LoadBanlance> extensionLoader = ExtensionLoader
                .getExtensionLoader(LoadBanlance.class);
        extensionLoader.getExtension("random").load();
        extensionLoader.getExtension("random2").load();
        
        LoadBanlance random = extensionLoader.getExtension("random");
        LoadBanlance random2 = extensionLoader.getExtension("random2");
        System.out.println(random);
        System.out.println(random2);
    }
}

编写测试类并运行,结果如下:

img

这里可以看出,Dubbo可以获取指定的实现类,而Dubbo其实还有一些其他的特性这里就不一 一展开

总结

维度Java SPISpring SPIDubbo SPI
核心类ServiceLoaderSpringFactoriesLoaderExtensionLoader
配置文件路径META-INF/services/接口全限定名META-INF/spring.factoriesMETA-INF/dubbo/接口全限定名
加载方式全量加载,无缓存按需加载,支持 Spring 容器缓存按需加载,支持多级缓存和懒初始化
扩展点发现基于文件系统遍历基于 Spring 容器扫描基于类加载器和配置文件优先级合并
依赖注入不支持支持 Spring 容器注入支持 Setter 注入和自动装配
动态性静态加载运行时动态加载(结合 Spring 上下文)动态代理(如 @Adaptive)和自适应路由

内容主要参考:阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别大家好,我是三友~~ 今天来跟大家聊一聊Ja - 掘金