Java的SPI

SPI全称为Service Provider Interface,是jdk内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现的机制。

SPI规定,所有要预先声明的类都应该放在META-INF/services中。配置的文件名是接口/抽象类的全限定名,文件内容是抽象类的子类或接口的实现类的全限定类名,如果有多个,借助换行符,一行一个。

具体使用时,使用jdk内置的ServiceLoader类来加载预先配置好的实现类。

举个例子:

META-INF/services中声明一个文件名为site.milanchen.demo.SpiDemoInterface的文件,文件内容为:

site.milanchen.demo.SpiDemoInterfaceImpl

site.milanchen.demo包下新建一个接口,类名必须跟上面配置的文件名一样:SpiDemoInterface

在接口中声明一个test()方法:

public interface SpiDemoInterface {
    void test();
}

接下来再新建一个SpiDemoInterfaceImpl,并实现SpiDemoInterface

public class SpiDemoInterfaceImpl implements SpiDemoInterface {
    @Override
    public void test() {
        System.out.println("SpiDemoInterfaceImpl#test() run...");
    }
}

编写主运行类,测试效果:

public class App {
    public static void main(String[] args) {
        ServiceLoader<SpiDemoInterface> loaders = ServiceLoader.load(SpiDemoInterface.class);
        loaders.foreach(SpiDemoInterface::test);
    }
}

运行结果:

SpiDemoInterfaceImpl#test() run...

Spring的SPI

SpringFramework 利用 SpringFactoriesLoader 都是调用 loadFactoryNames 方法:

/**
 * Load the fully qualified class names of factory implementations of the
 * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
 * class loader.
 * @param factoryClass the interface or abstract class representing the factory
 * @param classLoader the ClassLoader to use for loading resources; can be
 * {@code null} to use the default
 * @throws IllegalArgumentException if an error occurs while loading factory names
 * @see #loadFactories
 */
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

使用给定的类加载器从 META-INF/spring.factories 中加载给定类型的工厂实现的全限定类名。

结合之前SpringBoot自动配置提到的spring.factories内容,可以知道它只是key-value的关系。

这么设计的好处:不再局限于接口-实现类的模式,key可以随意定义。(如上面的 org.springframework.boot.autoconfigure.EnableAutoConfiguration是一个注解)

来看方法实现,第一行代码获取的是要被加载的接口/抽象类的全限定名,下面的 return 分为两部分:loadSpringFactoriesgetOrDefault
getOrDefault方法很明显是Map中的方法,不再解释,主要来详细看loadSpringFactories方法。

loadSpringFactories

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

// 这个方法仅接收了一个类加载器
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // 第一次取不到
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        Enumeration<URL> urls = (classLoader != null ?
                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryClassName = ((String) entry.getKey()).trim();
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                                       FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

它拿到每一个文件,并用Properties方式加载文件,之后把这个文件中每一组键值对都加载出来,放入MultiValueMap中。

如果一个接口/抽象类有多个对应的目标类,则使用英文逗号隔开。StringUtils.commaDelimitedListToStringArray会将大字符串拆成一个一个的全限定类名。

整理完后,整个result放入cache中。下一次再加载时就无需再次加载spring.factories文件了。

Last modification:September 13, 2021
如果觉得我的文章对你有用,请随意赞赏