diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index ab359e9585..2aa5207aad 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -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]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java index 410b7f2794..14d6e7fbe0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java @@ -50,8 +50,8 @@ import org.springframework.util.StringUtils; * {@link Environment} and adding the initial set of imports. *

* 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 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 getInitialImportContributors(Binder binder) { List 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, diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java index 3f7c0eb58a..4bc000564f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java index 9aa0c07219..935a205e45 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java @@ -40,25 +40,30 @@ class ConfigDataLoaders { private final Log logger; + private final boolean allLocationsOptional; + private final List> loaders; private final List> 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 names) { + ConfigDataLoaders(DeferredLogFactory logFactory, boolean allLocationsOptional, List names) { this.logger = logFactory.getLog(getClass()); + this.allLocationsOptional = allLocationsOptional; Instantiator> 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; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java index bfe2991ef5..34d7b35b5f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java @@ -43,29 +43,35 @@ class ConfigDataLocationResolvers { private final Log logger; + private final boolean allLocationsOptional; + private final List> 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 names) { + ConfigDataLocationResolvers(DeferredLogFactory logFactory, boolean allLocationsOptional, Binder binder, + ResourceLoader resourceLoader, List names) { this.logger = logFactory.getLog(getClass()); + this.allLocationsOptional = allLocationsOptional; Instantiator> 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; } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorsTests.java index 012fff9912..2c8c117084 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorsTests.java @@ -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); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java index 53e2b06cb5..431eb3b102 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java index a4988661d3..9b6bf67a1e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java @@ -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() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java index d8311df8a3..08ca86e348 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java index b3c16b24e6..09c7f42ab9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java @@ -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 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 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)