需求

现有一个时区配置集合(key=value 形式),类似如下:

text
1
2
BJS=Asia/Shanghai
TYO=Asia/Tokyo

需要将其保存在一个文件中并在程序启动时将其加载为 Map,时区映射为 ZoneId 类型。同时参考 Spring Boot 支持配置文件在 resources 目录下、jar 同级目录、config/ 目录下。

分析

配置文件选择

配置文件的选择,yaml、properties、ini、txt 等。Spring Boot 支持 yaml 和 properties,优选这两种类型,yaml 更适合嵌套层次的配置,所以我们选用 properties 保存时区配置。

文件加载

需要参考 Spring Boot 的加载顺序,classpath 目录、classpath:config/、jar 同级目录、jar 同级目录 config/ 子目录。 如果仅需以优先级为顺序, 加载一处的配置文件,那么在各个路径进行文件判断,以优先级最高的文件进行加载,其他文件则自动忽略。不过 Spring Boot 的配置是多个目录下配置文件的合集, 因此我们此处也以合集为例。

解析方式

默认情况下配置无公共前缀,所以我们无法使用 @PropertySource + @ConfigurationProperties 的组合。

当然,如果可以修改配置文件添加个公共前缀,也能使用这个组合。例如:

properties
timezones.properties
1
2
timezone.BJS=Asia/Shanghai
timezone.TYO=Asia/Tokyo

加载 properties 文件可使用工具类:

java
1
2
3
import org.springframework.core.io.support.PropertiesLoaderUtils;

PropertiesLoaderUtils.loadProperties(resource);

对于 classpath 路径,我们可以使用 ClassPathResource,对于 jar 同级目录,使用 FileSystemResource

编码

java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.time.ZoneId;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

@Component
@Slf4j
public class TimezonesProperties {

    /**
     * CLASSPATH 路径
     */
    private static final String CLASSPATH_PREFIX = "classpath:";

    /**
     * 配置文件名
     */
    private static final String CONFIG_FILENAME = "timezones.properties";

    /**
     * 配置文件 (按优先级 低到高 排序)
     */
    private static final String[] TIME_ZONE_CONFIG_LOCATIONS = new String[]{
            CLASSPATH_PREFIX + CONFIG_FILENAME,                 // 最低优先级 - classpath 目录
            CLASSPATH_PREFIX + "config/" + CONFIG_FILENAME,     // classpath 下 config 目录
            "./" + CONFIG_FILENAME,                             // 次优先级 - 当前目录
            "./config/" + CONFIG_FILENAME,                      // 最高优先级 - 当前目录下的 config 子目录
    };


    /**
     * 时区 Map
     */
    private final Map<String, ZoneId> timezones = new LinkedHashMap<>(16);

    /**
     * 初始化加载配置文件
     */
    @PostConstruct
    public void init() {
        for (String configLocation : TIME_ZONE_CONFIG_LOCATIONS) {
            Properties props;
            try {
                props = loadProperties(configLocation);
            } catch (Exception ex) {
                log.debug("Failed to load properties from: {}", configLocation);
                continue;
            }
            if (props == null || props.isEmpty()) {
                continue;
            }
            props.forEach((key, value) -> {
                try {
                    ZoneId zoneId = ZoneId.of(value.toString().trim());
                    this.timezones.put(key.toString().toUpperCase(), zoneId);
                } catch (Exception ex) {
                    throw new IllegalArgumentException("Invalid timezone '" + value + "'", ex);
                }
            });
        }

        if (CollectionUtils.isEmpty(this.timezones)) {
            log.warn("No timezone properties files were found.");
        }
    }

    /**
     * 加载配置
     * @param location 配置文件路径
     * @return 配置文件内容
     * @throws IOException if loading failed
     */
    private Properties loadProperties(String location) throws IOException {
        if (location.startsWith("classpath:")) {
            String classpathLocation = location.substring(CLASSPATH_PREFIX.length());
            ClassPathResource resource = new ClassPathResource(classpathLocation);
            if (resource.exists()) {
                return PropertiesLoaderUtils.loadProperties(resource);
            }
        } else {
            FileSystemResource resource = new FileSystemResource(location);
            if (resource.exists()) {
                return PropertiesLoaderUtils.loadProperties(resource);
            }
        }
        return null;
    }

    /**
     * 获取全部时区
     * @return 全部时区信息
     */
    public Map<String, ZoneId> getAllTimezones() {
        return Collections.unmodifiableMap(timezones);
    }

    /**
     * 获取指定时区
     * @param key key
     * @return key 对应的时区
     */
    public ZoneId getTimeZone(String key) {
        return timezones.get(key);
    }

    /**
     * 获取指定时区,如果没有则使用默认时区
     * @param key key
     * @param defaultZoneId 默认时区
     * @return key 对应的时区
     */
    public ZoneId getTimeZoneOrDefault(String key, ZoneId defaultZoneId) {
        return timezones.getOrDefault(key, defaultZoneId);
    }
}