Java 通用配置版本配置实现

Java 通用配置版本配置实现

Java 通用配置

(一)设计

(二)JVM和环境变量实现

(三)用户配置实现

(四)版本配置实现

本系列参考实现:

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

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

版本配置设计

版本配置是提供给 模块开发者 使用的, 模块的使用者 可以使用默认的最新版本配置或者通过参数指定要使用的版本配置。

版本配置文件规范

模块开发者一般会把配置和代码一起打包,配置文件通常都在 jar 包内,所以需要从 jar 包读取资源,在读取资源时有很多情况需要考虑,如果允许配置文件出现在任意的 jar 包中,就需要对所有 jar 包中的资源进行扫描,就和 Spring 中配置的扫描包一样,指定范围越小,扫描处理越快。

综合考虑性能和功能,这里的版本配置中指定了如下的要求:

  • 配置文件和版本配置子类:在同一个 jar 包文件中在同一个包名中
  • 版本配置文件使用相同的前缀,后面带上版本号,版本号为两位,示例如下:mybatis-mapper-v1.0.propertiesmybatis-mapper-v1.1.propertiesmybatis-mapper-v2.0.propertiesmybatis-mapper-v2.5.properties
  • 给自己用的工具能实现需求即可,不用为了灵活性搞得太复杂。

    版本配置选择逻辑

    有了上述规则后,就需要确定该使用哪个版本的配置文件。

    假设项目刚开始 1.0 版本,此时组件有一些默认值设置。后期增加了新的配置,可以直接在 1.0 版本中新增,也可以创建一个和代码版本号对应的新配置文件进行维护。前一种方式操作简单,后一种方式的配置也是文档,可以清晰地展示出当前版本可以配置的参数。

    当项目有不兼容的版本改动时,一定要创建新的配置文件,假设发展到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 时,发现有个默认 true 的配置变成了 false ,此时如果组件支持 用户配置 ,可以直接在里面配置位置 true ,如果有版本配置,就可以指定要使用的版本配置,此时指定 mapper.version=v1.0 ,就会让 1.0 的配置优先级更高,如果想用 2.0 以前的配置,配置为 mapper.version=v1.9 时会使用 1.1 的配置,当指定的版本号在两个版本区间时,向下选择低版本。

    选择之外的其他版本,按照版本从高到低的优先级进行获取,这种方式可以保证新版本的代码在运行时不至于找不到新的配置参数。

    代码实现

    当前版本配置的抽象类中,提供了下面两个抽象方法

    /** * 获取配置文件名前缀 */protected abstract String getConfigName();/** * 获取版本号对应的 key */protected abstract String getVersionKey();

    人类只需要实现这两个方法:

    • 第一个方法是配置文件的前缀,例如 mybatis-mapper , mybatis-provider 。
    • 第二个方法是返回的配置名,用户可以通过这个配置名指定自己想要使用的版本,配置名示例如: mapper.version , provider.version ,用户可以在前面介绍的 JVM、环境变量、用户配置文件中指定这里的版本号,例如 mapper.version=1.0 , provider.version=v1.1 。

    子类本身很好实现,除此之外还需要在子类相同的包名下面提供对应的版本配置文件,例如:

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

    为了方便比较版本号,提供了一个 ConfigVersion 内部类:

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

    这里的版本规则就是这样的 可以 v 开头,版本号为两位,合法的版本号如: v1.0 , 2.0 , 2.1 ,支持两位主要考虑到如果有三位版本号,第三位一般是修复bug,很少会涉及配置的改动。

    为了更好地讲解代码,下面会按照下图的执行逻辑逐段介绍:

    首先从第一个方法开始 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);}

    这里是获取配置值得入口,进入方法后,首先经过 skipKey(key) 判断:

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

    注意看图中的 ① ,在后续初始化过程中,会通过 ConfigHelper.getStr(getVersionKey()) 读取用户指定的版本,当前配置文件也是 ConfigHelper.CONFIGS 中间的一环,如果这里不做处理就会导致死循环的产生。

    接下来就是双重加锁方式的单例初始化, 为什么要在 getStr 中进行初始化,为什么不放到构造方法中?

    还是和后续的 ConfigHelper.getStr(getVersionKey()) 有关,由于初始化会读取配置,如果配置还没有实例化,就会产生**“鸡生蛋、蛋生鸡”**的问题,所以在构造方法中没有任何逻辑,单纯创建了一个类,只有真正调用时,才通过加锁的方式初始化,此时所有实例都已经存在,逻辑就能正常走下去。

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

    初始化中就是调用 buildVersionProperties() 方法进行创建:

    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()) 就是很关键的一个点,在配置文件初始化过程中去获取某个配置的值,此时方法会经过 JVM 实现、环境变量实现、用户配置实现,然后进入到当前的版本配置实现中,如果不加控制,就会在继续进入到 init 方法( synchronized 是可重入锁,不会在此产生死锁),然后再次调用 ConfigHelper.getStr(getVersionKey()) 形成一个死循环。

    破局的关键就是前面的 skipKey ,当前类发现找自己要 getVersionKey() 到时,自己作为当事人可以直接给出答案,也可以不给答案,然后在自己的后续逻辑中解决没有配置的情况。这里直接不给答案,返回 null ,然后在 init 后续逻辑中处理,没有指定版本号时,默认使用最新的配置文件。

    这个方法的下一行代码是什么 URL resource = getClass().getResource(“”) ,读取的就是当前类所在包的路径,这种读取方式就要求版本配置文件必须和配置类在相同模块的相同包下面,虽然这个限制了灵活性,但是可以避免在运行时扫描所有类路径下的所有文件,可以有效的提升性能。

    这个方法在不同场景下运行时,获取的数值不同,下面分几种情况介绍。

    在源码中(config项目)中执行时

    此时读取的 target 下面编译的代码,因此会是文件路径,例如:

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

    这种情况下代码就会执行到 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; } });}

    这里就可以在当前包路径中直接 file.listFiles() 获取目录下面所有的文件,找到所有符合条件的文件后,根据文件创建排序后的 List versions ,后续逻辑在后面继续说。

    在作为依赖 jar 包中执行时

    当 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/

    后续处理和 Spring Boot 一样,放在下面一起分析。

    没有测试出 file:/…/xxx.jar 的情况

    在 Spring Boot 可执行 Jar 包中执行时

    打包后的 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 文件后,就是调用 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 方法后续就没太大差别了:

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

    从文件名提取版本号,转换为有序的 ConfigVersion 集合。

    然后就是根据提供的版本选择要使用的版本:

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

    选择出要优先使用的版本后,后续就是根据下面规则创建配置:

    对应的就是下面的方法:

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

    按照版本递增的顺序依次构建 Properties ,小版本最为高版本的默认值,在 Properties 中,当前配置不存在时,会从传入的低版本查找:

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

    最后再用选择的版本作为优先级最高的配置进行初始化,到此就完成了整个配置初始化。

    到这里所有基本的功能都实现了,一个可扩展的 Java 通用配置就成型了,其他组件可以简单的扩展用户配置和版本配置实现配置信息的管理。

    但是在目前 Spring Boot 流行的今天,我们大多数配置文件都在 application.properties 或 application.yaml 中,能否把自己的配置文件也放到 Spring 中一起管理呢?能否支持 Spring Boot 的命令行参数?支持 Spring Boot 的完整的外部化配置规则

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

    郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
    (0)
    用户投稿
    上一篇 2022年7月11日
    下一篇 2022年7月11日

    相关推荐

    • 一台电脑主机怎样才能连接两个显示器?有哪些方法?

      “网络极客”,全新视角、全新思路,伴你遨游神奇的网络世界。 双屏显示,无论是在我们的办公环境,家庭娱乐中的应用变得越来越突出。 双屏显示能够提升程序员的工作效率,一屏用于调试代码,…

      2022年6月1日
    • 三星新机发布:搭载Exyons 850芯片+6000mAh大电池,售价约1031元

      据了解,三星一款新机在印度发布了,这款新机为Galaxy F13,搭载三星Exyons 850芯片,电池是它的一个亮点,配备了6000mAh大电池。该机的设计和搭载的芯片与此前在欧…

      2022年6月25日
    • 优雅关闭线程实践

      前言 在多线程编程中会需要动态创建线程来执行任务,在完成后释放该线程,使用Excuotrs创建的线程,提供了shutDown方法进行线程的关闭,但使用原生的Thread和Runab…

      2022年6月15日
    • 2022年火爆传奇手游排行榜

      小编个人体验的排名,如有建议可以讨论 首先第一名是最近火爆的魂环版本,【自由之刃】三职业攻速版本,这一款上线不管是战士、法师还是道士攻速都是百分之三百攻速,道士召唤出来的高等月灵攻…

      2022年7月13日
    • 《中国移动新一代超级SIM芯片技术白皮书》发布,紫光同芯参编

      6月24日,中国移动2022科技周暨移动信息产业链创新大会号卡分论坛圆满召开。本次活动以“号卡创新升级,打造数智生态新基石”为主题,聚焦与号卡相关的各类创新升级技术与应用案例,邀请…

      2022年6月29日
    • 美媒:美芯的时代正宣告结束

      熟悉芯片产业发展历史的朋友会知道,在最初的发端时代,美企率先行动,所以占据了主动权,这种主动权更多的意义在于,它们建立了芯片产业的诸多标准,也就形成了“护城河”的效果。 因为后续的…

      2022年7月19日
    • 美云智数星谋云:高效精准的互联网数据支撑,全链路驱动业务提升

      作为数字经济和信息社会的核心资源,数据被誉为“21世纪的石油和钻石矿”,已成为和土地、劳动力、资本、技术并列的五种生产要素之一。数据正逐步对国家治理能力、经济运行机制、社会生活方式…

      2022年6月14日
    • 华为Mate50RS这款手机,顶配16+512G版本,售价居然要15999

      华为手机的保时捷系列,是很多喜欢华为手机的用户,渴望而不可及的存在。虽然很喜欢,但是买不起。不过使用过它的人,应该都会有这样的感受。那就是:真的好,对得起这个售价。 最近网上曝光出…

      2022年6月25日
    • Rust-Druid概念

      druid-shell Druid 工具包使用 Druid-shell 作为平台抽象应用程序 shell。Druid-shell 负责启动本机平台运行循环,监听事件,将它们转换为与…

      2022年6月28日
    • 千亿功效护肤赛道,野兽代码如何摆脱内卷焦虑

      野兽代码的反内卷之路。 作者|龙 菲 编辑|邱月烨 “化妆品市场内卷的本质是高度的同质化,但与此同时,这也给了新品牌在激烈的竞争中突围的机会。”新锐品牌「野兽代码」创始人Dino如…

      2022年8月20日

    联系我们

    联系邮箱:admin#wlmqw.com
    工作时间:周一至周五,10:30-18:30,节假日休息