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

 更新时间:2023年07月27日 10:49:31   作者:恐龙弟旺仔  
这篇文章主要介绍了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,在上述我们的配置加载中,再加一层,从配置中心中加载配置,就可以实现配置即时生效。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • JAVA实现简单系统登陆注册模块

    JAVA实现简单系统登陆注册模块

    这篇文章主要介绍了一个简单完整的登陆注册模块的实现过程,文章条理清晰,在实现过程中加深了对相关概念的理解,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2015-07-07
  • tk.Mybatis 插入数据获取Id问题

    tk.Mybatis 插入数据获取Id问题

    本文主要介绍了tk.Mybatis 插入数据获取Id问题,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • JavaPoet的使用指南小结

    JavaPoet的使用指南小结

    这篇文章主要介绍了JavaPoet的使用指南小结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-10-10
  • logstash将mysql数据同步到elasticsearch方法详解

    logstash将mysql数据同步到elasticsearch方法详解

    这篇文章主要为大家介绍了logstash将mysql数据同步到elasticsearch方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 解决@SpringBootTest 单元测试遇到的坑

    解决@SpringBootTest 单元测试遇到的坑

    这篇文章主要介绍了解决@SpringBootTest 单元测试遇到的坑,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • java利用递归算法实现对文件夹的删除功能

    java利用递归算法实现对文件夹的删除功能

    这篇文章主要介绍了java利用递归算法实现对文件夹的删除功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • Java 创建线程的3种方法及各自的优点

    Java 创建线程的3种方法及各自的优点

    这篇文章主要介绍了Java 创建线程的3种方法及各自的优点,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • Spring MVC启动之HandlerMapping作用及实现详解

    Spring MVC启动之HandlerMapping作用及实现详解

    这篇文章主要为大家介绍了Spring MVC启动之HandlerMapping作用及实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • 在Java代码中解析html,获取其中的值方法

    在Java代码中解析html,获取其中的值方法

    今天小编就为大家分享一篇在Java代码中解析html,获取其中的值方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • Java实现基于JDBC操作mysql数据库的方法

    Java实现基于JDBC操作mysql数据库的方法

    这篇文章主要介绍了Java实现基于JDBC操作mysql数据库的方法,结合实例形式分析了java使用JDBC实现针对mysql数据库的连接、查询、输出等相关操作技巧,需要的朋友可以参考下
    2017-12-12

最新评论