Spring Boot扩展机制Spring Factories(spring.factories)

时间:2022-06-01 12:34:19

背景说明

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;
}

这里对代码中标注的三点做下说明:

  1. ClassLoader是读取所有jar下的META-INF/spring.factories文件,各自jar的spring.factories文件不会覆盖以及互相影响。如果多个jar包定义了相同接口的不同实现,会合并到接口的实现类列表。
  2. spring.factories是一个Properties文件,每个接口一行,属性名为接口名,属性值为实现类列表。
  3. 多个实现类使用逗号隔开“,”。

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;
}

代码说明:

  1. 首先通过loadFactoryNames获取接口的实现类列表名
  2. 迭代调用instantiateFactory实例化实现类。内部是通过反射机制ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance()实现。
  3. 如果有定义@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);