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 0d331ef0063..2d34c20703f 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 @@ -1426,7 +1426,7 @@ Consider the following example: [source,java,indent=0] ---- -include::{code-examples}/context/properties/bind/AppSystemProperties.java[tag=example] +include::{code-examples}/context/properties/bind/javabean/AppSystemProperties.java[tag=example] ---- To specify a session timeout of 30 seconds, `30`, `PT30S` and `30s` are all equivalent. @@ -1445,6 +1445,14 @@ These are: The default unit is milliseconds and can be overridden using `@DurationUnit` as illustrated in the sample above. +If you prefer to use constructor binding, the same properties can be exposed, as shown in the following example: + +[source,java,indent=0] +---- +include::{code-examples}/context/properties/bind/constructor/AppSystemProperties.java[tag=example] +---- + + TIP: If you are upgrading a `Long` property, make sure to define the unit (using `@DurationUnit`) if it isn't milliseconds. Doing so gives a transparent upgrade path while supporting a much richer format. @@ -1482,7 +1490,7 @@ Consider the following example: [source,java,indent=0] ---- -include::{code-examples}/context/properties/bind/AppIoProperties.java[tag=example] +include::{code-examples}/context/properties/bind/javabean/AppIoProperties.java[tag=example] ---- To specify a buffer size of 10 megabytes, `10` and `10MB` are equivalent. @@ -1499,6 +1507,13 @@ These are: The default unit is bytes and can be overridden using `@DataSizeUnit` as illustrated in the sample above. +If you prefer to use constructor binding, the same properties can be exposed, as shown in the following example: + +[source,java,indent=0] +---- +include::{code-examples}/context/properties/bind/constructor/AppIoProperties.java[tag=example] +---- + TIP: If you are upgrading a `Long` property, make sure to define the unit (using `@DataSizeUnit`) if it isn't bytes. Doing so gives a transparent upgrade path while supporting a much richer format. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/constructor/AppIoProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/constructor/AppIoProperties.java new file mode 100644 index 00000000000..86bcb9e76a5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/constructor/AppIoProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2020 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.docs.context.properties.bind.constructor; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +/** + * A {@link ConfigurationProperties @ConfigurationProperties} example that uses + * {@link DataSize}. + * + * @author Stephane Nicoll + */ +// tag::example[] +@ConfigurationProperties("app.io") +@ConstructorBinding +public class AppIoProperties { + + private final DataSize bufferSize; + + private final DataSize sizeThreshold; + + public AppIoProperties(@DataSizeUnit(DataUnit.MEGABYTES) @DefaultValue("2MB") DataSize bufferSize, + @DefaultValue("512B") DataSize sizeThreshold) { + this.bufferSize = bufferSize; + this.sizeThreshold = sizeThreshold; + } + + public DataSize getBufferSize() { + return this.bufferSize; + } + + public DataSize getSizeThreshold() { + return this.sizeThreshold; + } + +} +// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/constructor/AppSystemProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/constructor/AppSystemProperties.java new file mode 100644 index 00000000000..d30b635c839 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/constructor/AppSystemProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 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.docs.context.properties.bind.constructor; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.convert.DurationUnit; + +/** + * A {@link ConfigurationProperties @ConfigurationProperties} example that uses + * {@link Duration}. + * + * @author Stephane Nicoll + */ +// tag::example[] +@ConfigurationProperties("app.system") +@ConstructorBinding +public class AppSystemProperties { + + private final Duration sessionTimeout; + + private final Duration readTimeout; + + public AppSystemProperties(@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("30s") Duration sessionTimeout, + @DefaultValue("1000ms") Duration readTimeout) { + this.sessionTimeout = sessionTimeout; + this.readTimeout = readTimeout; + } + + public Duration getSessionTimeout() { + return this.sessionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + +} +// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppIoProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/javabean/AppIoProperties.java similarity index 92% rename from spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppIoProperties.java rename to spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/javabean/AppIoProperties.java index 4a89b76034e..15862ff55f4 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppIoProperties.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/javabean/AppIoProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.context.properties.bind; +package org.springframework.boot.docs.context.properties.bind.javabean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DataSizeUnit; diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppSystemProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/javabean/AppSystemProperties.java similarity index 92% rename from spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppSystemProperties.java rename to spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/javabean/AppSystemProperties.java index 230d267033a..eca2bb44071 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppSystemProperties.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/javabean/AppSystemProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.context.properties.bind; +package org.springframework.boot.docs.context.properties.bind.javabean; import java.time.Duration; import java.time.temporal.ChronoUnit; diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/constructor/AppSystemPropertiesTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/constructor/AppSystemPropertiesTests.java new file mode 100644 index 00000000000..f64e4958968 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/constructor/AppSystemPropertiesTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 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.docs.context.properties.bind.constructor; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AppSystemProperties}. + * + * @author Stephane Nicoll + */ +class AppSystemPropertiesTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(Config.class); + + @Test + void bindWithDefaultUnit() { + this.contextRunner.withPropertyValues("app.system.session-timeout=40", "app.system.read-timeout=5000") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasSeconds(40); + assertThat(properties.getReadTimeout()).hasMillis(5000); + })); + } + + @Test + void bindWithExplicitUnit() { + this.contextRunner.withPropertyValues("app.system.session-timeout=1h", "app.system.read-timeout=5s") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasMinutes(60); + assertThat(properties.getReadTimeout()).hasMillis(5000); + })); + } + + @Test + void bindWithIso8601Format() { + this.contextRunner.withPropertyValues("app.system.session-timeout=PT15S", "app.system.read-timeout=PT0.5S") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasSeconds(15); + assertThat(properties.getReadTimeout()).hasMillis(500); + })); + } + + private ContextConsumer assertBinding(Consumer properties) { + return (context) -> { + assertThat(context).hasSingleBean(AppSystemProperties.class); + properties.accept(context.getBean(AppSystemProperties.class)); + }; + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(AppSystemProperties.class) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/AppSystemPropertiesTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/javabean/AppSystemPropertiesTests.java similarity index 97% rename from spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/AppSystemPropertiesTests.java rename to spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/javabean/AppSystemPropertiesTests.java index f6fd2d21f43..f9155f5a0cc 100644 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/AppSystemPropertiesTests.java +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/javabean/AppSystemPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.context.properties.bind; +package org.springframework.boot.docs.context.properties.bind.javabean; import java.util.function.Consumer; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DataSizeUnit.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DataSizeUnit.java index 658868e6459..c68ac8dbe3b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DataSizeUnit.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DataSizeUnit.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -32,7 +32,7 @@ import org.springframework.util.unit.DataUnit; * @author Stephane Nicoll * @since 2.1.0 */ -@Target(ElementType.FIELD) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSizeUnit { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java index c8b60c9b538..ab70d284df7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -31,7 +31,7 @@ import java.time.temporal.ChronoUnit; * @author Phillip Webb * @since 2.0.0 */ -@Target(ElementType.FIELD) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DurationUnit { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/PeriodUnit.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/PeriodUnit.java index e34c43948af..c2f3d1ba6b9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/PeriodUnit.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/PeriodUnit.java @@ -32,7 +32,7 @@ import java.time.temporal.ChronoUnit; * @author Edson Chávez * @since 2.3.0 */ -@Target(ElementType.FIELD) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PeriodUnit { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 3ebdac21185..e26fd815af0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -19,6 +19,8 @@ package org.springframework.boot.context.properties; import java.beans.PropertyEditorSupport; import java.io.File; import java.time.Duration; +import java.time.Period; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -54,6 +56,8 @@ import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.boot.context.properties.bind.validation.BindValidationException; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.boot.convert.PeriodUnit; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -107,6 +111,7 @@ import static org.mockito.Mockito.verify; * @author Phillip Webb * @author Stephane Nicoll * @author Madhura Bhave + * @author Vladislav Kisel */ @ExtendWith(OutputCaptureExtension.class) class ConfigurationPropertiesTests { @@ -762,6 +767,22 @@ class ConfigurationPropertiesTests { assertThat(bean.getBar()).isEqualTo(5); } + @Test + void loadWhenBindingToConstructorParametersWithCustomDataUnitShouldBind() { + MutablePropertySources sources = this.context.getEnvironment().getPropertySources(); + Map source = new HashMap<>(); + source.put("test.duration", "12"); + source.put("test.size", "13"); + source.put("test.period", "14"); + sources.addLast(new MapPropertySource("test", source)); + load(ConstructorParameterWithUnitConfiguration.class); + ConstructorParameterWithUnitProperties bean = this.context + .getBean(ConstructorParameterWithUnitProperties.class); + assertThat(bean.getDuration()).isEqualTo(Duration.ofDays(12)); + assertThat(bean.getSize()).isEqualTo(DataSize.ofMegabytes(13)); + assertThat(bean.getPeriod()).isEqualTo(Period.ofYears(14)); + } + @Test // gh-17831 void loadWhenBindingConstructorParametersViaImportShouldThrowException() { assertThatExceptionOfType(BeanCreationException.class) @@ -777,6 +798,16 @@ class ConfigurationPropertiesTests { assertThat(bean.getBar()).isEqualTo(0); } + @Test + void loadWhenBindingToConstructorParametersWithDefaultDataUnitShouldBind() { + load(ConstructorParameterWithUnitConfiguration.class); + ConstructorParameterWithUnitProperties bean = this.context + .getBean(ConstructorParameterWithUnitProperties.class); + assertThat(bean.getDuration()).isEqualTo(Duration.ofDays(2)); + assertThat(bean.getSize()).isEqualTo(DataSize.ofMegabytes(3)); + assertThat(bean.getPeriod()).isEqualTo(Period.ofYears(4)); + } + @Test void loadWhenBindingToConstructorParametersShouldValidate() { assertThatExceptionOfType(Exception.class) @@ -1933,6 +1964,38 @@ class ConfigurationPropertiesTests { } + @ConstructorBinding + @ConfigurationProperties(prefix = "test") + static class ConstructorParameterWithUnitProperties { + + private final Duration duration; + + private final DataSize size; + + private final Period period; + + ConstructorParameterWithUnitProperties(@DefaultValue("2") @DurationUnit(ChronoUnit.DAYS) Duration duration, + @DefaultValue("3") @DataSizeUnit(DataUnit.MEGABYTES) DataSize size, + @DefaultValue("4") @PeriodUnit(ChronoUnit.YEARS) Period period) { + this.size = size; + this.duration = duration; + this.period = period; + } + + Duration getDuration() { + return this.duration; + } + + DataSize getSize() { + return this.size; + } + + Period getPeriod() { + return this.period; + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties @Import(ConstructorParameterProperties.class) @@ -1963,6 +2026,11 @@ class ConfigurationPropertiesTests { } + @EnableConfigurationProperties(ConstructorParameterWithUnitProperties.class) + static class ConstructorParameterWithUnitConfiguration { + + } + @EnableConfigurationProperties(ConstructorParameterValidatedProperties.class) static class ConstructorParameterValidationConfiguration {