• 默认组件加载类


    引子

    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<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);
        }
    
  • 相关阅读:
    POJ 3189
    POJ_2112 二分图多重匹配
    二分图多重匹配问题
    二分图匹配问题
    一般图最大匹配带花树
    hdu-6699 Block Breaker
    深度学习框架集成平台C++ Guide指南
    机器视觉系统的几个问题解析
    高效Tensor张量生成
    图像复原的神经网络稀疏表示
  • 原文地址:https://www.cnblogs.com/linjisong/p/8763626.html
Copyright © 2020-2023  润新知