Add global optional config data opt-out

Support a `spring.config.all-locations-optional` property which can be
set to `true` if all config data locations should be considered
optional.

Closes gh-23097
This commit is contained in:
Phillip Webb
2020-08-26 20:11:40 -07:00
parent c1be5cb5e0
commit aa0f204732
10 changed files with 75 additions and 40 deletions

View File

@@ -623,6 +623,9 @@ You can use this prefix with the `spring.config.location` and `spring.config.add
For example, a `spring.config.import` value of `optional:file:./myconfig.properties` allows your application to start, even if the `myconfig.properties` file is missing.
If you want all locations to be considered optional, you can use the `spring.config.all-locations-optional` property.
Set the value to `true` using `SpringApplication.setDefaultProperties(...)` or with a system/environment variable.
[[boot-features-external-config-files-wildcards]]

View File

@@ -50,8 +50,8 @@ import org.springframework.util.StringUtils;
* {@link Environment} and adding the initial set of imports.
* <p>
* The initial imports can be influenced via the {@link #LOCATION_PROPERTY},
* {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #SPRING_CONFIG_IMPORT} properties.
* If not explicit properties are set, the {@link #DEFAULT_SEARCH_LOCATIONS} will be used.
* {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #IMPORT_PROPERTY} properties. If not
* explicit properties are set, the {@link #DEFAULT_SEARCH_LOCATIONS} will be used.
*
* @author Phillip Webb
* @author Madhura Bhave
@@ -71,7 +71,13 @@ class ConfigDataEnvironment {
/**
* Property used to provide additional locations to import.
*/
static final String SPRING_CONFIG_IMPORT = "spring.config.import";
static final String IMPORT_PROPERTY = "spring.config.import";
/**
* Property used to determine if all locations are optional and
* {@code ConfigDataLocationNotFoundExceptions} should be ignored.
*/
static final String ALL_LOCATIONS_OPTIONAL_PROPERTY = "spring.config.all-locations-optional";
/**
* Default search locations used if not {@link #LOCATION_PROPERTY} is found.
@@ -114,19 +120,20 @@ class ConfigDataEnvironment {
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
Binder binder = Binder.get(environment);
UseLegacyConfigProcessingException.throwIfRequested(binder);
boolean allLocationsOptional = binder.bind(ALL_LOCATIONS_OPTIONAL_PROPERTY, Boolean.class).orElse(false);
this.logFactory = logFactory;
this.logger = logFactory.getLog(getClass());
this.bootstrapRegistry = bootstrapRegistry;
this.environment = environment;
this.resolvers = createConfigDataLocationResolvers(logFactory, binder, resourceLoader);
this.resolvers = createConfigDataLocationResolvers(logFactory, allLocationsOptional, binder, resourceLoader);
this.additionalProfiles = additionalProfiles;
this.loaders = new ConfigDataLoaders(logFactory);
this.loaders = new ConfigDataLoaders(logFactory, allLocationsOptional);
this.contributors = createContributors(binder);
}
protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
Binder binder, ResourceLoader resourceLoader) {
return new ConfigDataLocationResolvers(logFactory, binder, resourceLoader);
boolean allLocationsOptional, Binder binder, ResourceLoader resourceLoader) {
return new ConfigDataLocationResolvers(logFactory, allLocationsOptional, binder, resourceLoader);
}
private ConfigDataEnvironmentContributors createContributors(Binder binder) {
@@ -159,7 +166,7 @@ class ConfigDataEnvironment {
private List<ConfigDataEnvironmentContributor> getInitialImportContributors(Binder binder) {
List<ConfigDataEnvironmentContributor> initialContributors = new ArrayList<>();
addInitialImportContributors(initialContributors,
binder.bind(SPRING_CONFIG_IMPORT, String[].class).orElse(EMPTY_LOCATIONS));
binder.bind(IMPORT_PROPERTY, String[].class).orElse(EMPTY_LOCATIONS));
addInitialImportContributors(initialContributors,
binder.bind(ADDITIONAL_LOCATION_PROPERTY, String[].class).orElse(EMPTY_LOCATIONS));
addInitialImportContributors(initialContributors,

View File

@@ -50,6 +50,12 @@ public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProces
*/
public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
/**
* Property used to determine if all locations are optional and
* {@code ConfigDataLocationNotFoundExceptions} should be ignored.
*/
public static final String ALL_LOCATIONS_OPTIONAL_PROPERTY = ConfigDataEnvironment.ALL_LOCATIONS_OPTIONAL_PROPERTY;
private final DeferredLogFactory logFactory;
private final Log logger;

View File

@@ -40,25 +40,30 @@ class ConfigDataLoaders {
private final Log logger;
private final boolean allLocationsOptional;
private final List<ConfigDataLoader<?>> loaders;
private final List<Class<?>> locationTypes;
/**
* Create a new {@link ConfigDataLoaders} instance.
* @param allLocationsOptional if all locations are considered optional
* @param logFactory the deferred log factory
*/
ConfigDataLoaders(DeferredLogFactory logFactory) {
this(logFactory, SpringFactoriesLoader.loadFactoryNames(ConfigDataLoader.class, null));
ConfigDataLoaders(DeferredLogFactory logFactory, boolean allLocationsOptional) {
this(logFactory, allLocationsOptional, SpringFactoriesLoader.loadFactoryNames(ConfigDataLoader.class, null));
}
/**
* Create a new {@link ConfigDataLoaders} instance.
* @param logFactory the deferred log factory
* @param allLocationsOptional if all locations are considered optional
* @param names the {@link ConfigDataLoader} class names instantiate
*/
ConfigDataLoaders(DeferredLogFactory logFactory, List<String> names) {
ConfigDataLoaders(DeferredLogFactory logFactory, boolean allLocationsOptional, List<String> names) {
this.logger = logFactory.getLog(getClass());
this.allLocationsOptional = allLocationsOptional;
Instantiator<ConfigDataLoader<?>> instantiator = new Instantiator<>(ConfigDataLoader.class,
(availableParameters) -> availableParameters.add(Log.class, logFactory::getLog));
this.loaders = instantiator.instantiate(names);
@@ -99,11 +104,11 @@ class ConfigDataLoaders {
return loader.load(context, location);
}
catch (ConfigDataLocationNotFoundException ex) {
if (!optional) {
throw ex;
if (this.allLocationsOptional || optional) {
this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location));
return null;
}
this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location));
return null;
throw ex;
}
}

View File

@@ -43,29 +43,35 @@ class ConfigDataLocationResolvers {
private final Log logger;
private final boolean allLocationsOptional;
private final List<ConfigDataLocationResolver<?>> resolvers;
/**
* Create a new {@link ConfigDataLocationResolvers} instance.
* @param logFactory a {@link DeferredLogFactory} used to inject {@link Log} instances
* @param allLocationsOptional if all locations are considered optional
* @param binder a binder providing values from the initial {@link Environment}
* @param resourceLoader {@link ResourceLoader} to load resource locations
*/
ConfigDataLocationResolvers(DeferredLogFactory logFactory, Binder binder, ResourceLoader resourceLoader) {
this(logFactory, binder, resourceLoader,
ConfigDataLocationResolvers(DeferredLogFactory logFactory, boolean allLocationsOptional, Binder binder,
ResourceLoader resourceLoader) {
this(logFactory, allLocationsOptional, binder, resourceLoader,
SpringFactoriesLoader.loadFactoryNames(ConfigDataLocationResolver.class, null));
}
/**
* Create a new {@link ConfigDataLocationResolvers} instance.
* @param logFactory a {@link DeferredLogFactory} used to inject {@link Log} instances
* @param allLocationsOptional if all locations are considered optional
* @param binder {@link Binder} providing values from the initial {@link Environment}
* @param resourceLoader {@link ResourceLoader} to load resource locations
* @param names the {@link ConfigDataLocationResolver} class names
*/
ConfigDataLocationResolvers(DeferredLogFactory logFactory, Binder binder, ResourceLoader resourceLoader,
List<String> names) {
ConfigDataLocationResolvers(DeferredLogFactory logFactory, boolean allLocationsOptional, Binder binder,
ResourceLoader resourceLoader, List<String> names) {
this.logger = logFactory.getLog(getClass());
this.allLocationsOptional = allLocationsOptional;
Instantiator<ConfigDataLocationResolver<?>> instantiator = new Instantiator<>(ConfigDataLocationResolver.class,
(availableParameters) -> {
availableParameters.add(Log.class, logFactory::getLog);
@@ -146,11 +152,11 @@ class ConfigDataLocationResolvers {
return resolved;
}
catch (ConfigDataLocationNotFoundException ex) {
if (!optional) {
throw ex;
if (this.allLocationsOptional || optional) {
this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location));
return Collections.emptyList();
}
this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location));
return Collections.emptyList();
throw ex;
}
}

View File

@@ -80,8 +80,9 @@ class ConfigDataEnvironmentContributorsTests {
void setup() {
this.environment = new MockEnvironment();
this.binder = Binder.get(this.environment);
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder, null);
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory);
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, true, this.binder,
null);
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, false);
this.importer = new ConfigDataImporter(resolvers, loaders);
this.activationContext = new ConfigDataActivationContext(CloudPlatform.KUBERNETES, null);
}

View File

@@ -549,6 +549,12 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
() -> this.application.run("--spring.config.location=classpath:missing-appplication.properties"));
}
@Test
void runWhenHasNonOptionalImportAndFailFalsePropertyIgnoresException() {
this.application.run("--spring.config.fail-on-location-not-found=false",
"--spring.config.location=classpath:missing-appplication.properties");
}
@Test
void runWhenHasIncludedProfilesActivatesProfiles() {
ConfigurableApplicationContext context = this.application

View File

@@ -214,9 +214,10 @@ class ConfigDataEnvironmentTests {
@Override
protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
Binder binder, ResourceLoader resourceLoader) {
boolean failOnConfigDataLocationNotFound, Binder binder, ResourceLoader resourceLoader) {
this.configDataLocationResolversBinder = binder;
return super.createConfigDataLocationResolvers(logFactory, binder, resourceLoader);
return super.createConfigDataLocationResolvers(logFactory, failOnConfigDataLocationNotFound, binder,
resourceLoader);
}
Binder getConfigDataLocationResolversBinder() {

View File

@@ -46,13 +46,13 @@ class ConfigDataLoadersTests {
@Test
void createWhenLoaderHasLogParameterInjectsLog() {
new ConfigDataLoaders(this.logFactory, Arrays.asList(LoggingConfigDataLoader.class.getName()));
new ConfigDataLoaders(this.logFactory, true, Arrays.asList(LoggingConfigDataLoader.class.getName()));
}
@Test
void loadWhenSingleLoaderSupportsLocationReturnsLoadedConfigData() throws Exception {
TestConfigDataLocation location = new TestConfigDataLocation("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory,
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, false,
Arrays.asList(TestConfigDataLoader.class.getName()));
ConfigData loaded = loaders.load(this.context, location);
assertThat(getLoader(loaded)).isInstanceOf(TestConfigDataLoader.class);
@@ -61,7 +61,7 @@ class ConfigDataLoadersTests {
@Test
void loadWhenMultipleLoadersSupportLocationThrowsException() throws Exception {
TestConfigDataLocation location = new TestConfigDataLocation("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory,
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, false,
Arrays.asList(LoggingConfigDataLoader.class.getName(), TestConfigDataLoader.class.getName()));
assertThatIllegalStateException().isThrownBy(() -> loaders.load(this.context, location))
.withMessageContaining("Multiple loaders found for location test");
@@ -70,7 +70,7 @@ class ConfigDataLoadersTests {
@Test
void loadWhenNoLoaderSupportsLocationThrowsException() {
TestConfigDataLocation location = new TestConfigDataLocation("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory,
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, false,
Arrays.asList(NonLoadableConfigDataLoader.class.getName()));
assertThatIllegalStateException().isThrownBy(() -> loaders.load(this.context, location))
.withMessage("No loader found for location 'test'");
@@ -79,7 +79,7 @@ class ConfigDataLoadersTests {
@Test
void loadWhenGenericTypeDoesNotMatchSkipsLoader() throws Exception {
TestConfigDataLocation location = new TestConfigDataLocation("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory,
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, false,
Arrays.asList(OtherConfigDataLoader.class.getName(), SpecificConfigDataLoader.class.getName()));
ConfigData loaded = loaders.load(this.context, location);
assertThat(getLoader(loaded)).isInstanceOf(SpecificConfigDataLoader.class);

View File

@@ -63,7 +63,7 @@ class ConfigDataLocationResolversTests {
@Test
void createWhenInjectingBinderCreatesResolver() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder,
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader, Collections.singletonList(TestBoundResolver.class.getName()));
assertThat(resolvers.getResolvers()).hasSize(1);
assertThat(resolvers.getResolvers().get(0)).isExactlyInstanceOf(TestBoundResolver.class);
@@ -72,7 +72,7 @@ class ConfigDataLocationResolversTests {
@Test
void createWhenNotInjectingBinderCreatesResolver() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder,
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader, Collections.singletonList(TestResolver.class.getName()));
assertThat(resolvers.getResolvers()).hasSize(1);
assertThat(resolvers.getResolvers().get(0)).isExactlyInstanceOf(TestResolver.class);
@@ -81,8 +81,8 @@ class ConfigDataLocationResolversTests {
@Test
void createWhenNameIsNotConfigDataLocationResolverThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ConfigDataLocationResolvers(this.logFactory, this.binder, this.resourceLoader,
Collections.singletonList(InputStream.class.getName())))
.isThrownBy(() -> new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader, Collections.singletonList(InputStream.class.getName())))
.withMessageContaining("Unable to instantiate").havingCause().withMessageContaining("not assignable");
}
@@ -92,7 +92,7 @@ class ConfigDataLocationResolversTests {
names.add(TestResolver.class.getName());
names.add(LowestTestResolver.class.getName());
names.add(HighestTestResolver.class.getName());
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder,
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader, names);
assertThat(resolvers.getResolvers().get(0)).isExactlyInstanceOf(HighestTestResolver.class);
assertThat(resolvers.getResolvers().get(1)).isExactlyInstanceOf(TestResolver.class);
@@ -101,7 +101,7 @@ class ConfigDataLocationResolversTests {
@Test
void resolveAllResolvesUsingFirstSupportedResolver() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder,
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader,
Arrays.asList(LowestTestResolver.class.getName(), HighestTestResolver.class.getName()));
List<ConfigDataLocation> resolved = resolvers.resolveAll(this.context,
@@ -115,7 +115,7 @@ class ConfigDataLocationResolversTests {
@Test
void resolveAllWhenProfileMergesResolvedLocations() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder,
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader,
Arrays.asList(LowestTestResolver.class.getName(), HighestTestResolver.class.getName()));
List<ConfigDataLocation> resolved = resolvers.resolveAll(this.context,
@@ -133,7 +133,7 @@ class ConfigDataLocationResolversTests {
@Test
void resolveWhenNoResolverThrowsException() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.binder,
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, false, this.binder,
this.resourceLoader,
Arrays.asList(LowestTestResolver.class.getName(), HighestTestResolver.class.getName()));
assertThatExceptionOfType(UnsupportedConfigDataLocationException.class)