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 0a1500025f..302177bce4 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 @@ -1227,7 +1227,7 @@ Spring Boot provides infrastructure to bind `@ConfigurationProperties` types and You can either enable configuration properties on a class-by-class basis or enable configuration property scanning that works in a similar manner to component scanning. Sometimes, classes annotated with `@ConfigurationProperties` might not be suitable for scanning, for example, if you're developing your own auto-configuration or you want to enable them conditionally. -In these cases, specify the list of types to process using the `@EnableConfigurationProperties` annotation. +In these cases, specify the list of types to process using the `@EnableConfigurationProperties` or `@ImportConfigurationPropertiesBean` annotations. This can be done on any `@Configuration` class, as shown in the following example: [source,java,indent=0] @@ -1253,7 +1253,7 @@ If you want to define specific packages to scan, you can do so as shown in the f [NOTE] ==== -When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties`, the bean has a conventional name: `-`, where `` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `` is the fully qualified name of the bean. +When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties` or `@ImportConfigurationPropertiesBean`, the bean has a conventional name: `-`, where `` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `` is the fully qualified name of the bean. If the annotation does not provide any prefix, only the fully qualified name of the bean is used. The bean name in the example above is `acme-com.example.AcmeProperties`. @@ -1325,12 +1325,26 @@ To configure a bean from the `Environment` properties, add `@ConfigurationProper ---- @ConfigurationProperties(prefix = "another") @Bean - public AnotherComponent anotherComponent() { + public ExampleItem exampleItem() { ... } ---- -Any JavaBean property defined with the `another` prefix is mapped onto that `AnotherComponent` bean in manner similar to the preceding `AcmeProperties` example. +Any JavaBean property defined with the `another` prefix is mapped onto that `ExampleItem` bean in manner similar to the preceding `AcmeProperties` example. + +If you want to use constructor binding with a third-party class, you can't use a `@Bean` method since Spring will need to create the object instance. +For those situations, you can use an `@ImportConfigurationPropertiesBean` annotation on your `@Configuration` or `@SpringBootApplication` class. + +[source,java,indent=0] +---- + @SpringBootApplication + @ImportConfigurationPropertiesBean(type = ExampleItem.class, prefix = "another") + public class MyApp { + ... + } +---- + +TIP: `@ImportConfigurationPropertiesBean` also works for JavaBean bindings as long as the type has a single no-arg constructor diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 823f589aa6..b0dd63ba13 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -22,6 +22,7 @@ import java.io.StringWriter; import java.time.Duration; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -40,6 +41,7 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic.Kind; @@ -60,17 +62,13 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; @SupportedAnnotationTypes({ "*" }) public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor { - static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot." - + "configurationprocessor.additionalMetadataLocations"; + static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot.configurationprocessor.additionalMetadataLocations"; - static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot." - + "context.properties.ConfigurationProperties"; + static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties"; - static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot." - + "context.properties.NestedConfigurationProperty"; + static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.NestedConfigurationProperty"; - static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot." - + "context.properties.DeprecatedConfigurationProperty"; + static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty"; static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.context.properties.ConstructorBinding"; @@ -78,11 +76,14 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor static final String ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.Endpoint"; - static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate." - + "endpoint.annotation.ReadOperation"; + static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.ReadOperation"; static final String NAME_ANNOTATION = "org.springframework.boot.context.properties.bind.Name"; + static final String IMPORT_CONFIGURATION_PROPERTIES_BEAN_ANNOATION = "org.springframework.boot.context.properties.ImportConfigurationPropertiesBean"; + + static final String IMPORT_CONFIGURATION_PROPERTIES_BEANS_ANNOATION = "org.springframework.boot.context.properties.ImportConfigurationPropertiesBeans"; + private static final Set SUPPORTED_OPTIONS = Collections .unmodifiableSet(Collections.singleton(ADDITIONAL_METADATA_LOCATIONS_OPTION)); @@ -124,6 +125,14 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor return NAME_ANNOTATION; } + protected String importConfigurationPropertiesBeanAnnotation() { + return IMPORT_CONFIGURATION_PROPERTIES_BEAN_ANNOATION; + } + + protected String importConfigurationPropertiesBeansAnnotation() { + return IMPORT_CONFIGURATION_PROPERTIES_BEANS_ANNOATION; + } + @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); @@ -142,22 +151,16 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(), nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(), constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotation(), - readOperationAnnotation(), nameAnnotation()); + readOperationAnnotation(), nameAnnotation(), importConfigurationPropertiesBeanAnnotation(), + importConfigurationPropertiesBeansAnnotation()); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { this.metadataCollector.processing(roundEnv); - TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement(); - if (annotationType != null) { // Is @ConfigurationProperties available - for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) { - processElement(element); - } - } - TypeElement endpointType = this.metadataEnv.getEndpointAnnotationElement(); - if (endpointType != null) { // Is @Endpoint available - getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint); - } + processConfigurationProperties(roundEnv); + processEndpoint(roundEnv); + processImportConfigurationPropertiesBean(roundEnv); if (roundEnv.processingOver()) { try { writeMetaData(); @@ -169,6 +172,40 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor return false; } + private void processConfigurationProperties(RoundEnvironment roundEnv) { + TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement(); + if (annotationType != null) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) { + processElement(element); + } + } + } + + private void processEndpoint(RoundEnvironment roundEnv) { + TypeElement endpointType = this.metadataEnv.getEndpointAnnotationElement(); + if (endpointType != null) { + getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint); + } + } + + private void processImportConfigurationPropertiesBean(RoundEnvironment roundEnv) { + TypeElement importConfigurationPropertiesBeanType = this.metadataEnv + .getImportConfigurationPropertiesBeanAnnotationElement(); + TypeElement importConfigurationPropertiesBeansType = this.metadataEnv + .getImportConfigurationPropertiesBeansAnnotationElement(); + if (importConfigurationPropertiesBeanType == null && importConfigurationPropertiesBeansType == null) { + return; + } + Set elements = new LinkedHashSet<>(); + if (importConfigurationPropertiesBeanType != null) { + elements.addAll(roundEnv.getElementsAnnotatedWith(importConfigurationPropertiesBeanType)); + } + if (importConfigurationPropertiesBeansType != null) { + elements.addAll(roundEnv.getElementsAnnotatedWith(importConfigurationPropertiesBeansType)); + } + elements.forEach(this::processImportConfigurationPropertiesBean); + } + private Map> getElementsAnnotatedOrMetaAnnotatedWith(RoundEnvironment roundEnv, TypeElement annotation) { Map> result = new LinkedHashMap<>(); @@ -187,7 +224,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor if (annotation != null) { String prefix = getPrefix(annotation); if (element instanceof TypeElement) { - processAnnotatedTypeElement(prefix, (TypeElement) element, new Stack<>()); + processAnnotatedTypeElement(prefix, (TypeElement) element, false, new Stack<>()); } else if (element instanceof ExecutableElement) { processExecutableElement(prefix, (ExecutableElement) element, new Stack<>()); @@ -199,10 +236,11 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor } } - private void processAnnotatedTypeElement(String prefix, TypeElement element, Stack seen) { + private void processAnnotatedTypeElement(String prefix, TypeElement element, boolean fromImport, + Stack seen) { String type = this.metadataEnv.getTypeUtils().getQualifiedName(element); this.metadataCollector.add(ItemMetadata.newGroup(prefix, type, type, null)); - processTypeElement(prefix, element, null, seen); + processTypeElement(prefix, element, fromImport, null, seen); } private void processExecutableElement(String prefix, ExecutableElement element, Stack seen) { @@ -220,25 +258,26 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor } else { this.metadataCollector.add(group); - processTypeElement(prefix, (TypeElement) returns, element, seen); + processTypeElement(prefix, (TypeElement) returns, false, element, seen); } } } } - private void processTypeElement(String prefix, TypeElement element, ExecutableElement source, + private void processTypeElement(String prefix, TypeElement element, boolean fromImport, ExecutableElement source, Stack seen) { if (!seen.contains(element)) { seen.push(element); - new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> { - this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv)); - if (descriptor.isNested(this.metadataEnv)) { - TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils() - .asElement(descriptor.getType()); - String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, descriptor.getName()); - processTypeElement(nestedPrefix, nestedTypeElement, source, seen); - } - }); + new PropertyDescriptorResolver(this.metadataEnv).resolve(element, fromImport, source) + .forEach((descriptor) -> { + this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv)); + if (descriptor.isNested(this.metadataEnv)) { + TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils() + .asElement(descriptor.getType()); + String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, descriptor.getName()); + processTypeElement(nestedPrefix, nestedTypeElement, false, source, seen); + } + }); seen.pop(); } } @@ -275,6 +314,21 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor } } + private void processImportConfigurationPropertiesBean(Element element) { + this.metadataEnv.getImportConfigurationPropertiesBeanAnnotations(element) + .forEach(this::processImportConfigurationPropertiesBean); + } + + @SuppressWarnings("unchecked") + private void processImportConfigurationPropertiesBean(AnnotationMirror annotation) { + String prefix = getPrefix(annotation); + List types = (List) this.metadataEnv.getAnnotationElementValues(annotation).get("type"); + for (TypeMirror type : types) { + Element element = this.metadataEnv.getTypeUtils().asElement(type); + processAnnotatedTypeElement(prefix, (TypeElement) element, true, new Stack<>()); + } + } + private boolean hasMainReadOperation(TypeElement element) { for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) { if (this.metadataEnv.getReadOperationAnnotation(method) != null diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index b3006911ba..74b2d97363 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -97,10 +97,15 @@ class MetadataGenerationEnvironment { private final String nameAnnotation; + private final String importConfigurationPropertiesBeanAnnotation; + + private final String importConfigurationPropertiesBeansAnnotation; + MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation, String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation, String constructorBindingAnnotation, String defaultValueAnnotation, String endpointAnnotation, - String readOperationAnnotation, String nameAnnotation) { + String readOperationAnnotation, String nameAnnotation, String importConfigurationPropertiesBeanAnnotation, + String importConfigurationPropertiesBeansAnnotation) { this.typeUtils = new TypeUtils(environment); this.elements = environment.getElementUtils(); this.messager = environment.getMessager(); @@ -113,6 +118,8 @@ class MetadataGenerationEnvironment { this.endpointAnnotation = endpointAnnotation; this.readOperationAnnotation = readOperationAnnotation; this.nameAnnotation = nameAnnotation; + this.importConfigurationPropertiesBeanAnnotation = importConfigurationPropertiesBeanAnnotation; + this.importConfigurationPropertiesBeansAnnotation = importConfigurationPropertiesBeansAnnotation; } private static FieldValuesParser resolveFieldValuesParser(ProcessingEnvironment env) { @@ -258,6 +265,14 @@ class MetadataGenerationEnvironment { return this.elements.getTypeElement(this.configurationPropertiesAnnotation); } + TypeElement getImportConfigurationPropertiesBeanAnnotationElement() { + return this.elements.getTypeElement(this.importConfigurationPropertiesBeanAnnotation); + } + + TypeElement getImportConfigurationPropertiesBeansAnnotationElement() { + return this.elements.getTypeElement(this.importConfigurationPropertiesBeansAnnotation); + } + AnnotationMirror getConfigurationPropertiesAnnotation(Element element) { return getAnnotation(element, this.configurationPropertiesAnnotation); } @@ -282,6 +297,22 @@ class MetadataGenerationEnvironment { return getAnnotation(element, this.nameAnnotation); } + List getImportConfigurationPropertiesBeanAnnotations(Element element) { + List annotations = new ArrayList<>(); + AnnotationMirror importBean = getAnnotation(element, this.importConfigurationPropertiesBeanAnnotation); + if (importBean != null) { + annotations.add(importBean); + } + AnnotationMirror importBeans = getAnnotation(element, this.importConfigurationPropertiesBeansAnnotation); + if (importBeans != null) { + AnnotationValue value = importBeans.getElementValues().values().iterator().next(); + for (Object contained : (List) value.getValue()) { + annotations.add((AnnotationMirror) contained); + } + } + return Collections.unmodifiableList(annotations); + } + boolean hasNullableAnnotation(Element element) { return getAnnotation(element, NULLABLE_ANNOTATION) != null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index 886b55f9d2..9d54ff49a7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -49,16 +49,19 @@ class PropertyDescriptorResolver { * specified {@link TypeElement type} based on the specified {@link ExecutableElement * factory method}, if any. * @param type the target type + * @param fromImport it the type was imported via a + * {@code @ImportConfigurationPropertiesBean} * @param factoryMethod the method that triggered the metadata for that {@code type} * or {@code null} * @return the candidate properties for metadata generation */ - Stream> resolve(TypeElement type, ExecutableElement factoryMethod) { + Stream> resolve(TypeElement type, boolean fromImport, ExecutableElement factoryMethod) { TypeElementMembers members = new TypeElementMembers(this.environment, type); if (factoryMethod != null) { return resolveJavaBeanProperties(type, factoryMethod, members); } - return resolve(ConfigurationPropertiesTypeElement.of(type, this.environment), factoryMethod, members); + return resolve(ConfigurationPropertiesTypeElement.of(type, fromImport, this.environment), factoryMethod, + members); } private Stream> resolve(ConfigurationPropertiesTypeElement type, @@ -178,20 +181,29 @@ class PropertyDescriptorResolver { return boundConstructor; } - static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) { - boolean constructorBoundType = isConstructorBoundType(type, env); + static ConfigurationPropertiesTypeElement of(TypeElement type, boolean fromImport, + MetadataGenerationEnvironment env) { List constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); List boundConstructors = constructors.stream() .filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList()); + boolean constructorBoundType = isConstructorBoundType(type, fromImport, constructors, env); return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors); } - private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) { + private static boolean isConstructorBoundType(TypeElement type, boolean fromImport, + List constructors, MetadataGenerationEnvironment env) { if (env.hasConstructorBindingAnnotation(type)) { return true; } if (type.getNestingKind() == NestingKind.MEMBER) { - return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env); + return isConstructorBoundType((TypeElement) type.getEnclosingElement(), false, constructors, env); + } + if (fromImport) { + for (ExecutableElement constructor : constructors) { + if (!constructor.getParameters().isEmpty()) { + return true; + } + } } return false; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImportBeanTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImportBeanTests.java new file mode 100644 index 0000000000..20feac4115 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImportBeanTests.java @@ -0,0 +1,72 @@ +/* + * 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.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.ImportConfigurationPropertiesBean; +import org.springframework.boot.configurationsample.ImportConfigurationPropertiesBeans; +import org.springframework.boot.configurationsample.importbean.ImportJavaBeanConfigurationPropertiesBean; +import org.springframework.boot.configurationsample.importbean.ImportMultipleTypeConfigurationPropertiesBean; +import org.springframework.boot.configurationsample.importbean.ImportRepeatedConfigurationPropertiesBean; +import org.springframework.boot.configurationsample.importbean.ImportValueObjectConfigurationPropertiesBean; +import org.springframework.boot.configurationsample.importbean.ImportedJavaBean; +import org.springframework.boot.configurationsample.importbean.ImportedValueObject; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ImportConfigurationPropertiesBean} and + * {@link ImportConfigurationPropertiesBeans}. + * + * @author Phillip Webb + */ +public class ImportBeanTests extends AbstractMetadataGenerationTests { + + @Test + void importValueObjectConfigurationPropertiesBean() { + ConfigurationMetadata metadata = compile(ImportValueObjectConfigurationPropertiesBean.class); + assertThat(metadata) + .has(Metadata.withProperty("importbean.value", String.class).fromSource(ImportedValueObject.class)); + } + + @Test + void importJavaBeanConfigurationPropertiesBean() { + ConfigurationMetadata metadata = compile(ImportJavaBeanConfigurationPropertiesBean.class); + assertThat(metadata) + .has(Metadata.withProperty("importbean.name", String.class).fromSource(ImportedJavaBean.class)); + } + + @Test + void importMultipleTypeConfigurationPropertiesBean() { + ConfigurationMetadata metadata = compile(ImportMultipleTypeConfigurationPropertiesBean.class); + assertThat(metadata) + .has(Metadata.withProperty("importbean.value", String.class).fromSource(ImportedValueObject.class)); + assertThat(metadata) + .has(Metadata.withProperty("importbean.name", String.class).fromSource(ImportedJavaBean.class)); + } + + @Test + void importRepeatedConfigurationPropertiesBean() { + ConfigurationMetadata metadata = compile(ImportRepeatedConfigurationPropertiesBean.class); + assertThat(metadata).has(Metadata.withProperty("vo.value", String.class).fromSource(ImportedValueObject.class)); + assertThat(metadata).has(Metadata.withProperty("jb.name", String.class).fromSource(ImportedJavaBean.class)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java index 0b67c60a03..046876da4e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java @@ -39,7 +39,9 @@ class MetadataGenerationEnvironmentFactory implements Function { PropertyDescriptorResolver resolver = new PropertyDescriptorResolver(metadataEnv); - assertThat(resolver.resolve(type, null).map(PropertyDescriptor::getName)).containsExactly("third", - "second", "first"); - assertThat(resolver.resolve(type, null) + assertThat(resolver.resolve(type, false, null).map(PropertyDescriptor::getName)) + .containsExactly("third", "second", "first"); + assertThat(resolver.resolve(type, false, null) .map((descriptor) -> descriptor.resolveItemMetadata("test", metadataEnv)) .map(ItemMetadata::getDefaultValue)).containsExactly("three", "two", "one"); }); @@ -155,7 +155,7 @@ class PropertyDescriptorResolverTests { Consumer>> stream) { return (element, metadataEnv) -> { PropertyDescriptorResolver resolver = new PropertyDescriptorResolver(metadataEnv); - stream.accept(resolver.resolve(element, null)); + stream.accept(resolver.resolve(element, false, null)); }; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java index 149a746c0e..4ad6b9d1d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java @@ -57,6 +57,10 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM public static final String NAME_ANNOTATION = "org.springframework.boot.configurationsample.Name"; + public static final String IMPORT_CONFIGURATION_PROPERTIES_BEAN_ANNOATION = "org.springframework.boot.configurationsample.ImportConfigurationPropertiesBean"; + + public static final String IMPORT_CONFIGURATION_PROPERTIES_BEANS_ANNOATION = "org.springframework.boot.configurationsample.ImportConfigurationPropertiesBeans"; + private ConfigurationMetadata metadata; private final File outputLocation; @@ -105,6 +109,16 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM return NAME_ANNOTATION; } + @Override + protected String importConfigurationPropertiesBeanAnnotation() { + return IMPORT_CONFIGURATION_PROPERTIES_BEAN_ANNOATION; + } + + @Override + protected String importConfigurationPropertiesBeansAnnotation() { + return IMPORT_CONFIGURATION_PROPERTIES_BEANS_ANNOATION; + } + @Override protected ConfigurationMetadata writeMetaData() throws Exception { super.writeMetaData(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ImportConfigurationPropertiesBean.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ImportConfigurationPropertiesBean.java new file mode 100644 index 0000000000..fe20e089f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ImportConfigurationPropertiesBean.java @@ -0,0 +1,52 @@ +/* + * 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Alternative to Spring Boot's {@code ImportConfigurationPropertiesBean} for testing + * (removes the need for a dependency on the real annotation). + * + * @author Phillip Webb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ConfigurationProperties +@Repeatable(ImportConfigurationPropertiesBeans.class) +public @interface ImportConfigurationPropertiesBean { + + Class[] type(); + + @AliasFor(annotation = ConfigurationProperties.class) + String prefix() default ""; + + @AliasFor(annotation = ConfigurationProperties.class) + boolean ignoreInvalidFields() default false; + + @AliasFor(annotation = ConfigurationProperties.class) + boolean ignoreUnknownFields() default true; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ImportConfigurationPropertiesBeans.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ImportConfigurationPropertiesBeans.java new file mode 100644 index 0000000000..90e3fd3bdb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ImportConfigurationPropertiesBeans.java @@ -0,0 +1,38 @@ +/* + * 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code ImportConfigurationPropertiesBeans} for testing + * (removes the need for a dependency on the real annotation). + * + * @author Phillip Webb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ImportConfigurationPropertiesBeans { + + ImportConfigurationPropertiesBean[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportJavaBeanConfigurationPropertiesBean.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportJavaBeanConfigurationPropertiesBean.java new file mode 100644 index 0000000000..73e6221fda --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportJavaBeanConfigurationPropertiesBean.java @@ -0,0 +1,29 @@ +/* + * 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.importbean; + +import org.springframework.boot.configurationsample.ImportConfigurationPropertiesBean; + +/** + * An import of a java bean. + * + * @author Phillip Webb + */ +@ImportConfigurationPropertiesBean(type = ImportedJavaBean.class, prefix = "importbean") +public class ImportJavaBeanConfigurationPropertiesBean { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportMultipleTypeConfigurationPropertiesBean.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportMultipleTypeConfigurationPropertiesBean.java new file mode 100644 index 0000000000..8b23d04a4d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportMultipleTypeConfigurationPropertiesBean.java @@ -0,0 +1,29 @@ +/* + * 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.importbean; + +import org.springframework.boot.configurationsample.ImportConfigurationPropertiesBean; + +/** + * An import of a java bean and a value object. + * + * @author Phillip Webb + */ +@ImportConfigurationPropertiesBean(type = { ImportedJavaBean.class, ImportedValueObject.class }, prefix = "importbean") +public class ImportMultipleTypeConfigurationPropertiesBean { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportRepeatedConfigurationPropertiesBean.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportRepeatedConfigurationPropertiesBean.java new file mode 100644 index 0000000000..5f09dd3808 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportRepeatedConfigurationPropertiesBean.java @@ -0,0 +1,30 @@ +/* + * 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.importbean; + +import org.springframework.boot.configurationsample.ImportConfigurationPropertiesBean; + +/** + * An import of a java bean and a value object. + * + * @author Phillip Webb + */ +@ImportConfigurationPropertiesBean(type = ImportedJavaBean.class, prefix = "jb") +@ImportConfigurationPropertiesBean(type = ImportedValueObject.class, prefix = "vo") +public class ImportRepeatedConfigurationPropertiesBean { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportValueObjectConfigurationPropertiesBean.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportValueObjectConfigurationPropertiesBean.java new file mode 100644 index 0000000000..1412b14c54 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportValueObjectConfigurationPropertiesBean.java @@ -0,0 +1,29 @@ +/* + * 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.importbean; + +import org.springframework.boot.configurationsample.ImportConfigurationPropertiesBean; + +/** + * An import of a value object. + * + * @author Phillip Webb + */ +@ImportConfigurationPropertiesBean(type = ImportedValueObject.class, prefix = "importbean") +public class ImportValueObjectConfigurationPropertiesBean { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportedJavaBean.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportedJavaBean.java new file mode 100644 index 0000000000..cbb7ed71f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportedJavaBean.java @@ -0,0 +1,36 @@ +/* + * 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.importbean; + +/** + * Java bean that can be imported. + * + * @author Phillip Webb + */ +public class ImportedJavaBean { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportedValueObject.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportedValueObject.java new file mode 100644 index 0000000000..3dc35d11cf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/importbean/ImportedValueObject.java @@ -0,0 +1,36 @@ +/* + * 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.importbean; + +/** + * Value object that can be imported. + * + * @author Phillip Webb + */ +public class ImportedValueObject { + + private final String value; + + public ImportedValueObject(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java index b5628a5f68..25c0b1182f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java @@ -18,6 +18,7 @@ package org.springframework.boot.context.properties; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Iterator; import java.util.LinkedHashMap; @@ -75,7 +76,7 @@ public final class ConfigurationPropertiesBean { this.instance = instance; this.annotation = annotation; this.bindTarget = bindTarget; - this.bindMethod = BindMethod.forType(bindTarget.getType().resolve()); + this.bindMethod = BindMethod.forBindable(bindTarget); } /** @@ -175,7 +176,8 @@ public final class ConfigurationPropertiesBean { if (beanFactory.findAnnotationOnBean(beanName, ConfigurationProperties.class) != null) { return true; } - Method factoryMethod = findFactoryMethod(beanFactory, beanName); + BeanDefinition beanDefinition = findBeanDefinition(beanFactory, beanName); + Method factoryMethod = findFactoryMethod(beanFactory, beanDefinition); return findMergedAnnotation(factoryMethod, ConfigurationProperties.class).isPresent(); } catch (NoSuchBeanDefinitionException ex) { @@ -197,33 +199,41 @@ public final class ConfigurationPropertiesBean { * {@link ConfigurationProperties @ConfigurationProperties} */ public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) { - Method factoryMethod = findFactoryMethod(applicationContext, beanName); - return create(beanName, bean, bean.getClass(), factoryMethod); + ConfigurableListableBeanFactory beanFactory = getBeanFactory(applicationContext); + BeanDefinition beanDefinition = findBeanDefinition(beanFactory, beanName); + Method factoryMethod = findFactoryMethod(beanFactory, beanDefinition); + ConfigurationProperties annotation = findAnnotation(beanDefinition); + boolean deduceBindConstructor = (beanDefinition instanceof ConfigurationPropertiesValueObjectBeanDefinition) + ? ((ConfigurationPropertiesValueObjectBeanDefinition) beanDefinition).isDeduceBindConstructor() : false; + return create(beanName, bean, bean.getClass(), factoryMethod, annotation, deduceBindConstructor); } - private static Method findFactoryMethod(ApplicationContext applicationContext, String beanName) { + private static ConfigurableListableBeanFactory getBeanFactory(ApplicationContext applicationContext) { if (applicationContext instanceof ConfigurableApplicationContext) { - return findFactoryMethod((ConfigurableApplicationContext) applicationContext, beanName); + return ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); } return null; } - private static Method findFactoryMethod(ConfigurableApplicationContext applicationContext, String beanName) { - return findFactoryMethod(applicationContext.getBeanFactory(), beanName); + private static BeanDefinition findBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) { + if (beanFactory != null && beanFactory.containsBeanDefinition(beanName)) { + return beanFactory.getMergedBeanDefinition(beanName); + } + return null; } - private static Method findFactoryMethod(ConfigurableListableBeanFactory beanFactory, String beanName) { - if (beanFactory.containsBeanDefinition(beanName)) { - BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); - if (beanDefinition instanceof RootBeanDefinition) { - Method resolvedFactoryMethod = ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod(); - if (resolvedFactoryMethod != null) { - return resolvedFactoryMethod; - } + private static Method findFactoryMethod(ConfigurableListableBeanFactory beanFactory, + BeanDefinition beanDefinition) { + if (beanFactory == null || beanDefinition == null) { + return null; + } + if (beanDefinition instanceof RootBeanDefinition) { + Method resolvedFactoryMethod = ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod(); + if (resolvedFactoryMethod != null) { + return resolvedFactoryMethod; } - return findFactoryMethodUsingReflection(beanFactory, beanDefinition); } - return null; + return findFactoryMethodUsingReflection(beanFactory, beanDefinition); } private static Method findFactoryMethodUsingReflection(ConfigurableListableBeanFactory beanFactory, @@ -246,15 +256,19 @@ public final class ConfigurationPropertiesBean { return factoryMethod.get(); } - static ConfigurationPropertiesBean forValueObject(Class beanClass, String beanName) { - ConfigurationPropertiesBean propertiesBean = create(beanName, null, beanClass, null); + static ConfigurationPropertiesBean forValueObject(Class beanClass, String beanName, + MergedAnnotation annotation, boolean deduceBindConstructor) { + ConfigurationPropertiesBean propertiesBean = create(beanName, null, beanClass, null, + annotation.isPresent() ? annotation.synthesize() : null, deduceBindConstructor); Assert.state(propertiesBean != null && propertiesBean.getBindMethod() == BindMethod.VALUE_OBJECT, () -> "Bean '" + beanName + "' is not a @ConfigurationProperties value object"); return propertiesBean; } - private static ConfigurationPropertiesBean create(String name, Object instance, Class type, Method factory) { - ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class); + private static ConfigurationPropertiesBean create(String name, Object instance, Class type, Method factory, + ConfigurationProperties annotation, boolean deduceBindConstructor) { + annotation = (annotation != null) ? annotation + : findAnnotation(instance, type, factory, ConfigurationProperties.class); if (annotation == null) { return null; } @@ -267,9 +281,19 @@ public final class ConfigurationPropertiesBean { if (instance != null) { bindTarget = bindTarget.withExistingValue(instance); } + if (deduceBindConstructor) { + bindTarget = bindTarget.withAttribute( + ConfigurationPropertiesBindConstructorProvider.DEDUCE_BIND_CONSTRUCTOR_ATTRIUBTE, true); + } return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget); } + private static ConfigurationProperties findAnnotation(BeanDefinition beanDefinition) { + MergedAnnotation annotation = ConfigurationPropertiesBeanDefinition + .getAnnotation(beanDefinition); + return (annotation.isPresent()) ? annotation.synthesize() : null; + } + private static A findAnnotation(Object instance, Class type, Method factory, Class annotationType) { MergedAnnotation annotation = MergedAnnotation.missing(); @@ -308,8 +332,27 @@ public final class ConfigurationPropertiesBean { VALUE_OBJECT; static BindMethod forType(Class type) { - return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type, false) != null) - ? VALUE_OBJECT : JAVA_BEAN; + return forType(type, false); + } + + static BindMethod forType(Class type, boolean deduceBindConstructor) { + Constructor constructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE + .getBindConstructor(type, deduceBindConstructor, false); + if (deduceBindConstructor) { + Assert.state(constructor != null, + () -> "Unable to deduce @ConfigurationProperties bind method for " + type.getName()); + } + return hasParameters(constructor) ? VALUE_OBJECT : JAVA_BEAN; + } + + static BindMethod forBindable(Bindable bindable) { + Constructor constructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE + .getBindConstructor(bindable, false); + return hasParameters(constructor) ? VALUE_OBJECT : JAVA_BEAN; + } + + private static boolean hasParameters(Constructor constructor) { + return constructor != null && constructor.getParameterCount() > 0; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java new file mode 100644 index 0000000000..3b2a07eaca --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java @@ -0,0 +1,47 @@ +/* + * 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.context.properties; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.Conventions; +import org.springframework.core.annotation.MergedAnnotation; + +/** + * {@link BeanDefinition} that is used for registering + * {@link ConfigurationProperties @ConfigurationProperties} beans. + * + * @author Phillip Webb + */ +class ConfigurationPropertiesBeanDefinition extends GenericBeanDefinition { + + private static final String ANNOTATION_ATTRIBUTE = Conventions + .getQualifiedAttributeName(ConfigurationPropertiesBeanDefinition.class, "annotation"); + + ConfigurationPropertiesBeanDefinition(Class beanClass, MergedAnnotation annotation) { + setBeanClass(beanClass); + setAttribute(ANNOTATION_ATTRIBUTE, annotation); + } + + @SuppressWarnings("unchecked") + static MergedAnnotation getAnnotation(BeanDefinition beanDefinition) { + MergedAnnotation annotation = (beanDefinition != null) + ? (MergedAnnotation) beanDefinition.getAttribute(ANNOTATION_ATTRIBUTE) : null; + return (annotation != null) ? annotation : MergedAnnotation.missing(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java index 791e31e32e..2da022bffc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.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. @@ -20,7 +20,6 @@ import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -54,9 +53,13 @@ final class ConfigurationPropertiesBeanRegistrar { } void register(Class type, MergedAnnotation annotation) { + register(type, annotation, false); + } + + void register(Class type, MergedAnnotation annotation, boolean deduceBindConstructor) { String name = getName(type, annotation); if (!containsBeanDefinition(name)) { - registerBeanDefinition(name, type, annotation); + registerBeanDefinition(name, type, annotation, deduceBindConstructor); } } @@ -81,19 +84,20 @@ final class ConfigurationPropertiesBeanRegistrar { } private void registerBeanDefinition(String beanName, Class type, - MergedAnnotation annotation) { + MergedAnnotation annotation, boolean deduceBindConstructor) { Assert.state(annotation.isPresent(), () -> "No " + ConfigurationProperties.class.getSimpleName() + " annotation found on '" + type.getName() + "'."); - this.registry.registerBeanDefinition(beanName, createBeanDefinition(beanName, type)); + this.registry.registerBeanDefinition(beanName, + createBeanDefinition(beanName, type, annotation, deduceBindConstructor)); } - private BeanDefinition createBeanDefinition(String beanName, Class type) { - if (BindMethod.forType(type) == BindMethod.VALUE_OBJECT) { - return new ConfigurationPropertiesValueObjectBeanDefinition(this.beanFactory, beanName, type); + private BeanDefinition createBeanDefinition(String beanName, Class type, + MergedAnnotation annotation, boolean deduceBindConstructor) { + if (BindMethod.forType(type, deduceBindConstructor) == BindMethod.VALUE_OBJECT) { + return new ConfigurationPropertiesValueObjectBeanDefinition(this.beanFactory, beanName, type, annotation, + deduceBindConstructor); } - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(type); - return definition; + return new ConfigurationPropertiesBeanDefinition(type, annotation); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java index d9e4723c66..eed5ed737c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java @@ -21,6 +21,7 @@ import java.lang.reflect.Constructor; import org.springframework.beans.BeanUtils; import org.springframework.boot.context.properties.bind.BindConstructorProvider; import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.Conventions; import org.springframework.core.KotlinDetector; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; @@ -36,19 +37,35 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP static final ConfigurationPropertiesBindConstructorProvider INSTANCE = new ConfigurationPropertiesBindConstructorProvider(); + static final String DEDUCE_BIND_CONSTRUCTOR_ATTRIUBTE = Conventions + .getQualifiedAttributeName(ConfigurationPropertiesBindConstructorProvider.class, "deduceBindConstructor"); + @Override public Constructor getBindConstructor(Bindable bindable, boolean isNestedConstructorBinding) { - return getBindConstructor(bindable.getType().resolve(), isNestedConstructorBinding); + Boolean deduceBindConstructor = (Boolean) bindable.getAttribute(DEDUCE_BIND_CONSTRUCTOR_ATTRIUBTE); + return getBindConstructor(bindable.getType().resolve(), Boolean.TRUE.equals(deduceBindConstructor), + isNestedConstructorBinding); } - Constructor getBindConstructor(Class type, boolean isNestedConstructorBinding) { + Constructor getBindConstructor(Class type, boolean deduceBindConstructor, + boolean isNestedConstructorBinding) { if (type == null) { return null; } Constructor constructor = findConstructorBindingAnnotatedConstructor(type); - if (constructor == null && (isConstructorBindingAnnotatedType(type) || isNestedConstructorBinding)) { + if (constructor != null) { + return constructor; + } + boolean isConstructorBindingAnnotatedType = isConstructorBindingAnnotatedType(type); + if (deduceBindConstructor || isNestedConstructorBinding || isConstructorBindingAnnotatedType) { constructor = deduceBindConstructor(type); } + if (deduceBindConstructor && isConstructorBindingAnnotatedType && !isNestedConstructorBinding) { + Assert.state(constructor != null, + () -> "Unable to deduce constructor for @ConstructorBinding class " + type.getName()); + Assert.state(constructor.getParameterCount() > 0, + () -> "Deduced no-args constructor for @ConstructorBinding class " + type.getName()); + } return constructor; } @@ -86,7 +103,7 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP return deducedKotlinBindConstructor(type); } Constructor[] constructors = type.getDeclaredConstructors(); - if (constructors.length == 1 && constructors[0].getParameterCount() > 0) { + if (constructors.length == 1) { return constructors[0]; } return null; @@ -94,7 +111,7 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP private Constructor deducedKotlinBindConstructor(Class type) { Constructor primaryConstructor = BeanUtils.findPrimaryConstructor(type); - if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) { + if (primaryConstructor != null) { return primaryConstructor; } return null; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index c78fc0898a..0d8ada28bd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -25,6 +25,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -206,7 +207,14 @@ class ConfigurationPropertiesBinder { } static ConfigurationPropertiesBinder get(BeanFactory beanFactory) { - return beanFactory.getBean(BEAN_NAME, ConfigurationPropertiesBinder.class); + try { + return beanFactory.getBean(BEAN_NAME, ConfigurationPropertiesBinder.class); + } + catch (NoSuchBeanDefinitionException ex) { + throw new NoSuchBeanDefinitionException(ex.getBeanName(), + "Unable to find ConfigurationPropertiesBinder bean '" + BEAN_NAME + + "', ensure @EnableConfigurationProperties has been specified"); + } } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java index 08af79b96d..95671a6181 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.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. @@ -18,7 +18,7 @@ package org.springframework.boot.context.properties; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.annotation.MergedAnnotation; /** * {@link BeanDefinition} that is used for registering @@ -29,21 +29,30 @@ import org.springframework.beans.factory.support.GenericBeanDefinition; * @author Madhura Bhave * @author Phillip Webb */ -final class ConfigurationPropertiesValueObjectBeanDefinition extends GenericBeanDefinition { +final class ConfigurationPropertiesValueObjectBeanDefinition extends ConfigurationPropertiesBeanDefinition { private final BeanFactory beanFactory; private final String beanName; - ConfigurationPropertiesValueObjectBeanDefinition(BeanFactory beanFactory, String beanName, Class beanClass) { + private final boolean deduceBindConstructor; + + ConfigurationPropertiesValueObjectBeanDefinition(BeanFactory beanFactory, String beanName, Class beanClass, + MergedAnnotation annotation, boolean deduceBindConstructor) { + super(beanClass, annotation); this.beanFactory = beanFactory; this.beanName = beanName; - setBeanClass(beanClass); + this.deduceBindConstructor = deduceBindConstructor; setInstanceSupplier(this::createBean); } + boolean isDeduceBindConstructor() { + return this.deduceBindConstructor; + } + private Object createBean() { - ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.forValueObject(getBeanClass(), this.beanName); + ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.forValueObject(getBeanClass(), this.beanName, + getAnnotation(this), this.deduceBindConstructor); ConfigurationPropertiesBinder binder = ConfigurationPropertiesBinder.get(this.beanFactory); try { return binder.bindOrCreate(bean); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java index f16136cb2b..37c40f4ca6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.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. @@ -51,6 +51,7 @@ public @interface EnableConfigurationProperties { * {@link ConfigurationProperties @ConfigurationProperties} annotated beans with * Spring. Standard Spring Beans will also be scanned regardless of this value. * @return {@code @ConfigurationProperties} annotated beans to register + * @see ImportConfigurationPropertiesBean */ Class[] value() default {}; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBean.java new file mode 100644 index 0000000000..14fcac0910 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBean.java @@ -0,0 +1,88 @@ +/* + * 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.context.properties; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates one or more {@link ConfigurationProperties @ConfigurationProperties} classes + * to import as Spring Beans. Typically used on {@link Configuration @Configuration} + * classes to expose third-party classes as configuration property beans. + *

+ * Classes imported via this annotation that have a default constructor will use + * {@code setter} binding, those with a non-default constructor will use + * {@link ConstructorBinding @ConstructorBinding}. If you are looking to inject beans into + * a constructor, you should use a regular {@link Configuration @Configuration} class + * {@code @Bean} method instead. + * + * @author Phillip Webb + * @since 2.4.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableConfigurationProperties +@ConfigurationProperties +@Repeatable(ImportConfigurationPropertiesBeans.class) +@Import(ImportConfigurationPropertiesBeanRegistrar.class) +public @interface ImportConfigurationPropertiesBean { + + /** + * One or more types that should be imported as a bean. + * @return the types to import + */ + Class[] type(); + + /** + * The prefix of the properties that are valid to bind to this object. A valid prefix + * is defined by one or more words separated with dots (e.g. + * {@code "acme.system.feature"}). + * @return the prefix of the properties to bind + * @see ConfigurationProperties#prefix() + */ + @AliasFor(annotation = ConfigurationProperties.class) + String prefix() default ""; + + /** + * Flag to indicate that when binding to this object invalid fields should be ignored. + * Invalid means invalid according to the binder that is used, and usually this means + * fields of the wrong type (or that cannot be coerced into the correct type). + * @return the flag value (default false) + * @see ConfigurationProperties#ignoreInvalidFields() + */ + @AliasFor(annotation = ConfigurationProperties.class) + boolean ignoreInvalidFields() default false; + + /** + * Flag to indicate that when binding to this object unknown fields should be ignored. + * An unknown field could be a sign of a mistake in the Properties. + * @return the flag value (default true) + * @see ConfigurationProperties#ignoreUnknownFields() + */ + @AliasFor(annotation = ConfigurationProperties.class) + boolean ignoreUnknownFields() default true; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeanRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeanRegistrar.java new file mode 100644 index 0000000000..abe37a5cc9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeanRegistrar.java @@ -0,0 +1,67 @@ +/* + * 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.context.properties; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; + +/** + * {@link ImportBeanDefinitionRegistrar} for + * {@link ImportConfigurationPropertiesBean @ImportConfigurationPropertiesBean}. + * + * @author Phillip Webb + */ +class ImportConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, + BeanNameGenerator importBeanNameGenerator) { + ConfigurationPropertiesBeanRegistrar registrar = new ConfigurationPropertiesBeanRegistrar(registry); + MergedAnnotations annotations = importingClassMetadata.getAnnotations(); + registerBeans(registrar, annotations.get(ImportConfigurationPropertiesBeans.class)); + registerBean(registrar, annotations.get(ImportConfigurationPropertiesBean.class)); + } + + private void registerBeans(ConfigurationPropertiesBeanRegistrar registrar, + MergedAnnotation annotation) { + if (!annotation.isPresent()) { + return; + } + for (MergedAnnotation containedAnnotation : annotation + .getAnnotationArray(MergedAnnotation.VALUE, ImportConfigurationPropertiesBean.class)) { + registerBean(registrar, containedAnnotation); + } + } + + private void registerBean(ConfigurationPropertiesBeanRegistrar registrar, + MergedAnnotation annotation) { + if (!annotation.isPresent()) { + return; + } + Class[] types = annotation.getClassArray("type"); + MergedAnnotation configurationPropertiesAnnotation = MergedAnnotations + .from(annotation.synthesize()).get(ConfigurationProperties.class); + for (Class type : types) { + registrar.register(type, configurationPropertiesAnnotation, true); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeans.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeans.java new file mode 100644 index 0000000000..320af34b75 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeans.java @@ -0,0 +1,48 @@ +/* + * 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.context.properties; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Container annotation that aggregates several {@link ImportConfigurationPropertiesBean} + * annotations. + * + * @author Phillip Webb + * @since 2.4.0 + * @see ImportConfigurationPropertiesBean + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableConfigurationProperties +@Import(ImportConfigurationPropertiesBeanRegistrar.class) +public @interface ImportConfigurationPropertiesBeans { + + /** + * The contained {@link ImportConfigurationPropertiesBean} annotations. + * @return the contained annotations + */ + ImportConfigurationPropertiesBean[] value(); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java index a7f2bdbaaf..e307b5256a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.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. @@ -18,6 +18,8 @@ package org.springframework.boot.context.properties.bind; import java.lang.annotation.Annotation; import java.lang.reflect.Array; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,6 +44,8 @@ public final class Bindable { private static final Annotation[] NO_ANNOTATIONS = {}; + private static final Map NO_ATTRIBUTES = Collections.emptyMap(); + private final ResolvableType type; private final ResolvableType boxedType; @@ -50,11 +54,15 @@ public final class Bindable { private final Annotation[] annotations; - private Bindable(ResolvableType type, ResolvableType boxedType, Supplier value, Annotation[] annotations) { + private final Map attributes; + + private Bindable(ResolvableType type, ResolvableType boxedType, Supplier value, Annotation[] annotations, + Map attributes) { this.type = type; this.boxedType = boxedType; this.value = value; this.annotations = annotations; + this.attributes = attributes; } /** @@ -105,6 +113,16 @@ public final class Bindable { return null; } + /** + * Return the value of an attribute that has been associated with this + * {@link Bindable}. + * @param name the attribute name + * @return the associated attribute value or {@code null} + */ + public Object getAttribute(String name) { + return this.attributes.get(name); + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -117,6 +135,7 @@ public final class Bindable { boolean result = true; result = result && nullSafeEquals(this.type.resolve(), other.type.resolve()); result = result && nullSafeEquals(this.annotations, other.annotations); + result = result && nullSafeEquals(this.attributes, other.attributes); return result; } @@ -126,6 +145,7 @@ public final class Bindable { int result = 1; result = prime * result + ObjectUtils.nullSafeHashCode(this.type); result = prime * result + ObjectUtils.nullSafeHashCode(this.annotations); + result = prime * result + ObjectUtils.nullSafeHashCode(this.attributes); return result; } @@ -135,6 +155,7 @@ public final class Bindable { creator.append("type", this.type); creator.append("value", (this.value != null) ? "provided" : "none"); creator.append("annotations", this.annotations); + creator.append("attributes", this.attributes); return creator.toString(); } @@ -149,7 +170,19 @@ public final class Bindable { */ public Bindable withAnnotations(Annotation... annotations) { return new Bindable<>(this.type, this.boxedType, this.value, - (annotations != null) ? annotations : NO_ANNOTATIONS); + (annotations != null) ? annotations : NO_ANNOTATIONS, this.attributes); + } + + /** + * Create an updated {@link Bindable} instance with the specified attribute. + * @param name the attribute name + * @param value the attribute value + * @return an updated {@link Bindable} + */ + public Bindable withAttribute(String name, Object value) { + Map attributes = new HashMap<>(this.attributes); + attributes.put(name, value); + return new Bindable<>(this.type, this.boxedType, this.value, this.annotations, attributes); } /** @@ -162,7 +195,7 @@ public final class Bindable { existingValue == null || this.type.isArray() || this.boxedType.resolve().isInstance(existingValue), () -> "ExistingValue must be an instance of " + this.type); Supplier value = (existingValue != null) ? () -> existingValue : null; - return new Bindable<>(this.type, this.boxedType, value, this.annotations); + return new Bindable<>(this.type, this.boxedType, value, this.annotations, this.attributes); } /** @@ -171,7 +204,7 @@ public final class Bindable { * @return an updated {@link Bindable} */ public Bindable withSuppliedValue(Supplier suppliedValue) { - return new Bindable<>(this.type, this.boxedType, suppliedValue, this.annotations); + return new Bindable<>(this.type, this.boxedType, suppliedValue, this.annotations, this.attributes); } /** @@ -244,7 +277,7 @@ public final class Bindable { public static Bindable of(ResolvableType type) { Assert.notNull(type, "Type must not be null"); ResolvableType boxedType = box(type); - return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS); + return new Bindable<>(type, boxedType, null, NO_ANNOTATIONS, NO_ATTRIBUTES); } private static ResolvableType box(ResolvableType type) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionTests.java new file mode 100644 index 0000000000..f9a9752303 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionTests.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.context.properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.annotation.MergedAnnotation; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesBeanDefinition}. + * + * @author Phillip Webb + */ +class ConfigurationPropertiesBeanDefinitionTests { + + @Test + void getAnnotationGetsAnnotation() { + MergedAnnotation annotation = MergedAnnotation.of(ConfigurationProperties.class); + BeanDefinition definition = new ConfigurationPropertiesBeanDefinition(Example.class, annotation); + assertThat(ConfigurationPropertiesBeanDefinition.getAnnotation(definition)).isSameAs(annotation); + } + + @Test + void getAnnotationWhenNullReturnsMissing() { + assertThat(ConfigurationPropertiesBeanDefinition.getAnnotation(null)).isEqualTo(MergedAnnotation.missing()); + } + + @Test + void getAnnotationWhenNoAttributeReturnsMissing() { + GenericBeanDefinition definition = new GenericBeanDefinition(); + assertThat(ConfigurationPropertiesBeanDefinition.getAnnotation(definition)) + .isEqualTo(MergedAnnotation.missing()); + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java index 865c41f640..0d8b3a3bf6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.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. @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.annotation.MergedAnnotation; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -74,11 +75,29 @@ class ConfigurationPropertiesBeanRegistrarTests { } @Test - void registerWhenNotValueObjectRegistersGenericBeanDefinition() { + void registerWhenNotValueObjectRegistersConfigurationPropertiesBeanDefinition() { String beanName = MultiConstructorBeanConfigurationProperties.class.getName(); this.registrar.register(MultiConstructorBeanConfigurationProperties.class); BeanDefinition definition = this.registry.getBeanDefinition(beanName); - assertThat(definition).isInstanceOf(GenericBeanDefinition.class); + assertThat(definition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + } + + @Test + void registerWhenDeduceBindConstructorRegistersValueObjectBeanDefinition() { + String beanName = DeducedValueObjectConfigurationProperties.class.getName(); + MergedAnnotation annotation = MergedAnnotation.of(ConfigurationProperties.class); + this.registrar.register(DeducedValueObjectConfigurationProperties.class, annotation, true); + BeanDefinition definition = this.registry.getBeanDefinition(beanName); + assertThat(definition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); + } + + @Test + void registerWhenDeduceBindConstructorRegistersJavaBeanObjectBeanDefinition() { + String beanName = DeducedJavaBeanConfigurationProperties.class.getName(); + MergedAnnotation annotation = MergedAnnotation.of(ConfigurationProperties.class); + this.registrar.register(DeducedJavaBeanConfigurationProperties.class, annotation, true); + BeanDefinition definition = this.registry.getBeanDefinition(beanName); + assertThat(definition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); } @ConfigurationProperties(prefix = "beancp") @@ -110,4 +129,15 @@ class ConfigurationPropertiesBeanRegistrarTests { } + static class DeducedValueObjectConfigurationProperties { + + DeducedValueObjectConfigurationProperties(String name) { + } + + } + + static class DeducedJavaBeanConfigurationProperties { + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java index 867b68150f..a8b3e300c1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.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. @@ -30,6 +30,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.context.annotation.Lazy; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotationMetadata; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @@ -202,8 +203,8 @@ class ConfigurationPropertiesBeanTests { @Test void forValueObjectReturnsBean() { - ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean - .forValueObject(ConstructorBindingOnConstructor.class, "valueObjectBean"); + ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean.forValueObject( + ConstructorBindingOnConstructor.class, "valueObjectBean", MergedAnnotation.missing(), false); assertThat(propertiesBean.getName()).isEqualTo("valueObjectBean"); assertThat(propertiesBean.getInstance()).isNull(); assertThat(propertiesBean.getType()).isEqualTo(ConstructorBindingOnConstructor.class); @@ -213,17 +214,34 @@ class ConfigurationPropertiesBeanTests { assertThat(target.getType()).isEqualTo(ResolvableType.forClass(ConstructorBindingOnConstructor.class)); assertThat(target.getValue()).isNull(); assertThat(ConfigurationPropertiesBindConstructorProvider.INSTANCE - .getBindConstructor(ConstructorBindingOnConstructor.class, false)).isNotNull(); + .getBindConstructor(ConstructorBindingOnConstructor.class, false, false)).isNotNull(); + } + + @Test + void forValueObjectWhenDeduceConstructorReturnsBean() { + ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean + .forValueObject(DeducedConstructorBinding.class, "valueObjectBean", MergedAnnotation.missing(), true); + assertThat(propertiesBean.getName()).isEqualTo("valueObjectBean"); + assertThat(propertiesBean.getInstance()).isNull(); + assertThat(propertiesBean.getType()).isEqualTo(DeducedConstructorBinding.class); + assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.VALUE_OBJECT); + assertThat(propertiesBean.getAnnotation()).isNotNull(); + Bindable target = propertiesBean.asBindTarget(); + assertThat(target.getType()).isEqualTo(ResolvableType.forClass(DeducedConstructorBinding.class)); + assertThat(target.getValue()).isNull(); + assertThat(ConfigurationPropertiesBindConstructorProvider.INSTANCE + .getBindConstructor(ConstructorBindingOnConstructor.class, false, false)).isNotNull(); } @Test void forValueObjectWhenJavaBeanBindTypeThrowsException() { assertThatIllegalStateException() - .isThrownBy(() -> ConfigurationPropertiesBean.forValueObject(AnnotatedBean.class, "annotatedBean")) + .isThrownBy(() -> ConfigurationPropertiesBean.forValueObject(AnnotatedBean.class, "annotatedBean", + MergedAnnotation.missing(), false)) .withMessage("Bean 'annotatedBean' is not a @ConfigurationProperties value object"); assertThatIllegalStateException() - .isThrownBy( - () -> ConfigurationPropertiesBean.forValueObject(NonAnnotatedBean.class, "nonAnnotatedBean")) + .isThrownBy(() -> ConfigurationPropertiesBean.forValueObject(NonAnnotatedBean.class, "nonAnnotatedBean", + MergedAnnotation.missing(), false)) .withMessage("Bean 'nonAnnotatedBean' is not a @ConfigurationProperties value object"); } @@ -241,7 +259,7 @@ class ConfigurationPropertiesBeanTests { } @Test - void bindTypeForTypeWhenNoConstructorBindingOnConstructorReturnsValueObject() { + void bindTypeForTypeWhenConstructorBindingOnConstructorReturnsValueObject() { BindMethod bindType = BindMethod.forType(ConstructorBindingOnConstructor.class); assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT); } @@ -254,6 +272,18 @@ class ConfigurationPropertiesBeanTests { + " has more than one @ConstructorBinding constructor"); } + @Test + void bindTypeForTypeWhenDeducedConstructorBindingOnValueObjectReturnsValueObject() { + BindMethod bindType = BindMethod.forType(DeducedConstructorBinding.class, true); + assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT); + } + + @Test + void bindTypeForTypeWhenDeducedConstructorBindingOnJavaBeanReturnsJavABean() { + BindMethod bindType = BindMethod.forType(NonAnnotatedBean.class, true); + assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN); + } + private void get(Class configuration, String beanName, ThrowingConsumer consumer) throws Throwable { get(configuration, beanName, true, consumer); @@ -474,6 +504,14 @@ class ConfigurationPropertiesBeanTests { } + @ConfigurationProperties + static class DeducedConstructorBinding { + + DeducedConstructorBinding(String name) { + } + + } + @Configuration(proxyBeanMethods = false) @Import(NonAnnotatedBeanConfigurationImportSelector.class) static class NonAnnotatedBeanImportConfiguration { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java index 553c0a7c08..c527f6d2df 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java @@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.context.properties.scan.combined.c.CombinedConfiguration; import org.springframework.boot.context.properties.scan.combined.d.OtherCombinedConfiguration; import org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration; @@ -53,8 +52,8 @@ class ConfigurationPropertiesScanRegistrarTests { "foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties"); BeanDefinition barDefinition = this.beanFactory.getBeanDefinition( "bar-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$BarProperties"); - assertThat(bingDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); - assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(bingDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + assertThat(fooDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); assertThat(barDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); } @@ -66,7 +65,7 @@ class ConfigurationPropertiesScanRegistrarTests { getAnnotationMetadata(ConfigurationPropertiesScanConfiguration.TestConfiguration.class), beanFactory); BeanDefinition fooDefinition = beanFactory.getBeanDefinition( "foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties"); - assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(fooDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); } @Test @@ -85,11 +84,11 @@ class ConfigurationPropertiesScanRegistrarTests { "b.first-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BFirstProperties"); BeanDefinition bSecondDefinition = beanFactory.getBeanDefinition( "b.second-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BSecondProperties"); - assertThat(aDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(aDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); // Constructor injection assertThat(bFirstDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); // Post-processing injection - assertThat(bSecondDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(bSecondDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java index d9c0442e89..913822ab05 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.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. @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.core.type.AnnotationMetadata; import static org.assertj.core.api.Assertions.assertThat; @@ -52,11 +51,11 @@ class EnableConfigurationPropertiesRegistrarTests { } @Test - void typeWithDefaultConstructorShouldRegisterGenericBeanDefinition() throws Exception { + void typeWithDefaultConstructorShouldRegisterConfigurationPropertiesBeanDefinition() throws Exception { register(TestConfiguration.class); BeanDefinition beanDefinition = this.beanFactory .getBeanDefinition("foo-" + getClass().getName() + "$FooProperties"); - assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); } @Test @@ -68,11 +67,11 @@ class EnableConfigurationPropertiesRegistrarTests { } @Test - void typeWithMultipleConstructorsShouldRegisterGenericBeanDefinition() throws Exception { + void typeWithMultipleConstructorsShouldRegisterConfigurationPropertiesBeanDefinition() throws Exception { register(TestConfiguration.class); BeanDefinition beanDefinition = this.beanFactory .getBeanDefinition("bing-" + getClass().getName() + "$BingProperties"); - assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeanTests.java new file mode 100644 index 0000000000..3f67130e26 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ImportConfigurationPropertiesBeanTests.java @@ -0,0 +1,160 @@ +/* + * 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.context.properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.support.TestPropertySourceUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ImportConfigurationPropertiesBean}. + * + * @author Phillip Webb + */ +class ImportConfigurationPropertiesBeanTests { + + @Test + void importJavaBean() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context.getEnvironment(), "test.name=spring"); + context.register(JavaBeanConfig.class); + context.refresh(); + assertThat(context.getBean(JavaBean.class).getName()).isEqualTo("spring"); + } + } + + @Test + void importValueObject() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context.getEnvironment(), "test.value=spring"); + context.register(ValueObjectConfig.class); + context.refresh(); + assertThat(context.getBean(ValueObject.class).getValue()).isEqualTo("spring"); + } + } + + @Test + void importMultiConstructorValueObjectFails() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context.getEnvironment(), "test.name=spring"); + context.register(MultiConstructorValueObjectConfig.class); + assertThatIllegalStateException().isThrownBy(context::refresh).withMessageContaining("Unable to deduce"); + } + } + + @Test + void importMultipleTypes() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context.getEnvironment(), "test.name=spring", + "test.value=boot"); + context.register(ImportMultipleTypesConfig.class); + context.refresh(); + assertThat(context.getBean(JavaBean.class).getName()).isEqualTo("spring"); + assertThat(context.getBean(ValueObject.class).getValue()).isEqualTo("boot"); + } + } + + @Test + void importRepeatedAnnotations() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context.getEnvironment(), "jb.name=spring", + "vo.value=boot"); + context.register(ImportRepeatedAnnotationsConfig.class); + context.refresh(); + // assertThat(context.getBean(JavaBean.class).getName()).isEqualTo("spring"); + // assertThat(context.getBean(ValueObject.class).getValue()).isEqualTo("boot"); + } + } + + @Configuration + @ImportConfigurationPropertiesBean(prefix = "test", type = JavaBean.class) + static class JavaBeanConfig { + + } + + static class JavaBean { + + private String name; + + String getName() { + return this.name; + } + + void setName(String name) { + this.name = name; + } + + } + + @Configuration + @ImportConfigurationPropertiesBean(prefix = "test", type = ValueObject.class) + static class ValueObjectConfig { + + } + + static class ValueObject { + + private final String value; + + ValueObject(String value) { + this.value = value; + } + + String getValue() { + return this.value; + } + + } + + @Configuration + @ImportConfigurationPropertiesBean(prefix = "test", type = MultiConstructorValueObject.class) + static class MultiConstructorValueObjectConfig { + + } + + static class MultiConstructorValueObject { + + MultiConstructorValueObject() { + } + + MultiConstructorValueObject(String name) { + } + + MultiConstructorValueObject(String name, int age) { + } + + } + + @Configuration + @ImportConfigurationPropertiesBean(type = { ValueObject.class, JavaBean.class }, prefix = "test") + static class ImportMultipleTypesConfig { + + } + + @Configuration + @ImportConfigurationPropertiesBean(type = ValueObject.class, prefix = "vo") + @ImportConfigurationPropertiesBean(type = JavaBean.class, prefix = "jb") + static class ImportRepeatedAnnotationsConfig { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java index f44a5518f0..701689abe7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java @@ -139,6 +139,19 @@ class BindableTests { assertThat(Bindable.of(String.class).withAnnotations(annotation).getAnnotation(Bean.class)).isNull(); } + @Test + void withAttributeShouldSetAttribute() { + Bindable bindable = Bindable.of(String.class); + Bindable withOne = bindable.withAttribute("one", 1); + Bindable withOneAndTwo = withOne.withAttribute("two", 2); + assertThat(bindable.getAttribute("one")).isNull(); + assertThat(bindable.getAttribute("two")).isNull(); + assertThat(withOne.getAttribute("one")).isEqualTo(1); + assertThat(withOne.getAttribute("two")).isNull(); + assertThat(withOneAndTwo.getAttribute("one")).isEqualTo(1); + assertThat(withOneAndTwo.getAttribute("two")).isEqualTo(2); + } + @Test void toStringShouldShowDetails() { Annotation annotation = AnnotationUtils.synthesizeAnnotation(TestAnnotation.class); @@ -154,9 +167,10 @@ class BindableTests { Bindable bindable1 = Bindable.of(String.class).withExistingValue("foo").withAnnotations(annotation); Bindable bindable2 = Bindable.of(String.class).withExistingValue("foo").withAnnotations(annotation); Bindable bindable3 = Bindable.of(String.class).withExistingValue("fof").withAnnotations(annotation); + Bindable bindable4 = Bindable.of(String.class).withExistingValue("foo").withAnnotations(annotation) + .withAttribute("bar", "bar"); assertThat(bindable1.hashCode()).isEqualTo(bindable2.hashCode()); - assertThat(bindable1).isEqualTo(bindable1).isEqualTo(bindable2); - assertThat(bindable1).isEqualTo(bindable3); + assertThat(bindable1).isEqualTo(bindable1).isEqualTo(bindable2).isEqualTo(bindable3).isNotEqualTo(bindable4); } @Test // gh-18218 diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt index 1144590a6f..709af3ed05 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt @@ -3,7 +3,6 @@ package org.springframework.boot.context.properties import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.beans.factory.support.DefaultListableBeanFactory -import org.springframework.beans.factory.support.GenericBeanDefinition import org.springframework.core.type.AnnotationMetadata import org.springframework.core.type.classreading.SimpleMetadataReaderFactory @@ -24,7 +23,7 @@ class KotlinConfigurationPropertiesBeanRegistrarTests { this.registrar.register(FooProperties::class.java) val beanDefinition = this.beanFactory.getBeanDefinition( "foo-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$FooProperties") - assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition::class.java) + assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition::class.java) } @Test @@ -41,7 +40,7 @@ class KotlinConfigurationPropertiesBeanRegistrarTests { this.registrar.register(BingProperties::class.java) val beanDefinition = this.beanFactory.getBeanDefinition( "bing-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BingProperties") - assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition::class.java) + assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition::class.java) } @ConfigurationProperties(prefix = "foo")