默认组件加载类

引子

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

这段代码没什么问题,如果非要挑问题的话,也可以:

  1. 不管有没有设置新的TokenService,只要TokenHolder初始化了,就会在内存中一直保存DefaultTokenService类的实例,而实际上,如果我们没有使用默认实现,是不需要保存这个实例的。当然,如果只是一个这样的类实例,也无关大雅,但实例多了,还是会有一些影响
  2. 类似的代码散落在各个角度,也不便于默认实现的统一管理
  3. 对于框架型的代码,设置默认值是必要的,这里的问题是,默认值被写死了,然而有时候默认值也需要是一个可配置的(我把这种配置称为元配置,而注入的实现类称之为应用配置)

下面是使用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的使用案例,颇有点测试驱动的意味

简单实现

针对上面的问题,我们先来实现一个简单版本:

  1. 设计 ** 一类 ** 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
  1. Defaults初始化时,初始化加载这些配置到Properties中
  2. 在调用方法返回实例时,根据Properties中的配置初始化实例(并缓存实例至componentMap中),并移除Properties中的缓存
public class Defaults {

    private static final Properties properties = new Properties();

    private static final Map componentMap = new ConcurrentHashMap();

    static {
        loadDefaults();
    }

    /**
     * 获取默认组件,将组件类型的类名称作为key值从属性文件中获取相应配置的实现类,然后实例化并返回
     * 
     * @param cls
     *            组件类型,一般为接口
     * @return 配置的组件实现类
     */
    public static  T getDefaultComponent(Class cls) {
        return getDefaultInner(componentMap, cls.getName(), cls);
    }

    /**
     * 如果传入组件为null,将组件类型作为key值查找对应的默认组件
     * 
     * @param component
     *            用户配置组件
     * @param cls
     *            组件类型
     * @return 配置组件
     */
    public static  E getDefaultComponentIfNull(E component, Class cls) {
        if (null == component) {
            return getDefaultComponent(cls);
        }
        return component;
    }


    @SuppressWarnings({ "unchecked" })
    private static  T getDefaultInner(Map map, String name, Class 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 list = new ArrayList();
                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  List getDefaultComponents(Class cls) {
        return getDefaultInners(componentMap, cls.getName(), cls);
    }

    /**
     * 如果传入集合为空,将组件类型作为key值查找对应的默认组件组
     * 
     * @param components 用户配置组件组
     * @param cls 组件类型
     * @return 配置组件组
     */
    public static  List getDefaultComponentsIfEmpty(List components, Class cls) {
        if (null == components || components.isEmpty()) {
            return getDefaultComponents(cls);
        }
        return components;
    }

   @SuppressWarnings("unchecked")
    private static  List getDefaultInners(Map map, String name, Class cls) {
        if (!map.containsKey(name)) {
            synchronized (map) {
                if (!map.containsKey(name)) {
                    List 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();
                    } else {
                        String[] names = vp.split("\\s*,\\s*");
                        rs = new ArrayList(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) map.get(name);
    }
        

多个类路径下的默认组件加载

上文中的加载默认组件配置有一段代码:

                   ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resolver.getResources("classpath*:META-INF/defaults.properties");
            if (null != resources) {
                List list = new ArrayList();
                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> combines = new HashMap>();
            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 l = combines.get(key);
                                        if (null == l) {
                                            l = new LinkedHashSet();
                                            combines.put(key, l);
                                        }
                                        l.add(v);
                                    }
                                }
                            }
                        }
                    } catch (Exception e) {
                        // ignore
                    } finally {
                        Utils.closeQuietly(input);
                    }
                }
                for (String key : combines.keySet()) {
                    Set 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() {
                    @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  void registerDefaultComponent(Class 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 lookups = applicationContext.getBeansWithAnnotation(Lookup.class);
        if (null != lookups) {
            for (Object bean : lookups.values()) {
                Set> 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  void registerDefaultComponent(Class cls, Object bean) {
        Defaults.registerDefaultComponent(cls, (E) bean);
    }

你可能感兴趣的:(默认组件加载类)