Spring自定义加载配置文件(分层次加载)

前言:

    Spring会默认加载application.properties文件,我们一般可以将配置写在此处。这基本可以满足我们的常用demo项目使用。

    但是在实际项目开发中,我们会将配置文件外置,这样在我们需要修改配置的时候就不用将项目重新打包部署了。

    下面我们来看一下实际项目开发的需求。

 

针对配置分层次加载的需求:

    举给例子:

    1.我们希望项目启动后会加载内部配置文件(统一命名为env.properties)

    2.如果有外置配置文件的话(路径设置为/envconfig/${app.name}/env.properties),则加载外置配置文件,并覆盖内部配置文件的相同key的项

    3.如果在项目启动时候指定了命令行参数,则该参数级别最高,可以覆盖外置配置文件相同key的项

 

    以上这个需求,我们用目前Spring的加载配置的方式就有点难以完成了。所以这时候我们需要自定义加载方式。

 

环境准备:

    笔者新建了一个SpringBoot项目,maven基本配置如下:

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
    </dependencies>

自定义配置加载器:

    1.配置加载器processor

/**
 * 客户端自定义加载配置
 *
 * @author lucky
 * @create 2020/3/7
 * @since 1.0.0
 */
public class CustomerConfigLoadProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 我们将主要逻辑都放在ConfigLoader去做
        environment.getPropertySources().addFirst(new ConfigLoader().getPropertySource());
    }
}

    2.在/resources/META-INF/下创建spring.factories文件,并添加

org.springframework.boot.env.EnvironmentPostProcessor=com.xw.study.configload.processor.CustomerConfigLoadProcessor

    3.实现配置加载逻辑

        以上spring environment框架搭建好之后,在项目启动时候就会去加载ConfigLoader对应的Properties信息到当前运行环境中。

        下面就来看下加载逻辑:

/**
 * 配置加载器
 *
 * @author lucky
 * @create 2020/3/7
 * @since 1.0.0
 */
public class ConfigLoader {

    private static Properties prop = new Properties();
    public static final String DEFAULT_CONFIG_FILE_NAME = "env.properties";
    public static final String SLASH = File.separator;

    public ConfigLoader() {
        loadProperties();
    }

    /**
     * 加载配置文件分为三个层次
     * 1.加载项目内置classpath:env.properties
     * 2.加载外部配置文件env.properties(会给定一个默认路径)
     * 3.加载JVM命令行参数
     */
    private void loadProperties() {
        loadLocalProperties();
        loadExtProperties();
        loadSystemEnvProperties();
    }

    /**
     * 加载JVM命令行参数、Environment参数
     */
    private void loadSystemEnvProperties() {
        prop.putAll(System.getenv());
        prop.putAll(System.getProperties());
    }

    /**
     * 加载外部配置文件env.properties(会给定一个默认路径)
     * 笔者所在公司,会根据不同的项目名,统一路径设置为
     * /envconfig/{app.name}/env.properties
     */
    private void loadExtProperties() {
        // 获取全路径
        // 所以需要首先在内部env.properties中配置上app.name
        if (prop.containsKey("app.name")) {
            String appName = prop.getProperty("app.name");
            String path = SLASH + "envconfig" + SLASH + appName + SLASH + DEFAULT_CONFIG_FILE_NAME;

            Properties properties = ConfigUtil.loadProperties(path);
            if (null != properties) {
                prop.putAll(properties);
            }
        }
    }


    /**
     * 对外提供的方法,获取配置信息
     * @param key key
     * @return 配置值
     */
    public static String getValue(String key) {
        return prop.getProperty(key);
    }


    /**
     * 加载项目内置classpath:env.properties
     */
    private void loadLocalProperties() {
        Properties properties = ConfigUtil.loadProperties(ConfigUtil.CLASSPATH_FILE_FLAG + DEFAULT_CONFIG_FILE_NAME);
        if (null != properties) {
            prop.putAll(properties);
        }
    }

    // 提供给environment.getPropertySources()的加载方法
    public PropertiesPropertySource getPropertySource() {
        return new PropertiesPropertySource("configLoader", prop);
    }
}

    工具类:ConfigUtil

/**
 * 工具类
 * 直接从Sentinel项目拷贝过来的
 *
 * @author lucky
 * @create 2020/3/7
 * @since 1.0.0
 */
public class ConfigUtil {
    public static final String CLASSPATH_FILE_FLAG = "classpath:";

    /**
     * <p>Load the properties from provided file.</p>
     * <p>Currently it supports reading from classpath file or local file.</p>
     *
     * @param fileName valid file path
     * @return the retrieved properties from the file; null if the file not exist
     */
    public static Properties loadProperties(String fileName) {
        if (StringUtils.isNotBlank(fileName)) {
            if (absolutePathStart(fileName)) {
                return loadPropertiesFromAbsoluteFile(fileName);
            } else if (fileName.startsWith(CLASSPATH_FILE_FLAG)) {
                return loadPropertiesFromClasspathFile(fileName);
            } else {
                return loadPropertiesFromRelativeFile(fileName);
            }
        } else {
            return null;
        }
    }

    private static Properties loadPropertiesFromAbsoluteFile(String fileName) {
        Properties properties = null;
        try {

            File file = new File(fileName);
            if (!file.exists()) {
                return null;
            }

            try (BufferedReader bufferedReader =
                         new BufferedReader(new InputStreamReader(new FileInputStream(file), getCharset()))) {
                properties = new Properties();
                properties.load(bufferedReader);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return properties;
    }

    private static boolean absolutePathStart(String path) {
        File[] files = File.listRoots();
        for (File file : files) {
            if (path.startsWith(file.getPath())) {
                return true;
            }
        }
        return false;
    }


    private static Properties loadPropertiesFromClasspathFile(String fileName) {
        fileName = fileName.substring(CLASSPATH_FILE_FLAG.length()).trim();

        List<URL> list = new ArrayList<>();
        try {
            Enumeration<URL> urls = getClassLoader().getResources(fileName);
            list = new ArrayList<>();
            while (urls.hasMoreElements()) {
                list.add(urls.nextElement());
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }

        if (list.isEmpty()) {
            return null;
        }

        Properties properties = new Properties();
        for (URL url : list) {
            try (BufferedReader bufferedReader =
                         new BufferedReader(new InputStreamReader(url.openStream(), getCharset()))) {
                Properties p = new Properties();
                p.load(bufferedReader);
                properties.putAll(p);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        return properties;
    }

    private static Properties loadPropertiesFromRelativeFile(String fileName) {
        return loadPropertiesFromAbsoluteFile(fileName);
    }

    private static ClassLoader getClassLoader() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader == null) {
            classLoader = ConfigUtil.class.getClassLoader();
        }
        return classLoader;
    }

    private static Charset getCharset() {
        // avoid static loop dependencies: SentinelConfig -> SentinelConfigLoader -> ConfigUtil -> SentinelConfig
        // so not use SentinelConfig.charset()
        return Charset.forName(System.getProperty("csp.sentinel.charset", StandardCharsets.UTF_8.name()));
    }

    public static String addSeparator(String dir) {
        if (!dir.endsWith(File.separator)) {
            dir += File.separator;
        }
        return dir;
    }

    public ConfigUtil() {
    }
}

    代码不算复杂,笔者不再详述。

    根据以上的加载顺序,就可以实现 命令行 > 外部配置文件 > 内部配置文件的需求。

 

    4.测试

        这个比较简单了,用户可自行测试

        1)只有内部配置文件

            在/resources下创建env.properties文件

        2)内部配置文件、外部配置文件均存在

            满足1)的同时(注意有一个必备项为app.name,笔者自定义为configload),在本地磁盘创建/envconfig/configload/env.properties文件

        3)添加命令行参数

            在满足2)的同时,在启动行添加参数(-D的方式)

 

        笔者测试代码:

@SpringBootTest(classes = ConfigloadApplication.class)
@RunWith(SpringRunner.class)
public class ConfigloadApplicationTests {

    @Test
    public void contextLoads() {

        String s = ConfigLoader.getValue("zookeeper.serverList");
        System.out.println(s);
    }

}

总结:

    在中大型公司,统一项目配置文件路径和日志路径都是一项政治正确的事。

    统一这些基本规范后,可以避免很多奇奇怪怪的问题。

 

    这样就满足了嘛?

    就目前看来这个是基本满足了需求,略微修改下,打成一个jar包,就可以直接使用了。

 

    但是目前的这种方式,在需要修改配置的时候,还是需要关闭应用然后修改外部配置文件或者命令行参数后,再重启的。

    有没有那种可以即时生效的方案呢?答案是:肯定是有的。那就是配置中心。

 

    我们可以引入配置中心,比如开源的Apollo,在上述我们的配置加载中,再加一层,从配置中心中加载配置,就可以实现配置即时生效。

    鉴于时间,笔者就先写到这。江湖再见!

            

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恐龙弟旺仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值