硬核!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,并加载对应的application和bootstrap` 文件名。
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);
}
}
}
为什么这段代码是“神技”?
它巧妙利用了 StandardEnvironment 的 addFirst() 特性。我们按照低优先级路径到高优先级路径的顺序加载文件,但每次都把新加载的配置“压在栈顶”。结果就是:最后加载的文件(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.


