<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          造輪子!8個類手寫一個配置中心!

          共 12285字,需瀏覽 25分鐘

           ·

          2022-02-12 11:23

          點擊關注公眾號,Java干貨及時送達??


          配置中心是我們平常使用微服務架構時重要的一個模塊,常用的配置中心組件也比較多,從早期的Spring Cloud Config,到Disconf、Apollo、Nacos等,它們支持的功能、產品的性能以及給用戶的體驗也各有不同。

          雖然說功能上有不少差異,但是它們解決的最核心問題,無疑是配置文件修改后的實時生效,有時候在搬磚之余Hydra就在好奇實時生效是如何實現的、如果讓我來設計又會怎么去實現,于是這幾天抽出了點空閑時間,摸魚摸出了個簡易版的單機配置中心,先來看看效果:

          之所以說是簡易版本,首先是因為實現的核心功能就只有配置修改后實時生效,并且代碼的實現也非常簡單,一共只用了8個類就實現了這個核心功能,看一下代碼的結構,核心類就是core包中的這8個類:

          看到這是不是有點好奇,雖說是低配版,就憑這么幾個類也能實現一個配置中心?那么先看一下總體的設計流程,下面我們再細說代碼。

          代碼簡要說明

          下面對8個核心類進行一下簡要說明并貼出核心代碼,有的類中代碼比較長,可能對手機瀏覽的小伙伴不是非常友好,建議收藏后以后電腦瀏覽器打開(騙波收藏,計劃通!)。另外Hydra已經把項目的全部代碼上傳到了git,有需要的小伙伴可以移步文末獲取地址。

          1、ScanRunner

          ScanRunner實現了CommandLineRunner接口,可以保證它在springboot啟動最后執(zhí)行,這樣就能確保其他的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?fileList?=?FileScanner.findFileByType(rootPath,null,FileScanner.TYPE_CLASS);
          ????????doFilter(rootPath,fileList);
          ????????EnvInitializer.init();
          ????}

          ????private?void?doFilter(String?rootPath,?List?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對變量進行處理。

          2、FileScanner

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

          查找文件的邏輯很簡單,就是在給定的根目錄rootPath下,循環(huán)遍歷每一個目錄,對找到的文件再進行后綴名的比對,如果符合條件就加到返回的文件名列表中。

          至于下面的這個getRealRootPath方法,是因為在windows環(huán)境下,獲取到項目的運行目錄是這樣的:

          /F:/Workspace/hermit-purple-config/target/classes/

          而class文件名是這樣的:

          F:\Workspace\hermit-purple-config\target\classes\com\cn\hermimt\purple\test\service\UserService.class

          如果想要獲取一個類的全限定名,那么首先要去掉運行目錄,再把文件名中的反斜杠\替換成點.,這里就是為了刪掉文件名中的運行路徑提前做好準備。

          3、VariablePool

          回到上面的主流程中,每個在ScanRunner中掃描出的帶有@Component、@Controller@Service注解的Class,都會交給VariablePool進行處理。顧名思義,VariablePool就是變量池的意思,下面會用這個容器封裝所有帶@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;
          ????}
          }

          簡單說一下這塊代碼的設計思路:

          • 通過反射拿到Class對象中所有的屬性,并判斷屬性是否加了@Value注解
          • @Value如果要注入配置文件中的值,一定要符合${xxx}的格式(這里先暫時不考慮${xxx:defaultValue}這種設置了默認值的格式),所以需要使用正則表達式驗證是否符合,并校驗通過后去掉開頭的${和結尾的},獲取真正對應的配置文件中的字段
          • VariablePool中聲明了一個靜態(tài)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執(zhí)行完add方法后,變量池pool中的數據是這樣的:

          可以看到在pool中,person.name對應的內層Map中包含了兩條數據,分別是UserService中的name字段,以及UserDeptService中的pname字段。

          4、EnvInitializer

          VariablePool封裝完所有變量數據后,ScanRunner會調用EnvInitializerinit方法,開始對yml文件進行解析,完成配置中心環(huán)境的初始化。其實說白了,這個環(huán)境就是一個靜態(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結尾的文件,并使用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

          先來看一看環(huán)境完成初始化后,生成的數據格式是這樣的:

          5、YamlConverter

          YamlConverter主要實現的方法有三個:

          • doConvert():將EnvInitializer中提供的多個Map合并成一個單層Map
          • monoToMultiLayer():將單層Map轉換為多層Map(為了生成yml格式字符串)
          • convert():yml格式的字符串解析為Map(為了判斷屬性是否發(fā)生變化)

          由于后面兩個功能暫時還沒有涉及,我們先看第一段代碼:

          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);
          ????????????}
          ????????});
          ????}
          ?//...
          }????

          邏輯也很簡單,通過循環(huán)遍歷的方式,將多個Map最終都合并到了目的envMap中,并且如果遇到多層Map嵌套的情況,那么將多層Map的key通過點.進行了連接,最終得到了上面那張圖中樣式的單層Map。

          其余兩個方法,我們在下面使用到的場景再說。

          6、ConfigController

          ConfigController作為控制器,用于和前端進行交互,只有兩個接口saveget,下面分別介紹。

          get

          前端頁面在開啟時會調用ConfigController中的get接口,填充到textArea中。先看一下get方法的實現:

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

          之前在項目啟動時,就已經把配置文件屬性封裝到了EnvInitializerenvMap中,并且這個envMap是一個單層的Map,不存在嵌套關系。但是我們這里要使用jackson生成標準格式的yml文檔,這種格式不符合要求,需要將它還原成一個具有層級關系的多層Map,就需要調用YamlConvertermonoToMultiLayer()方法。

          monoToMultiLayer()方法的代碼有點長,就不貼在這里了,主要是根據key中的.進行拆分并不斷創(chuàng)建子級的Map,轉換完成后得到的多層Map數據如下:

          在獲得這種格式后的Map后,就可以調用jackson中的方法將Map轉換為yml格式的字符串傳遞給前端了,看一下處理完成后返回給前端的字符串:

          save

          在前端頁面修改了yml內容后點擊保存時,會調用save方法保存并更新配置,方法的實現如下:

          @PostMapping("save")
          public?String?save(@RequestBody?Map?newValue)?{
          ????String?ymlContent?=(String)?newValue.get("yml");
          ????PropertyTrigger.change(ymlContent);
          ????return?"success";
          }

          在拿到前端傳過來的yml字符串后,調用PropertyTriggerchange方法,實現后續(xù)的更改邏輯。

          7、PropertyTrigger

          在調用change方法后,主要做的事情有兩件:

          • 修改EnvInitializer中的環(huán)境envMap,用于前端頁面刷新時返回新的數據,以及下一次屬性改變時進行對比使用
          • 修改bean中屬性的值,這也是整個配置中心最重要的功能

          先看一下代碼:

          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();
          ????????????}
          ????????});
          ????}
          }

          前面鋪墊了那么多,其實就是為了實現這段代碼中的功能,具體邏輯如下:

          • 調用YamlConverterconvert方法,將前端傳來的yml格式字符串解析封裝成單層Map,數據格式和EnvInitializer中的envMap相同
          • 遍歷舊的envMap,查看其中的key在新的Map中對應的屬性值是否發(fā)生了改變,如果沒有改變則不做之后的任何操作
          • 如果發(fā)生改變,用新的值替換envMap中的舊值
          • 通過屬性名稱,從VariablePool中拿到涉及改變的Class,以及類中的字段Field。并通過后面的SpringContextUtil中的方法獲取到這個bean的實例對象,再通過反射改變字段的值
          • 將修改后的Map寫回EnvInitializer中的envMap

          到這里,就實現了全部的功能。

          8、SpringContextUtil

          SpringContextUtil通過實現ApplicationContextAware接口獲得了spring容器,而通過容器的getBean()方法就可以容易的拿到spring中的bean,方便進行后續(xù)的更改操作。

          @Component
          public?class?SpringContextUtil?implements?ApplicationContextAware?{
          ????private?static?ApplicationContext?applicationContext;
          ????@Override
          ????public?void?setApplicationContext(ApplicationContext?applicationContext)?throws?BeansException?{
          ????????this.applicationContext?=?applicationContext;
          ????}
          ????public?static??T?getBean(Class?t)?{
          ????????return?applicationContext.getBean(t);
          ????}
          }

          9、前端代碼

          至于前端代碼,就是一個非常簡單的表單,代碼的話可以移步git查看。

          最后

          到這里全部的代碼介紹完了,最后做一個簡要的總結吧,雖然通過這幾個類能夠實現一個簡易版的配置中心功能,但是還有不少的缺陷,例如:

          • 沒有處理@ConfigurationProperties注解
          • 只處理了yml文件,沒有處理properties文件
          • 目前處理的bean都是基于singleton模式,如果作用域為prototype,也會存在問題
          • 反射性能低,如果某個屬性涉及的類很多會影響性能
          • 目前只能代碼嵌入到項目中使用,還不支持獨立部署及遠程注冊功能
          • ……

          總的來說,后續(xù)需要完善的點還有不少,真是感覺任重道遠。

          項目git地址:

          https://github.com/trunks2008/hermit-purple-config

          1.?不是我吹,這款 IDEA 插件你真沒用過!

          2.?抗住千萬流量的大型分布式系統(tǒng)架構設計

          3.?網傳鐵飯碗職業(yè)排名,公務員僅排第八!

          4.?Vue涉及國家安全漏洞?尤雨溪親自回應

          最近面試BAT,整理一份面試資料Java面試BATJ通關手冊,覆蓋了Java核心技術、JVM、Java并發(fā)、SSM、微服務、數據庫、數據結構等等。

          獲取方式:點“在看”,關注公眾號并回復?Java?領取,更多內容陸續(xù)奉上。

          PS:因公眾號平臺更改了推送規(guī)則,如果不想錯過內容,記得讀完點一下在看,加個星標,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。

          “在看”支持小哈呀,謝謝啦??!

          瀏覽 69
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  AV性淫| 日韩视频中文字幕在线 | 婷婷亚洲综合五月天小说 | 天天操综合网 | 欧美一级操逼片 |