Spring Boot扩展机制Spring Factories(spring.factories)
背景说明
Spring Boot之前
基于Spring框架搭建的项目,要我们清楚知道使用Spring的哪些部分,如使用什么数据库类型等,并且需要清楚这些Spring构建需要哪些配置,为每构建手动配置其上下文。
显然,这是很繁琐的工作。
引入Spring Boot之后
Spring框架繁琐的配置工作正是Spring Boot要解决的问题。Spring Boot会扫描classpath下jar包,按照约定的方式自动配置Spring 构建。这大大提高了基于Spring框架工程的开发效率以及入门门槛。
Spring Boot是怎么能动态发现这些jar包,把它们组合起来构建Spring工程呢?这其中的机制就是有Spring Framework提供的Spring Factories功能。通过spring.factories文件,每个模块都可以提供自己的配置,使用时就会被合并到Spring的上下文中。
Spring Factories
Spring Factories类似于Java的SPI服务加载机制,它给Spring Boot提供了一种解耦的扩展机制。
接口配置
Spring Factories不同于Java SPI定义接口实现在META-INF/services/xxx.xxx.XXXService,Spring Factories把接口实现配置在META-INF/spring.factories。
spring.factories是一个properties文件,定义接口实现配置示例:
com.example.MyService=\
com.example.MyServiceImpl1,\
com.example.MyServiceImpl2
其中com.example.MyService是一个接口,这里配置了它的两个实现com.example.MyServiceImpl1和com.example.MyServiceImpl2。
配置中,同一个接口,多种实现由逗号“,”隔开。
接口实现类的加载
Spring Framework的SpringFactoriesLoader类时用于加载META-INF/spring.factories配置的定义的接口实现类。
SpringFactoriesLoader是一个工具类,提供了两个静态方法:
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader)
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)
- loadFactories:获取在spring.factories定义接口的实现类,并构造其实现类的实例返回。接口可以有多个实现类,所以返回的是对象列表。
- loadFactoryNames:获取在spring.factories所定义接口的实现类名列表。
读取spring.factories在SpringFactoriesLoader实现
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
//其他代码....
result = new HashMap<>();
try {
// 1.classLoader获取所有FACTORIES_RESOURCE_LOCATION(值为META-INF/spring.factories)的路径
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 2.使用Properties读取spring.factories文件
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
// 3.把以逗号“,”分开的实现类列表,分割为数组。
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
// 其他代码.....
}
//其他代码....
return result;
}
这里对代码中标注的三点做下说明:
- ClassLoader是读取所有jar下的META-INF/spring.factories文件,各自jar的spring.factories文件不会覆盖以及互相影响。如果多个jar包定义了相同接口的不同实现,会合并到接口的实现类列表。
- spring.factories是一个Properties文件,每个接口一行,属性名为接口名,属性值为实现类列表。
- 多个实现类使用逗号隔开“,”。
spring.factories定义接口实现类的实例化
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
//其他代码省略...
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
List<T> result = new ArrayList<>(factoryImplementationNames.size());
for (String factoryImplementationName : factoryImplementationNames) {
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
代码说明:
- 首先通过loadFactoryNames获取接口的实现类列表名
- 迭代调用instantiateFactory实例化实现类。内部是通过反射机制ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance()实现。
- 如果有定义@Order注解,按定义的顺序排序。
Spring Facories的使用
从上面的代码讲解,大概就可以知道怎么使用SpringFactories了。下面以HelloServcie做说明
定义接口
public interface HelloService {
String sayHello();
}
实现类ChineseHelloService和EnglishHelloService
public class ChineseHelloService {
public String sayHello() { return "你好!"; }
}
public class EnglishHelloService{
public String sayHello() { return "Nice to meet you."; }
}
在工程的META-INF/spring.factories定义接口实现:
com.example.HelloService=com.example.ChineseHelloSerevice,com.example.EnglishHelloService
properties文件中,如果一行太长,可以在行末使用反斜杠“\”换行,表示换行后新的一行是跟在反斜杠前面的字符串的。上面例子可以改为:
com.example.HelloService=com.example.ChineseHelloSerevice,\
com.example.EnglishHelloService
使用SpringFactoriesLoader加载实现类
要加载HelloService的实现类就很简单了:
List<HelloService> helloServices= SpringFactoriesLoader.loadFactories(HelloService.class, null);