From 421f2fac6769be39aa443b764cad54d433a40aa6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 15 Nov 2022 17:52:31 +0000 Subject: [PATCH] Fail fast when constructor bound and not compiled with -parameters Closes gh-33182 --- .../asciidoc/features/external-config.adoc | 3 ++ .../bind/BindableRuntimeHintsRegistrar.java | 30 ++++++++++--- ...ngParametersCompilerArgumentException.java | 42 +++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MissingParametersCompilerArgumentException.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc index d1ef680bec97..353ab9f444e6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc @@ -740,6 +740,9 @@ include::code:nonnull/MyProperties[tag=*] 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 (for example `@Component` beans, beans created by using `@Bean` methods or beans loaded by using `@Import`) +NOTE: To use constructor binding in a native image the class must be compiled with `-parameters`. +This will happen automatically if you use Spring Boot's Gradle plugin or if you use Maven and `spring-boot-starter-parent`. + NOTE: The use of `java.util.Optional` with `@ConfigurationProperties` is not recommended as it is primarily intended for use as a return type. As such, it is not well-suited to configuration property injection. For consistency with properties of other types, if you do declare an `Optional` property and it has no value, `null` rather than an empty `Optional` will be bound. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java index 09814a2641ec..d2984e1fee69 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java @@ -37,7 +37,9 @@ import org.springframework.beans.BeanInfoFactory; import org.springframework.beans.ExtendedBeanInfoFactory; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; +import org.springframework.core.StandardReflectionParameterNameDiscoverer; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -71,8 +73,12 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { } public void registerHints(RuntimeHints hints) { + Set> compiledWithoutParameters = new HashSet<>(); for (Class type : this.types) { - new Processor(type).process(hints.reflection()); + new Processor(type, compiledWithoutParameters).process(hints.reflection()); + } + if (!compiledWithoutParameters.isEmpty()) { + throw new MissingParametersCompilerArgumentException(compiledWithoutParameters); } } @@ -87,6 +93,8 @@ public static BindableRuntimeHintsRegistrar forTypes(Class... types) { private final class Processor { + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new StandardReflectionParameterNameDiscoverer(); + private final Class type; private final Constructor bindConstructor; @@ -95,15 +103,19 @@ private final class Processor { private final Set> seen; - Processor(Class type) { - this(type, false, new HashSet<>()); + private final Set> compiledWithoutParameters; + + Processor(Class type, Set> compiledWithoutParameters) { + this(type, false, new HashSet<>(), compiledWithoutParameters); } - private Processor(Class type, boolean nestedType, Set> seen) { + private Processor(Class type, boolean nestedType, Set> seen, + Set> compiledWithoutParameters) { this.type = type; this.bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(Bindable.of(type), nestedType); this.beanInfo = getBeanInfo(type); this.seen = seen; + this.compiledWithoutParameters = compiledWithoutParameters; } private static BeanInfo getBeanInfo(Class beanType) { @@ -135,6 +147,7 @@ else if (this.beanInfo != null) { private void handleConstructor(ReflectionHints hints) { if (this.bindConstructor != null) { + verifyParameterNamesAreAvailable(); hints.registerConstructor(this.bindConstructor, ExecutableMode.INVOKE); return; } @@ -142,6 +155,13 @@ private void handleConstructor(ReflectionHints hints) { .ifPresent((constructor) -> hints.registerConstructor(constructor, ExecutableMode.INVOKE)); } + private void verifyParameterNamesAreAvailable() { + String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(this.bindConstructor); + if (parameterNames == null) { + this.compiledWithoutParameters.add(this.bindConstructor.getDeclaringClass()); + } + } + private boolean hasNoParameters(Constructor candidate) { return candidate.getParameterCount() == 0; } @@ -205,7 +225,7 @@ else if (isNestedType(propertyName, propertyClass)) { } private void processNested(Class type, ReflectionHints hints) { - new Processor(type, true, this.seen).process(hints); + new Processor(type, true, this.seen, this.compiledWithoutParameters).process(hints); } private Class getComponentClass(ResolvableType type) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MissingParametersCompilerArgumentException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MissingParametersCompilerArgumentException.java new file mode 100644 index 000000000000..a8028280af99 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MissingParametersCompilerArgumentException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties.bind; + +import java.util.Set; + +/** + * Exception thrown to indicate that a class has not been compiled with + * {@code -parameters}. + * + * @author Andy Wilkinson + */ +class MissingParametersCompilerArgumentException extends RuntimeException { + + MissingParametersCompilerArgumentException(Set> faultyClasses) { + super(message(faultyClasses)); + } + + private static String message(Set> faultyClasses) { + StringBuilder message = new StringBuilder(String.format( + "Constructor binding in a native image requires compilation with -parameters but the following classes were compiled without it:%n")); + for (Class faultyClass : faultyClasses) { + message.append(String.format("\t%s%n", faultyClass.getName())); + } + return message.toString(); + } + +}