Generate hints for all methods that JavaBeanBinder may call

Fixes gh-35397
This commit is contained in:
Andy Wilkinson 2023-05-11 13:34:43 +01:00
parent c254610e4d
commit acafb907f6
3 changed files with 93 additions and 44 deletions

View File

@ -16,7 +16,6 @@
package org.springframework.boot.context.properties.bind;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@ -35,8 +34,9 @@ import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.bind.JavaBeanBinder.BeanProperties;
import org.springframework.boot.context.properties.bind.JavaBeanBinder.BeanProperty;
import org.springframework.core.KotlinDetector;
import org.springframework.core.KotlinReflectionParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
@ -45,7 +45,6 @@ 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.ObjectUtils;
import org.springframework.util.ReflectionUtils;
/**
@ -131,7 +130,7 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
private final Constructor<?> bindConstructor;
private final PropertyDescriptor[] propertyDescriptors;
private final BeanProperties bean;
private final Set<Class<?>> seen;
@ -145,7 +144,7 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
Set<Class<?>> compiledWithoutParameters) {
this.type = type;
this.bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(Bindable.of(type), nestedType);
this.propertyDescriptors = BeanUtils.getPropertyDescriptors(type);
this.bean = JavaBeanBinder.BeanProperties.of(Bindable.of(type));
this.seen = seen;
this.compiledWithoutParameters = compiledWithoutParameters;
}
@ -159,7 +158,7 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
if (this.bindConstructor != null) {
handleValueObjectProperties(hints);
}
else if (!ObjectUtils.isEmpty(this.propertyDescriptors)) {
else if (this.bean != null && !this.bean.getProperties().isEmpty()) {
handleJavaBeanProperties(hints);
}
}
@ -201,33 +200,18 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
}
private void handleJavaBeanProperties(ReflectionHints hints) {
for (PropertyDescriptor propertyDescriptor : this.propertyDescriptors) {
Method writeMethod = propertyDescriptor.getWriteMethod();
if (writeMethod != null) {
hints.registerMethod(writeMethod, ExecutableMode.INVOKE);
Map<String, BeanProperty> properties = this.bean.getProperties();
properties.forEach((name, property) -> {
Method getter = property.getGetter();
if (getter != null) {
hints.registerMethod(getter, ExecutableMode.INVOKE);
}
Method readMethod = propertyDescriptor.getReadMethod();
if (readMethod != null) {
ResolvableType propertyType = ResolvableType.forMethodReturnType(readMethod, this.type);
String propertyName = propertyDescriptor.getName();
if (isSetterMandatory(propertyName, propertyType) && writeMethod == null) {
continue;
}
handleProperty(hints, propertyName, propertyType);
hints.registerMethod(readMethod, ExecutableMode.INVOKE);
Method setter = property.getSetter();
if (setter != null) {
hints.registerMethod(setter, ExecutableMode.INVOKE);
}
}
}
private boolean isSetterMandatory(String propertyName, ResolvableType propertyType) {
Class<?> propertyClass = propertyType.resolve();
if (propertyClass == null) {
return true;
}
if (isContainer(propertyType)) {
return false;
}
return !isNestedType(propertyName, propertyClass);
handleProperty(hints, name, property.getType());
});
}
private void handleProperty(ReflectionHints hints, String propertyName, ResolvableType propertyType) {

View File

@ -110,21 +110,17 @@ class JavaBeanBinder implements DataObjectBinder {
}
/**
* The bean being bound.
*
* @param <T> the bean type
* The properties of a bean that may be bound.
*/
static class Bean<T> {
static class BeanProperties {
private static Bean<?> cached;
private final Map<String, BeanProperty> properties = new LinkedHashMap<>();
private final ResolvableType type;
private final Class<?> resolvedType;
private final Map<String, BeanProperty> properties = new LinkedHashMap<>();
Bean(ResolvableType type, Class<?> resolvedType) {
BeanProperties(ResolvableType type, Class<?> resolvedType) {
this.type = type;
this.resolvedType = resolvedType;
addProperties(resolvedType);
@ -202,10 +198,39 @@ class JavaBeanBinder implements DataObjectBinder {
}
}
Map<String, BeanProperty> getProperties() {
protected final ResolvableType getType() {
return this.type;
}
protected final Class<?> getResolvedType() {
return this.resolvedType;
}
final Map<String, BeanProperty> getProperties() {
return this.properties;
}
static BeanProperties of(Bindable<?> bindable) {
ResolvableType type = bindable.getType();
Class<?> resolvedType = type.resolve(Object.class);
return new BeanProperties(type, resolvedType);
}
}
/**
* The bean being bound.
*
* @param <T> the bean type
*/
static class Bean<T> extends BeanProperties {
private static Bean<?> cached;
Bean(ResolvableType type, Class<?> resolvedType) {
super(type, resolvedType);
}
@SuppressWarnings("unchecked")
BeanSupplier<T> getSupplier(Bindable<T> target) {
return new BeanSupplier<>(() -> {
@ -214,7 +239,7 @@ class JavaBeanBinder implements DataObjectBinder {
instance = target.getValue().get();
}
if (instance == null) {
instance = (T) BeanUtils.instantiateClass(this.resolvedType);
instance = (T) BeanUtils.instantiateClass(getResolvedType());
}
return instance;
});
@ -255,10 +280,10 @@ class JavaBeanBinder implements DataObjectBinder {
}
private boolean isOfType(ResolvableType type, Class<?> resolvedType) {
if (this.type.hasGenerics() || type.hasGenerics()) {
return this.type.equals(type);
if (getType().hasGenerics() || type.hasGenerics()) {
return getType().equals(type);
}
return this.resolvedType != null && this.resolvedType.equals(resolvedType);
return getResolvedType() != null && getResolvedType().equals(resolvedType);
}
}
@ -376,6 +401,14 @@ class JavaBeanBinder implements DataObjectBinder {
}
}
Method getGetter() {
return this.getter;
}
Method getSetter() {
return this.setter;
}
}
}

View File

@ -235,6 +235,14 @@ class BindableRuntimeHintsRegistrarTests {
.accepts(runtimeHints);
}
@Test
void registerHintsWhenHasPackagePrivateGettersAndSetters() {
RuntimeHints runtimeHints = registerHints(PackagePrivateGettersAndSetters.class);
assertThat(runtimeHints.reflection().typeHints()).singleElement()
.satisfies(javaBeanBinding(PackagePrivateGettersAndSetters.class, "getAlpha", "setAlpha", "getBravo",
"setBravo"));
}
private Consumer<TypeHint> javaBeanBinding(Class<?> type, String... expectedMethods) {
return javaBeanBinding(type, type.getDeclaredConstructors()[0], expectedMethods);
}
@ -458,6 +466,30 @@ class BindableRuntimeHintsRegistrarTests {
}
public static class PackagePrivateGettersAndSetters {
private String alpha;
private Map<String, String> bravo;
String getAlpha() {
return this.alpha;
}
void setAlpha(String alpha) {
this.alpha = alpha;
}
Map<String, String> getBravo() {
return this.bravo;
}
void setBravo(Map<String, String> bravo) {
this.bravo = bravo;
}
}
public static class Address {
}