硬核!Spring容器还没启动,怎么偷看 server.port?三种骚操作带你起飞


在日常开发中,我们习惯了 @Value 或者 @ConfigurationProperties 来获取配置。但是,你是否遇到过以下极端场景

  • 日志系统初始化:Logback 或 Log4j2 需要在 Spring Context 启动前就确定日志文件的路径或名称(比如包含端口号)。
  • 服务注册:需要在 Spring Boot 启动的第一时间,甚至在 main 方法执行之初,就将服务信息上报给注册中心或监控系统。
  • Quartz:自定义集群实例ID中组成部分使用服务名或者应用端口号,其注册实例id在spring容器初始化之前,无法等待容器实例化之后再返回数据。
  • Agent 探针开发:作为 Java Agent 运行,完全独立于 Spring 生命周期,但需要读取业务应用的 application.yml 配置。

今天,我们就来聊聊一个既硬核又实用的需求:在 Spring 容器初始化之前,如何精准读取配置文件中的 server.port

这可不是简单的读文件,因为 Spring Boot 的配置解析逻辑极其复杂,涉及 优先级排序、Profile(多环境)激活、占位符解析($ 等一系列操作。

本文将提供三种不同维度的解决方案,丰俭由人,任君选择!


方案一:手写解析器(纯 Java 实现)

如果你需要在 Spring 启动之前(例如 public static void main 的第一行),或者在非 Spring 环境下读取,你需要“造轮子”手动加载文件。

适用场景:非 Spring 环境、极简依赖环境、追求极致启动速度。

1. 依赖说明

解析 YAML 需要 SnakeYAML 库(Spring Boot spring-boot-starter 默认已包含)。如果是纯 Java 项目,需引入:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>2.0</version> <!-- 请使用最新版本 -->
</dependency>

2. 核心代码实现

编写一个 ConfigPropertyResolver,它模拟了 Spring Boot 的默认查找顺序(严格遵循 file:./config/ > file:./ > classpath:/config/ > classpath:),并支持能自动识别spring.profiles.active、spring.profiles.include 指定的profile,并加载对应的applicationbootstrap` 文件名。

ConfigPropertyResolver.java
(滑动查看完整代码)

package com.alianga.utils.config;

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.CollectionUtils;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

@Slf4j
public class ConfigPropertyResolver {

    private static final String[] BASE_NAMES = {
            "bootstrap",
            "application"
    };

    private static final String[] EXTENSIONS = {
            ".yml", ".yaml", ".properties"
    };

    /**
     * Spring Boot 官方搜索路径(按优先级从低到高)
     */
    private static final List<ResourceLoaderEntry> SEARCH_LOCATIONS = Lists.newArrayList(
            // jar 内(低优先级)
            new ResourceLoaderEntry("classpath:/", false),
            new ResourceLoaderEntry("classpath:/config/", false),

            // jar 外(高优先级)
            new ResourceLoaderEntry("file:./", true),
            new ResourceLoaderEntry("file:./config/", true)
    );

    private final Map<String, Object> properties = new LinkedHashMap<>();

    // =================== 对外 API ===================

    public static ConfigPropertyResolver load() {
        ConfigPropertyResolver resolver = new ConfigPropertyResolver();
        resolver.loadAll();
        return resolver;
    }

    public String getString(String key) {
        Object val = properties.get(key);
        return val == null ? null : val.toString();
    }

    public List<Object> getList(String key) {
        Object val = properties.get(key);
        List<Object> list = new ArrayList<>();
        if (val != null) {
            list.add(val);
        }
        for (int i = 0; i < properties.size(); i++) {
            String indexKey = key + "[" + i + "]";
            val = properties.get(indexKey);
            if (val == null) {
                return list;
            }
            list.add(val);
        }
        return list;
    }

    public Object get(String key) {
        return properties.get(key);
    }

    public Integer getInt(String key) {
        String v = getString(key);
        return v == null ? null : Integer.parseInt(v);
    }

    // =================== 核心逻辑 ===================

    private void loadAll() {
        long start = System.currentTimeMillis();
        List<Object> profiles = detectActiveProfile();

        for (String base : BASE_NAMES) {
            // base
            loadBySearchLocations(base, null);

            if (profiles.isEmpty()) {
                profiles = detectActiveProfile();
            }
            // base-profile
            if (!profiles.isEmpty()) {
                loadBySearchLocations(base, profiles);
            }
        }

        // JVM / env 覆盖
        overrideFromSystemProperties();
        overrideFromEnv();

        //占位符解析
        PlaceholderResolver.resolveAll(properties);
        long cost = System.currentTimeMillis() - start;
        log.info("ConfigPropertyResolver loaded, cost: {} ms", cost);
    }

    /**
     * 按 Spring Boot 官方路径顺序加载
     */
    private void loadBySearchLocations(String baseName, List<Object> profiles) {
        for (ResourceLoaderEntry entry : SEARCH_LOCATIONS) {
            for (String ext : EXTENSIONS) {
                if (CollectionUtils.isEmpty(profiles)) {
                    loadProfileConfig(baseName, entry, ext, null);
                    continue;
                }
                for (Object profile : profiles) {
                    loadProfileConfig(baseName, entry, ext, profile);
                }

            }
        }
    }

    private void loadProfileConfig(String baseName, ResourceLoaderEntry entry, String ext, Object profile) {
        String fileName = baseName +
                (profile == null ? "" : "-" + profile) +
                ext;

        Resource resource = entry.resolve(fileName);
        if (resource != null && resource.exists()) {
            loadResource(resource, entry.highPriority);
        }
    }

    /**
     * 加载资源(支持 yaml / properties)
     */
    private void loadResource(Resource resource, boolean highPriority) {
        try {
            String name = resource.getFilename();
            if (name == null) {
                return;
            }

            if (name.endsWith(".yml") || name.endsWith(".yaml")) {
                loadYaml(resource);
            } else {
                loadProperties(resource);
            }
        } catch (Exception ignored) {
        }
    }

    // =================== 加载实现 ===================

    private void loadYaml(Resource resource) throws Exception {
        YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
        List<PropertySource<?>> sources =
                loader.load(resource.getFilename(), resource);

        for (PropertySource<?> ps : sources) {
            Object source = ps.getSource();
            if (source instanceof Map) {
                ((Map<?, ?>) source).entrySet().forEach(entry -> {
                    Object value = entry.getValue();
                    if (value instanceof OriginTrackedValue) {
                        value = ((OriginTrackedValue) value).getValue();
                    }
                    properties.put(entry.getKey().toString(), value);
                });

            }
        }
    }

    private void loadProperties(Resource resource) throws Exception {
        Properties props =
                PropertiesLoaderUtils.loadProperties(resource);
        for (String name : props.stringPropertyNames()) {
            properties.put(name, props.getProperty(name));
        }
    }

    // =================== 覆盖逻辑 ===================

    private void overrideFromSystemProperties() {
        Properties sys = System.getProperties();
        for (String name : sys.stringPropertyNames()) {
            properties.put(name, sys.getProperty(name));
        }
    }

    private void overrideFromEnv() {
        System.getenv().forEach((k, v) -> {
            // SERVER_PORT -> server.port
            String key = k.toLowerCase().replace('_', '.');
            properties.put(key, v);
        });
    }

    // =================== profile 探测 ===================

    private List<Object> detectActiveProfile() {
        // 1. JVM 参数
        String profile = System.getProperty("spring.profiles.active");
        if (profile != null && !profile.isEmpty()) {
            return Arrays.asList(profile.split(","));
        }
        // 2. 环境变量
        profile = System.getenv("SPRING_PROFILES_ACTIVE");
        if (profile != null && !profile.isEmpty()) {
            return Arrays.asList(profile.split(","));
        }
        // 3. 配置文件中定义(bootstrap / application)
        List<Object> profiles = getList("spring.profiles.active");

        List<Object> include = getList("spring.profiles.include");
        profiles.addAll(include);
        return profiles;
    }

    // =================== 内部结构 ===================

    private static class ResourceLoaderEntry {
        private final String location;
        private final boolean highPriority;

        ResourceLoaderEntry(String location, boolean highPriority) {
            this.location = location;
            this.highPriority = highPriority;
        }

        Resource resolve(String fileName) {
            try {
                if (location.startsWith("classpath:")) {
                    return new ClassPathResource(
                            location.substring("classpath:".length()) + fileName
                    );
                } else {
                    File file = new File(
                            location.substring("file:".length()), fileName
                    );
                    return file.exists() ? new FileSystemResource(file) : null;
                }
            } catch (Exception e) {
                return null;
            }
        }
    }
}

为了解决 ${server.port:8080} 这种占位符,我们还需要一个辅助类:

PlaceholderResolver.java

// ... 省略 imports ...
@Slf4j
public class PlaceholderResolver {
    private static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)}");

    public static void resolveAll(Map<String, Object> props) {
        for (String key : props.keySet()) {
            Object value = props.get(key);
            if (value instanceof String) {
                String resolved = resolveValue((String) value, props, new HashSet<>());
                props.put(key, resolved);
            }
        }
    }
    // ... resolveValue 递归解析逻辑,见原内容 ...
    private static String resolveValue(String value, Map<String, Object> props, Set<String> visiting) {
        Matcher matcher = PATTERN.matcher(value);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String expr = matcher.group(1); 
            String key;
            String defaultValue = null;
            int idx = expr.indexOf(':');
            if (idx >= 0) {
                key = expr.substring(0, idx);
                defaultValue = expr.substring(idx + 1);
            } else {
                key = expr;
            }
            if (visiting.contains(key)) throw new IllegalStateException("Circular reference: " + key);
            visiting.add(key);

            Object replacement = props.get(key);
            String resolved;
            if (replacement != null) {
                resolved = resolveValue(replacement.toString(), props, visiting);
            } else if (defaultValue != null) {
                resolved = resolveValue(defaultValue, props, visiting);
            } else {
                resolved = "";
            }
            visiting.remove(key);
            matcher.appendReplacement(sb, Matcher.quoteReplacement(resolved));
        }
        matcher.appendTail(sb);
        return sb.toString();
    }
}

使用示例:

public class ServerPortConfig {
    public static Integer getServerPort() {
        try {
            ConfigPropertyResolver resolver = ConfigPropertyResolver.load();
            Integer serverPort = resolver.getInt("server.port");
            return serverPort != null ? serverPort : 8080;
        } catch (Exception e) {
            return 8080;
        }
    }
}

方案二:源码级复刻(Spring Boot 仿真器)

Spring Boot 解析配置文件的核心逻辑非常复杂(ConfigData API, YamlPropertySourceLoader 等)。

要完全“手写”一个和 Spring Boot 逻辑一模一样的解析器非常困难。最高明的做法是:直接调用 Spring Boot 的底层 API,组装一个“轻量级环境”。

下面的 SpringBootConfigLoader 实现了以下功能:

  • 完美复刻优先级:严格遵循 file:./config/ > file:./ > classpath:/config/ > classpath:/
  • 支持 Profile:能自动识别 spring.profiles.active 并加载对应的 application-dev.yml
  • 支持占位符:完美解析 ${random.int}${JAVA_HOME} 等变量。

SpringBootConfigLoader.java

import org.springframework.boot.env.PropertiesPropertySourceLoader;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.Collections;
import java.util.List;

public class SpringBootConfigLoader {

    private final StandardEnvironment environment;
    private final ResourceLoader resourceLoader;
    private final YamlPropertySourceLoader yamlLoader;
    private final PropertiesPropertySourceLoader propsLoader;

    // 搜索路径(配合 addFirst 实现最终的高优先级覆盖)
    private static final String[] LOCATION_ORDER_LOW_TO_HIGH = {
            "classpath:/",           // 优先级 4 (最低)
            "classpath:/config/",    // 优先级 3
            "file:./",               // 优先级 2
            "file:./config/"         // 优先级 1 (最高)
    };

    public SpringBootConfigLoader() {
        this.environment = new StandardEnvironment();
        this.resourceLoader = new DefaultResourceLoader();
        this.yamlLoader = new YamlPropertySourceLoader();
        this.propsLoader = new PropertiesPropertySourceLoader();
    }

    public int getPort() {
        loadAllConfigs();
        return environment.getProperty("server.port", Integer.class, 8080);
    }

    public String getProperty(String key) {
        loadAllConfigs();
        return environment.getProperty(key);
    }

    private void loadAllConfigs() {
        // 1. 加载默认配置 (application.yml)
        loadByNames(new String[]{"application"});

        // 2. 检测激活的 Profile
        String activeProfiles = environment.getProperty("spring.profiles.active");

        // 3. 如果有 Profile,加载对应的 Profile 配置
        if (StringUtils.hasText(activeProfiles)) {
            String[] profiles = StringUtils.commaDelimitedListToStringArray(activeProfiles);
            for (String profile : profiles) {
                loadByNames(new String[]{ "application-" + profile.trim() });
            }
        }
    }

    private void loadByNames(String[] baseNames) {
        for (String location : LOCATION_ORDER_LOW_TO_HIGH) {
            for (String baseName : baseNames) {
                loadResource(location + baseName + ".properties");
                loadResource(location + baseName + ".yml");
                loadResource(location + baseName + ".yaml");
            }
        }
    }

    private void loadResource(String path) {
        Resource resource = resourceLoader.getResource(path);
        if (!resource.exists()) return;

        try {
            List<PropertySource<?>> loadedSources = Collections.emptyList();
            if (path.endsWith(".properties")) {
                loadedSources = propsLoader.load(path, resource);
            } else if (path.endsWith(".yml") || path.endsWith(".yaml")) {
                loadedSources = yamlLoader.load(path, resource);
            }

            // 关键点:使用 addFirst。
            // 后加载的(file:./config/) 处于最高优先级
            if (loadedSources != null) {
                for (PropertySource<?> source : loadedSources) {
                    environment.getPropertySources().addFirst(source);
                }
            }
        } catch (IOException e) {
            System.err.println("Warning: Failed to load config from " + path);
        }
    }
}

为什么这段代码是“神技”?
它巧妙利用了 StandardEnvironmentaddFirst() 特性。我们按照低优先级路径到高优先级路径的顺序加载文件,但每次都把新加载的配置“压在栈顶”。结果就是:最后加载的文件(External Config)在查询时最先被命中,完美复刻了 Spring Boot 的覆盖逻辑。


方案三:降维打击(极简但准确)

如果你不想写上面的工具类,而是想利用 Spring Boot 现成的能力(哪怕多花 100ms 启动时间),可以使用 SpringApplicationBuilder 创建一个不启动 Web 容器的纯上下文

这是最最准确的方式,因为它就是 Spring Boot 本身。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

public class UltraAccuratePortReader {

    public static int getPort() {
        // 创建一个空的 Spring Boot 应用
        SpringApplicationBuilder builder = new SpringApplicationBuilder(Object.class)
                .web(WebApplicationType.NONE) // 关键:不启动 Tomcat,只加载配置
                .bannerMode(SpringApplication.Banner.Mode.OFF) // 关闭 Banner
                .logStartupInfo(false); // 减少日志

        // 运行 run() 会执行完整的配置解析流程
        ConfigurableApplicationContext context = builder.run();
        
        ConfigurableEnvironment env = context.getEnvironment();
        Integer port = env.getProperty("server.port", Integer.class, 8080);
        
        // 用完记得关闭,释放资源
        context.close();
        
        return port;
    }
    
    public static void main(String[] args) {
        System.out.println("Port: " + getPort());
    }
}

总结对比

方案名称优点缺点适用场景
方案一手动解析0依赖(除SnakeYAML),轻量,完全可控代码量大,很难完全对齐Spring的所有特性(如config.import,加载java的环境运行变量等)非Spring环境,或对Jar包体积极度敏感
方案二模拟器 (推荐)速度极快,逻辑精准,复用Spring核心组件需要引入Spring Core依赖Agent探针,启动辅助工具,main方法早期
方案三降维打击逻辑100%准确,支持Spring Boot所有新特性会初始化一个轻量Context,稍重(几百毫秒)对启动时间不敏感,但要求配置绝对准确的场景

看完记得点赞收藏,下次遇到“先有鸡还是先有蛋”的配置加载问题,直接复制粘贴!

Q.E.D.


寻门而入,破门而出