摘要:本文我们重点分析一下Spring框架中的SpringFactoriesLoader类以及META-INF/spring.factories的使用。在详细分析之前,我们可以思考一个问题?在我们设计一套API供别人调用的时候,如果同一个功能的要求特别多,或者同一个接口要面对很复杂的业务场景,这个时候我们该怎么办呢?其一:我们可以规范不同的系统调用,也就是传递一个系统标识;其二:我们在内部编码的时候可以使用不同的条件判断语句进行处理;其三:我们可以写几个策略类来来应对这个复杂的业务逻辑,比如同一个功能的实现,A实现类与B实现类的逻辑一点也不一样,但是目标是一样的,这个时候使用策略类是毋庸置疑的?上述的问题我们是在API层面进行处理的?那万一有一天让我们自己设计一套框架,然后让别人直接使用?我们该如何处理上述的这个问题呢?这个可能就涉及到SPI的一些规范了跟技巧了,比如同一套API可能有很多实现类,这个时候我们该如何内置一系列实现类供框架使用呢?或者让用户也可以自定义这些API的实现类,相互之间协作运转。带着这些问题我们看一下Spring框架中的SpringFactoriesLoader以及META-INF/spring.factories的使用。
1.1 属性配置
首先,我们来看一下属性的配置方式,在传统的开发模式中(无springboot),属性文件的格式无外乎就是两种,第一种是XML,第二种是key、value形式(properties文件)。当然springboot引入了yaml方式。这里我们重点看一下XML以及properties的定义以及获取方式。
1.1.1 properties方式
1.1.1.1. 单个属性配置
首先,我们新建一个shareniu-single.factories文件,该文件的目录结构如下图所示:
shareniu-single.factories的内容如下:
shareniu=http://www.shareniu.com/
1.1.1.2. 多个属性配置
单个属性的定义比较简单,就是key、value形式即可。对于同一个属性有多个值的定义格式如下:
com.example.demo.ch3.IShareniu=\
com.example.demo.ch3.ShareniuA,\
com.example.demo.ch3.ShareniuB
1.1.1.3. properties读取工具类
上述的属性定义完毕之后,我们写一个工具类进行测试,在这里我们直接调用了PropertiesLoaderUtils类中的方法。实例代码如下:
1 public class PropertiesLoaderUtilsTest {
2 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/shareniu-single.factories";
3 public static void main(String[] args) throws IOException {
4 Properties properties = PropertiesLoaderUtils.loadAllProperties(FACTORIES_RESOURCE_LOCATION,null);
5 System.out.println(properties);
6 }
7 }
上述的代码直接调用了PropertiesLoaderUtils类中的loadAllProperties方法,PropertiesLoaderUtils的全路径名称为:org.springframework.core.io.support.PropertiesLoaderUtils。该类位于spring-core-5.0.0.RC3.jar包中。
运行上面的代码,程序的输出如下:
{shareniu=http://www.shareniu.com/, com.example.demo.ch3.IShareniu=com.example.demo.ch3.ShareniuA,com.example.demo.ch3.ShareniuB}
果真我们自定义的属性都可以完美的获取到。
关于PropertiesLoaderUtils.loadAllProperties的核心代码如下:
1 public static Properties loadAllProperties(String resourceName, @Nullable ClassLoader classLoader) throws IOException {
2 ClassLoader classLoaderToUse = classLoader;
3 if (classLoaderToUse == null) {
4 classLoaderToUse = ClassUtils.getDefaultClassLoader();
5 }
6 Enumeration<URL> urls = (classLoaderToUse != null ? classLoaderToUse.getResources(resourceName) :
7 ClassLoader.getSystemResources(resourceName));
8 Properties props = new Properties();
9 while (urls.hasMoreElements()) {
10 URL url = urls.nextElement();
11 URLConnection con = url.openConnection();
12 ResourceUtils.useCachesIfNecessary(con);
13 InputStream is = con.getInputStream();
14 try {
15 if (resourceName.endsWith(XML_FILE_EXTENSION)) {
16 props.loadFromXML(is);
17 }
18 else {
19 props.load(is);
20 }
21 }
22 finally {
23 is.close();
24 }
25 }
26 return props;
27 }
loadAllProperties方法,首先会根据类加载器去获取指定的资源(也就是我们调用的时候,传递的resourceName参数值)。然后判断资源的后缀是否为xml,如果后缀是xml则使用xml方式加载资源,否则都是用Properties方式进行资源的加载。
注意:虽然上述的代码我们指定的资源名称是:META-INF/shareniu-single.factories,但是上述的类加载器不仅扫描我们项目的META-INF/shareniu-single.factories,还会扫描当前类加载所加载的jar包中的META-INF/shareniu-single.factories文件。
1.1.2 xml方式
了解了上述代码的处理逻辑之后,我们看一下xml方式如何定义,shareniu.xml定义内容如下:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
3 <properties>
4 <comment>shareniu xml配置文件</comment>
5 <entry key="username">shareniu</entry>
6 <entry key="url">http://www.shareniu.com/</entry>
7 </properties>
运行上述的属性工具类,控制台的输出信息如下:
{url=http://www.shareniu.com/, username=shareniu}
1.2 SpringFactoriesLoader使用
了解了XML以及properties的定义以及获取方式之后,接下来学习SpringFactoriesLoader类就简单的多了。
首先,看一下SpringFactoriesLoader类定义的方法如下所示:
1. loadFactoryNames:加载指定的factoryClass并进行实例化。
2. loadSpringFactories:加载指定的factoryClass。
3. instantiateFactory:对指定的factoryClass进行实例化。
通过上文可知:loadFactoryNames方法内部直接调用loadSpringFactories方法,loadSpringFactories方法则会调用instantiateFactory方法。
loadSpringFactories方法内部会加载META-INF/spring.factories文件,这里加载的文件不仅包含项目中的,还包换我们项目环境所依赖的jar包中的META-INF/spring.factories文件。
1.现在,我们写一个简单的测试类,加载spring.factories文件,实例代码如下:
spring.factories文件的内容如下所示:
com.example.demo.ch3.IShareniu=\
com.example.demo.ch3.ShareniuA,\
com.example.demo.ch3.ShareniuB
其中:IShareniu为接口,ShareniuA以及ShareniuB实现了IShareniu接口。结构如下图所示:
2.自定义测试类并调用SpringFactoriesLoader类中的相关方法,如下所示:
1 public class DemoApplication {
2 public static void main(String[] args) {
3 List<String> loadFactoryNames = SpringFactoriesLoader.loadFactoryNames(IShareniu.class, null);
4 System.out.println(loadFactoryNames);
5 }
6 }
自行上述代码,程序的输出信息如下:
[com.example.demo.ch3.ShareniuA, com.example.demo.ch3.ShareniuB]
通过上述的代码可知,我们确实完成了自身项目中META-INF/spring.factories文件的属性读取。
那我们能否能够通过Spring框架实例化这些类呢?答案是肯定的?实例代码如下:
1 List<IShareniu> loadFactories = SpringFactoriesLoader.loadFactories(IShareniu.class, null);
2 System.out.println(loadFactories);
自行上述代码,程序的输出信息如下:
[com.example.demo.ch3.ShareniuA@53fd30, com.example.demo.ch3.ShareniuB@cbc42f]
loadFactories方法返回的已经是实例化完毕的对象了。
1.3 SpringFactoriesLoader原理
接下来,我们看一下SpringFactoriesLoader类中的loadFactories方法,如下所示:
1 public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
2 Assert.notNull(factoryClass, "'factoryClass' must not be null");
3 ClassLoader classLoaderToUse = classLoader;
4 if (classLoaderToUse == null) {
5 classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
6 }
7 List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
8 if (logger.isTraceEnabled()) {
9 logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
10 }
11 List<T> result = new ArrayList<>(factoryNames.size());
12 for (String factoryName : factoryNames) {
13 result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
14 }
15 AnnotationAwareOrderComparator.sort(result);
16 return result;
17 }
loadFactories方法首先获取类加载器,然后调用loadFactoryNames方法获取所有的制定资源的名称集合、其次调用instantiateFactory方法实例化这些资源类并将其添加到result集合中。最后调用AnnotationAwareOrderComparator.sort方法进行集合的排序。
1.3.1 loadFactoryNames方法
loadFactoryNames方法核心代码如下:
18 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
19 private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
20 MultiValueMap<String, String> result = cache.get(classLoader);
21 if (result != null)
22 return result;
23 try {
24 Enumeration<URL> urls = (classLoader != null ?
25 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
26 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
27 result = new LinkedMultiValueMap<>();
28 while (urls.hasMoreElements()) {
29 URL url = urls.nextElement();
30 UrlResource resource = new UrlResource(url);
31 Properties properties = PropertiesLoaderUtils.loadProperties(resource);
32 for (Map.Entry<?, ?> entry : properties.entrySet()) {
33 List<String> factoryClassNames = Arrays.asList(
34 StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
35 result.addAll((String) entry.getKey(), factoryClassNames);
36 }
37 }
38 cache.put(classLoader, result);
39 return result;
40 }
41 catch (IOException ex) {
42 }
43 }
loadSpringFactories方法直接加载所有的META-INF/spring.factories文件内容,其内部还是调用PropertiesLoaderUtils.loadProperties方法进行处理。该方法前面我们也详细的演示了,再次不再累赘。
唯一需要了解的是,这个地方使用了缓存策略。
1.3.2 instantiateFactory方法
instantiateFactory方法的核心逻辑如下:
1 private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
2 try {
3 Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
4 if (!factoryClass.isAssignableFrom(instanceClass)) {
5 throw new IllegalArgumentException(
6 }
7 return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();
8 }
9 catch (Throwable ex) {
10 throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
11 }
12 }
直接调用了ClassUtils.forName方法,然后调用ReflectionUtils.accessibleConstructor方法进行实例对象进行对象的实例化工作,原来这里直接使用了反射技术进行对象的实例化工作。原来如此。
至此,XML以及properties的定义以及获取方式,SpringFactoriesLoader类的使用以及原理已经讲解完毕。
欢迎关注我的微信公众号,第一时间获得博客更新提醒,以及更多成体系的Java相关原创技术干货。
扫一扫下方二维码或者长按识别二维码,即可关注。