在分析 Spring Boot
自动配置的工作原理时,我们并没有深入“如何获得配置在 spring.factories
中的自动配置类”。本文将从图解和源码两个角度分析 Spring Boot SPI
机制,了解 spring.factories
中的配置是如何被加载和解析成为缓存中的“接口-实现”键值对。
介绍
Spring Boot SPI
是一个框架内部使用的工厂加载机制。
SpringFactoriesLoader
从 “META-INF/spring.factories
” 文件加载并实例化给定类型的工厂,这些文件可能存在于类路径下的多个 JAR
q 文件中。
spring.factories
文件必须采用 Properties
格式,其中键是接口或抽象类的全限定名称,值是逗号分隔的实现类的全限定名称列表。例如:
1
| example.MyService=example.MyServiceImpl1,example.MyServiceImpl2
|
图解
loadSpringFactories
方法加载以及缓存所有工厂是以 ClassLoader
为单位的,过程如下:
资源从一个位置名称字符串转换为缓存中的键值对,经历了以下过程。
缓存的结构示意图
源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| public abstract class SpringFactoriesLoader {
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<>();
public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) { Assert.notNull(factoryClass, "'factoryClass' must not be null"); ClassLoader classLoaderToUse = classLoader; if (classLoaderToUse == null) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse); if (logger.isTraceEnabled()) { logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames); } List<T> result = new ArrayList<>(factoryNames.size()); for (String factoryName : factoryNames) { result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse)); } AnnotationAwareOrderComparator.sort(result); return result; }
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryClassName, 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.loadPropertie(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { List<String> factoryClassNames = Arrays.asList( StringUtils.commaDelimitedListToStringArray((String) entry.getValue())); result.addAll((String) entry.getKey(), factoryClassNames); } } 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 instanceClassName, Class<T> factoryClass, ClassLoader classLoader) { try { Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader); if (!factoryClass.isAssignableFrom(instanceClass)) { throw new IllegalArgumentException( "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]"); } return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance(); } catch (Throwable ex) { throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex); } } }
|
spring.factories
中的键值对并非严格的接口-实现关系,比如 Spring Boot
自动配置机制中,EnableAutoConfiguration
的值是标准的配置类(被 Configuration 注解标注的类)。因此在实例化方法中,需要检测是否可赋值。
loadFactories
方法默认实例化给定类型的所有工厂,如果需要自定义实例化策略,可以通过 loadFactoryNames 方法获取所有注册的工厂。
SpringFactoriesLoader
的源码既简单又不简单,代码较少,逻辑也清晰,但是查找资源并加载部分用到的 API
,如 UrlResource
、MultiMap
,并不大熟悉,感觉很多框架里加载和解析资源部份的代码都不大好学以致用。