diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 00c96ea941..f0f134cff9 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -354,11 +354,39 @@ spring: +[[features.external-config.files.env-variables]] +=== Using Environment Variables + +When running applications on a cloud platform (such as Kubernetes) you often need to read config values that the platform supplies. +You can either use environment variables for such purpose, or you can use xref:reference:features/external-config.adoc#features.external-config.files.configtree[configuration trees]. + +You can even store whole configurations in properties or yaml format in (multiline) environment variables and load them using the `env:` prefix. +Assume there's an environment variable called `MY_CONFIGURATION` with this content: + +[source,properties] +---- +my.name=Service1 +my.cluster=Cluster1 +---- + +Using the `env:` prefix it is possible to import all properties from this variable: + +[configprops,yaml] +---- +spring: + config: + import: "env:MY_CONFIGURATION" +---- + +TIP: This feature also supports xref:reference:features/external-config.adoc#features.external-config.files.importing-extensionless[specifying the extension]. +The default extension is `.properties`. + + + [[features.external-config.files.configtree]] === Using Configuration Trees -When running applications on a cloud platform (such as Kubernetes) you often need to read config values that the platform supplies. -It is not uncommon to use environment variables for such purposes, but this can have drawbacks, especially if the value is supposed to be kept secret. +Storing config values in environment variables has drawbacks, especially if the value is supposed to be kept secret. As an alternative to environment variables, many cloud platforms now allow you to map configuration into mounted data volumes. For example, Kubernetes can volume mount both https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#populate-a-volume-with-data-stored-in-a-configmap[`ConfigMaps`] and https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-files-from-a-pod[`Secrets`]. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataLoader.java new file mode 100644 index 0000000000..31db4cbe4d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataLoader.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +import org.springframework.core.io.ByteArrayResource; + +/** + * {@link ConfigDataLoader} to load data from environment variables. + * + * @author Moritz Halbritter + */ +class EnvConfigDataLoader implements ConfigDataLoader { + + private final Function readEnvVariable; + + EnvConfigDataLoader() { + this.readEnvVariable = System::getenv; + } + + EnvConfigDataLoader(Function readEnvVariable) { + this.readEnvVariable = readEnvVariable; + } + + @Override + public ConfigData load(ConfigDataLoaderContext context, EnvConfigDataResource resource) + throws IOException, ConfigDataResourceNotFoundException { + String content = this.readEnvVariable.apply(resource.getVariableName()); + if (content == null) { + throw new ConfigDataResourceNotFoundException(resource); + } + String name = String.format("Environment variable '%s' via location '%s'", resource.getVariableName(), + resource.getLocation()); + return new ConfigData(resource.getLoader().load(name, createResource(content))); + } + + private ByteArrayResource createResource(String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataLocationResolver.java new file mode 100644 index 0000000000..3e28b35ec3 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataLocationResolver.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; + +/** + * {@link ConfigDataLocationResolver} to resolve {@code env:} locations. + * + * @author Moritz Halbritter + */ +class EnvConfigDataLocationResolver implements ConfigDataLocationResolver { + + private static final String PREFIX = "env:"; + + private static final Pattern EXTENSION_HINT_PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)](?!\\[)$"); + + private static final String DEFAULT_EXTENSION = ".properties"; + + private final List loaders; + + private final Function readEnvVariable; + + EnvConfigDataLocationResolver() { + this.loaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); + this.readEnvVariable = System::getenv; + } + + EnvConfigDataLocationResolver(List loaders, Function readEnvVariable) { + this.loaders = loaders; + this.readEnvVariable = readEnvVariable; + } + + @Override + public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { + return location.hasPrefix(PREFIX); + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) + throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { + String value = location.getNonPrefixedValue(PREFIX); + Matcher matcher = EXTENSION_HINT_PATTERN.matcher(value); + String extension = getExtension(matcher); + String variableName = getVariableName(matcher, value); + PropertySourceLoader loader = getLoader(extension); + if (hasEnvVariable(variableName)) { + return List.of(new EnvConfigDataResource(location, variableName, loader)); + } + if (location.isOptional()) { + return Collections.emptyList(); + } + throw new ConfigDataLocationNotFoundException(location, + "Environment variable '%s' is not set".formatted(variableName), null); + } + + private PropertySourceLoader getLoader(String extension) { + if (extension == null) { + extension = DEFAULT_EXTENSION; + } + if (extension.startsWith(".")) { + extension = extension.substring(1); + } + for (PropertySourceLoader loader : this.loaders) { + for (String supportedExtension : loader.getFileExtensions()) { + if (supportedExtension.equalsIgnoreCase(extension)) { + return loader; + } + } + } + throw new IllegalStateException( + "File extension '%s' is not known to any PropertySourceLoader".formatted(extension)); + } + + private boolean hasEnvVariable(String variableName) { + return this.readEnvVariable.apply(variableName) != null; + } + + private String getVariableName(Matcher matcher, String value) { + if (matcher.matches()) { + return matcher.group(1); + } + return value; + } + + private String getExtension(Matcher matcher) { + if (matcher.matches()) { + return matcher.group(2); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataResource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataResource.java new file mode 100644 index 0000000000..1c7b0c1aa9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/EnvConfigDataResource.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.Objects; + +import org.springframework.boot.env.PropertySourceLoader; + +/** + * {@link ConfigDataResource} used by {@link EnvConfigDataLoader}. + * + * @author Moritz Halbritter + */ +class EnvConfigDataResource extends ConfigDataResource { + + private final ConfigDataLocation location; + + private final String variableName; + + private final PropertySourceLoader loader; + + EnvConfigDataResource(ConfigDataLocation location, String variableName, PropertySourceLoader loader) { + super(location.isOptional()); + this.location = location; + this.variableName = variableName; + this.loader = loader; + } + + ConfigDataLocation getLocation() { + return this.location; + } + + String getVariableName() { + return this.variableName; + } + + PropertySourceLoader getLoader() { + return this.loader; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + EnvConfigDataResource that = (EnvConfigDataResource) o; + return Objects.equals(this.location, that.location) && Objects.equals(this.variableName, that.variableName) + && Objects.equals(this.loader, that.loader); + } + + @Override + public int hashCode() { + return Objects.hash(this.location, this.variableName, this.loader); + } + + @Override + public String toString() { + return "env variable [" + this.variableName + "]"; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index 194863fb9e..b00a286143 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -12,11 +12,13 @@ org.springframework.boot.env.YamlPropertySourceLoader # ConfigData Location Resolvers org.springframework.boot.context.config.ConfigDataLocationResolver=\ org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\ +org.springframework.boot.context.config.EnvConfigDataLocationResolver,\ org.springframework.boot.context.config.StandardConfigDataLocationResolver # ConfigData Loaders org.springframework.boot.context.config.ConfigDataLoader=\ org.springframework.boot.context.config.ConfigTreeConfigDataLoader,\ +org.springframework.boot.context.config.EnvConfigDataLoader,\ org.springframework.boot.context.config.StandardConfigDataLoader # Application Context Factories diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataLoaderTests.java new file mode 100644 index 0000000000..c4f4c8f2b9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataLoaderTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.env.PropertiesPropertySourceLoader; +import org.springframework.core.env.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EnvConfigDataLoader}. + * + * @author Moritz Halbritter + */ +class EnvConfigDataLoaderTests { + + private ConfigDataLoaderContext context; + + private Map envVariables; + + private EnvConfigDataLoader loader; + + @BeforeEach + void setUp() { + this.context = mock(ConfigDataLoaderContext.class); + this.envVariables = new HashMap<>(); + this.loader = new EnvConfigDataLoader(this.envVariables::get); + } + + @Test + void shouldLoadFromVariable() throws IOException { + this.envVariables.put("VAR1", "key1=value1"); + ConfigData data = this.loader.load(this.context, createResource("VAR1")); + assertThat(data.getPropertySources()).hasSize(1); + PropertySource propertySource = data.getPropertySources().get(0); + assertThat(propertySource.getProperty("key1")).isEqualTo("value1"); + } + + @Test + void shouldFailIfVariableIsNotSet() { + assertThatExceptionOfType(ConfigDataResourceNotFoundException.class) + .isThrownBy(() -> this.loader.load(this.context, createResource("VAR1"))) + .withMessage("Config data resource 'env variable [VAR1]' cannot be found"); + } + + private static EnvConfigDataResource createResource(String variableName) { + return new EnvConfigDataResource(ConfigDataLocation.of("env:" + variableName), variableName, + new PropertiesPropertySourceLoader()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataLocationResolverTests.java new file mode 100644 index 0000000000..d9db28d57e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataLocationResolverTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.env.PropertiesPropertySourceLoader; +import org.springframework.boot.env.YamlPropertySourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EnvConfigDataLocationResolver}. + * + * @author Moritz Halbritter + */ +class EnvConfigDataLocationResolverTests { + + private EnvConfigDataLocationResolver resolver; + + private Map envVariables; + + private ConfigDataLocationResolverContext context; + + @BeforeEach + void setUp() { + this.context = mock(ConfigDataLocationResolverContext.class); + this.envVariables = new HashMap<>(); + this.resolver = new EnvConfigDataLocationResolver( + List.of(new PropertiesPropertySourceLoader(), new YamlPropertySourceLoader()), this.envVariables::get); + } + + @Test + void isResolvable() { + assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("env:VAR1"))).isTrue(); + assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("dummy:VAR1"))).isFalse(); + assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("VAR1"))).isFalse(); + } + + @Test + void shouldResolve() { + this.envVariables.put("VAR1", "VALUE1"); + ConfigDataLocation location = ConfigDataLocation.of("env:VAR1"); + List resolved = this.resolver.resolve(this.context, location); + assertThat(resolved).hasSize(1); + EnvConfigDataResource resource = resolved.get(0); + assertThat(resource.getLocation()).isEqualTo(location); + assertThat(resource.getVariableName()).isEqualTo("VAR1"); + assertThat(resource.getLoader()).isInstanceOf(PropertiesPropertySourceLoader.class); + } + + @Test + void shouldResolveOptional() { + this.envVariables.put("VAR1", "VALUE1"); + ConfigDataLocation location = ConfigDataLocation.of("optional:env:VAR1"); + List resolved = this.resolver.resolve(this.context, location); + assertThat(resolved).hasSize(1); + EnvConfigDataResource resource = resolved.get(0); + assertThat(resource.getLocation()).isEqualTo(location); + assertThat(resource.getVariableName()).isEqualTo("VAR1"); + assertThat(resource.getLoader()).isInstanceOf(PropertiesPropertySourceLoader.class); + } + + @Test + void shouldResolveOptionalIfVariableIsNotSet() { + ConfigDataLocation location = ConfigDataLocation.of("optional:env:VAR1"); + List resolved = this.resolver.resolve(this.context, location); + assertThat(resolved).isEmpty(); + } + + @Test + void shouldResolveWithPropertiesExtension() { + this.envVariables.put("VAR1", "VALUE1"); + ConfigDataLocation location = ConfigDataLocation.of("env:VAR1[.properties]"); + List resolved = this.resolver.resolve(this.context, location); + assertThat(resolved).hasSize(1); + EnvConfigDataResource resource = resolved.get(0); + assertThat(resource.getLocation()).isEqualTo(location); + assertThat(resource.getVariableName()).isEqualTo("VAR1"); + assertThat(resource.getLoader()).isInstanceOf(PropertiesPropertySourceLoader.class); + } + + @Test + void shouldResolveWithYamlExtension() { + this.envVariables.put("VAR1", "VALUE1"); + ConfigDataLocation location = ConfigDataLocation.of("env:VAR1[.yaml]"); + List resolved = this.resolver.resolve(this.context, location); + assertThat(resolved).hasSize(1); + EnvConfigDataResource resource = resolved.get(0); + assertThat(resource.getLocation()).isEqualTo(location); + assertThat(resource.getVariableName()).isEqualTo("VAR1"); + assertThat(resource.getLoader()).isInstanceOf(YamlPropertySourceLoader.class); + } + + @Test + void shouldFailIfVariableIsNotSet() { + assertThatExceptionOfType(ConfigDataLocationNotFoundException.class) + .isThrownBy(() -> this.resolver.resolve(this.context, ConfigDataLocation.of("env:VAR1"))) + .withMessage("Environment variable 'VAR1' is not set"); + } + + @Test + void shouldFailIfUnknownExtensionIsGiven() { + assertThatIllegalStateException() + .isThrownBy(() -> this.resolver.resolve(this.context, ConfigDataLocation.of("env:VAR1[.dummy]"))) + .withMessage("File extension 'dummy' is not known to any PropertySourceLoader"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataResourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataResourceTests.java new file mode 100644 index 0000000000..1362732d10 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/EnvConfigDataResourceTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.env.PropertiesPropertySourceLoader; +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.boot.env.YamlPropertySourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EnvConfigDataResource}. + * + * @author Moritz Halbritter + */ +class EnvConfigDataResourceTests { + + private final YamlPropertySourceLoader yamlPropertySourceLoader = new YamlPropertySourceLoader(); + + private final PropertiesPropertySourceLoader propertiesPropertySourceLoader = new PropertiesPropertySourceLoader(); + + @Test + void shouldHaveEqualsAndHashcode() { + EnvConfigDataResource var1 = createResource("VAR1"); + EnvConfigDataResource var2 = createResource("VAR2"); + EnvConfigDataResource var3 = createResource("VAR1", this.yamlPropertySourceLoader); + EnvConfigDataResource var4 = createResource("VAR1"); + assertThat(var1).isNotEqualTo(var2); + assertThat(var1).isNotEqualTo(var3); + assertThat(var1).isEqualTo(var4); + assertThat(var1).hasSameHashCodeAs(var4); + } + + @Test + void shouldHaveToString() { + EnvConfigDataResource resource = createResource("VAR1"); + assertThat(resource).hasToString("env variable [VAR1]"); + } + + private EnvConfigDataResource createResource(String variableName) { + return createResource(variableName, this.propertiesPropertySourceLoader); + } + + private EnvConfigDataResource createResource(String variableName, PropertySourceLoader propertySourceLoader) { + return new EnvConfigDataResource(ConfigDataLocation.of("env:" + variableName), variableName, + propertySourceLoader); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/build.gradle new file mode 100644 index 0000000000..da5ed3abc4 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "java" +} + +description = "Spring Boot Config smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) +} + +tasks.named("test", Test) { + environment "SMOKE_TEST_CONFIG_ENV", "from-env.key1=value1" +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/java/smoketest/config/FromEnvConfigurationProperties.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/java/smoketest/config/FromEnvConfigurationProperties.java new file mode 100644 index 0000000000..85835c2741 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/java/smoketest/config/FromEnvConfigurationProperties.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("from-env") +class FromEnvConfigurationProperties { + + private String key1; + + String getKey1() { + return this.key1; + } + + void setKey1(String key1) { + this.key1 = key1; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/java/smoketest/config/SampleConfigApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/java/smoketest/config/SampleConfigApplication.java new file mode 100644 index 0000000000..8454782c6e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/java/smoketest/config/SampleConfigApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.config; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties(FromEnvConfigurationProperties.class) +public class SampleConfigApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleConfigApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/resources/application.properties new file mode 100644 index 0000000000..9ff483ff04 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.config.import=optional:env:SMOKE_TEST_CONFIG_ENV diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/test/java/smoketest/config/FromEnvConfigurationPropertiesTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/test/java/smoketest/config/FromEnvConfigurationPropertiesTests.java new file mode 100644 index 0000000000..3473f51eb1 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-config/src/test/java/smoketest/config/FromEnvConfigurationPropertiesTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FromEnvConfigurationProperties}. + * + * @author Moritz Halbritter + */ +@SpringBootTest +class FromEnvConfigurationPropertiesTests { + + @Autowired + private FromEnvConfigurationProperties properties; + + @Test + void shouldHaveImportedValues() { + assertThat(this.properties.getKey1()).isEqualTo("value1"); + } + +}