Document @ConstructorBinding and @DefaultValue with records
Closes gh-30460
This commit is contained in:
parent
24e748d5cd
commit
d574563616
|
|
@ -721,11 +721,14 @@ include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/con
|
|||
|
||||
In this setup, the `@ConstructorBinding` annotation is used to indicate that constructor binding should be used.
|
||||
This means that the binder will expect to find a constructor with the parameters that you wish to have bound.
|
||||
If you are using Java 16 or later, constructor binding can be used with records.
|
||||
|
||||
Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor.
|
||||
|
||||
Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property.
|
||||
By default, if no properties are bound to `Security`, the `MyProperties` instance will contain a `null` value for `security`.
|
||||
Default values can be specified using `@DefaultValue` on a constructor parameter or, when using Java 16 or later, a record component.
|
||||
The conversion service will be applied to coerce the `String` value to the target type of a missing property.
|
||||
|
||||
Referring to the previous example, if no properties are bound to `Security`, the `MyProperties` instance will contain a `null` value for `security`.
|
||||
If you wish you return a non-null instance of `Security` even when no properties are bound to it, you can use an empty `@DefaultValue` annotation to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.configurationprocessor;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
|
@ -54,4 +55,11 @@ public abstract class AbstractMetadataGenerationTests {
|
|||
return processor.getMetadata();
|
||||
}
|
||||
|
||||
protected ConfigurationMetadata compile(File... sources) {
|
||||
TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
|
||||
this.compiler.getOutputLocation());
|
||||
this.compiler.getTask(Arrays.asList(sources)).call(processor);
|
||||
return processor.getMetadata();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
|
@ -16,7 +16,15 @@
|
|||
|
||||
package org.springframework.boot.configurationprocessor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
|
||||
|
|
@ -402,4 +410,40 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
|
|||
compile(RecursiveProperties.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_16)
|
||||
void explicityBoundRecordProperties(@TempDir File temp) throws IOException {
|
||||
File exampleRecord = new File(temp, "ExampleRecord.java");
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) {
|
||||
writer.println("@org.springframework.boot.configurationsample.ConstructorBinding");
|
||||
writer.println("@org.springframework.boot.configurationsample.ConfigurationProperties(\"explicit\")");
|
||||
writer.println("public record ExampleRecord(String someString, Integer someInteger) {");
|
||||
writer.println("}");
|
||||
}
|
||||
ConfigurationMetadata metadata = compile(exampleRecord);
|
||||
assertThat(metadata).has(Metadata.withProperty("explicit.some-string"));
|
||||
assertThat(metadata).has(Metadata.withProperty("explicit.some-integer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_16)
|
||||
void explicitlyBoundRecordPropertiesWithDefaultValues(@TempDir File temp) throws IOException {
|
||||
File exampleRecord = new File(temp, "ExampleRecord.java");
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) {
|
||||
writer.println("@org.springframework.boot.configurationsample.ConstructorBinding");
|
||||
writer.println(
|
||||
"@org.springframework.boot.configurationsample.ConfigurationProperties(\"record.defaults\")");
|
||||
writer.println("public record ExampleRecord(");
|
||||
writer.println("@org.springframework.boot.configurationsample.DefaultValue(\"An1s9n\") String someString,");
|
||||
writer.println("@org.springframework.boot.configurationsample.DefaultValue(\"594\") Integer someInteger");
|
||||
writer.println(") {");
|
||||
writer.println("}");
|
||||
}
|
||||
ConfigurationMetadata metadata = compile(exampleRecord);
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withProperty("record.defaults.some-string", String.class).withDefaultValue("An1s9n"));
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withProperty("record.defaults.some-integer", Integer.class).withDefaultValue(594));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
|
@ -16,9 +16,15 @@
|
|||
|
||||
package org.springframework.boot.context.properties;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Map;
|
||||
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.ClassFileVersion;
|
||||
import net.bytebuddy.description.annotation.AnnotationDescription;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.function.ThrowingConsumer;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
|
||||
|
|
@ -216,6 +222,32 @@ class ConfigurationPropertiesBeanTests {
|
|||
.getBindConstructor(ConstructorBindingOnConstructor.class, false)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_16)
|
||||
void forValueObjectWithRecordReturnsBean() {
|
||||
Class<?> constructorBindingRecord = new ByteBuddy(ClassFileVersion.JAVA_V16).makeRecord()
|
||||
.name("org.springframework.boot.context.properties.RecordProperties")
|
||||
.annotateType(AnnotationDescription.Builder.ofType(ConfigurationProperties.class)
|
||||
.define("prefix", "explicit").build())
|
||||
.annotateType(AnnotationDescription.Builder.ofType(ConstructorBinding.class).build())
|
||||
.defineRecordComponent("someString", String.class).defineRecordComponent("someInteger", Integer.class)
|
||||
.make().load(getClass().getClassLoader()).getLoaded();
|
||||
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
|
||||
.forValueObject(constructorBindingRecord, "constructorBindingRecord");
|
||||
assertThat(propertiesBean.getName()).isEqualTo("constructorBindingRecord");
|
||||
assertThat(propertiesBean.getInstance()).isNull();
|
||||
assertThat(propertiesBean.getType()).isEqualTo(constructorBindingRecord);
|
||||
assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.VALUE_OBJECT);
|
||||
assertThat(propertiesBean.getAnnotation()).isNotNull();
|
||||
Bindable<?> target = propertiesBean.asBindTarget();
|
||||
assertThat(target.getType()).isEqualTo(ResolvableType.forClass(constructorBindingRecord));
|
||||
assertThat(target.getValue()).isNull();
|
||||
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
|
||||
.getBindConstructor(constructorBindingRecord, false);
|
||||
assertThat(bindConstructor).isNotNull();
|
||||
assertThat(bindConstructor.getParameterTypes()).containsExactly(String.class, Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forValueObjectWhenJavaBeanBindTypeThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
|
@ -16,23 +16,35 @@
|
|||
|
||||
package org.springframework.boot.context.properties.bind;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
|
||||
import org.springframework.boot.testsupport.compiler.TestCompiler;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -357,6 +369,30 @@ class ValueObjectBinderTests {
|
|||
assertThat(bound.getImportName()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_16)
|
||||
void bindToRecordWithDefaultValue(@TempDir File tempDir) throws IOException, ClassNotFoundException {
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("test.record.property1", "value-from-config-1");
|
||||
this.sources.add(source);
|
||||
File recordProperties = new File(tempDir, "RecordProperties.java");
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(recordProperties))) {
|
||||
writer.println("public record RecordProperties(");
|
||||
writer.println(
|
||||
"@org.springframework.boot.context.properties.bind.DefaultValue(\"default-value-1\") String property1,");
|
||||
writer.println(
|
||||
"@org.springframework.boot.context.properties.bind.DefaultValue(\"default-value-2\") String property2");
|
||||
writer.println(") {");
|
||||
writer.println("}");
|
||||
}
|
||||
TestCompiler compiler = new TestCompiler(tempDir);
|
||||
compiler.getTask(Arrays.asList(recordProperties)).call();
|
||||
ClassLoader ucl = new URLClassLoader(new URL[] { tempDir.toURI().toURL() });
|
||||
Object bean = this.binder.bind("test.record", Class.forName("RecordProperties", true, ucl)).get();
|
||||
assertThat(ReflectionTestUtils.getField(bean, "property1")).isEqualTo("value-from-config-1");
|
||||
assertThat(ReflectionTestUtils.getField(bean, "property2")).isEqualTo("default-value-2");
|
||||
}
|
||||
|
||||
private void noConfigurationProperty(BindException ex) {
|
||||
assertThat(ex.getProperty()).isNull();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue