Allow @ConstructorBinding to be optional

This commit makes @ConstructorBinding optional for a type
that has a single parameterized constructor. An @Autowired annotation
on any of the constructors indicates that the type should not be constructor
bound.

Since @ConstructorBinding is now deduced for a single parameterized constructor,
the annotation is no longer needed at the type level.

Closes gh-23216
This commit is contained in:
Madhura Bhave
2021-12-10 09:45:48 -08:00
parent bc2c637d63
commit 44b88cc88c
44 changed files with 693 additions and 391 deletions

View File

@@ -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.
@@ -79,6 +79,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.context.properties.ConstructorBinding";
static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired";
static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.context.properties.bind.DefaultValue";
static final String CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint";
@@ -122,6 +124,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return CONSTRUCTOR_BINDING_ANNOTATION;
}
protected String autowiredAnnotation() {
return AUTOWIRED_ANNOTATION;
}
protected String defaultValueAnnotation() {
return DEFAULT_VALUE_ANNOTATION;
}
@@ -156,7 +162,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata());
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
constructorBindingAnnotation(), autowiredAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
readOperationAnnotation(), nameAnnotation());
}

View File

@@ -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.
@@ -99,10 +99,12 @@ class MetadataGenerationEnvironment {
private final String nameAnnotation;
private final String autowiredAnnotation;
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
String constructorBindingAnnotation, String defaultValueAnnotation, Set<String> endpointAnnotations,
String readOperationAnnotation, String nameAnnotation) {
String constructorBindingAnnotation, String autowiredAnnotation, String defaultValueAnnotation,
Set<String> endpointAnnotations, String readOperationAnnotation, String nameAnnotation) {
this.typeUtils = new TypeUtils(environment);
this.elements = environment.getElementUtils();
this.messager = environment.getMessager();
@@ -111,6 +113,7 @@ class MetadataGenerationEnvironment {
this.nestedConfigurationPropertyAnnotation = nestedConfigurationPropertyAnnotation;
this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation;
this.constructorBindingAnnotation = constructorBindingAnnotation;
this.autowiredAnnotation = autowiredAnnotation;
this.defaultValueAnnotation = defaultValueAnnotation;
this.endpointAnnotations = endpointAnnotations;
this.readOperationAnnotation = readOperationAnnotation;
@@ -180,14 +183,14 @@ class MetadataGenerationEnvironment {
return new ItemDeprecation(reason, replacement);
}
boolean hasConstructorBindingAnnotation(TypeElement typeElement) {
return hasAnnotationRecursive(typeElement, this.constructorBindingAnnotation);
}
boolean hasConstructorBindingAnnotation(ExecutableElement element) {
return hasAnnotation(element, this.constructorBindingAnnotation);
}
boolean hasAutowiredAnnotation(ExecutableElement element) {
return hasAnnotation(element, this.autowiredAnnotation);
}
boolean hasAnnotation(Element element, String type) {
return getAnnotation(element, type) != null;
}

View File

@@ -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,6 +16,7 @@
package org.springframework.boot.configurationprocessor;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -191,16 +192,29 @@ class PropertyDescriptorResolver {
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
boolean constructorBoundType = isConstructorBoundType(type, env);
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
List<ExecutableElement> boundConstructors = constructors.stream()
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
List<ExecutableElement> boundConstructors = getBoundConstructors(env, constructors);
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
}
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
if (env.hasConstructorBindingAnnotation(type)
|| "java.lang.Record".equals(type.getSuperclass().toString())) {
return true;
private static List<ExecutableElement> getBoundConstructors(MetadataGenerationEnvironment env,
List<ExecutableElement> constructors) {
ExecutableElement bindConstructor = deduceBindConstructor(constructors, env);
if (bindConstructor != null) {
return Collections.singletonList(bindConstructor);
}
return constructors.stream().filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
}
private static ExecutableElement deduceBindConstructor(List<ExecutableElement> constructors,
MetadataGenerationEnvironment env) {
if (constructors.size() == 1 && constructors.get(0).getParameters().size() > 0
&& !env.hasAutowiredAnnotation(constructors.get(0))) {
return constructors.get(0);
}
return null;
}
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
if (type.getNestingKind() == NestingKind.MEMBER) {
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env);
}

View File

@@ -410,21 +410,6 @@ 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 implicitlyBoundRecordProperties(@TempDir File temp) throws IOException {

View File

@@ -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.
@@ -45,6 +45,7 @@ class MetadataGenerationEnvironmentFactory implements Function<ProcessingEnviron
TestConfigurationMetadataAnnotationProcessor.NESTED_CONFIGURATION_PROPERTY_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.CONSTRUCTOR_BINDING_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.AUTOWIRED_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, endpointAnnotations,
TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.NAME_ANNOTATION);

View File

@@ -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.
@@ -35,6 +35,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester;
import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor;
import org.springframework.boot.configurationsample.immutable.ImmutableClassConstructorBindingProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableDeducedConstructorBindingProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableMultiConstructorProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableNameAnnotationProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties;
@@ -42,12 +43,11 @@ import org.springframework.boot.configurationsample.lombok.LombokExplicitPropert
import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleValueProperties;
import org.springframework.boot.configurationsample.simple.AutowiredProperties;
import org.springframework.boot.configurationsample.simple.HierarchicalProperties;
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesGrandparent;
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesParent;
import org.springframework.boot.configurationsample.simple.SimpleProperties;
import org.springframework.boot.configurationsample.specific.MatchingConstructorNoDirectiveProperties;
import org.springframework.boot.configurationsample.specific.TwoConstructorsClassConstructorBindingExample;
import org.springframework.boot.configurationsample.specific.TwoConstructorsExample;
import org.springframework.boot.testsupport.compiler.TestCompiler;
@@ -111,6 +111,14 @@ class PropertyDescriptorResolverTests {
(stream) -> assertThat(stream).containsExactly("name", "description", "counter", "number", "items")));
}
@Test
void propertiesWithDeducedConstructorBinding() throws IOException {
process(ImmutableDeducedConstructorBindingProperties.class,
propertyNames((stream) -> assertThat(stream).containsExactly("theName", "flag")));
process(ImmutableDeducedConstructorBindingProperties.class, properties((stream) -> assertThat(stream)
.allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor)));
}
@Test
void propertiesWithConstructorWithConstructorBinding() throws IOException {
process(ImmutableSimpleProperties.class, propertyNames(
@@ -128,16 +136,9 @@ class PropertyDescriptorResolverTests {
}
@Test
void propertiesWithConstructorAndClassConstructorBindingAndSeveralCandidates() throws IOException {
process(TwoConstructorsClassConstructorBindingExample.class,
propertyNames((stream) -> assertThat(stream).isEmpty()));
}
@Test
void propertiesWithConstructorNoDirective() throws IOException {
process(MatchingConstructorNoDirectiveProperties.class,
propertyNames((stream) -> assertThat(stream).containsExactly("name")));
process(MatchingConstructorNoDirectiveProperties.class, properties((stream) -> assertThat(stream)
void propertiesWithAutowiredConstructor() throws IOException {
process(AutowiredProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("theName")));
process(AutowiredProperties.class, properties((stream) -> assertThat(stream)
.allMatch((predicate) -> predicate instanceof JavaBeanPropertyDescriptor)));
}

View File

@@ -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.
@@ -59,6 +59,8 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
public static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.configurationsample.ConstructorBinding";
public static final String AUTOWIRED_ANNOTATION = "org.springframework.boot.configurationsample.Autowired";
public static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.configurationsample.DefaultValue";
public static final String CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.ControllerEndpoint";
@@ -105,6 +107,11 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
return CONSTRUCTOR_BINDING_ANNOTATION;
}
@Override
protected String autowiredAnnotation() {
return AUTOWIRED_ANNOTATION;
}
@Override
protected String defaultValueAnnotation() {
return DEFAULT_VALUE_ANNOTATION;

View File

@@ -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.
@@ -22,10 +22,15 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
/**
* Alternative to Spring Framework's {@code @Autowired} for testing (removes the need for
* a dependency on the real annotation).
*
* @author Madhura Bhave
*/
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConstructorBinding
public @interface MetaConstructorBinding {
public @interface Autowired {
}

View File

@@ -28,7 +28,7 @@ import java.lang.annotation.Target;
*
* @author Stephane Nicoll
*/
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {

View File

@@ -17,7 +17,6 @@
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
import org.springframework.boot.configurationsample.DefaultValue;
/**
@@ -26,7 +25,6 @@ import org.springframework.boot.configurationsample.DefaultValue;
* @author Phillip Webb
*/
@ConfigurationProperties("test")
@ConstructorBinding
public class DeducedImmutableClassProperties {
private final Nested nested;

View File

@@ -16,15 +16,12 @@
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.MetaConstructorBinding;
/**
* Simple immutable properties with several constructors.
*
* @author Stephane Nicoll
*/
@SuppressWarnings("unused")
@MetaConstructorBinding
public class ImmutableClassConstructorBindingProperties {
private final String name;

View File

@@ -0,0 +1,51 @@
/*
* 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.
* 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.DefaultValue;
/**
* @author Madhura Bhave
*/
@ConfigurationProperties("immutable")
public class ImmutableDeducedConstructorBindingProperties {
/**
* The name of these properties.
*/
private final String theName;
/**
* A simple flag.
*/
private final boolean flag;
public ImmutableDeducedConstructorBindingProperties(@DefaultValue("boot") String theName, boolean flag) {
this.theName = theName;
this.flag = flag;
}
public String getTheName() {
return this.theName;
}
public boolean isFlag() {
return this.flag;
}
}

View File

@@ -17,7 +17,6 @@
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
import org.springframework.boot.configurationsample.Name;
/**
@@ -26,7 +25,6 @@ import org.springframework.boot.configurationsample.Name;
* @author Phillip Webb
*/
@ConfigurationProperties("named")
@ConstructorBinding
public class ImmutableNameAnnotationProperties {
private final String imports;

View File

@@ -0,0 +1,46 @@
/*
* 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.
* 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.simple;
import org.springframework.boot.configurationsample.Autowired;
/**
* Properties with autowired constructor.
*
* @author Madhura Bhave
*/
public class AutowiredProperties {
/**
* The name of this simple properties.
*/
private String theName;
@Autowired
public AutowiredProperties(String theName) {
this.theName = theName;
}
public String getTheName() {
return this.theName;
}
public void setTheName(String name) {
this.theName = name;
}
}

View File

@@ -17,7 +17,6 @@
package org.springframework.boot.configurationsample.specific;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
import org.springframework.boot.configurationsample.DefaultValue;
/**
@@ -27,7 +26,6 @@ import org.springframework.boot.configurationsample.DefaultValue;
* @author Stephane Nicoll
*/
@ConfigurationProperties("test")
@ConstructorBinding
public class InvalidDefaultValueFloatingPointProperties {
private final Double ratio;

View File

@@ -18,7 +18,6 @@ package org.springframework.boot.configurationsample.specific;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.DefaultValue;
import org.springframework.boot.configurationsample.MetaConstructorBinding;
/**
* Demonstrates that an invalid default number value leads to a compilation failure.
@@ -26,7 +25,6 @@ import org.springframework.boot.configurationsample.MetaConstructorBinding;
* @author Stephane Nicoll
*/
@ConfigurationProperties("test")
@MetaConstructorBinding
public class InvalidDefaultValueNumberProperties {
private final int counter;

View File

@@ -1,51 +0,0 @@
/*
* 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.configurationsample.specific;
import org.springframework.boot.configurationsample.MetaConstructorBinding;
/**
* A type that declares constructor binding but with two available constructors.
*
* @author Stephane Nicoll
*/
@MetaConstructorBinding
@SuppressWarnings("unused")
public class TwoConstructorsClassConstructorBindingExample {
private String name;
private String description;
public TwoConstructorsClassConstructorBindingExample(String name) {
this(name, null);
}
public TwoConstructorsClassConstructorBindingExample(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}