Relax @ConstructorBinding member class requirement

Update `@ConfigurationProperties` so that `@ConstructorBinding` classes
no longer need to repeat the annotation for their members.

Closes gh-18481
This commit is contained in:
Phillip Webb 2019-10-02 14:59:37 -07:00
parent e6bb7a0a6f
commit 386c0a60a7
6 changed files with 174 additions and 8 deletions

View File

@ -881,7 +881,6 @@ The example in the previous section can be rewritten in an immutable fashion as
private final List<String> roles;
@ConstructorBinding
public Security(String username, String password,
@DefaultValue("USER") List<String> roles) {
this.username = username;
@ -903,16 +902,16 @@ The example in the previous section can be rewritten in an immutable fashion as
In this setup, the `@ImmutableConfigurationProperties` 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.
Nested classes that also require constructor binding (such as `Security` in the example above) should use the `@ConstructorBinding` annotation.
Nested members of a `@ImmutableConfigurationProperties` 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.
TIP: You can also use `@ConstructorBinding` on the actual constructor that should be bound.
This is required if you have more than one constructor for your class.
NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning.
You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`)
TIP: `@ImmutableConfigurationProperties` is actually a meta-annotation composed of `@ConfigurationProperties` and `@ConstructorBinding`.
If you have more than one constructor for your class you can also use `@ConstructorBinding` directly on actual constructor that should be bound.
[[boot-features-external-config-enabling]]

View File

@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
@ -169,13 +170,24 @@ class PropertyDescriptorResolver {
}
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
boolean constructorBoundType = env.hasConstructorBindingAnnotation(type);
boolean constructorBoundType = isConstructorBoundType(type, env);
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
List<ExecutableElement> boundConstructors = constructors.stream()
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
}
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
if (env.hasConstructorBindingAnnotation(type)) {
return true;
}
if (type.getNestingKind() == NestingKind.MEMBER) {
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env);
}
return false;
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2012-2019 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.configurationprocessor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.Metadata;
import org.springframework.boot.configurationsample.immutable.DeducedImmutableClassProperties;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Metadata generation tests for immutable properties deduced because they're nested.
*
* @author Phillip Webb
*/
class DeducedImmutablePropertiesMetadataGenerationTests extends AbstractMetadataGenerationTests {
@Test
void immutableSimpleProperties() {
ConfigurationMetadata metadata = compile(DeducedImmutableClassProperties.class);
assertThat(metadata).has(Metadata.withGroup("test").fromSource(DeducedImmutableClassProperties.class));
assertThat(metadata).has(Metadata.withProperty("test.nested.name", String.class));
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2012-2019 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.configurationsample.immutable;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
/**
* Inner properties, in immutable format.
*
* @author Phillip Webb
*/
@ConfigurationProperties("test")
@ConstructorBinding
public class DeducedImmutableClassProperties {
private final Nested nested;
public DeducedImmutableClassProperties(Nested nested) {
this.nested = nested;
}
public Nested getNested() {
return this.nested;
}
public static class Nested {
private String name;
public Nested(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
}

View File

@ -265,13 +265,17 @@ public final class ConfigurationPropertiesBean {
VALUE_OBJECT;
static BindMethod forClass(Class<?> type) {
if (MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(ConstructorBinding.class)
|| findBindConstructor(type) != null) {
if (isConstructorBindingType(type) || findBindConstructor(type) != null) {
return VALUE_OBJECT;
}
return JAVA_BEAN;
}
private static boolean isConstructorBindingType(Class<?> type) {
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
.isPresent(ConstructorBinding.class);
}
static Constructor<?> findBindConstructor(Class<?> type) {
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);

View File

@ -836,6 +836,19 @@ class ConfigurationPropertiesTests {
assertThat(nested.getAge()).isEqualTo(0);
}
@Test // gh-18481
void loadWhenBindingToNestedConstructorPropertiesWithDeducedNestedShouldBind() {
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("test.name", "spring");
source.put("test.nested.age", "5");
sources.addLast(new MapPropertySource("test", source));
load(DeducedNestedConstructorPropertiesConfiguration.class);
DeducedNestedConstructorProperties bean = this.context.getBean(DeducedNestedConstructorProperties.class);
assertThat(bean.getName()).isEqualTo("spring");
assertThat(bean.getNested().getAge()).isEqualTo(5);
}
private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {
return load(new Class<?>[] { configuration }, inlinedProperties);
}
@ -2014,4 +2027,46 @@ class ConfigurationPropertiesTests {
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(DeducedNestedConstructorProperties.class)
static class DeducedNestedConstructorPropertiesConfiguration {
}
@ImmutableConfigurationProperties("test")
static class DeducedNestedConstructorProperties {
private final String name;
private final Nested nested;
DeducedNestedConstructorProperties(String name, Nested nested) {
this.name = name;
this.nested = nested;
}
String getName() {
return this.name;
}
Nested getNested() {
return this.nested;
}
static class Nested {
private final int age;
Nested(int age) {
this.age = age;
}
int getAge() {
return this.age;
}
}
}
}