引子
SpringBoot的根基在于自动配置和条件配置,而在实现自动配置的时候,使用了一个SpringFactoriesLoader
的工具类,用于加载类路径下"META-INF/spring.factories"
文件中的配置,该配置是一个properties
文件,键为接口名/类名/注解类名等(下文统称为接口),值为一个或多个实现类。这个工具类实际上并不是Spring Boot的,而是spring-core包中,只是由于spring boot才为大家所关注。开始看到这个类时,大吃一惊,因为之前我们在开发Java EE平台时,也曾经实现过类似的一个工具类Defaults
,这篇博客就来分享一下。
问题
先看如下的一段典型代码:
public class TokenHolder {
private static final TokenService DEFAULT_TOKEN_SERVICE = new DefaultTokenService();
private TokenService tokenService = DEFAULT_TOKEN_SERVICE;
public TokenService getTokenService() {
return tokenService;
}
public void setTokenService(TokenService tokenService) {
this.tokenService = tokenService;
}
}
这段代码没什么问题,如果非要挑问题的话,也可以:
- 不管有没有设置新的
TokenService
,只要TokenHolder
初始化了,就会在内存中一直保存DefaultTokenService
类的实例,而实际上,如果我们没有使用默认实现,是不需要保存这个实例的。当然,如果只是一个这样的类实例,也无关大雅,但实例多了,还是会有一些影响 - 类似的代码散落在各个角度,也不便于默认实现的统一管理
- 对于框架型的代码,设置默认值是必要的,这里的问题是,默认值被写死了,然而有时候默认值也需要是一个可配置的(我把这种配置称为元配置,而注入的实现类称之为应用配置)
下面是使用Defaults
工具类后的一个版本:
public class TokenHolder {
private TokenService tokenService;
public TokenService getTokenService() {
return Defaults.getDefaultComponentIfNull(tokenService, TokenService.class);
}
public void setTokenService(TokenService tokenService) {
this.tokenService = tokenService;
}
}
其中获取服务的方法调用了Defaults,如果传入的第一个参数为null,就根据第二个参数查找默认值。
这里先给出Defaults
的使用案例,颇有点测试驱动的意味
简单实现
针对上面的问题,我们先来实现一个简单版本:
- 设计 ** 一类 **
properties
文件classpath*:META-INF/defaults.properties
,类似于"META-INF/spring.factories"
,配置接口和实现名,如:
org.springframework.core.env.Environment=org.springframework.core.env.StandardEnvironment
org.springframework.context.MessageSource=org.springframework.context.support.ReloadableResourceBundleMessageSource
org.springframework.core.io.support.ResourcePatternResolver=org.springframework.core.io.support.PathMatchingResourcePatternResolver
org.springframework.cache.CacheManager=org.springframework.cache.concurrent.ConcurrentMapCacheManager
org.springframework.core.convert.ConversionService=org.springframework.format.support.DefaultFormattingConversionService
- 在
Defaults
初始化时,初始化加载这些配置到Properties中 - 在调用方法返回实例时,根据Properties中的配置初始化实例(并缓存实例至componentMap中),并移除Properties中的缓存
public class Defaults {
private static final Properties properties = new Properties();
private static final Map<String, Object> componentMap = new ConcurrentHashMap<String, Object>();
static {
loadDefaults();
}
/**
* 获取默认组件,将组件类型的类名称作为key值从属性文件中获取相应配置的实现类,然后实例化并返回
*
* @param cls
* 组件类型,一般为接口
* @return 配置的组件实现类
*/
public static <T> T getDefaultComponent(Class<T> cls) {
return getDefaultInner(componentMap, cls.getName(), cls);
}
/**
* 如果传入组件为null,将组件类型作为key值查找对应的默认组件
*
* @param component
* 用户配置组件
* @param cls
* 组件类型
* @return 配置组件
*/
public static <E> E getDefaultComponentIfNull(E component, Class<E> cls) {
if (null == component) {
return getDefaultComponent(cls);
}
return component;
}
@SuppressWarnings({ "unchecked" })
private static <T> T getDefaultInner(Map<String, Object> map, String name, Class<T> cls) {
if (!map.containsKey(name)) {
synchronized (map) {
if (!map.containsKey(name)) {
String vp = properties.getProperty(name);
T rs = convertValue(cls, vp);
properties.remove(name);
if (null != rs) {
map.put(name, rs);
}
}
}
}
return (T) map.get(name);
}
/**
* 加载默认配置
*/
private synchronized static void loadDefaults() {
try {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:META-INF/defaults.properties");
if (null != resources) {
List<Properties> list = new ArrayList<Properties>();
for (Resource resource : resources) {
InputStream input = null;
try {
Properties properties = new Properties();
input = resource.getInputStream();
properties.load(input);
list.add(properties);
} catch (Exception e) {
// ignore
} finally {
Utils.closeQuietly(input);
}
}
}
} catch (IOException ignore) {
}
}
}
一个接口多个默认实例的情形
上面的实现只能处理一个接口一个默认实例,但很多时候一个接口会有多个默认实例(比如SpringBoot的org.springframework.boot.autoconfigure.EnableAutoConfiguration
),为了应对这种情况,可以将属性文件中配置的value部分使用逗号分隔,每个部分都创建一个实例,添加如下的方法即可:
/**
* 获取默认组件集合,将组件类型的类名称作为key值从属性文件中获取相应配置的实现类,然后实例化并返回
*
* @param cls 组件类型,一般为接口
* @return 配置的组件实现类组,使用逗号分隔配置多个值
*/
public static <T> List<T> getDefaultComponents(Class<T> cls) {
return getDefaultInners(componentMap, cls.getName(), cls);
}
/**
* 如果传入集合为空,将组件类型作为key值查找对应的默认组件组
*
* @param components 用户配置组件组
* @param cls 组件类型
* @return 配置组件组
*/
public static <E> List<E> getDefaultComponentsIfEmpty(List<E> components, Class<E> cls) {
if (null == components || components.isEmpty()) {
return getDefaultComponents(cls);
}
return components;
}
@SuppressWarnings("unchecked")
private static <T> List<T> getDefaultInners(Map<String, Object> map, String name, Class<T> cls) {
if (!map.containsKey(name)) {
synchronized (map) {
if (!map.containsKey(name)) {
List<T> rs = null;
String vp = properties.getProperty(name);
if (Utils.isBlank(vp)) {
rs = null;
Logs.debug("the default value of [" + name + "] is null");
} else if ("[]".equals(vp)) {
rs = new ArrayList<T>();
} else {
String[] names = vp.split("\s*,\s*");
rs = new ArrayList<T>(names.length);
for (int i = 0, l = names.length; i < l; i++) {
rs.add(convertValue(cls, names[i]));
}
}
properties.remove(name);
if (null != rs) {
map.put(name, rs);
}
}
}
}
return (List<T>) map.get(name);
}
多个类路径下的默认组件加载
上文中的加载默认组件配置有一段代码:
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:META-INF/defaults.properties");
if (null != resources) {
List<Properties> list = new ArrayList<Properties>();
for (Resource resource : resources) {
InputStream input = null;
try {
Properties properties = new Properties();
input = resource.getInputStream();
properties.load(input);
list.add(properties);
} catch (Exception e) {
// ignore
} finally {
Utils.closeQuietly(input);
}
}
}
每个properties
文件都是一个Properties
对象,然后将这些对象合并到一起,但是如果多个对象中有key相同的情形,就会取最后一个配置。
但是很多时候,要求不能覆盖配置,而是merge
多个文件的配置,为此,我再设计了另外一个配置文件模式:"classpath*:META-INF/mergeDefaults.properties"
,通过是否需要合并value来区分,分别配置在default.properties和mergeDefaults.properties中,只是加载的时候稍微注意一下:
private synchronized static void loadMergeDefaults() {
try {
/**
* 加载平台包下面的mergeDefaults.properties文件
*/
Map<String, Set<String>> combines = new HashMap<String, Set<String>>();
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:META-INF/mergeDefaults.properties");
if (null != resources) {
for (Resource resource : resources) {
InputStream input = null;
try {
Properties properties = new Properties();
input = resource.getInputStream();
properties.load(input);
for (String key : properties.stringPropertyNames()) {
String value = properties.getProperty(key);
if (!Utils.isBlank(value)) {
String[] values = value.split("\s*,\s*");
for (String v : values) {
if (!Utils.isBlank(v)) {
Set<String> l = combines.get(key);
if (null == l) {
l = new LinkedHashSet<String>();
combines.put(key, l);
}
l.add(v);
}
}
}
}
} catch (Exception e) {
// ignore
} finally {
Utils.closeQuietly(input);
}
}
for (String key : combines.keySet()) {
Set<String> l = combines.get(key);
if (null != l && !l.isEmpty()) {
StringBuffer sb = new StringBuffer();
for (String s : l) {
sb.append(",").append(s);
}
properties.put(key, sb.substring(1));
}
}
}
} catch (IOException ignore) {
}
}
到此,我们已经完成了SpringFactoriesLoader
的功能了。
通过上面的描述,还存在一个潜在的问题:如果多个properties
中含有相同key的配置,但是只能取其中一个,那应该取哪一个呢?为了处理这个问题,可以在配置文件中添加一项配置
#配置文件优先级
order=1
然后在加载的时候,比较一下优先级,优先级数值越小,级别越高,可以通过如下代码片段来实现排序:
/**
* 根据配置文件中的order排序
*/
Collections.sort(list, new Comparator<Properties>() {
@Override
public int compare(Properties o1, Properties o2) {
return Integer.parseInt(o2.getProperty("order", "0")) - Integer.parseInt(o1.getProperty("order", "0"));
}
});
for (Properties p : list) {
p.remove("order");
properties.putAll(p);
}
简单类型默认值加载
前面说的配置,都是以接口名为key,实现类名为value的默认组件加载。实际上,对于简单类型的配置,也可以通过Defaults
加载,只是此时key将不再是类型名,而是配置项名称。
再进一步
在Spring应用中,很多组件都是由Spring初始化和管理的,那么Defaults
中的默认组件能否到Spring容器中查找呢?
为了这个功能,首先需要添加默认组件注册方法:
/**
* 注册默认组件
*
* @param cls
* @param component
*/
public static <E> void registerDefaultComponent(Class<E> cls, E component) {
componentMap.put(cls.getName(), component);
}
然后添加一个标志注解,并将该注解添加到服务类接口声明中:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Lookup {
}
最后,实现Spring容器的InitializingBean
接口,搜索所有包含@Lookup
的bean,将其注册到Defaults
中来就可以了。
@Override
public void afterPropertiesSet() throws Exception {
registerDefaultComponents();
}
private void registerDefaultComponents() {
Map<String, Object> lookups = applicationContext.getBeansWithAnnotation(Lookup.class);
if (null != lookups) {
for (Object bean : lookups.values()) {
Set<Class<?>> interfaces = ClassUtils.getAllInterfacesAsSet(bean);
if (null != interfaces && !interfaces.isEmpty()) {
for (Class<?> cls : interfaces) {
if (cls.isAnnotationPresent(Lookup.class)) {
this.registerDefaultComponent(cls, bean);
}
}
}
}
}
}
@SuppressWarnings("unchecked")
private <E> void registerDefaultComponent(Class<E> cls, Object bean) {
Defaults.registerDefaultComponent(cls, (E) bean);
}