属性占位符 ${...} 解析机制全解
概述
Spring 的属性占位符(${...})解析机制是将 BeanDefinition 中的占位符替换为实际配置值的核心能力。从 PropertySourcesPlaceholderConfigurer 的 BeanFactory 后置处理到 PropertyPlaceholderHelper 的递归解析,再到 @Value 注解的注入,整个机制贯穿 Spring IoC 容器的初始化过程。
本文将深入拆解属性占位符解析的 10 个关键细节,涵盖配置器的后置处理流程、占位符递归解析算法、两种占位符的区别、值解析器的创建与委托、BeanDefinition 遍历替换、不可解析占位符的语义、spring.config.import 和 @Value 的占位符处理等核心内容。
本文基于 Spring Framework 6.1.6 + Spring Boot 3.2.5 源码分析。
1. PropertySourcesPlaceholderConfigurer.postProcessBeanFactory() 流程
PropertySourcesPlaceholderConfigurer 是 Spring Boot 中处理 ${...} 占位符的核心类,实现了 BeanFactoryPostProcessor 接口,在 Bean 实例化之前扫描并替换所有 BeanDefinition 中的占位符。
public class PropertySourcesPlaceholderConfigurer
extends PlaceholderConfigurerSupport
implements EnvironmentAware {
// 从 Environment 中获取的 PropertySources
private PropertySources propertySources;
private Environment environment;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
// 1. 确定属性源(PropertySources)
PropertySources propertySourcesToUse = this.propertySources;
if (propertySourcesToUse == null) {
// 从 Environment 中获取 PropertySources
propertySourcesToUse = new EnvironmentPropertySources(
(ConfigurableEnvironment) this.environment);
}
// 2. 创建占位符属性源解析器
// 将 PropertySources 中的每个 PropertySource 包装为 PlaceholderResolver
PropertySourcesPlaceholderResolver placeholderResolver =
new PropertySourcesPlaceholderResolver(propertySourcesToUse);
// 3. 创建 StringValueResolver(将 ${...} 占位符转换为实际值)
StringValueResolver valueResolver = new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
String resolved = placeholderResolver.resolvePlaceholder(strVal);
if (resolved == null && strVal.startsWith(propertyResolver.getRequiredPrefix())) {
// 使用 requiredPrefix(如 ${...:required})时,未解析到值抛出异常
throw new IllegalArgumentException("未解析占位符 '" + strVal + "'");
}
return resolved != null ? resolved : strVal;
}
};
// 4. 处理 @Value 注解中的占位符
// 将 StringValueResolver 注册到 BeanFactory 的 EmbeddedValueResolver
processPlaceholders((ConfigurableApplicationContext) beanFactory, valueResolver);
}
// 处理所有 BeanDefinition 中的占位符
private void processPlaceholders(ConfigurableApplicationContext context,
StringValueResolver valueResolver) {
// 将 StringValueResolver 注册到 BeanFactory
// 使其可以在 @Value 解析时被 EmbeddedValueResolver 调用
context.getBeanFactory().addEmbeddedValueResolver(valueResolver);
// 遍历所有 BeanDefinition,替换其中的占位符
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
String[] beanNames = context.getBeanFactory().getBeanDefinitionNames();
for (String beanName : beanNames) {
if (!(beanName.equals(this.beanName) // 跳过自身
&& context.getBeanFactory().equals(this.applicationContext.getAutowireCapableBeanFactory()))) {
BeanDefinition bd = context.getBeanFactory().getBeanDefinition(beanName);
try {
visitor.visitBeanDefinition(bd);
} catch (Exception ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanName, ex.getMessage());
}
}
}
}
}完整流程:
postProcessBeanFactory()
↓
① 确定 PropertySources(从 Environment 获取)
↓
② 创建 PropertySourcesPlaceholderResolver
↓
③ 创建 StringValueResolver(包装占位符解析逻辑)
↓
④ 注册到 BeanFactory(addEmbeddedValueResolver)
↓
⑤ 遍历所有 BeanDefinition
↓
⑥ 使用 BeanDefinitionVisitor 替换占位符2. PropertyPlaceholderHelper.parseStringValue() 递归解析
PropertyPlaceholderHelper 是占位符解析的核心工具类,负责将字符串中的 ${...} 占位符替换为实际值。
public class PropertyPlaceholderHelper {
// 占位符前缀(默认 ${)
private final String placeholderPrefix = "${";
// 占位符后缀(默认 })
private final String placeholderSuffix = "}";
// 值分隔符(默认 :,用于 ${key:defaultValue})
private final String valueSeparator = ":";
// 是否忽略不可解析的占位符
private final boolean ignoreUnresolvablePlaceholders;
// 递归解析字符串中的占位符
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
// 调用 parseStringValue 执行递归解析
return parseStringValue(value, placeholderResolver, null);
}
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver,
@Nullable Set<String> visitedPlaceholders) {
// 1. 查找第一个 ${ 的位置
int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
// 没有占位符 → 直接返回原字符串
return value;
}
// 初始化结果字符串缓存
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
// 2. 查找匹配的 }
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// 3. 提取占位符内部的表达式
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
// 4. 记录已访问的占位符(防止循环引用)
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>();
}
if (!visitedPlaceholders.add(placeholder)) {
throw new IllegalArgumentException(
"循环占位符引用: '" + placeholder + "' 在属性值中 '" + value + "'");
}
// 5. **递归解析**:占位符内部可能包含嵌套的 ${...}
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 6. 解析占位符值:从 PropertySources 中查找
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
// 7. 处理默认值:${key:defaultValue}
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue; // 使用默认值
}
}
}
if (propVal != null) {
// 8. **递归解析**:解析出的值可能还包含占位符
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
// 9. 替换占位符为实际值
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
// 继续查找下一个占位符
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
} else if (this.ignoreUnresolvablePlaceholders) {
// 占位符无法解析且 ignore=true → 保持原样,跳过
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
} else {
throw new IllegalArgumentException("无法解析占位符 '" + placeholder + "' 在 '" + value + "'");
}
// 清理已访问的占位符
visitedPlaceholders.remove(placeholder);
} else {
// 没有找到闭合的 } → 停止解析
startIndex = -1;
}
}
return result.toString();
}
}递归解析示例:
输入: "jdbc:${db.host}:3306/${db.name}?user=${db.user}"
第 1 轮:
→ 找到 "${db.host}"
→ resolvePlaceholder("db.host") → "localhost"
→ 替换后: "jdbc:localhost:3306/${db.name}?user=${db.user}"
第 2 轮:
→ 找到 "${db.name}"
→ resolvePlaceholder("db.name") → "mydb"
→ 替换后: "jdbc:localhost:3306/mydb?user=${db.user}"
第 3 轮:
→ 找到 "${db.user}"
→ resolvePlaceholder("db.user") → "admin"
→ 替换后: "jdbc:localhost:3306/mydb?user=admin"
最终输出: "jdbc:localhost:3306/mydb?user=admin"嵌套占位符解析:
输入: "${${prefix}.url}"
第 1 轮:
→ 找到 "${${prefix}.url}"
→ 提取内部: "${prefix}.url"
→ 递归解析内部: parseStringValue("${prefix}.url")
→ 找到 "${prefix}"
→ resolvePlaceholder("prefix") → "db"
→ 内部解析结果: "db.url"
→ 外部占位符变为: "db.url"
→ resolvePlaceholder("db.url") → "jdbc:mysql://localhost:3306/db"
→ 最终结果: "jdbc:mysql://localhost:3306/db"3. ${...} vs #{...} 的区别
Spring 中存在两种占位符语法,由不同的机制处理。
| 特性 | ${...} | #{...} |
|---|---|---|
| 名称 | 属性占位符 | SpEL 表达式 |
| 处理器 | PropertySourcesPlaceholderConfigurer | StandardBeanExpressionResolver |
| 执行阶段 | BeanFactoryPostProcessor(Bean 实例化前) | Bean 实例化阶段 |
| 功能 | 从 PropertySources 查找属性值 | 完整的表达式求值语言 |
| 支持的语法 | ${key}、${key:default} | #{bean.method()}、#{T(Math).PI}、#{@bean.field} |
| 是否可递归 | 是(解析结果可包含更多 ${}) | 单次求值 |
| 支持方法调用 | ❌ | ✅ |
| 支持类型操作 | ❌ | ✅ |
| 支持集合运算 | ❌ | ✅ |
处理时机:
容器启动时序:
BeanFactoryPostProcessor 阶段(refresh() 中的 invokeBeanFactoryPostProcessors)
├── PropertySourcesPlaceholderConfigurer.postProcessBeanFactory()
│ └── 替换所有 ${...} 占位符
│
└── 其他 BeanFactoryPostProcessor...
Bean 实例化阶段(refresh() 中的 finishBeanFactoryInitialization)
├── AbstractAutowireCapableBeanFactory.createBean()
│ └── AutowiredAnnotationBeanPostProcessor 处理 @Value("#{...}")
│ └── StandardBeanExpressionResolver.evaluate()
└── ...混合使用示例:
@Component
public class AppConfig {
// ${} — 从 PropertySource 获取属性值
@Value("${app.name}")
private String appName;
// #{} — SpEL 表达式
@Value("#{systemProperties['user.dir']}")
private String userDir;
// 混合:先在 BeanFactoryPostProcessor 阶段解析 ${},剩下 #{} 在 SpEL 阶段解析
@Value("#{systemProperties['user.home']}/${app.name}/data")
private String dataPath;
// #{} 内部嵌套 ${}:先由 PropertySourcesPlaceholderConfigurer 解析 ${},
// 结果传给 SpEL 求值
@Value("#{${app.maxThreads} * 2}")
private int maxThreads;
}4. StringValueResolver 的 2 种创建
StringValueResolver 是占位符解析的函数式接口,Spring 中通过两种方式创建和注册。
@FunctionalInterface
public interface StringValueResolver {
// 将输入字符串中的占位符替换为实际值
String resolveStringValue(String strVal);
}方式 1:PropertySourcesPlaceholderConfigurer 创建:
// PropertySourcesPlaceholderConfigurer 中创建
StringValueResolver valueResolver = strVal -> {
PropertySourcesPlaceholderResolver resolver =
new PropertySourcesPlaceholderResolver(propertySourcesToUse);
// 创建 PropertyPlaceholderHelper 并解析
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(
"${", "}", ":", ignoreUnresolvablePlaceholders);
return helper.replacePlaceholders(strVal,
placeholderName -> resolver.resolvePlaceholder(placeholderName));
};方式 2:EmbeddedValueResolver 包装:
// 在 AbstractApplicationContext 中创建的 EmbeddedValueResolver
public class EmbeddedValueResolver implements StringValueResolver {
private final BeanExpressionContext exprContext;
private final BeanExpressionResolver exprResolver;
public EmbeddedValueResolver(ConfigurableBeanFactory beanFactory) {
this.exprContext = new BeanExpressionContext(beanFactory, null);
// 使用 BeanFactory 中的 BeanExpressionResolver(处理 #{} SpEL)
this.exprResolver = beanFactory.getBeanExpressionResolver();
}
@Override
public String resolveStringValue(String strVal) {
// 先处理 ${...} → 委托给已注册的 StringValueResolver 链
String resolved = this.exprContext.getBeanFactory()
.resolveEmbeddedValue(strVal);
// 再处理 #{...} SpEL 表达式
if (this.exprResolver != null && resolved != null) {
Object evaluated = this.exprResolver.evaluate(
resolved, this.exprContext);
return (evaluated != null) ? evaluated.toString() : resolved;
}
return resolved;
}
}两种方式对比:
| 创建方式 | 创建者 | 处理范围 | 注册位置 |
|---|---|---|---|
PropertySourcesPlaceholderConfigurer | postProcessBeanFactory() | ${...} 属性占位符 | ConfigurableBeanFactory.addEmbeddedValueResolver() |
EmbeddedValueResolver | AbstractApplicationContext | ${...} + #{...} SpEL | ConfigurableBeanFactory 内置 |
5. EmbeddedValueResolver.resolveStringValue() 的委托
EmbeddedValueResolver 的 resolveStringValue() 通过 ConfigurableBeanFactory.resolveEmbeddedValue() 委托给已注册的 StringValueResolver 链。
public abstract class AbstractBeanFactory implements ConfigurableBeanFactory {
// StringValueResolver 链(允许多个解析器)
private final List<StringValueResolver> embeddedValueResolvers =
new ArrayList<>();
@Override
public void addEmbeddedValueResolver(StringValueResolver valueResolver) {
// 添加 StringValueResolver 到链
this.embeddedValueResolvers.add(valueResolver);
}
@Override
public boolean hasEmbeddedValueResolver() {
return !this.embeddedValueResolvers.isEmpty();
}
@Override
public String resolveEmbeddedValue(@Nullable String value) {
if (value == null) return null;
String result = value;
// 遍历所有已注册的 StringValueResolver
for (StringValueResolver resolver : this.embeddedValueResolvers) {
// 每个解析器依次处理
result = resolver.resolveStringValue(result);
if (result == null) return null;
}
return result;
}
}@Value 注解的占位符解析流程:
// AutowiredAnnotationBeanPostProcessor 处理 @Value
public class AutowiredAnnotationBeanPostProcessor {
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 处理 @Autowired、@Value、@Inject 注解
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
metadata.inject(bean, beanName, pvs);
return pvs;
}
// @Value 字段注入
private void injectField(Field field, Object bean, String beanName) {
Value value = field.getAnnotation(Value.class);
if (value != null) {
// 1. 获取 @Value 的值(可能包含 ${...} 或 #{...})
String strVal = value.value();
// 2. 解析占位符 → 委托给 EmbeddedValueResolver
String resolved = beanFactory.resolveEmbeddedValue(strVal);
// 3. 类型转换(String → 目标类型)
Object converted = typeConverter.convertIfNecessary(resolved, field.getType());
// 4. 设置字段值
ReflectionUtils.makeAccessible(field);
field.set(bean, converted);
}
}
}委托链:
@Value("${app.name:defaultName}")
↓
beanFactory.resolveEmbeddedValue("${app.name:defaultName}")
↓
遍历 embeddedValueResolvers 链
↓
① PropertySourcesPlaceholderConfigurer 注册的 StringValueResolver
→ PropertyPlaceholderHelper.parseStringValue()
→ PropertySourcesPlaceholderResolver.resolvePlaceholder("app.name")
→ 如果找到 → 替换为实际值
→ 如果未找到 → 使用默认值 "defaultName"
↓
② EmbeddedValueResolver(#{} 处理)
→ 检查是否包含 #{}
→ 包含 → 调用 BeanExpressionResolver.evaluate()
↓
解析完成 → 类型转换 → 注入字段6. BeanDefinitionVisitor.visitBeanDefinition() 遍历
BeanDefinitionVisitor 负责遍历 BeanDefinition 的所有属性,替换其中的占位符。
public class BeanDefinitionVisitor {
private final StringValueResolver valueResolver;
public BeanDefinitionVisitor(StringValueResolver valueResolver) {
this.valueResolver = valueResolver;
}
public void visitBeanDefinition(BeanDefinition beanDefinition) {
// 1. 访问构造器参数值
visitConstructorArguments(beanDefinition);
// 2. 访问属性值
visitPropertyValues(beanDefinition);
// 3. 访问工厂方法名
visitFactoryMethodName(beanDefinition);
// 4. 访问工厂 Bean 名
visitFactoryBeanName(beanDefinition);
}
// 遍历构造器参数
protected void visitConstructorArguments(BeanDefinition beanDefinition) {
ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
// 遍历索引参数
for (ConstructorArgumentValues.ValueHolder holder : cas.getIndexedArgumentValues().values()) {
Object newVal = resolveValue(holder.getValue());
if (newVal != holder.getValue()) {
holder.setValue(newVal);
}
}
// 遍历通用参数(无名参数)
for (ConstructorArgumentValues.ValueHolder holder : cas.getGenericArgumentValues()) {
Object newVal = resolveValue(holder.getValue());
if (newVal != holder.getValue()) {
holder.setValue(newVal);
}
}
}
// 遍历属性值
protected void visitPropertyValues(BeanDefinition beanDefinition) {
MutablePropertyValues pvs = beanDefinition.getPropertyValues();
for (PropertyValue pv : pvs.getPropertyValueList()) {
Object newVal = resolveValue(pv.getValue());
if (newVal != pv.getValue()) {
pvs.add(pv.getName(), newVal);
}
}
}
// 解析单个值(递归处理嵌套结构)
protected Object resolveValue(@Nullable Object value) {
if (value instanceof TypedStringValue typedStringValue) {
// ① 字符串类型值:替换占位符
String stringValue = typedStringValue.getValue();
if (stringValue != null) {
String resolved = this.valueResolver.resolveStringValue(stringValue);
typedStringValue.setValue(resolved);
}
} else if (value instanceof BeanDefinition bd) {
// ② 内联 BeanDefinition:递归访问
visitBeanDefinition(bd);
} else if (value instanceof BeanDefinitionHolder holder) {
// ③ BeanDefinitionHolder:递归访问
visitBeanDefinition(holder.getBeanDefinition());
} else if (value instanceof RuntimeBeanReference ref) {
// ④ Bean 引用:替换 Bean 名称中的占位符
String beanName = ref.getBeanName();
String resolved = this.valueResolver.resolveStringValue(beanName);
if (!resolved.equals(beanName)) {
return new RuntimeBeanReference(resolved);
}
} else if (value instanceof ManagedList managedList) {
// ⑤ List:递归解析每个元素
for (int i = 0; i < managedList.size(); i++) {
managedList.set(i, resolveValue(managedList.get(i)));
}
} else if (value instanceof ManagedSet managedSet) {
// ⑥ Set:递归解析每个元素
managedSet.forEach(this::resolveValue);
} else if (value instanceof ManagedMap managedMap) {
// ⑦ Map:递归解析每个 key 和 value
for (Object key : managedMap.keySet()) {
managedMap.put(
resolveValue(key), // 解析 key
resolveValue(managedMap.get(key)) // 解析 value
);
}
}
return value;
}
}遍历覆盖的范围:
BeanDefinition
├── constructorArgumentValues ← 构造器参数 ${...}
│ ├── indexedArgumentValues ← 索引参数
│ └── genericArgumentValues ← 无名参数
├── propertyValues ← 属性值 ${...}
│ └── 每个 PropertyValue
│ ├── TypedStringValue ← 字符串值
│ ├── RuntimeBeanReference ← Bean 引用名
│ ├── ManagedList/Set/Map ← 集合元素
│ └── 嵌套 BeanDefinition ← 内联 Bean
├── factoryMethodName ← 工厂方法名
└── factoryBeanName ← 工厂 Bean 名7. BeanDefinitionValueResolver.resolveValueIfNecessary()
BeanDefinitionValueResolver 在 Bean 实例化阶段解析实际值,与 BeanDefinitionVisitor 在 BeanDefinition 处理阶段的占位符替换形成互补。
public class BeanDefinitionValueResolver {
private final AbstractBeanFactory beanFactory;
private final String beanName;
private final BeanDefinition beanDefinition;
// 在需要时解析值(实例化阶段)
public Object resolveValueIfNecessary(Object arg, @Nullable List<PropertyValue> pvs) {
// 1. TypedStringValue:已由 BeanDefinitionVisitor 替换了 ${} 占位符,
// 这里只需类型转换
if (arg instanceof TypedStringValue typedStringValue) {
// 获取目标类型
String targetTypeName = typedStringValue.getTargetTypeName();
Object value;
if (targetTypeName != null) {
// 类型转换
Class<?> targetType = ClassUtils.forName(targetTypeName, ...);
value = this.beanFactory.getTypeConverter()
.convertIfNecessary(typedStringValue.getValue(), targetType);
} else {
value = typedStringValue.getValue();
}
return value;
}
// 2. RuntimeBeanReference:解析 Bean 引用
if (arg instanceof RuntimeBeanReference ref) {
String refName = ref.getBeanName();
// 这里 refName 中的占位符已在 BeanDefinitionVisitor 阶段被替换
return this.beanFactory.getBean(refName, ref.getBeanType());
}
// 3. BeanDefinitionHolder / BeanDefinition:递归创建内联 Bean
if (arg instanceof BeanDefinitionHolder holder) {
return resolveInnerBean(holder.getBeanName(), holder.getBeanDefinition());
}
if (arg instanceof BeanDefinition bd) {
return resolveInnerBean("(inner bean)", bd);
}
// 4. ManagedList/Set/Map:递归解析每个元素
if (arg instanceof ManagedList<?> list) {
List<Object> resolved = new ArrayList<>(list.size());
for (Object val : list) {
resolved.add(resolveValueIfNecessary(val, pvs));
}
return resolved;
}
// 5. 其他情况直接返回
return arg;
}
}两个阶段的交接:
BeanFactoryPostProcessor 阶段(占位符替换):
BeanDefinitionVisitor.resolveValue()
→ 将 TypedStringValue 中的 "${app.name:default}" 替换为 "myApp"
→ beanDefinition.getPropertyValues() 中的 ${} 全部替换为实际字符串
↓
Bean 实例化阶段(类型转换 + 注入):
BeanDefinitionValueResolver.resolveValueIfNecessary()
→ 拿到替换后的字符串 "myApp"
→ 类型转换(String → String 不需要转换)
→ 注入到 Bean 属性8. ignoreUnresolvablePlaceholders=true 的语义
ignoreUnresolvablePlaceholders 控制当占位符无法在 PropertySources 中找到对应值时如何处理。
public abstract class PlaceholderConfigurerSupport
extends PropertyResourceConfigurer {
// 是否忽略不可解析的占位符
private boolean ignoreUnresolvablePlaceholders = false;
public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) {
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
}
}两种行为的对比:
ignoreUnresolvablePlaceholders | 占位符未找到时 | 适用场景 |
|---|---|---|
false(默认) | 抛出 IllegalArgumentException | 严格模式,确保所有配置都被解析 |
true | 保持占位符原样(不替换) | 多配置文件分层处理,允许下游解析器处理 |
多 PropertySourcesPlaceholderConfigurer 协作场景:
@Configuration
public class MultiPlaceholderConfig {
// 第一个配置器:处理 A.properties,忽略未解析的占位符
@Bean
public static PropertySourcesPlaceholderConfigurer configurerA() {
PropertySourcesPlaceholderConfigurer configurer =
new PropertySourcesPlaceholderConfigurer();
configurer.setLocation(new ClassPathResource("A.properties"));
configurer.setIgnoreUnresolvablePlaceholders(true); // 忽略未解析
configurer.setOrder(1);
return configurer;
}
// 第二个配置器:处理 B.properties,抛出异常(严格模式)
@Bean
public static PropertySourcesPlaceholderConfigurer configurerB() {
PropertySourcesPlaceholderConfigurer configurer =
new PropertySourcesPlaceholderConfigurer();
configurer.setLocation(new ClassPathResource("B.properties"));
configurer.setIgnoreUnresolvablePlaceholders(false); // 未解析即报错
configurer.setOrder(2);
return configurer;
}
}PropertyPlaceholderHelper 中的处理逻辑:
// PropertyPlaceholderHelper.parseStringValue() 中
if (propVal != null) {
// 成功解析 → 替换
result.replace(startIndex, endIndex + 1, propVal);
startIndex = result.indexOf(placeholderPrefix, startIndex + propVal.length());
} else if (ignoreUnresolvablePlaceholders) {
// 未解析 + ignore=true → 保持占位符不变,跳过
logger.trace("无法解析占位符 '" + placeholder + "',保持原样");
startIndex = result.indexOf(placeholderPrefix, endIndex + placeholderSuffix.length());
} else {
// 未解析 + ignore=false → 抛出异常
throw new IllegalArgumentException(
"无法在属性源中解析占位符 '" + placeholder + "'");
}9. spring.config.import 的占位符解析
spring.config.import 中的占位符解析在环境准备阶段进行,此时 PropertySourcesPlaceholderConfigurer 尚未注册,使用 Environment 直接解析。
// application.yml
spring:
config:
import: optional:config/${spring.profiles.active:dev}/application.yml
// spring.profiles.active 的默认值为 dev
// → 实际导入: optional:config/dev/application.yml环境准备阶段的占位符解析:
// ConfigDataEnvironment 中处理 spring.config.import
class ConfigDataEnvironment {
private final Environment environment;
void processImports(ConfigDataLocation[] locations) {
for (ConfigDataLocation location : locations) {
// 解析占位符(使用 Environment 直接解析)
String resolved = resolvePlaceholders(location.getValue());
}
}
// 使用 PropertySourcesPlaceholderResolver 的简化版本
private String resolvePlaceholders(String value) {
// 因为此时 PropertySourcesPlaceholderConfigurer 尚未注册,
// 所以使用 ConfigurableEnvironment 自身的 PropertySources
PropertySourcesPlaceholderResolver resolver =
new PropertySourcesPlaceholderResolver(
new EnvironmentPropertySources((ConfigurableEnvironment) this.environment));
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true);
return helper.replacePlaceholders(value, resolver::resolvePlaceholder);
}
}时序问题:
ConfigDataEnvironment 初始化
↓ 使用 Environment PropertySources 直接解析 ${}
↓ spring.config.import 中的占位符被解析
↓
PropertySourcesPlaceholderConfigurer.postProcessBeanFactory()
↓ 使用已合并的 PropertySources 解析 BeanDefinition 中的 ${}
↓
@Value 注入
↓ 通过 BeanFactory.resolveEmbeddedValue() 委托链关键区别:
| 解析目标 | 处理时间 | 使用的解析器 | PropertySource 状态 |
|---|---|---|---|
spring.config.import | 环境准备阶段 | Environment 直接解析 | PropertySource 链已构建但未合并 |
| BeanDefinition 属性 | refresh() 中 invokeBeanFactoryPostProcessors 阶段 | PropertySourcesPlaceholderConfigurer | 完整 PropertySource 链 |
@Value 注解 | Bean 实例化阶段 | BeanFactory.resolveEmbeddedValue() 委托链 | 完整 PropertySource 链 |
10. @Value("${property:defaultValue}") 的默认值处理
@Value 注解支持 ${property:defaultValue} 语法,用 : 分隔属性和默认值。
@Value("${app.name:MyApp}")
private String appName;
@Value("${server.port:8080}")
private int port;
@Value("${logging.level.root:INFO}")
private String rootLogLevel;
@Value("${app.timeout:${default.timeout:3000}}")
private long timeout; // 嵌套默认值默认值解析源码:
// PropertyPlaceholderHelper.parseStringValue() 中的默认值处理
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
// placeholder 可能包含 ":" 分隔的默认值
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
// ${key:default} → key 部分
String actualPlaceholder = placeholder.substring(0, separatorIndex);
// ${key:default} → default 部分
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
// 尝试解析 key 部分
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
// key 未找到 → 使用默认值
propVal = defaultValue;
}
}
}: 分隔符的处理规则:
输入: "${app.name:MyApp}"
↓
parseStringValue 提取 placeholder = "app.name:MyApp"
↓
resolvePlaceholder("app.name:MyApp") → null(整个字符串不匹配)
↓
找到 ":" → separatorIndex = 8
actualPlaceholder = "app.name"
defaultValue = "MyApp"
↓
resolvePlaceholder("app.name") → "MyCustomApp"(找到)
↓
propVal = "MyCustomApp" ← 使用配置值,非默认值
---
输入: "${app.name:MyApp}"
↓
resolvePlaceholder("app.name") → null(未配置)
↓
propVal = "MyApp" ← 使用默认值指定类型转换:
@Value("${app.threadPoolSize:10}")
private int threadPoolSize;
// → "10" → int 10
@Value("${app.enabled:true}")
private boolean enabled;
// → "true" → boolean true
@Value("${app.timeout:PT30S}")
private Duration timeout;
// → "PT30S" → Duration.ofSeconds(30)
@Value("${app.maxFileSize:10MB}")
private DataSize maxFileSize;
// → "10MB" → DataSize.ofMegabytes(10)默认值中的嵌套占位符:
// 嵌套默认值:${outerKey:${innerKey:default}}
@Value("${app.timeout:${default.timeout:3000}}")
private long timeout;
// 解析过程:
// ① 提取 placeholder = "app.timeout:${default.timeout:3000}"
// ② separatorIndex 指向第一个 ":" → actualPlaceholder = "app.timeout"
// ③ resolvePlaceholder("app.timeout") → null
// ④ defaultValue = "${default.timeout:3000}" ← 嵌套占位符
// ⑤ 递归解析 defaultValue → parseStringValue("${default.timeout:3000}")
// → resolvePlaceholder("default.timeout") → null
// → 使用默认值 "3000"
// ⑥ 最终 timeout = 3000总结
属性占位符解析机制的 10 个细节点总结如下:
| # | 细节点 | 核心类/机制 |
|---|---|---|
| ① | PropertySourcesPlaceholderConfigurer.postProcessBeanFactory() 流程 | PropertySources → PlaceholderResolver → StringValueResolver → 替换所有 BeanDefinition |
| ② | PropertyPlaceholderHelper.parseStringValue() 递归解析 | 从内到外多层展开 ${...},支持嵌套、循环检测、默认值 |
| ③ | ${...} vs #{...} 的区别 | PropertySourcesPlaceholderConfigurer 处理 ${} → BeanFactory 处理 #{} SpEL |
| ④ | StringValueResolver 的 2 种创建 | PropertySourcesPlaceholderConfigurer 创建 → EmbeddedValueResolver 注册到 BeanFactory |
| ⑤ | EmbeddedValueResolver.resolveStringValue() 的委托 | ConfigurableBeanFactory.resolveEmbeddedValue() → 遍历 embeddedValueResolvers 链 |
| ⑥ | BeanDefinitionVisitor.visitBeanDefinition() 遍历 | 递归访问构造器参数 / 属性值 / 工厂方法名 / 集合元素中的占位符 |
| ⑦ | BeanDefinitionValueResolver.resolveValueIfNecessary() | 在实例化阶段将替换后的字符串进行类型转换和注入 |
| ⑧ | ignoreUnresolvablePlaceholders=true 的语义 | 占位符未解析时保持原样(默认 false — 抛出异常) |
| ⑨ | spring.config.import 的占位符解析 | 环境准备阶段使用 Environment 直接解析,早于 PropertySourcesPlaceholderConfigurer |
| ⑩ | @Value("${property:defaultValue}") 默认值处理 | : 后为默认值,支持嵌套占位符 outer:${inner:default} |