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:
@@ -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]]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user