Relax @ConstructorBinding member class requirement
Update `@ConfigurationProperties` so that `@ConstructorBinding` classes no longer need to repeat the annotation for their members. Closes gh-18481
This commit is contained in:
		
							parent
							
								
									e6bb7a0a6f
								
							
						
					
					
						commit
						386c0a60a7
					
				|  | @ -881,7 +881,6 @@ The example in the previous section can be rewritten in an immutable fashion as | |||
| 
 | ||||
| 			private final List<String> roles; | ||||
| 
 | ||||
| 			@ConstructorBinding | ||||
| 			public Security(String username, String password, | ||||
| 					@DefaultValue("USER") List<String> roles) { | ||||
| 				this.username = username; | ||||
|  | @ -903,16 +902,16 @@ The example in the previous section can be rewritten in an immutable fashion as | |||
| In this setup, the `@ImmutableConfigurationProperties` annotation is used to indicate that constructor binding should be used. | ||||
| This means that the binder will expect to find a constructor with the parameters that you wish to have bound. | ||||
| 
 | ||||
| Nested classes that also require constructor binding (such as `Security` in the example above) should use the `@ConstructorBinding` annotation. | ||||
| Nested members of a `@ImmutableConfigurationProperties` class (such as `Security` in the example above) will also be bound via their constructor. | ||||
| 
 | ||||
| Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property. | ||||
| 
 | ||||
| TIP: You can also use `@ConstructorBinding` on the actual constructor that should be bound. | ||||
| This is required if you have more than one constructor for your class. | ||||
| 
 | ||||
| NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning. | ||||
| You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`) | ||||
| 
 | ||||
| TIP: `@ImmutableConfigurationProperties` is actually a meta-annotation composed of `@ConfigurationProperties` and `@ConstructorBinding`. | ||||
| If you  have more than one constructor for your class you can also use `@ConstructorBinding` directly on actual constructor that should be bound. | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| [[boot-features-external-config-enabling]] | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ import java.util.stream.Collectors; | |||
| import java.util.stream.Stream; | ||||
| 
 | ||||
| import javax.lang.model.element.ExecutableElement; | ||||
| import javax.lang.model.element.NestingKind; | ||||
| import javax.lang.model.element.TypeElement; | ||||
| import javax.lang.model.element.VariableElement; | ||||
| import javax.lang.model.type.TypeMirror; | ||||
|  | @ -169,13 +170,24 @@ class PropertyDescriptorResolver { | |||
| 		} | ||||
| 
 | ||||
| 		static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) { | ||||
| 			boolean constructorBoundType = env.hasConstructorBindingAnnotation(type); | ||||
| 			boolean constructorBoundType = isConstructorBoundType(type, env); | ||||
| 			List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); | ||||
| 			List<ExecutableElement> boundConstructors = constructors.stream() | ||||
| 					.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList()); | ||||
| 			return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors); | ||||
| 		} | ||||
| 
 | ||||
| 		private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) { | ||||
| 			if (env.hasConstructorBindingAnnotation(type)) { | ||||
| 				return true; | ||||
| 			} | ||||
| 			if (type.getNestingKind() == NestingKind.MEMBER) { | ||||
| 				return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env); | ||||
| 			} | ||||
| 			return false; | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,41 @@ | |||
| /* | ||||
|  * Copyright 2012-2019 the original author or authors. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package org.springframework.boot.configurationprocessor; | ||||
| 
 | ||||
| import org.junit.jupiter.api.Test; | ||||
| 
 | ||||
| import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; | ||||
| import org.springframework.boot.configurationprocessor.metadata.Metadata; | ||||
| import org.springframework.boot.configurationsample.immutable.DeducedImmutableClassProperties; | ||||
| 
 | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| 
 | ||||
| /** | ||||
|  * Metadata generation tests for immutable properties deduced because they're nested. | ||||
|  * | ||||
|  * @author Phillip Webb | ||||
|  */ | ||||
| class DeducedImmutablePropertiesMetadataGenerationTests extends AbstractMetadataGenerationTests { | ||||
| 
 | ||||
| 	@Test | ||||
| 	void immutableSimpleProperties() { | ||||
| 		ConfigurationMetadata metadata = compile(DeducedImmutableClassProperties.class); | ||||
| 		assertThat(metadata).has(Metadata.withGroup("test").fromSource(DeducedImmutableClassProperties.class)); | ||||
| 		assertThat(metadata).has(Metadata.withProperty("test.nested.name", String.class)); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,55 @@ | |||
| /* | ||||
|  * Copyright 2012-2019 the original author or authors. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package org.springframework.boot.configurationsample.immutable; | ||||
| 
 | ||||
| import org.springframework.boot.configurationsample.ConfigurationProperties; | ||||
| import org.springframework.boot.configurationsample.ConstructorBinding; | ||||
| 
 | ||||
| /** | ||||
|  * Inner properties, in immutable format. | ||||
|  * | ||||
|  * @author Phillip Webb | ||||
|  */ | ||||
| @ConfigurationProperties("test") | ||||
| @ConstructorBinding | ||||
| public class DeducedImmutableClassProperties { | ||||
| 
 | ||||
| 	private final Nested nested; | ||||
| 
 | ||||
| 	public DeducedImmutableClassProperties(Nested nested) { | ||||
| 		this.nested = nested; | ||||
| 	} | ||||
| 
 | ||||
| 	public Nested getNested() { | ||||
| 		return this.nested; | ||||
| 	} | ||||
| 
 | ||||
| 	public static class Nested { | ||||
| 
 | ||||
| 		private String name; | ||||
| 
 | ||||
| 		public Nested(String name) { | ||||
| 			this.name = name; | ||||
| 		} | ||||
| 
 | ||||
| 		public String getName() { | ||||
| 			return this.name; | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -265,13 +265,17 @@ public final class ConfigurationPropertiesBean { | |||
| 		VALUE_OBJECT; | ||||
| 
 | ||||
| 		static BindMethod forClass(Class<?> type) { | ||||
| 			if (MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(ConstructorBinding.class) | ||||
| 					|| findBindConstructor(type) != null) { | ||||
| 			if (isConstructorBindingType(type) || findBindConstructor(type) != null) { | ||||
| 				return VALUE_OBJECT; | ||||
| 			} | ||||
| 			return JAVA_BEAN; | ||||
| 		} | ||||
| 
 | ||||
| 		private static boolean isConstructorBindingType(Class<?> type) { | ||||
| 			return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES) | ||||
| 					.isPresent(ConstructorBinding.class); | ||||
| 		} | ||||
| 
 | ||||
| 		static Constructor<?> findBindConstructor(Class<?> type) { | ||||
| 			if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) { | ||||
| 				Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type); | ||||
|  |  | |||
|  | @ -836,6 +836,19 @@ class ConfigurationPropertiesTests { | |||
| 		assertThat(nested.getAge()).isEqualTo(0); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test // gh-18481 | ||||
| 	void loadWhenBindingToNestedConstructorPropertiesWithDeducedNestedShouldBind() { | ||||
| 		MutablePropertySources sources = this.context.getEnvironment().getPropertySources(); | ||||
| 		Map<String, Object> source = new HashMap<>(); | ||||
| 		source.put("test.name", "spring"); | ||||
| 		source.put("test.nested.age", "5"); | ||||
| 		sources.addLast(new MapPropertySource("test", source)); | ||||
| 		load(DeducedNestedConstructorPropertiesConfiguration.class); | ||||
| 		DeducedNestedConstructorProperties bean = this.context.getBean(DeducedNestedConstructorProperties.class); | ||||
| 		assertThat(bean.getName()).isEqualTo("spring"); | ||||
| 		assertThat(bean.getNested().getAge()).isEqualTo(5); | ||||
| 	} | ||||
| 
 | ||||
| 	private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) { | ||||
| 		return load(new Class<?>[] { configuration }, inlinedProperties); | ||||
| 	} | ||||
|  | @ -2014,4 +2027,46 @@ class ConfigurationPropertiesTests { | |||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Configuration(proxyBeanMethods = false) | ||||
| 	@EnableConfigurationProperties(DeducedNestedConstructorProperties.class) | ||||
| 	static class DeducedNestedConstructorPropertiesConfiguration { | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@ImmutableConfigurationProperties("test") | ||||
| 	static class DeducedNestedConstructorProperties { | ||||
| 
 | ||||
| 		private final String name; | ||||
| 
 | ||||
| 		private final Nested nested; | ||||
| 
 | ||||
| 		DeducedNestedConstructorProperties(String name, Nested nested) { | ||||
| 			this.name = name; | ||||
| 			this.nested = nested; | ||||
| 		} | ||||
| 
 | ||||
| 		String getName() { | ||||
| 			return this.name; | ||||
| 		} | ||||
| 
 | ||||
| 		Nested getNested() { | ||||
| 			return this.nested; | ||||
| 		} | ||||
| 
 | ||||
| 		static class Nested { | ||||
| 
 | ||||
| 			private final int age; | ||||
| 
 | ||||
| 			Nested(int age) { | ||||
| 				this.age = age; | ||||
| 			} | ||||
| 
 | ||||
| 			int getAge() { | ||||
| 				return this.age; | ||||
| 			} | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue