在线不卡日本ⅴ一区v二区_精品一区二区中文字幕_天堂v在线视频_亚洲五月天婷婷中文网站

  • <menu id="lky3g"></menu>
  • <style id="lky3g"></style>
    <pre id="lky3g"><tt id="lky3g"></tt></pre>

    Java 通用配置版本配置實現(xiàn)

    Java 通用配置版本配置實現(xiàn)

    Java 通用配置

    (一)設(shè)計

    (二)JVM和環(huán)境變量實現(xiàn)

    (三)用戶配置實現(xiàn)

    (四)版本配置實現(xiàn)

    本系列參考實現(xiàn):

    https://gitee.com/mybatis-mapper/config

    https://github.com/mybatis-mapper/config

    版本配置設(shè)計

    版本配置是提供給 模塊開發(fā)者 使用的, 模塊的使用者 可以使用默認的最新版本配置或者通過參數(shù)指定要使用的版本配置。

    版本配置文件規(guī)范

    模塊開發(fā)者一般會把配置和代碼一起打包,配置文件通常都在 jar 包內(nèi),所以需要從 jar 包讀取資源,在讀取資源時有很多情況需要考慮,如果允許配置文件出現(xiàn)在任意的 jar 包中,就需要對所有 jar 包中的資源進行掃描,就和 Spring 中配置的掃描包一樣,指定范圍越小,掃描處理越快。

    綜合考慮性能和功能,這里的版本配置中指定了如下的要求:

  • 配置文件和版本配置子類:在同一個 jar 包文件中在同一個包名中
  • 版本配置文件使用相同的前綴,后面帶上版本號,版本號為兩位,示例如下:mybatis-mapper-v1.0.propertiesmybatis-mapper-v1.1.propertiesmybatis-mapper-v2.0.propertiesmybatis-mapper-v2.5.properties
  • 給自己用的工具能實現(xiàn)需求即可,不用為了靈活性搞得太復(fù)雜。

    版本配置選擇邏輯

    有了上述規(guī)則后,就需要確定該使用哪個版本的配置文件。

    假設(shè)項目剛開始 1.0 版本,此時組件有一些默認值設(shè)置。后期增加了新的配置,可以直接在 1.0 版本中新增,也可以創(chuàng)建一個和代碼版本號對應(yīng)的新配置文件進行維護。前一種方式操作簡單,后一種方式的配置也是文檔,可以清晰地展示出當(dāng)前版本可以配置的參數(shù)。

    當(dāng)項目有不兼容的版本改動時,一定要創(chuàng)建新的配置文件,假設(shè)發(fā)展到2.5版本時,存在下面幾個版本配置:

  • mybatis-mapper-v1.0.properties
  • mybatis-mapper-v1.1.properties
  • mybatis-mapper-v2.0.properties
  • mybatis-mapper-v2.5.properties
  • 某個用戶從 1.0 升級到 2.5 時,發(fā)現(xiàn)有個默認 true 的配置變成了 false ,此時如果組件支持 用戶配置 ,可以直接在里面配置位置 true ,如果有版本配置,就可以指定要使用的版本配置,此時指定 mapper.version=v1.0 ,就會讓 1.0 的配置優(yōu)先級更高,如果想用 2.0 以前的配置,配置為 mapper.version=v1.9 時會使用 1.1 的配置,當(dāng)指定的版本號在兩個版本區(qū)間時,向下選擇低版本。

    選擇之外的其他版本,按照版本從高到低的優(yōu)先級進行獲取,這種方式可以保證新版本的代碼在運行時不至于找不到新的配置參數(shù)。

    代碼實現(xiàn)

    當(dāng)前版本配置的抽象類中,提供了下面兩個抽象方法

    /** * 獲取配置文件名前綴 */protected abstract String getConfigName();/** * 獲取版本號對應(yīng)的 key */protected abstract String getVersionKey();

    人類只需要實現(xiàn)這兩個方法:

    • 第一個方法是配置文件的前綴,例如 mybatis-mapper , mybatis-provider 。
    • 第二個方法是返回的配置名,用戶可以通過這個配置名指定自己想要使用的版本,配置名示例如: mapper.version , provider.version ,用戶可以在前面介紹的 JVM、環(huán)境變量、用戶配置文件中指定這里的版本號,例如 mapper.version=1.0 , provider.version=v1.1 。

    子類本身很好實現(xiàn),除此之外還需要在子類相同的包名下面提供對應(yīng)的版本配置文件,例如:

    mybatis-mapper-v1.0.propertiesmybatis-mapper-v1.1.propertiesmybatis-mapper-v1.2.propertiesmybatis-mapper-v2.0.properties

    為了方便比較版本號,提供了一個 ConfigVersion 內(nèi)部類:

    public static class ConfigVersion implements Comparable { private final int x; private final int y; private final String fileName; public ConfigVersion(String version) { this(version, null); } public ConfigVersion(String version, String fileName) { this.fileName = fileName; if (version.startsWith(“v”)) { version = version.substring(1); } String[] strings = version.split(“.”); this.x = Integer.parseInt(strings[0]); this.y = Integer.parseInt(strings[1]); } public String getFileName() { return fileName; } @Override public int compareTo(ConfigVersion o) { if (this.x == o.x) { return this.y – o.y; } return this.x – o.x; }}

    這里的版本規(guī)則就是這樣的 可以 v 開頭,版本號為兩位,合法的版本號如: v1.0 , 2.0 , 2.1 ,支持兩位主要考慮到如果有三位版本號,第三位一般是修復(fù)bug,很少會涉及配置的改動。

    為了更好地講解代碼,下面會按照下圖的執(zhí)行邏輯逐段介紹:

    首先從第一個方法開始 getStr(String key) 開始:

    @Overridepublic String getStr(String key) { if (skipKey(key)) { return null; } if (this.properties == null) { synchronized (this) { if (this.properties == null) { this.init(); } } } return properties.getProperty(key);}

    這里是獲取配置值得入口,進入方法后,首先經(jīng)過 skipKey(key) 判斷:

    /** * 跳過讀取指定的 key * * @param key 屬性 */protected boolean skipKey(String key) { return getVersionKey().equals(key);}

    注意看圖中的 ① ,在后續(xù)初始化過程中,會通過 ConfigHelper.getStr(getVersionKey()) 讀取用戶指定的版本,當(dāng)前配置文件也是 ConfigHelper.CONFIGS 中間的一環(huán),如果這里不做處理就會導(dǎo)致死循環(huán)的產(chǎn)生。

    接下來就是雙重加鎖方式的單例初始化, 為什么要在 getStr 中進行初始化,為什么不放到構(gòu)造方法中?

    還是和后續(xù)的 ConfigHelper.getStr(getVersionKey()) 有關(guān),由于初始化會讀取配置,如果配置還沒有實例化,就會產(chǎn)生**“雞生蛋、蛋生雞”**的問題,所以在構(gòu)造方法中沒有任何邏輯,單純創(chuàng)建了一個類,只有真正調(diào)用時,才通過加鎖的方式初始化,此時所有實例都已經(jīng)存在,邏輯就能正常走下去。

    /** * 初始化 */protected void init() { Properties props = buildVersionProperties(); if (props != null) { this.properties = props; } else { this.properties = new Properties(); }}

    初始化中就是調(diào)用 buildVersionProperties() 方法進行創(chuàng)建:

    protected Properties buildVersionProperties() { String version = ConfigHelper.getStr(getVersionKey()); // 讀取資源 URL resource = getClass().getResource(“”); if (resource == null) { return null; } if (resource.getProtocol().equals(“file”)) { if (resource.getPath().endsWith(“.jar”)) { try { JarFile jarFile = new JarFile(resource.getPath()); return chooseFromJarFile(jarFile, version); } catch (IOException e) { throw new RuntimeException(e); } } else { try { File file = new File(resource.toURI()); return chooseFromFile(file, version); } catch (Exception e) { throw new RuntimeException(e); } } } else if (resource.getProtocol().equals(“jar”)) { try { JarFile jarFile = ((JarURLConnection) resource.openConnection()).getJarFile(); return chooseFromJarFile(jarFile, version); } catch (IOException e) { throw new RuntimeException(e); } } return null;}

    方法的第一行代碼 ConfigHelper.getStr(getVersionKey()) 就是很關(guān)鍵的一個點,在配置文件初始化過程中去獲取某個配置的值,此時方法會經(jīng)過 JVM 實現(xiàn)、環(huán)境變量實現(xiàn)、用戶配置實現(xiàn),然后進入到當(dāng)前的版本配置實現(xiàn)中,如果不加控制,就會在繼續(xù)進入到 init 方法( synchronized 是可重入鎖,不會在此產(chǎn)生死鎖),然后再次調(diào)用 ConfigHelper.getStr(getVersionKey()) 形成一個死循環(huán)。

    破局的關(guān)鍵就是前面的 skipKey ,當(dāng)前類發(fā)現(xiàn)找自己要 getVersionKey() 到時,自己作為當(dāng)事人可以直接給出答案,也可以不給答案,然后在自己的后續(xù)邏輯中解決沒有配置的情況。這里直接不給答案,返回 null ,然后在 init 后續(xù)邏輯中處理,沒有指定版本號時,默認使用最新的配置文件。

    這個方法的下一行代碼是什么 URL resource = getClass().getResource(“”) ,讀取的就是當(dāng)前類所在包的路徑,這種讀取方式就要求版本配置文件必須和配置類在相同模塊的相同包下面,雖然這個限制了靈活性,但是可以避免在運行時掃描所有類路徑下的所有文件,可以有效的提升性能。

    這個方法在不同場景下運行時,獲取的數(shù)值不同,下面分幾種情況介紹。

    在源碼中(config項目)中執(zhí)行時

    此時讀取的 target 下面編譯的代碼,因此會是文件路徑,例如:

    file:/Users/xxx/mybatis-config/target/test-classes/io/mybatis/config/custom/

    這種情況下代碼就會執(zhí)行到 chooseFromFile(file, version); 代碼中:

    private Properties chooseFromFile(File file, String version) throws IOException { String configName = getConfigName(); File[] files = file.listFiles(); if (files == null || files.length == 0) { return null; } Map fileMap = new HashMap(); for (File f : files) { if (f.getName().startsWith(configName)) { fileMap.put(f.getName(), f); } } List versions = sortVersions(new ArrayList(fileMap.keySet())); ConfigVersion chooseVersion = chooseVersion(versions, version); return build(versions, chooseVersion, configVersion -> { try { return new FileInputStream(fileMap.get(configVersion.getFileName())); } catch (FileNotFoundException e) { return null; } });}

    這里就可以在當(dāng)前包路徑中直接 file.listFiles() 獲取目錄下面所有的文件,找到所有符合條件的文件后,根據(jù)文件創(chuàng)建排序后的 List versions ,后續(xù)邏輯在后面繼續(xù)說。

    在作為依賴 jar 包中執(zhí)行時

    當(dāng) mybatis-provider 作為依賴在 IDE 中(IDE不含mybatis-mapper源碼)運行時,看到的路徑如下:

    jar:file:/Users/xxx/.m2/repository/io/mybatis/mybatis-provider/2.0.0/mybatis-provider-2.0.0.jar!/io/mybatis/provider/config/

    后續(xù)處理和 Spring Boot 一樣,放在下面一起分析。

    沒有測試出 file:/…/xxx.jar 的情況

    在 Spring Boot 可執(zhí)行 Jar 包中執(zhí)行時

    打包后的 Spring Boot 通過 java -jar 運行時,輸出的路徑如下:

    jar:file:/Users/xxx/mybatis-mapper-springboot/target/mybatis-mapper-example-springboot-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/mybatis-provider-2.0.0.jar!/io/mybatis/provider/config/

    和前一種情況相比,這里是在 fat jar 中的一個 jar 包,多套了一層 jar。此時通過下面的方法獲取 Jar 文件:

    JarFile jarFile = ((JarURLConnection) resource.openConnection()).getJarFile()

    得到 Jar 文件后,就是調(diào)用 chooseFromJarFile(jarFile, version) 獲取配置文件:

    private Properties chooseFromJarFile(JarFile jarFile, String version) throws IOException { String configName = getConfigName(); String configPath = getConfigPath(); Enumeration entries = jarFile.entries(); Map entryMap = new HashMap(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); if (name.startsWith(configPath)) { name = name.substring(configPath.length()); if (name.startsWith(configName)) { entryMap.put(name, entry); } } } List versions = sortVersions(new ArrayList(entryMap.keySet())); ConfigVersion chooseVersion = chooseVersion(versions, version); return build(versions, chooseVersion, configVersion -> { try { return jarFile.getInputStream(entryMap.get(chooseVersion.getFileName())); } catch (IOException e) { return null; } });}

    到了 sortVersions 方法后續(xù)就沒太大差別了:

    private List sortVersions(Collection fileNames) { if(fileNames == null || fileNames.isEmpty()) { return null; } Pattern pattern = Pattern.compile(getConfigName() + “-(vd+.d+)” + FILE_TYPE); return fileNames.stream().map(fileName -> { Matcher matcher = pattern.matcher(fileName); if (matcher.find()) { return new ConfigVersion(matcher.group(1), fileName); } return null; }).filter(Objects::nonNull).sorted().collect(Collectors.toList());}

    從文件名提取版本號,轉(zhuǎn)換為有序的 ConfigVersion 集合。

    然后就是根據(jù)提供的版本選擇要使用的版本:

    private ConfigVersion chooseVersion(List versions, String version) { if (versions == null || versions.isEmpty()) { return null; } //沒有指定版本時使用最新版本 if (version == null || version.isEmpty()) { return versions.get(versions.size() – 1); } ConfigVersion configVersion = new ConfigVersion(version); //從最高版本進行比較,選擇的版本高于或等于配置版本時,就選擇該版本 for (int i = versions.size() – 1; i >= 0; i–) { if (configVersion.compareTo(versions.get(i)) >= 0) { return versions.get(i); } } //選擇的版本不高于所有版本時,使用最小版本 return versions.get(0);}

    選擇出要優(yōu)先使用的版本后,后續(xù)就是根據(jù)下面規(guī)則創(chuàng)建配置:

    對應(yīng)的就是下面的方法:

    private Properties build(List versions, ConfigVersion chooseVersion, Function toInputStream) throws IOException { if (chooseVersion == null) { return null; } InputStream is; Properties prop = null; for (ConfigVersion configVersion : versions) { if(configVersion != chooseVersion) { prop = new Properties(prop); is = toInputStream.apply(configVersion); if(is != null) { prop.load(is); is.close(); } } } prop = new Properties(prop); is = toInputStream.apply(chooseVersion); if(is != null) { prop.load(is); is.close(); } return prop;}

    按照版本遞增的順序依次構(gòu)建 Properties ,小版本最為高版本的默認值,在 Properties 中,當(dāng)前配置不存在時,會從傳入的低版本查找:

    public String getProperty(String key) { Object oval = map.get(key); String sval = (oval instanceof String) ? (String)oval : null; Properties defaults; return ((sval == null) && ((defaults = this.defaults) != null)) ? defaults.getProperty(key) : sval;}

    最后再用選擇的版本作為優(yōu)先級最高的配置進行初始化,到此就完成了整個配置初始化。

    到這里所有基本的功能都實現(xiàn)了,一個可擴展的 Java 通用配置就成型了,其他組件可以簡單的擴展用戶配置和版本配置實現(xiàn)配置信息的管理。

    但是在目前 Spring Boot 流行的今天,我們大多數(shù)配置文件都在 application.properties 或 application.yaml 中,能否把自己的配置文件也放到 Spring 中一起管理呢?能否支持 Spring Boot 的命令行參數(shù)?支持 Spring Boot 的完整的外部化配置規(guī)則

    原文鏈接:https://blog.csdn.net/isea533/article/details/125657163

    鄭重聲明:本文內(nèi)容及圖片均整理自互聯(lián)網(wǎng),不代表本站立場,版權(quán)歸原作者所有,如有侵權(quán)請聯(lián)系管理員(admin#wlmqw.com)刪除。
    上一篇 2022年7月11日 15:18
    下一篇 2022年7月11日 15:18

    相關(guān)推薦

    聯(lián)系我們

    聯(lián)系郵箱:admin#wlmqw.com
    工作時間:周一至周五,10:30-18:30,節(jié)假日休息