yokila
yokila
Published on 2023-11-23 / 7 Visits
0
0

Java自定义配置文件加载与修改配置并输出到配置文件

一、本文介绍

本文主要通过完整代码的形式,展示如何加载自定义配置文件中的配置项数据,并进行使用。以及如何区分内外部配置文件的加载。同时,又介绍了如何修改内存中的配置,并输出到配置文件中进行持久化保存的方法。

本文基于的环境:IDEA2022.2.5(社区版)、JDK1.8、spring-boot-starter-parent 2.6.0

注意:本文介绍内容少,主要信息都在代码的注释里。

二、加载配置

先假设有两个配置文件(注意文件的编码格式要弄成UTF-8,以支持中文)

IDEA中调整:IDEA File -> Settings -> Editor -> File Encodings -> Properties Files default encoding for properties files 需要设置为 UTF-8

内部配置文件(放在resources底下):

name=Alice(爱丽丝小盆友)
age=18岁

外部配置文件(放在项目文件夹之外的一处地方):

name=Bob(鲍勃同学)
# age=20岁 # 可以只修改需要替换内部配置值的部分

期望:

①项目打包时,会把【内部配置文件】打包进jar包中。同时,默认情况下,会自动加载【内部配置文件】。

②如果有配置【外部配置文件】路径,则会去加载【外部配置文件】数据,否则不加载。

③【外部配置文件】中,凡是出现的项,优先级更高,将覆盖【内部配置文件】的相同key的配置项。

④允许利用【外部配置文件】只替换【内部配置文件】中的部分配置项。

则可见配置加载类源码(CustomConfig.java):

package com.basic.happytest.modules.property.customProperty;

import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Properties;
import java.util.Vector;

/**
 * 自定义配置的加载和更新
 */

public class CustomConfig {

    /**
     * 默认的处于内部的配置文件的路径
     */
    private static final String CONFIG_FILE_NAME = "/properties/customConfig.properties";

    /**
     * 加载的配置(注意,该类继承自Hashtable)
     */
    private static Properties properties;

    /**
     * 负责记录传入的外部配置文件的绝对路径(不准备输出配置数据到配置文件中的话可以删除掉这个变量)
     */
    private static String extPropFilePath;

    /**
     * 初始化加载(一般会配置到启动加载项,项目启动时直接加载)
     * @param extPropFilePathVar 外部配置文件的绝对路径,null则表示没有配置外部配置文件
     */
    public static void init(final String extPropFilePathVar) {
        try (
                InputStream inputStream = CustomConfig.class.getResourceAsStream(CONFIG_FILE_NAME);
                // inputStream、inputStreamReader等未实现mark、reset方法,所以需要用到BufferedInputStream等实现了该方法的类
                BufferedInputStream bufferedInputStream = new BufferedInputStream(Objects.requireNonNull(inputStream));
                InputStreamReader inputStreamReader = new InputStreamReader(bufferedInputStream)) {

            // 标记当前位置(此时实际是流的起始位置),让等下reset的时候可以直接回到这个位置
            // 配置文件内容大小不要超过这里的readLimit,否则有可能会导致mark失效,到时候reset就会抛出异常:Resetting to invalid mark
            bufferedInputStream.mark(2048);

            properties = new Properties();
            // 有中文的话,不能直接使用inputStream去加载,因为是ISO 8859-1格式,读取中文会乱码。所以这里用的是Reader,解决中文乱码问题。
            // 内部是一行一行去加载,读取一个键值对,就去put一次设置值
            System.out.println("开始加载内部的自定义配置");
            properties.load(inputStreamReader);

            // 配置读取完以后,输入流到达末尾
            System.out.println("inputStream是否已经到达文件末尾:" + (inputStream.read() == -1));
            System.out.println("inputStreamReader是否已经到达文件末尾:" + (inputStreamReader.read() == -1));
            System.out.println("bufferedInputStream是否已经到达文件末尾:" + (bufferedInputStream.read() == -1));

            // 让输入流回到起始位置,方便等下可以重复读取数据
            bufferedInputStream.reset();

            // 外部配置文件路径不为空,说明要加载外部配置文件
            if (StringUtils.isNotBlank(extPropFilePathVar)) {
                loadExtProps(bufferedInputStream, extPropFilePathVar);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加载外部配置文件
     * @param innerInputStream 内部配置文件的输入流
     * @param extPropFilePathVar 外部配置文件路径
     */
    private static void loadExtProps(InputStream innerInputStream, String extPropFilePathVar) {
        File extPropFile = new File(extPropFilePathVar);
        // 如果外部配置文件不存在,则直接结束
        if (!extPropFile.exists()) {
            System.out.println("外部配置文件不存在,直接结束加载外部配置文件");
            return;
        }
        InputStream extInputStream;
        try {
            extInputStream = Files.newInputStream(Paths.get(extPropFilePathVar));
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("外部配置文件加载异常,直接结束加载外部配置文件");
            return;
        }
        // 内外部文件流拼接
        Vector<InputStream> inputStreamVector = new Vector<>(3);
        inputStreamVector.add(innerInputStream);
        // 补充换行,防止无法正常连接两个Properties数据流,默认连接后没有换行
        inputStreamVector.add(new ByteArrayInputStream("\n".getBytes(StandardCharsets.UTF_8)));
        inputStreamVector.add(extInputStream);
        try (SequenceInputStream sequenceInputStream = new SequenceInputStream(inputStreamVector.elements())){
            properties = new Properties();
            InputStreamReader inputStreamReader = new InputStreamReader(sequenceInputStream);
            // 内部是一行一行去加载,读取一个键值对,就去put一次设置值
            // 所以,外部配置文件流放后面,出现的键值对因为key和内部的配置文件的一样,所以会产生覆盖(即外部配置文件优先级更高)
            properties.load(inputStreamReader);
            extPropFilePath = extPropFilePathVar;
            System.out.println("加载外部配置文件成功,配置以外部的优先");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                extInputStream.close();
            } catch (IOException ignored) {
            }
        }
    }

    /**
     * 获取配置
     * @param key 配置项的key
     * @return 配置值
     */
    public static String getProp(String key) {
        return properties.getProperty(key);
    }
}

按照上面的源码,执行单元测试。

不加载外部配置时:

测试方法

可以看到把内部配置文件中的数据加载出来了。

输出结果

加载外部配置时:

测试方法

可以看到,因为外部配置文件中也配置了name,所以name被替换成外部配置文件中的配置值。但是age在外部配置文件中没有对应配置项,所以age依旧使用的是内部配置文件中的配置值。

输出结果

三、修改配置并输出到配置文件

注:没有确切需求的话,不需要考虑这个实现,有一定的局限性。

局限性:

  • 实际加载的是内部配置文件的话,不能用这个方法。IDE中执行时,修改的其实是这个文件夹里的对应配置文件 /target/classes/。如果是打包后执行jar包,因为内部配置文件在jar包里,所以更新的内容不能输出到内部配置文件。

  • 每次输出到配置文件(实际只会是外部配置文件),只能把当下内存中的配置信息全部输出,且会覆盖掉原文件的全部内容。(但是这个应该可以自行调整改造代码来优化)

期望:

  • 需要尽量保证内存中的配置和文件的配置是一致的。即要改就全改,不然就都不改。(提供的源码可以自行再优化,使之更健壮)

  • 修改配置后,无论是读取内存中的配置,还是配置文件中的配置,都是更新后的配置。

  • 支持中文配置值的输出(即不会变成乱码)

由此,得到源码(直接放在刚刚的CustomConfig.java文件中就行):

/**
     * 更新配置并输出到配置文件(使用外部配置文件时才允许修改)
     * @param name 名称(具体的配置项value)
     * @param age 年龄(具体的配置项value)
     */
    public static void updProps(String name, String age) {
        // 深度复制一份旧的配置,以备后面如果输出到文件失败的话,可以恢复
        Properties originalProps = SerializationUtils.clone(properties);
        // 记录是否修改了原配置
        boolean isModified = false;
        // 更新内存中的配置
        if (!properties.getProperty(CustomConfigEnum.NAME.getKeyName()).equals(name)) {
            properties.setProperty(CustomConfigEnum.NAME.getKeyName(), name);
            isModified = true;
        }
        if (!properties.getProperty(CustomConfigEnum.AGE.getKeyName()).equals(String.valueOf(age))) {
            properties.setProperty(CustomConfigEnum.AGE.getKeyName(), String.valueOf(age));
            isModified = true;
        }
        // 修改了配置,才需要尝试将新的配置输出到配置文件
        if (isModified) {
            System.out.println("内存中的配置发生了变化,开始将更新后的配置数据输出到配置文件");
            // 此处是:当前使用哪里的配置文件,就更新哪个配置文件
            String propsFilePath;
            if (StringUtils.isNotBlank(extPropFilePath)) {
                propsFilePath = extPropFilePath;
            } else {
                System.out.println("使用内部配置,不允许修改配置,修改内容不生效");
                properties = SerializationUtils.clone(originalProps);
                return;
                /*
                try {
                    // IDE中执行时,修改的其实是这个文件夹里的对应配置文件 /target/classes/
                    // 如果是打包后执行jar包,这里的输出是会有问题的,实际运行会报错:java.io.FileNotFoundException
                    URL fileURL = FileIO.class.getResource(CONFIG_FILE_NAME);
                    if (fileURL == null) {
                        System.out.println("定位文件路径失败!!!");
                        throw new IOException();
                    }
                    propsFilePath = fileURL.getPath();
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }
                */
            }
            // jdk11时可以指定FileWriter输出时的编码方式,低版本则不支持,只能输出时默认使用系统的编码方式(例如win经常时GBK),这样会导致容易出现中文乱码的情况,所以低版本Jdk的话,只能换其他的输出流
            try (FileOutputStream fileOutputStream = new FileOutputStream(propsFilePath);
                 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)){
                // 注意,这里整个文件的内容都会更新
                // comments中文会变成 \u8FD9 之类,没招,除非自己继承Properties去实现输出方法,特别是这个方法writeComments
                properties.store(outputStreamWriter, "这个内容将会输出到配置文件中第一行作为注释,不想设置可以为null");
                System.out.println("输出到配置文件成功");
            } catch (Exception e) {
                e.printStackTrace();
                // 输出到文件失败,则会恢复到原来的配置
                properties = SerializationUtils.clone(originalProps);
            }
        }
    }

开始测试。

不加载外部配置文件时:

测试方法

前面有提及,由于不能输出到内部配置文件中,所以这里直接拒绝修改。并且为了保持一致,所以修改的内容无效,内存中的配置依旧是修改前的。

输出结果

加载外部配置文件时:

测试方法

因为新的配置内容可以输出到外部配置文件,所以修改配置是成功的。且内存中和外部配置文件中,都是新的配置值。

执行结果

注意:输出配置到配置文件中,整个文件的内容都会更新。

设置的 comments,若有中文会变成 \u8FD9 之类,没招,除非自己继承Properties去实现输出方法,特别是这个方法 writeComments 。

变化后的配置文件内容,第一行是自定义输入的 comments 注释,第二行是Properties内部实现的输出注释(表示输出内容的时间),第三行开始就是输出的配置对。

变化后的外部配置文件


Comment