Support primitive types in BeanUtils.instantiateClass args

This commit adds support for primitive default values for
BeanUtils.instantiateClass arguments in Java in order to provide
a consistent behavior in both Java and Kotlin languages.

Closes gh-22531
This commit is contained in:
Sebastien Deleuze 2019-03-11 15:25:46 +01:00
parent 256f8549d0
commit aabfc5f0a1
3 changed files with 127 additions and 5 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-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.
@ -70,6 +70,18 @@ public abstract class BeanUtils {
private static final Set<Class<?>> unknownEditorTypes =
Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64));
private static final Map<Class<?>, Object> DEFAULT_TYPE_VALUES;
static {
Map<Class<?>, Object> values = new HashMap<>();
values.put(boolean.class, false);
values.put(byte.class, (byte) 0);
values.put(short.class, (short) 0);
values.put(int.class, 0);
values.put(long.class, (long) 0);
DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values);
}
/**
* Convenience method to instantiate a class using its no-arg constructor.
@ -159,7 +171,7 @@ public abstract class BeanUtils {
* with optional parameters and default values.
* @param ctor the constructor to instantiate
* @param args the constructor arguments to apply (use {@code null} for an unspecified
* parameter if needed for Kotlin classes with optional parameters and default values)
* parameter, Kotlin optional parameters and Java primitive types are supported)
* @return the new instance
* @throws BeanInstantiationException if the bean cannot be instantiated
* @see Constructor#newInstance
@ -168,8 +180,24 @@ public abstract class BeanUtils {
Assert.notNull(ctor, "Constructor must not be null");
try {
ReflectionUtils.makeAccessible(ctor);
return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ?
KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
return KotlinDelegate.instantiateClass(ctor, args);
}
else {
Class<?>[] parameterTypes = ctor.getParameterTypes();
Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
Object[] argsWithDefaultValues = new Object[args.length];
for (int i = 0 ; i < args.length; i++) {
if (args[i] == null) {
Class<?> parameterType = parameterTypes[i];
argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null);
}
else {
argsWithDefaultValues[i] = args[i];
}
}
return ctor.newInstance(argsWithDefaultValues);
}
}
catch (InstantiationException ex) {
throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-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.
@ -18,6 +18,7 @@ package org.springframework.beans;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@ -28,6 +29,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceEditor;
import org.springframework.lang.Nullable;
import org.springframework.tests.sample.beans.DerivedTestBean;
import org.springframework.tests.sample.beans.ITestBean;
import org.springframework.tests.sample.beans.TestBean;
@ -40,6 +42,7 @@ import static org.junit.Assert.*;
* @author Juergen Hoeller
* @author Rob Harrop
* @author Chris Beams
* @author Sebastien Deleuze
* @since 19.05.2003
*/
public class BeanUtilsTests {
@ -68,6 +71,31 @@ public class BeanUtilsTests {
}
}
@Test // gh-22531
public void testInstantiateClassWithOptionalNullableType() throws NoSuchMethodException {
Constructor<BeanWithNullableTypes> ctor = BeanWithNullableTypes.class.getDeclaredConstructor(
Integer.class, Boolean.class, String.class);
BeanWithNullableTypes bean = BeanUtils.instantiateClass(ctor, null, null, "foo");
assertNull(bean.getCounter());
assertNull(bean.isFlag());
assertEquals("foo", bean.getValue());
}
@Test // gh-22531
public void testInstantiateClassWithOptionalPrimitiveType() throws NoSuchMethodException {
Constructor<BeanWithPrimitiveTypes> ctor = BeanWithPrimitiveTypes.class.getDeclaredConstructor(int.class, boolean.class, String.class);
BeanWithPrimitiveTypes bean = BeanUtils.instantiateClass(ctor, null, null, "foo");
assertEquals(0, bean.getCounter());
assertEquals(false, bean.isFlag());
assertEquals("foo", bean.getValue());
}
@Test(expected = BeanInstantiationException.class) // gh-22531
public void testInstantiateClassWithMoreArgsThanParameters() throws NoSuchMethodException {
Constructor<BeanWithPrimitiveTypes> ctor = BeanWithPrimitiveTypes.class.getDeclaredConstructor(int.class, boolean.class, String.class);
BeanUtils.instantiateClass(ctor, null, null, "foo", null);
}
@Test
public void testGetPropertyDescriptors() throws Exception {
PropertyDescriptor[] actual = Introspector.getBeanInfo(TestBean.class).getPropertyDescriptors();
@ -458,4 +486,60 @@ public class BeanUtilsTests {
}
}
private static class BeanWithNullableTypes {
private Integer counter;
private Boolean flag;
private String value;
public BeanWithNullableTypes(@Nullable Integer counter, @Nullable Boolean flag, String value) {
this.counter = counter;
this.flag = flag;
this.value = value;
}
@Nullable
public Integer getCounter() {
return counter;
}
@Nullable
public Boolean isFlag() {
return flag;
}
public String getValue() {
return value;
}
}
private static class BeanWithPrimitiveTypes {
private int counter;
private boolean flag;
private String value;
public BeanWithPrimitiveTypes(int counter, boolean flag, String value) {
this.counter = counter;
this.flag = flag;
this.value = value;
}
public int getCounter() {
return counter;
}
public boolean isFlag() {
return flag;
}
public String getValue() {
return value;
}
}
}

View File

@ -59,6 +59,14 @@ class BeanUtilsKotlinTests {
assertEquals(12, bar.param2)
}
@Test // gh-22531
fun `Instantiate immutable class with nullable parameter`() {
val constructor = BeanUtils.findPrimaryConstructor(Qux::class.java)!!
val bar = BeanUtils.instantiateClass(constructor, "a", null)
assertEquals("a", bar.param1)
assertNull(bar.param2)
}
@Test // SPR-15851
fun `Instantiate mutable class with declared constructor and default values for all parameters`() {
val baz = BeanUtils.instantiateClass(Baz::class.java.getDeclaredConstructor())
@ -72,6 +80,8 @@ class BeanUtilsKotlinTests {
class Baz(var param1: String = "a", var param2: Int = 12)
class Qux(val param1: String, val param2: Int?)
class TwoConstructorsWithDefaultOne {
constructor()