Spring SPI和Spring Boot SPI - 第345篇_Java

 

前言

       在前面的章节《DriverManager SPI分析和Java SPI原理》中,我们通过分析DriverManager的SPI和Java SPI的原理,对于SPI有了一个比较深的了解。现在我们大部分的项目都是基于Spring或者Spring Boot开发的,所以这一节我们谈谈SPI在Spring | Spring Boot中的应用。

 

一、Spring SPI

1.1执行流程

Spring中使用的类是SpringFactoriesLoader,在org.springframework.core.io.support包中,大体的执行流程:

(1)SpringFactoriesLoader 会扫描 classpath 中的 META-INF/spring.factories文件。

(2)SpringFactoriesLoader 会加载并实例化META-INF/spring.factories 中的指定类型。

(3)META-INF/spring.factories 内容必须是 properties 的Key-Value形式,多值以逗号隔开。

 

1.2使用

       我们来看一个小小的例子来理解下上面的流程:

(1)创建META-INF/spring.factories文件

       在META-INF下创建spring.factories文件,在文件中配置如下信息:

  •  
com.kfit.id.IdGenerator=\  com.kfit.id.impl.UUIDIdGenerator,\  com.kfit.id.impl.TimestampGenerato

       然后在测试代码中使用SpringFactoriesLoader进行加载:

  •  
public static void main(String[] args) {    List<IdGenerator> idGeneratorList =  SpringFactoriesLoader.loadFactories(IdGenerator.class,null);    List<String> classNameList =  SpringFactoriesLoader.loadFactoryNames(IdGenerator.class,null);
//结果:[com.kfit.id.impl.UUIDIdGenerator@1a93a7ca, com.kfit.id.impl.TimestampGenerator@3d82c5f3] System.out.println(idGeneratorList); //结果:[com.kfit.id.impl.UUIDIdGenerator, com.kfit.id.impl.TimestampGenerator] System.out.println(classNameList);}

说明:

(1)spring.factories文件的key可以是接口,也可以是抽象类、具体的类。但是有个原则:=后面必须是key的实现类(子类)。

(2)在spring.factories文件中可以指定多个key,对于java spi如果多个接口的话,需要配置多个文件。

(3)key还可以是注解,比如SpringBoot中的的key:org.springframework.boot.autoconfigure.EnableAutoConfiguration,它就是一个注解。

(4)文件的格式需要保证正确,否则会返回[](不会报错)。

(5)=右边不能是抽象类,必须能够实例化。且有空的构造函数~

(6)loadFactories依赖方法loadFactoryNames。loadFactoryNames方法只拿全类名,loadFactories拿到全类名后会立马实例化

(7)loadFactories实例化完成所有实例后,会调用AnnotationAwareOrderComparator.sort(result)排序,所以它是支持Ordered接口排序的,这个特点特别的重要。

 

1.3原理

       核心加载类是SpringFactoriesLoader,主要暴露了两个方法:loadFactories和loadFactoryNames:

  •  
/** * @author Arjen Poutsma * @author Juergen Hoeller * @author Sam Brannen * @since 3.2 */public final class SpringFactoriesLoader {
/** * The location to look for factories. * <p>Can be present in multiple JAR files. */ public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

private SpringFactoriesLoader() { }


public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) { Assert.notNull(factoryType, "'factoryType' must not be null"); ClassLoader classLoaderToUse = classLoader; if (classLoaderToUse == null) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse); if (logger.isTraceEnabled()) { logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames); } List<T> result = new ArrayList<>(factoryImplementationNames.size()); for (String factoryImplementationName : factoryImplementationNames) { result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse)); } AnnotationAwareOrderComparator.sort(result); return result; }

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); }
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 factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }
@SuppressWarnings("unchecked") private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) { try { Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader); if (!factoryType.isAssignableFrom(factoryImplementationClass)) { throw new IllegalArgumentException( "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]"); } return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance(); } catch (Throwable ex) { throw new IllegalArgumentException( "Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", ex); } }
}

       我们从源码当中可以看到,loadFactories会调用loadFactoryNames,然后在loadFactoryNames方法中调用了方法loadSpringFactories:

  •  
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 中就是 spring.factories的文件的URL地址。 /xx/javaspidemo/target/classes/META-INF/spring.factories /xx/spring-beans-5.2.9.RELEASE.jar!/META-INF/spring.factories */ Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
//一个键对应多个值 result = new LinkedMultiValueMap<>();
/* * 遍历urls的 spring.factories 文件,将每个spring.factories中的配置信息进行读取。 */ while (urls.hasMoreElements()) {
URL url = urls.nextElement(); UrlResource resource = new UrlResource(url);
//将spring.factories进行加载为Properties文件, // 也就是key=value的形式,其中的value使用逗号分割 Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim();
//// 使用逗号,分隔成数组,遍历 //commaDelimitedListToStringArray 方法就是使用逗号分割 for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }}

       具体这里的注释已经写的很详细了,就不再过多进行说明了。

 

二、Spring-Boot-Starter

       SpringBoot的 @SpringBootApplication 注解,里面还有一个 @EnableAutoConfiguration 注解,开启自动配置的。

       可以发现这个自动配置注解在另一个工程,而这个工程里也有个spring.factories文件,如下图:

Spring SPI和Spring Boot SPI - 第345篇_Java _02

       我们知道在SpringBoot的目录下有个spring.factories文件,里面都是一些接口的具体实现类,可以由SpringFactoriesLoader加载。

那么同样的道理,spring-boot-autoconfigure模块也能动态加载了。看一下其中的内容:

  •  
# Initializersorg.springframework.context.ApplicationContextInitializer=\org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listenersorg.springframework.context.ApplicationListener=\org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listenersorg.springframework.boot.autoconfigure.AutoConfigurationImportListener=\org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filtersorg.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\org.springframework.boot.autoconfigure.condition.OnBeanCondition,\org.springframework.boot.autoconfigure.condition.OnClassCondition,\org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\

       在这里面配置了PropertySourceLoader和ApplicationListener等接口的具体实现类,然后通过SpringFactoriesLoader这个类去加载这个文件,并获得具体的类路径。

 

我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。