配置中心是我们平常使用微服务架构时重要的一个模块,常用的配置中心组件也比较多,从早期的Spring Cloud Config,到Disconf、Apollo、Nacos等,它们支持的功能、产品的性能以及给用户的体验也各有不同。
虽然说功能上有不少差异,但是它们解决的最核心问题,无疑是配置文件修改后的实时生效,有时候在搬砖之余Hydra就在好奇实时生效是如何实现的、如果让我来设计又会怎么去实现,于是这几天抽出了点空闲时间,摸鱼摸出了个简易版的单机配置中心,先来看看效果:
之所以说是简易版本,首先是因为实现的核心功能就只有配置修改后实时生效,并且代码的实现也非常简单,一共只用了8个类就实现了这个核心功能,看一下代码的结构,核心类就是core
包中的这8个类:
看到这是不是有点好奇,虽说是低配版,就凭这么几个类也能实现一个配置中心?那么先看一下总体的设计流程,下面我们再细说代码。
下面对8个核心类进行一下简要说明并贴出核心代码,有的类中代码比较长,可能对手机浏览的小伙伴不是非常友好,建议收藏后以后电脑浏览器打开(骗波收藏,计划通!)。另外Hydra已经把项目的全部代码上传到了git
,有需要的小伙伴可以移步文末获取地址。
ScanRunner
实现了CommandLineRunner
接口,可以保证它在springboot启动最后执行,这样就能确保其他的Bean已经实例化结束并被放入了容器中。至于为什么起名叫ScanRunner
,是因为这里要实现的主要就是扫描类相关功能。先看一下代码:
@Component
public class ScanRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
doScanComponent();
}
private void doScanComponent(){
String rootPath = this.getClass().getResource("/").getPath();
List<String> fileList = FileScanner.findFileByType(rootPath,null,FileScanner.TYPE_CLASS);
doFilter(rootPath,fileList);
EnvInitializer.init();
}
private void doFilter(String rootPath, List<String> fileList) {
rootPath = FileScanner.getRealRootPath(rootPath);
for (String fullPath : fileList) {
String shortName = fullPath.replace(rootPath, "")
.replace(FileScanner.TYPE_CLASS,"");
String packageFileName=shortName.replaceAll(Matcher.quoteReplacement(File.separator),"\\.");
try {
Class clazz = Class.forName(packageFileName);
if (clazz.isAnnotationPresent(Component.class)
|| clazz.isAnnotationPresent(Controller.class)
||clazz.isAnnotationPresent(Service.class)){
VariablePool.add(clazz);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
真正实现文件扫描功能是调用的FileScanner
,它的实现我们后面具体再说,在功能上它能够根据文件后缀名扫描某一目录下的全部文件,这里首先扫描出了target
目录下全部以.class
结尾的文件:
扫描到全部class
文件后,就可以利用类的全限定名获取到类的Class
对象,下一步是调用doFilter
方法对类进行过滤。这里我们暂时仅考虑通过@Value
注解的方式注入配置文件中属性值的方式,那么下一个问题来了,什么类中的@Value
注解会生效呢?答案是通过@Component
、@Controller
、@Service
这些注解交给spring容器管理的类。
综上,我们通过这些注解再次进行过滤出符合条件的类,找到后交给VariablePool
对变量进行处理。
FileScanner
是扫描文件的工具类,它可以根据文件后缀名筛选出需要的某个类型的文件,除了在ScanRunner
中用它扫描了class文件外,在后面的逻辑中还会用它扫描yml文件。下面,看一下FileScanner
中实现的文件扫描的具体代码:
public class FileScanner {
public static final String TYPE_CLASS=".class";
public static final String TYPE_YML=".yml";
public static List<String> findFileByType(String rootPath, List<String> fileList,String fileType){
if (fileList==null){
fileList=new ArrayList<>();
}
File rootFile=new File(rootPath);
if (!rootFile.isDirectory()){
addFile(rootFile.getPath(),fileList,fileType);
}else{
String[] subFileList = rootFile.list();
for (String file : subFileList) {
String subFilePath=rootPath + "\\" + file;
File subFile = new File(subFilePath);
if (!subFile.isDirectory()){
addFile(subFile.getPath(),fileList,fileType);
}else{
findFileByType(subFilePath,fileList,fileType);
}
}
}
return fileList;
}
private static void addFile(String fileName,List<String> fileList,String fileType){
if (fileName.endsWith(fileType)){
fileList.add(fileName);
}
}
public static String getRealRootPath(String rootPath){
if (System.getProperty("os.name").startsWith("Windows")
&& rootPath.startsWith("/")){
rootPath = rootPath.substring(1);
rootPath = rootPath.replaceAll("/", Matcher.quoteReplacement(File.separator));
}
return rootPath;
}
}
查找文件的逻辑很简单,就是在给定的根目录rootPath
下,循环遍历每一个目录,对找到的文件再进行后缀名的比对,如果符合条件就加到返回的文件名列表中。
至于下面的这个getRealRootPath
方法,是因为在windows环境下,获取到项目的运行目录是这样的:
/F:/Workspace/hermit-purple-config/target/classes/
而class文件名是这样的:
F:\Workspace\hermit-purple-config\target\classes\com\cn\hermimt\purple\test\service\UserService.class
如果想要获取一个类的全限定名,那么首先要去掉运行目录,再把文件名中的反斜杠\
替换成点.
,这里就是为了删掉文件名中的运行路径提前做好准备。
回到上面的主流程中,每个在ScanRunner
中扫描出的带有@Component
、@Controller
、@Service
注解的Class
,都会交给VariablePool
进行处理。顾名思义,VariablePool
就是变量池的意思,下面会用这个容器封装所有带@Value
注解的属性。
public class VariablePool {
public static Map<String, Map<Class,String>> pool=new HashMap<>();
private static final String regex="^(\\$\\{)(.)+(\\})$";
private static Pattern pattern;
static{
pattern=Pattern.compile(regex);
}
public static void add(Class clazz){
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Value.class)){
Value annotation = field.getAnnotation(Value.class);
String annoValue = annotation.value();
if (!pattern.matcher(annoValue).matches())
continue;
annoValue=annoValue.replace("${","");
annoValue=annoValue.substring(0,annoValue.length()-1);
Map<Class,String> clazzMap = Optional.ofNullable(pool.get(annoValue))
.orElse(new HashMap<>());
clazzMap.put(clazz,field.getName());
pool.put(annoValue,clazzMap);
}
}
}
public static Map<String, Map<Class,String>> getPool() {
return pool;
}
}
简单说一下这块代码的设计思路:
Class
对象中所有的属性,并判断属性是否加了@Value
注解@Value
如果要注入配置文件中的值,一定要符合${xxx}
的格式(这里先暂时不考虑${xxx:defaultValue}
这种设置了默认值的格式),所以需要使用正则表达式验证是否符合,并校验通过后去掉开头的${
和结尾的}
,获取真正对应的配置文件中的字段VariablePool
中声明了一个静态HashMap,用于存放所有配置文件中属性-类-类中属性的映射关系,接下来就要把这个关系存放到这个pool
中简单来说,变量池就是下面这样的结构:
这里如果不好理解的话可以看看例子,我们引入两个测试Service
:
@Service
public class UserService {
@Value("${person.name}")
String name;
@Value("${person.age}")
Integer age;
}
@Service
public class UserDeptService {
@Value("${person.name}")
String pname;
}
在所有Class
执行完add
方法后,变量池pool
中的数据是这样的:
可以看到在pool
中,person.name
对应的内层Map中包含了两条数据,分别是UserService
中的name
字段,以及UserDeptService
中的pname
字段。
在VariablePool
封装完所有变量数据后,ScanRunner
会调用EnvInitializer
的init
方法,开始对yml文件进行解析,完成配置中心环境的初始化。其实说白了,这个环境就是一个静态的HashMap,key
是属性名,value
就是属性的值。
public class EnvInitializer {
private static Map<String,Object> envMap=new HashMap<>();
public static void init(){
String rootPath = EnvInitializer.class.getResource("/").getPath();
List<String> fileList = FileScanner.findFileByType(rootPath,null,FileScanner.TYPE_YML);
for (String ymlFilePath : fileList) {
rootPath = FileScanner.getRealRootPath(rootPath);
ymlFilePath = ymlFilePath.replace(rootPath, "");
YamlMapFactoryBean yamlMapFb = new YamlMapFactoryBean();
yamlMapFb.setResources(new ClassPathResource(ymlFilePath));
Map<String, Object> map = yamlMapFb.getObject();
YamlConverter.doConvert(map,null,envMap);
}
}
public static void setEnvMap(Map<String, Object> envMap) {
EnvInitializer.envMap = envMap;
}
public static Map<String, Object> getEnvMap() {
return envMap;
}
}
首先还是使用FileScanner
扫描根目录下所有的.yml
结尾的文件,并使用spring自带的YamlMapFactoryBean
进行yml文件的解析。但是这里有一个问题,所有yml文件解析后都会生成一个独立的Map,需要进行Map的合并,生成一份配置信息表。至于这一块具体的操作,都交给了下面的YamlConverter
进行处理。
我们先进行一下演示,准备两个yml文件,配置文件一:application.yml
spring:
application:
name: hermit-purple
server:
port: 6879
person:
name: Hydra
age: 18
配置文件二:config/test.yml
my:
name: John
friend:
name: Jay
sex: male
run: yeah
先来看一看环境完成初始化后,生成的数据格式是这样的:
YamlConverter
主要实现的方法有三个:
doConvert()
:将EnvInitializer
中提供的多个Map合并成一个单层MapmonoToMultiLayer()
:将单层Map转换为多层Map(为了生成yml格式字符串)convert()
:yml格式的字符串解析为Map(为了判断属性是否发生变化)由于后面两个功能暂时还没有涉及,我们先看第一段代码:
public class YamlConverter {
public static void doConvert(Map<String,Object> map,String parentKey,Map<String,Object> propertiesMap){
String prefix=(Objects.isNull(parentKey))?"":parentKey+".";
map.forEach((key,value)->{
if (value instanceof Map){
doConvert((Map)value,prefix+key,propertiesMap);
}else{
propertiesMap.put(prefix+key,value);
}
});
}
//...
}
逻辑也很简单,通过循环遍历的方式,将多个Map最终都合并到了目的envMap
中,并且如果遇到多层Map嵌套的情况,那么将多层Map的key通过点.
进行了连接,最终得到了上面那张图中样式的单层Map。
其余两个方法,我们在下面使用到的场景再说。
ConfigController
作为控制器,用于和前端进行交互,只有两个接口save
和get
,下面分别介绍。
前端页面在开启时会调用ConfigController
中的get
接口,填充到textArea
中。先看一下get
方法的实现:
@GetMapping("get")
public String get(){
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
String yamlContent = null;
try {
Map<String, Object> envMap = EnvInitializer.getEnvMap();
Map<String, Object> map = YamlConverter.monoToMultiLayer(envMap, null);
yamlContent = objectMapper.writeValueAsString(map);
} catch (Exception e) {
e.printStackTrace();
}
return yamlContent;
}
之前在项目启动时,就已经把配置文件属性封装到了EnvInitializer
的envMap
中,并且这个envMap
是一个单层的Map,不存在嵌套关系。但是我们这里要使用jackson
生成标准格式的yml文档,这种格式不符合要求,需要将它还原成一个具有层级关系的多层Map,就需要调用YamlConverter
的monoToMultiLayer()
方法。
monoToMultiLayer()
方法的代码有点长,就不贴在这里了,主要是根据key中的.
进行拆分并不断创建子级的Map,转换完成后得到的多层Map数据如下:
在获得这种格式后的Map后,就可以调用jackson
中的方法将Map转换为yml格式的字符串传递给前端了,看一下处理完成后返回给前端的字符串:
在前端页面修改了yml内容后点击保存时,会调用save
方法保存并更新配置,方法的实现如下:
@PostMapping("save")
public String save(@RequestBody Map<String,Object> newValue) {
String ymlContent =(String) newValue.get("yml");
PropertyTrigger.change(ymlContent);
return "success";
}
在拿到前端传过来的yml字符串后,调用PropertyTrigger
的change
方法,实现后续的更改逻辑。
在调用change
方法后,主要做的事情有两件:
EnvInitializer
中的环境envMap
,用于前端页面刷新时返回新的数据,以及下一次属性改变时进行对比使用先看一下代码:
public class PropertyTrigger {
public static void change(String ymlContent) {
Map<String, Object> newMap = YamlConverter.convert(ymlContent);
Map<String, Object> oldMap = EnvInitializer.getEnvMap();
oldMap.keySet().stream()
.filter(key->newMap.containsKey(key))
.filter(key->!newMap.get(key).equals(oldMap.get(key)))
.forEach(key->{
System.out.println(key);
Object newVal = newMap.get(key);
oldMap.put(key, newVal);
doChange(key,newVal);
});
EnvInitializer.setEnvMap(oldMap);
}
private static void doChange(String propertyName, Object newValue) {
System.out.println("newValue:"+newValue);
Map<String, Map<Class, String>> pool = VariablePool.getPool();
Map<Class, String> classProMap = pool.get(propertyName);
classProMap.forEach((clazzName,realPropertyName)->{
try {
Object bean = SpringContextUtil.getBean(clazzName);
Field field = clazzName.getDeclaredField(realPropertyName);
field.setAccessible(true);
field.set(bean, newValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
});
}
}
前面铺垫了那么多,其实就是为了实现这段代码中的功能,具体逻辑如下:
YamlConverter
的convert
方法,将前端传来的yml格式字符串解析封装成单层Map,数据格式和EnvInitializer
中的envMap
相同envMap
,查看其中的key在新的Map中对应的属性值是否发生了改变,如果没有改变则不做之后的任何操作envMap
中的旧值VariablePool
中拿到涉及改变的Class
,以及类中的字段Field
。并通过后面的SpringContextUtil
中的方法获取到这个bean的实例对象,再通过反射改变字段的值EnvInitializer
中的envMap
到这里,就实现了全部的功能。
SpringContextUtil
通过实现ApplicationContextAware
接口获得了spring容器,而通过容器的getBean()
方法就可以容易的拿到spring中的bean,方便进行后续的更改操作。
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> t) {
return applicationContext.getBean(t);
}
}
至于前端代码,就是一个非常简单的表单,代码的话可以移步git
查看。
到这里全部的代码介绍完了,最后做一个简要的总结吧,虽然通过这几个类能够实现一个简易版的配置中心功能,但是还有不少的缺陷,例如:
@ConfigurationProperties
注解singleton
模式,如果作用域为prototype
,也会存在问题总的来说,后续需要完善的点还有不少,真是感觉任重道远。
最后再聊聊项目的名称,为什么取名叫hermit-purple
呢,来源是jojo中二乔的替身隐者之紫,感觉这个替身的能力和配置中心的感知功能还是蛮搭配的,所以就用了这个哈哈。
那么这次的分享就到这里,我是Hydra,预祝大家虎年春节快乐,我们下篇再见。
项目git地址:
https://github.com/trunks2008/hermit-purple-config
大家如果对代码有建议或者好的idea,欢迎在后台留言或加我微信好友一起讨论。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/2qtl7_tr0joxKWwSd5HXvg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。