java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring自定义加载配置文件(分层次加载)

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

作者:恐龙弟旺仔

这篇文章主要介绍了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,在上述我们的配置加载中,再加一层,从配置中心中加载配置,就可以实现配置即时生效。

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

您可能感兴趣的文章:
阅读全文