新聞中心
雖然說(shuō)功能上有不少差異,但是它們解決的最核心問(wèn)題,無(wú)疑是配置文件修改后的實(shí)時(shí)生效,有時(shí)候在搬磚之余Hydra就在好奇實(shí)時(shí)生效是如何實(shí)現(xiàn)的、如果讓我來(lái)設(shè)計(jì)又會(huì)怎么去實(shí)現(xiàn),于是這幾天抽出了點(diǎn)空閑時(shí)間,摸魚(yú)摸出了個(gè)簡(jiǎn)易版的單機(jī)配置中心,先來(lái)看看效果:

成都創(chuàng)新互聯(lián)主要從事成都做網(wǎng)站、網(wǎng)站建設(shè)、外貿(mào)營(yíng)銷(xiāo)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)呼倫貝爾,10多年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專(zhuān)業(yè),歡迎來(lái)電咨詢(xún)建站服務(wù):18982081108
之所以說(shuō)是簡(jiǎn)易版本,首先是因?yàn)閷?shí)現(xiàn)的核心功能就只有配置修改后實(shí)時(shí)生效,并且代碼的實(shí)現(xiàn)也非常簡(jiǎn)單,一共只用了8個(gè)類(lèi)就實(shí)現(xiàn)了這個(gè)核心功能,看一下代碼的結(jié)構(gòu),核心類(lèi)就是core包中的這8個(gè)類(lèi):
看到這是不是有點(diǎn)好奇,雖說(shuō)是低配版,就憑這么幾個(gè)類(lèi)也能實(shí)現(xiàn)一個(gè)配置中心?那么先看一下總體的設(shè)計(jì)流程,下面我們?cè)偌?xì)說(shuō)代碼。
代碼簡(jiǎn)要說(shuō)明
下面對(duì)8個(gè)核心類(lèi)進(jìn)行一下簡(jiǎn)要說(shuō)明并貼出核心代碼,有的類(lèi)中代碼比較長(zhǎng),可能對(duì)手機(jī)瀏覽的小伙伴不是非常友好,建議收藏后以后電腦瀏覽器打開(kāi)(騙波收藏,計(jì)劃通!)。另外Hydra已經(jīng)把項(xiàng)目的全部代碼上傳到了git,有需要的小伙伴可以移步文末獲取地址。
1、ScanRunner
ScanRunner實(shí)現(xiàn)了CommandLineRunner接口,可以保證它在springboot啟動(dòng)最后執(zhí)行,這樣就能確保其他的Bean已經(jīng)實(shí)例化結(jié)束并被放入了容器中。至于為什么起名叫ScanRunner,是因?yàn)檫@里要實(shí)現(xiàn)的主要就是掃描類(lèi)相關(guān)功能。先看一下代碼:
@Component
public class ScanRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
doScanComponent();
}
private void doScanComponent(){
String rootPath = this.getClass().getResource("/").getPath();
ListfileList = FileScanner.findFileByType(rootPath,null,FileScanner.TYPE_CLASS);
doFilter(rootPath,fileList);
EnvInitializer.init();
}
private void doFilter(String rootPath, ListfileList) {
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();
}
}
}
}
真正實(shí)現(xiàn)文件掃描功能是調(diào)用的FileScanner,它的實(shí)現(xiàn)我們后面具體再說(shuō),在功能上它能夠根據(jù)文件后綴名掃描某一目錄下的全部文件,這里首先掃描出了target目錄下全部以.class結(jié)尾的文件:
掃描到全部class文件后,就可以利用類(lèi)的全限定名獲取到類(lèi)的Class對(duì)象,下一步是調(diào)用doFilter方法對(duì)類(lèi)進(jìn)行過(guò)濾。這里我們暫時(shí)僅考慮通過(guò)@Value注解的方式注入配置文件中屬性值的方式,那么下一個(gè)問(wèn)題來(lái)了,什么類(lèi)中的@Value注解會(huì)生效呢?答案是通過(guò)@Component、@Controller、@Service這些注解交給spring容器管理的類(lèi)。
綜上,我們通過(guò)這些注解再次進(jìn)行過(guò)濾出符合條件的類(lèi),找到后交給VariablePool對(duì)變量進(jìn)行處理。
2、FileScanner
FileScanner是掃描文件的工具類(lèi),它可以根據(jù)文件后綴名篩選出需要的某個(gè)類(lèi)型的文件,除了在ScanRunner中用它掃描了class文件外,在后面的邏輯中還會(huì)用它掃描yml文件。下面,看一下FileScanner中實(shí)現(xiàn)的文件掃描的具體代碼:
public class FileScanner {
public static final String TYPE_CLASS=".class";
public static final String TYPE_YML=".yml";
public static List findFileByType(String rootPath, List 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 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;
}
}
查找文件的邏輯很簡(jiǎn)單,就是在給定的根目錄rootPath下,循環(huán)遍歷每一個(gè)目錄,對(duì)找到的文件再進(jìn)行后綴名的比對(duì),如果符合條件就加到返回的文件名列表中。
至于下面的這個(gè)getRealRootPath方法,是因?yàn)樵趙indows環(huán)境下,獲取到項(xiàng)目的運(yùn)行目錄是這樣的:
/F:/Workspace/hermit-purple-config/target/classes/
而class文件名是這樣的:
F:\Workspace\hermit-purple-config\target\classes\com\cn\hermimt\purple\test\service\UserService.class
如果想要獲取一個(gè)類(lèi)的全限定名,那么首先要去掉運(yùn)行目錄,再把文件名中的反斜杠\替換成點(diǎn).,這里就是為了刪掉文件名中的運(yùn)行路徑提前做好準(zhǔn)備。
3、VariablePool
回到上面的主流程中,每個(gè)在ScanRunner中掃描出的帶有@Component、@Controller、@Service注解的Class,都會(huì)交給VariablePool進(jìn)行處理。顧名思義,VariablePool就是變量池的意思,下面會(huì)用這個(gè)容器封裝所有帶@Value注解的屬性。
public class VariablePool {
public static Map> 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 clazzMap = Optional.ofNullable(pool.get(annoValue))
.orElse(new HashMap<>());
clazzMap.put(clazz,field.getName());
pool.put(annoValue,clazzMap);
}
}
}
public static Map> getPool() {
return pool;
簡(jiǎn)單說(shuō)一下這塊代碼的設(shè)計(jì)思路:
- 通過(guò)反射拿到Class對(duì)象中所有的屬性,并判斷屬性是否加了@Value注解
- @Value如果要注入配置文件中的值,一定要符合${xxx}的格式(這里先暫時(shí)不考慮${xxx:defaultValue}這種設(shè)置了默認(rèn)值的格式),所以需要使用正則表達(dá)式驗(yàn)證是否符合,并校驗(yàn)通過(guò)后去掉開(kāi)頭的${和結(jié)尾的},獲取真正對(duì)應(yīng)的配置文件中的字段
- VariablePool中聲明了一個(gè)靜態(tài)HashMap,用于存放所有配置文件中屬性-類(lèi)-類(lèi)中屬性的映射關(guān)系,接下來(lái)就要把這個(gè)關(guān)系存放到這個(gè)pool中
簡(jiǎn)單來(lái)說(shuō),變量池就是下面這樣的結(jié)構(gòu):
這里如果不好理解的話(huà)可以看看例子,我們引入兩個(gè)測(cè)試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執(zhí)行完add方法后,變量池pool中的數(shù)據(jù)是這樣的:
可以看到在pool中,person.name對(duì)應(yīng)的內(nèi)層Map中包含了兩條數(shù)據(jù),分別是UserService中的name字段,以及UserDeptService中的pname字段。
4、EnvInitializer
在VariablePool封裝完所有變量數(shù)據(jù)后,ScanRunner會(huì)調(diào)用EnvInitializer的init方法,開(kāi)始對(duì)yml文件進(jìn)行解析,完成配置中心環(huán)境的初始化。其實(shí)說(shuō)白了,這個(gè)環(huán)境就是一個(gè)靜態(tài)的HashMap,key是屬性名,value就是屬性的值。
public class EnvInitializer {
private static Map envMap=new HashMap<>();
public static void init(){
String rootPath = EnvInitializer.class.getResource("/").getPath();
List 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 map = yamlMapFb.getObject();
YamlConverter.doConvert(map,null,envMap);
}
}
public static void setEnvMap(Map envMap) {
EnvInitializer.envMap = envMap;
}
public static Map getEnvMap() {
return envMap;
}
}
首先還是使用FileScanner掃描根目錄下所有的.yml結(jié)尾的文件,并使用spring自帶的YamlMapFactoryBean進(jìn)行yml文件的解析。但是這里有一個(gè)問(wèn)題,所有yml文件解析后都會(huì)生成一個(gè)獨(dú)立的Map,需要進(jìn)行Map的合并,生成一份配置信息表。至于這一塊具體的操作,都交給了下面的YamlConverter進(jìn)行處理。
我們先進(jìn)行一下演示,準(zhǔn)備兩個(gè)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
先來(lái)看一看環(huán)境完成初始化后,生成的數(shù)據(jù)格式是這樣的:
5、YamlConverter
??YamlConverter??主要實(shí)現(xiàn)的方法有三個(gè):
- ?
?doConvert()???:將??EnvInitializer??中提供的多個(gè)Map合并成一個(gè)單層Map - ?
?monoToMultiLayer()??:將單層Map轉(zhuǎn)換為多層Map(為了生成yml格式字符串) - ?
?convert()??:yml格式的字符串解析為Map(為了判斷屬性是否發(fā)生變化)
由于后面兩個(gè)功能暫時(shí)還沒(méi)有涉及,我們先看第一段代碼:
public class YamlConverter {
public static void doConvert(Map map,String parentKey,Map 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);
}
});
}
//...
}
邏輯也很簡(jiǎn)單,通過(guò)循環(huán)遍歷的方式,將多個(gè)Map最終都合并到了目的??envMap???中,并且如果遇到多層Map嵌套的情況,那么將多層Map的key通過(guò)點(diǎn)??.??進(jìn)行了連接,最終得到了上面那張圖中樣式的單層Map。
其余兩個(gè)方法,我們?cè)谙旅媸褂玫降膱?chǎng)景再說(shuō)。
6、ConfigController
??ConfigController???作為控制器,用于和前端進(jìn)行交互,只有兩個(gè)接口??save???和??get??,下面分別介紹。
get
前端頁(yè)面在開(kāi)啟時(shí)會(huì)調(diào)用??ConfigController???中的??get???接口,填充到??textArea???中。先看一下??get??方法的實(shí)現(xiàn):
@GetMapping("get")
public String get(){
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
String yamlContent = null;
try {
Map envMap = EnvInitializer.getEnvMap();
Map map = YamlConverter.monoToMultiLayer(envMap, null);
yamlContent = objectMapper.writeValueAsString(map);
} catch (Exception e) {
e.printStackTrace();
}
return yamlContent;
}
之前在項(xiàng)目啟動(dòng)時(shí),就已經(jīng)把配置文件屬性封裝到了??EnvInitializer???的??envMap???中,并且這個(gè)??envMap???是一個(gè)單層的Map,不存在嵌套關(guān)系。但是我們這里要使用??jackson???生成標(biāo)準(zhǔn)格式的yml文檔,這種格式不符合要求,需要將它還原成一個(gè)具有層級(jí)關(guān)系的多層Map,就需要調(diào)用??YamlConverter???的??monoToMultiLayer()??方法。
??monoToMultiLayer()???方法的代碼有點(diǎn)長(zhǎng),就不貼在這里了,主要是根據(jù)key中的??.??進(jìn)行拆分并不斷創(chuàng)建子級(jí)的Map,轉(zhuǎn)換完成后得到的多層Map數(shù)據(jù)如下:
在獲得這種格式后的Map后,就可以調(diào)用??jackson??中的方法將Map轉(zhuǎn)換為yml格式的字符串傳遞給前端了,看一下處理完成后返回給前端的字符串:
save
在前端頁(yè)面修改了yml內(nèi)容后點(diǎn)擊保存時(shí),會(huì)調(diào)用??save??方法保存并更新配置,方法的實(shí)現(xiàn)如下:
@PostMapping("save")
public String save(@RequestBody Map newValue) {
String ymlContent =(String) newValue.get("yml");
PropertyTrigger.change(ymlContent);
return "success";
}
在拿到前端傳過(guò)來(lái)的yml字符串后,調(diào)用??PropertyTrigger???的??change??方法,實(shí)現(xiàn)后續(xù)的更改邏輯。
7、PropertyTrigger
在調(diào)用??change??方法后,主要做的事情有兩件:
- 修改?
?EnvInitializer???中的環(huán)境??envMap??,用于前端頁(yè)面刷新時(shí)返回新的數(shù)據(jù),以及下一次屬性改變時(shí)進(jìn)行對(duì)比使用 - 修改bean中屬性的值,這也是整個(gè)配置中心最重要的功能
先看一下代碼:
public class PropertyTrigger {
public static void change(String ymlContent) {
Map newMap = YamlConverter.convert(ymlContent);
Map 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> pool = VariablePool.getPool();
Map 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();
}
});
}
}
前面鋪墊了那么多,其實(shí)就是為了實(shí)現(xiàn)這段代碼中的功能,具體邏輯如下:
- 調(diào)用?
?YamlConverter???的??convert???方法,將前端傳來(lái)的yml格式字符串解析封裝成單層Map,數(shù)據(jù)格式和??EnvInitializer???中的??envMap??相同 - 遍歷舊的?
?envMap??,查看其中的key在新的Map中對(duì)應(yīng)的屬性值是否發(fā)生了改變,如果沒(méi)有改變則不做之后的任何操作 - 如果發(fā)生改變,用新的值替換?
?envMap??中的舊值 - 通過(guò)屬性名稱(chēng),從?
?VariablePool???中拿到涉及改變的??Class???,以及類(lèi)中的字段??Field???。并通過(guò)后面的??SpringContextUtil??中的方法獲取到這個(gè)bean的實(shí)例對(duì)象,再通過(guò)反射改變字段的值 - 將修改后的Map寫(xiě)回?
?EnvInitializer???中的??envMap??
到這里,就實(shí)現(xiàn)了全部的功能。
8、SpringContextUtil
??SpringContextUtil???通過(guò)實(shí)現(xiàn)??ApplicationContextAware???接口獲得了spring容器,而通過(guò)容器的??getBean()??方法就可以容易的拿到spring中的bean,方便進(jìn)行后續(xù)的更改操作。
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public staticT getBean(Class t) {
return applicationContext.getBean(t);
}
}
9、前端代碼
至于前端代碼,就是一個(gè)非常簡(jiǎn)單的表單,代碼的話(huà)可以移步??git??查看。
最后
到這里全部的代碼介紹完了,最后做一個(gè)簡(jiǎn)要的總結(jié)吧,雖然通過(guò)這幾個(gè)類(lèi)能夠?qū)崿F(xiàn)一個(gè)簡(jiǎn)易版的配置中心功能,但是還有不少的缺陷,例如:
- 沒(méi)有處理?
?@ConfigurationProperties??注解 - 只處理了yml文件,沒(méi)有處理properties文件
- 目前處理的bean都是基于?
?singleton???模式,如果作用域?yàn)??prototype??,也會(huì)存在問(wèn)題 - 反射性能低,如果某個(gè)屬性涉及的類(lèi)很多會(huì)影響性能
- 目前只能代碼嵌入到項(xiàng)目中使用,還不支持獨(dú)立部署及遠(yuǎn)程注冊(cè)功能
- ……
總的來(lái)說(shuō),后續(xù)需要完善的點(diǎn)還有不少,真是感覺(jué)任重道遠(yuǎn)。
最后再聊聊項(xiàng)目的名稱(chēng),為什么取名叫??hermit-purple??呢,來(lái)源是jojo中二喬的替身隱者之紫,感覺(jué)這個(gè)替身的能力和配置中心的感知功能還是蠻搭配的,所以就用了這個(gè)哈哈。
那么這次的分享就到這里,我是Hydra,預(yù)祝大家虎年春節(jié)快樂(lè),我們下篇再見(jiàn)。
新聞標(biāo)題:硬核!8個(gè)類(lèi)手寫(xiě)一個(gè)配置中心!
當(dāng)前網(wǎng)址:http://www.5511xx.com/article/djceecc.html


咨詢(xún)
建站咨詢(xún)
