AJ 组件库之 支持热加载的配置器 EasyConfig

Posted sp42a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AJ 组件库之 支持热加载的配置器 EasyConfig相关的知识,希望对你有一定的参考价值。

简介

无论 xml 还是 yml 配置都归属 Spring 管理的,每次修改配置要重启服务器才能生效,于是思考怎么做一个不重启服务器的实现。实际上我之前都有探索过,参见我之前的博文:

平心而论,我之前的实现写得并不好,做的方法有些奇怪。好了,现在累积之前的坑和经验,重新再写,——这次写就是顺畅多了,一气呵成,代码也简单明了。

目前的实现就一个类,我们管他叫 EasyConfig

用法

Spring 注入这个组件,支持带构造器参数和不带的,参数就是配置 JSON 文件的磁盘路径。

<bean class="com.ajaxjs.util.config.EasyConfig"  />

或:

<bean class="com.ajaxjs.util.config.EasyConfig" >
	<constructor-arg index="0" value="config file path" /> 
</bean>

不带的构造器参数,默认是 classpath 下的 config.json 文件。

用法如下:

@Autowired
private EasyConfig config;
	
config.load(); // 加载配置

//@formatter:off  
config.save("\\r\\n" +  // 保存配置
		"	\\"clientShortName\\": \\"TEST\\",\\r\\n" + 
		"	\\"FOO\\": \\r\\n" + 
		"		\\"NUMBER\\": 1221,\\r\\n" + 
		"		\\"STR\\": \\"BAR22\\",\\r\\n" + 
		"		\\"BOOLEAN\\": true,\\r\\n" + 
		"		\\"NULL\\": null,\\r\\n" + 
		"		\\"ARRAY\\": [\\r\\n" + 
		"			1,\\r\\n" + 
		"			\\"STR\\",\\r\\n" + 
		"			null\\r\\n" + 
		"		]\\r\\n" + 
		"	\\r\\n" + 
		"");
//@formatter:on

assertEquals("BAR22", config.getStr("FOO.STR")); // 读取

根据不同配置类型,有下面获取方法。

/**
	* 读取配置并转换其为字符串类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	* 
	* @param key 配置键值
	* @return 配置内容
	*/
public String getStr(String key);

/**
	* 读取配置并转换其为 布尔 类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	* 
	* @param key 配置键值
	* @return 配置内容
	*/
public boolean getBol(String key);

/**
	* 读取配置并转换其为 int 类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	* 
	* @param key 配置键值
	* @return 配置内容
	*/
public int getInt(String key);

/**
	* 读取配置并转换其为 long 类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	* 
	* @param key 配置键值
	* @return 配置内容
	*/
public long getLong(String key);

原理

先贴一下这个类的完整代码。

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import com.ajaxjs.util.io.FileHelper;
import com.ajaxjs.util.io.Resources;
import com.ajaxjs.util.logger.LogHelper;
import com.ajaxjs.util.map.JsonHelper;
import com.ajaxjs.util.map.ListMap;

/**
 * 不需要重启服务器的配置
 * 
 * @author sp42 frank@ajaxjs.com
 *
 */
public class EasyConfig extends HashMap<String, Object> 
	private static final LogHelper LOGGER = LogHelper.getLog(EasyConfig.class);

	private static final long serialVersionUID = 9099886055914666662L;

	/**
	 * 配置文件路径
	 */
	private String filePath;

	/**
	 * 创建一个配置器
	 * 
	 * @param filePath 配置文件路径
	 */
	public EasyConfig(String filePath) 
		super();
		this.filePath = filePath;
	

	/**
	 * 创建一个配置器
	 */
	public EasyConfig() 
		super();
		filePath = Resources.getResourcesFromClasspath("config.json");
	

	/**
	 * 加载 JSON 配置
	 */
	public void load() 
		if (!new File(filePath).exists()) 
			LOGGER.info("没有[0]项目配置文件", filePath);
			return;
		

		loaded = false;
		String jsonStr = FileHelper.openAsText(filePath);
		Map<String, Object> map = JsonHelper.parseMap(jsonStr);

		clear();
		putAll(ListMap.flatMap(map));
		loaded = true;

		LOGGER.infoGreen("加载[" + get("clientShortName") + "]项目配置成功!All config loaded.");
	

	/**
	 * 保存配置
	 * 
	 * @param jsonStr
	 */
	public void save(String jsonStr) 
		FileHelper.saveText(filePath, jsonStr);
		load();
	

	/**
	 * 是否加载成功
	 */
	private boolean loaded;

	/**
	 * 内部的获取方法
	 * 
	 * @param <T>         配置类型
	 * @param key         配置键值
	 * @param isNullValue 当配置为 null 时返回的值,相当于“默认值”
	 * @param vType       配置类型的引用
	 * @return 配置内容
	 */
	@SuppressWarnings("unchecked")
	private <T> T getAny(String key, T isNullValue, Class<T> vType) 
		if (!loaded) 
			LOGGER.warning("配置系统未准备好");
			return isNullValue;
		

		Object v = get(key);

		if (v == null) 
			LOGGER.warning("没发现[0]配置", key);
			return isNullValue;
		

		return (T) v;
	

	/**
	 * 读取配置并转换其为字符串类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	 * 
	 * @param key 配置键值
	 * @return 配置内容
	 */
	public String getStr(String key) 
		return getAny(key, null, String.class);
	

	/**
	 * 读取配置并转换其为 布尔 类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	 * 
	 * @param key 配置键值
	 * @return 配置内容
	 */
	public boolean getBol(String key) 
		return getAny(key, false, boolean.class);
	

	/**
	 * 读取配置并转换其为 int 类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	 * 
	 * @param key 配置键值
	 * @return 配置内容
	 */
	public int getInt(String key) 
		return getAny(key, 0, int.class);
	

	/**
	 * 读取配置并转换其为 long 类型。仅对扁平化后的配置有效,所以参数必须是扁平化的 aaa.bbb.ccc 格式。
	 * 
	 * @param key 配置键值
	 * @return 配置内容
	 */
	public long getLong(String key) 
		return getAny(key, 0L, long.class);
	

	public String getFilePath() 
		return filePath;
	

	public void setFilePath(String filePath) 
		this.filePath = filePath;
	

所为读取配置就是不断读取一个 Map 的 Key/Value。Key 为参数,Value 为返回的结果。这个类本身继承 HashMap,序列化为 JSON 保存,读取这个 JSON,是一个带结构的 Map(一棵“树”)。

在这个 Map 上,get("xx1").get("xx2").get("xx3")?——那样太傻。或者 get("xx1.xx2.xx3")?——那样不错,不过持久化也是那样吗?如:


	“xx1.xx2.xx1”: 1,
	“xx1.xx2.xx2”: 2,
	“xx1.xx2.xx3”: 3,

Key 重复得厉害,一改就得多处修改,——还是原来带层次的 JSON 结构好(一棵“树”),所以得把这个树“扁平化”处理。具体就是这个函数,原理都是比较基础的数据结构知识。

/**
 * 扁平化多层 map 为单层
 * 
 * @param map 多层 Map
 * @return 单层 Map
 */
public static Map<String, Object> flatMap(Map<String, Object> map) 
	final Stack<String> stack = new Stack<>();
	final Map<String, Object> _map = new HashMap<>();

	ListMapConfig config = new ListMapConfig();
	config.newKey = key -> stack.add(key); // 进栈
	config.exitKey = key -> stack.pop(); // 退栈
	config.mapEntryHandler = (key, obj, currentMap, superMap, i) -> 
		if (obj == null || obj instanceof String || obj instanceof Number || obj instanceof Boolean) 
			StringBuilder sb = new StringBuilder();

			for (String s : stack)
				sb.append(s + ".");

			_map.put(sb + key, obj);
		

		return true;
	;

	traveler(map, config);

	return _map;

这个类还调用了其他工具方法,很简单无非读取文件之类的,如果需要可以问我索取。

小结

这个配置器并不是要代替 Spring 的 properties/xml/yml 文件,其最大特点就是支持热加载,修改后程序马上生效。至于前端方面应该有个比较好的、通用的 JSON 修改器,下一期我们再讲。

以上是关于AJ 组件库之 支持热加载的配置器 EasyConfig的主要内容,如果未能解决你的问题,请参考以下文章

反应热加载器 3

Webpack + postcss + 主题 + 热加载

Go 每日一库之 viper

springboot热部署(spring-boot-devtools)配置及原理验证

sprinbboot 热部署 造成类加载器 不一致问题

类加载器ClassLoader的理解