Fix placeholder resolution in PropertySourcesPlaceholderConfigurer
Currently, the placeholder resolution algorithm in PropertySourcesPlaceholderConfigurer fails in several scenarios, and the root cause for this category of failures has actually existed since PropertySourcesPlaceholderConfigurer was introduced in Spring Framework 3.1. Specifically, PropertySourcesPlaceholderConfigurer creates its own PropertySourcesPropertyResolver that indirectly delegates to another "nested" PropertySourcesPropertyResolver to interact with PropertySources from the Environment, which results in double placeholder parsing and resolution attempts, and that behavior leads to a whole category of bugs. For example, #27947 was addressed in Spring Framework 5.3.16, and due to #34315 and #34326 we have recently realized that additional bugs exist with placeholder resolution: nested placeholder resolution can fail when escape characters are used, and it is currently impossible to disable the escape character support for nested resolution. To address this category of bugs, we no longer indirectly use or directly create a "nested" PropertySourcesPropertyResolver in PropertySourcesPlaceholderConfigurer. Instead, properties from property sources from the Environment are now accessed directly without duplicate/nested placeholder resolution. See gh-27947 See gh-34326 See gh-34862 Closes gh-34861
This commit is contained in:
parent
457e876303
commit
3295289e17
|
@ -24,12 +24,12 @@ import org.springframework.beans.factory.BeanInitializationException;
|
|||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.PlaceholderConfigurerSupport;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.ConfigurablePropertyResolver;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertyResolver;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.env.PropertySources;
|
||||
import org.springframework.core.env.PropertySourcesPropertyResolver;
|
||||
|
@ -133,28 +133,24 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
|
|||
if (this.propertySources == null) {
|
||||
this.propertySources = new MutablePropertySources();
|
||||
if (this.environment != null) {
|
||||
PropertyResolver propertyResolver = this.environment;
|
||||
// If the ignoreUnresolvablePlaceholders flag is set to true, we have to create a
|
||||
// local PropertyResolver to enforce that setting, since the Environment is most
|
||||
// likely not configured with ignoreUnresolvablePlaceholders set to true.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27947
|
||||
if (this.ignoreUnresolvablePlaceholders &&
|
||||
(this.environment instanceof ConfigurableEnvironment configurableEnvironment)) {
|
||||
PropertySourcesPropertyResolver resolver =
|
||||
new PropertySourcesPropertyResolver(configurableEnvironment.getPropertySources());
|
||||
resolver.setIgnoreUnresolvableNestedPlaceholders(true);
|
||||
propertyResolver = resolver;
|
||||
PropertySource<?> environmentPropertySource;
|
||||
if (this.environment instanceof ConfigurableEnvironment configurableEnvironment) {
|
||||
environmentPropertySource = new CompositePropertySource(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME,
|
||||
configurableEnvironment.getPropertySources());
|
||||
}
|
||||
PropertyResolver propertyResolverToUse = propertyResolver;
|
||||
this.propertySources.addLast(
|
||||
else {
|
||||
// Fallback code path that should never apply in a regular scenario, since the
|
||||
// Environment in the ApplicationContext should always be a ConfigurableEnvironment.
|
||||
environmentPropertySource =
|
||||
new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
|
||||
@Override
|
||||
@Nullable
|
||||
public String getProperty(String key) {
|
||||
return propertyResolverToUse.getProperty(key);
|
||||
public Object getProperty(String key) {
|
||||
return super.source.getProperty(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
this.propertySources.addLast(environmentPropertySource);
|
||||
}
|
||||
try {
|
||||
PropertySource<?> localPropertySource =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -19,11 +19,13 @@ package org.springframework.context.support;
|
|||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
@ -432,6 +434,150 @@ class PropertySourcesPlaceholderConfigurerTests {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that use the escape character (or disable it) with nested placeholder
|
||||
* resolution.
|
||||
*/
|
||||
@Nested
|
||||
class EscapedNestedPlaceholdersTests {
|
||||
|
||||
@Test // gh-34861
|
||||
void singleEscapeWithDefaultEscapeCharacter() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "admin")
|
||||
.withProperty("my.property", "\\DOMAIN\\${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
// \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void singleEscapeWithCustomEscapeCharacter() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "admin\\~${nested}")
|
||||
.withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
// Set custom escape character.
|
||||
ppc.setEscapeCharacter('~');
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void singleEscapeWithEscapeCharacterDisabled() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "admin\\")
|
||||
.withProperty("my.property", "\\DOMAIN\\${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
// Disable escape character.
|
||||
ppc.setEscapeCharacter(null);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
// \DOMAIN\${user.home} resolves to \DOMAIN\admin
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin\\");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void tripleEscapeWithDefaultEscapeCharacter() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "admin\\\\\\")
|
||||
.withProperty("my.property", "DOMAIN\\\\\\${user.home}#${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\\\${user.home}#admin\\\\\\");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void tripleEscapeWithCustomEscapeCharacter() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "admin\\~${enigma}")
|
||||
.withProperty("my.property", "DOMAIN~~~${user.home}#${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
// Set custom escape character.
|
||||
ppc.setEscapeCharacter('~');
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN~~${user.home}#admin\\${enigma}");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void singleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "${enigma}")
|
||||
.withProperty("my.property", "\\${DOMAIN}${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
ppc.setIgnoreUnresolvablePlaceholders(true);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}${enigma}");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void singleEscapeWithCustomEscapeCharacterAndIgnoreUnresolvablePlaceholders() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "${enigma}")
|
||||
.withProperty("my.property", "~${DOMAIN}\\${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
// Set custom escape character.
|
||||
ppc.setEscapeCharacter('~');
|
||||
ppc.setIgnoreUnresolvablePlaceholders(true);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}\\${enigma}");
|
||||
}
|
||||
|
||||
@Test // gh-34861
|
||||
void tripleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() {
|
||||
MockEnvironment env = new MockEnvironment()
|
||||
.withProperty("user.home", "${enigma}")
|
||||
.withProperty("my.property", "X:\\\\\\${DOMAIN}${user.home}");
|
||||
|
||||
DefaultListableBeanFactory bf = createBeanFactory();
|
||||
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
|
||||
ppc.setEnvironment(env);
|
||||
ppc.setIgnoreUnresolvablePlaceholders(true);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
|
||||
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("X:\\\\${DOMAIN}${enigma}");
|
||||
}
|
||||
|
||||
private static DefaultListableBeanFactory createBeanFactory() {
|
||||
BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${my.property}")
|
||||
.getBeanDefinition();
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("testBean",beanDefinition);
|
||||
return bf;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class OptionalTestBean {
|
||||
|
||||
private Optional<String> name;
|
||||
|
|
Loading…
Reference in New Issue