在springboot的各個依賴包下,我們經常看到META-INF/spring.factories這個文件。spring.factories文件的內容基本上都是這樣的格式:
1 # Initializers 2 org.springframework.context.ApplicationContextInitializer=\ 3 org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\ 4 org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
我們看到,這個文件配置了一個key:value格式的數據
1)key是:org.springframework.context.ApplicationContextInitializer
2)value是:org.springframework.context.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,org.springframework.context.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
key聲明的是一個接口,value則是這個接口對應的實現類,如果有多個則以","符號分割。
簡單來說,spring.factories文件包含了一些接口相對應的實現類的配置,我們通過這些配置就可以知道接口有哪些可選的實現類,並通過反射獲取對應的實例對象。就像是簡單工廠模式一樣,也因此spring將這個文件定義為spring.factories這個名字。
代碼實例
下面以ApplicationContextInitializer接口為示例,我們看看springboot是怎么使用spring.factories的。
首先會用classLoader加載類路徑下的所有spring.factories的配置內容,loadSpringFactories方法將返回一個key=接口名,value=實現類集合的Map結構
1 private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { 2 // 先試着取緩存 3 MultiValueMap<String, String> result = cache.get(classLoader); 4 if (result != null) { 5 return result; 6 } 7 8 try { 9 // 獲取所有spring.factories的URL 10 Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); 11 result = new LinkedMultiValueMap<>(); 12 // 遍歷URL 13 while (urls.hasMoreElements()) { 14 URL url = urls.nextElement(); 15 UrlResource resource = new UrlResource(url); 16 // 加載每個URL中的properties配置 17 Properties properties = PropertiesLoaderUtils.loadProperties(resource); 18 // 遍歷每個配置 19 for (Map.Entry<?, ?> entry : properties.entrySet()) { 20 String factoryClassName = ((String) entry.getKey()).trim(); 21 // 將實現類的配置按照","符號分割開 22 for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { 23 // 逐個添加到接口對應的集合當中 24 result.add(factoryClassName, factoryName.trim()); 25 } 26 } 27 } 28 // 加入緩存 29 cache.put(classLoader, result); 30 return result; 31 } catch (IOException ex) { 32 // ... 33 } 34 }
有了以上這個Map結構,就可以輕松拿到對應接口的實現類集合了,如:
1 public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { 2 String factoryClassName = factoryClass.getName(); 3 return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); 4 }
這里的factoryClass是接口,通過getName()方法獲取全限定名,然后根據該全限定名從Map結構中get出對應的實現類全限定名的集合。
到這里我們得到了一個實現類的集合,要獲取實現類具體的實例對象只需要通過反射得到實例對象即可,如:
1 private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, 2 ClassLoader classLoader, Object[] args, Set<String> names) { 3 List<T> instances = new ArrayList<>(names.size()); 4 // 遍歷實例對象的全限定名 5 for (String name : names) { 6 try { 7 // 加載該類 8 Class<?> instanceClass = ClassUtils.forName(name, classLoader); 9 // 斷言是否為該接口的實現類 10 Assert.isAssignable(type, instanceClass); 11 // 獲取構造方法 12 Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes); 13 // 實例化該類 14 T instance = (T) BeanUtils.instantiateClass(constructor, args); 15 // 添加到結果集當中 16 instances.add(instance); 17 } 18 catch (Throwable ex) { 19 throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex); 20 } 21 } 22 return instances; 23 }
總結
spring.factories就像是工廠一樣配置了大量的接口對應的實現類,我們通過這些配置 + 反射處理就可以拿到相應的實現類。這種類似於插件式的設計方式,只要引入對應的jar包,那么對應的spring.factories就會被掃描到,對應的實現類也就會被實例化,如果不需要的時候,直接把jar包移除即可。