diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 00982b83ef5..75559a1c072 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -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> unknownEditorTypes = Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64)); + private static final Map, Object> DEFAULT_TYPE_VALUES; + + static { + Map, 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); diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java index 21c49293af7..984c2a03b78 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -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 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 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 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; + } + } + } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt index 01b2c4a3b0d..74748ff0466 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt @@ -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()