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 374600c2e21..e5f10bf1ca3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -18,7 +18,6 @@ package org.springframework.beans; import java.beans.PropertyDescriptor; import java.beans.PropertyEditor; -import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -42,6 +41,7 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -70,21 +70,6 @@ public abstract class BeanUtils { private static final Set> unknownEditorTypes = Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64)); - @Nullable - private static final Class kotlinMetadata; - - static { - Class metadata; - try { - metadata = ClassUtils.forName("kotlin.Metadata", BeanUtils.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Kotlin API not available - no special support for Kotlin class instantiation - metadata = null; - } - kotlinMetadata = metadata; - } - /** * Convenience method to instantiate a class using its no-arg constructor. @@ -127,7 +112,7 @@ public abstract class BeanUtils { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } try { - Constructor ctor = (useKotlinSupport(clazz) ? + Constructor ctor = (KotlinDetector.isKotlinType(clazz) ? KotlinDelegate.findPrimaryConstructor(clazz) : clazz.getDeclaredConstructor()); if (ctor == null) { throw new BeanInstantiationException(clazz, "No default constructor found"); @@ -174,7 +159,7 @@ public abstract class BeanUtils { Assert.notNull(ctor, "Constructor must not be null"); try { ReflectionUtils.makeAccessible(ctor); - return (useKotlinSupport(ctor.getDeclaringClass()) ? + return (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); } catch (InstantiationException ex) { @@ -191,6 +176,28 @@ public abstract class BeanUtils { } } + /** + * Return the primary constructor of the provided class. For Kotlin classes, this + * returns the Java constructor corresponding to the Kotlin primary constructor + * (as defined in the Kotlin specification). Otherwise, in particular for non-Kotlin + * classes, this simply returns {@code null}. + * @param clazz the class to check + * @since 5.0 + * @see Kotlin docs + */ + @SuppressWarnings("unchecked") + @Nullable + public static Constructor findPrimaryConstructor(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + if (KotlinDetector.isKotlinType(clazz)) { + Constructor kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(clazz); + if (kotlinPrimaryConstructor != null) { + return kotlinPrimaryConstructor; + } + } + return null; + } + /** * Find a method with the given method name and the given parameter types, * declared on the given class or one of its superclasses. Prefers public methods, @@ -331,40 +338,6 @@ public abstract class BeanUtils { return targetMethod; } - /** - * Return the primary constructor of the provided class. For Java classes, it returns - * the single or the default constructor if any. For Kotlin classes, it returns the Java - * constructor corresponding to the Kotlin primary constructor (as defined in - * Kotlin specification), the single or the default constructor if any. - * - * @param clazz the class to check - * @since 5.0 - * @see Kotlin docs - */ - @SuppressWarnings("unchecked") - @Nullable - public static Constructor findPrimaryConstructor(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - if (useKotlinSupport(clazz)) { - Constructor kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(clazz); - if (kotlinPrimaryConstructor != null) { - return kotlinPrimaryConstructor; - } - } - Constructor[] ctors = (Constructor[]) clazz.getConstructors(); - if (ctors.length == 1) { - return ctors[0]; - } - else { - try { - return clazz.getDeclaredConstructor(); - } - catch (NoSuchMethodException ex) { - return null; - } - } - } - /** * Parse a method signature in the form {@code methodName[([arg_list])]}, * where {@code arg_list} is an optional, comma-separated list of fully-qualified @@ -712,15 +685,6 @@ public abstract class BeanUtils { } } - /** - * Return true if Kotlin is present and if the specified class is a Kotlin one. - */ - @SuppressWarnings("unchecked") - private static boolean useKotlinSupport(Class clazz) { - return (kotlinMetadata != null && - clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); - } - /** * Inner class to avoid a hard dependency on Kotlin at runtime. @@ -736,13 +700,13 @@ public abstract class BeanUtils { @Nullable public static Constructor findPrimaryConstructor(Class clazz) { try { - KFunction primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); - if (primaryConstructor == null) { + KFunction primaryCtor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); + if (primaryCtor == null) { return null; } - Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); + Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryCtor); Assert.notNull(constructor, - () -> "Failed to find Java constructor corresponding to Kotlin primary constructor: " + clazz.getName()); + () -> "Failed to find Java constructor for Kotlin primary constructor: " + clazz.getName()); return constructor; } catch (UnsupportedOperationException ex) { @@ -765,9 +729,9 @@ public abstract class BeanUtils { List parameters = kotlinConstructor.getParameters(); Map argParameters = new HashMap<>(parameters.size()); Assert.isTrue(args.length <= parameters.size(), - "The number of provided arguments should be less of equals than the number of constructor parameters"); + "Number of provided arguments should be less of equals than number of constructor parameters"); for (int i = 0 ; i < args.length ; i++) { - if (!(parameters.get(i).isOptional() && (args[i] == null))) { + if (!(parameters.get(i).isOptional() && args[i] == null)) { argParameters.put(parameters.get(i), args[i]); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 3cb69ffee8f..e2ed0eca8c3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -34,10 +34,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import kotlin.jvm.JvmClassMappingKt; -import kotlin.reflect.KFunction; -import kotlin.reflect.full.KClasses; -import kotlin.reflect.jvm.ReflectJvmMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -123,22 +119,6 @@ import org.springframework.util.StringUtils; public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { - @Nullable - private static final Class kotlinMetadata; - - static { - Class metadata; - try { - metadata = ClassUtils.forName("kotlin.Metadata", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Kotlin API not available - no Kotlin support - metadata = null; - } - kotlinMetadata = metadata; - } - - protected final Log logger = LogFactory.getLog(getClass()); private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(); @@ -303,12 +283,9 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean List> candidates = new ArrayList>(rawCandidates.length); Constructor requiredConstructor = null; Constructor defaultConstructor = null; - Constructor kotlinPrimaryConstructor = null; - if (useKotlinSupport(beanClass)) { - kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(beanClass); - } + Constructor primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass); for (Constructor candidate : rawCandidates) { - if (kotlinPrimaryConstructor != null && candidate.isSynthetic()) { + if (primaryConstructor != null && candidate.isSynthetic()) { continue; } AnnotationAttributes ann = findAutowiredAnnotation(candidate); @@ -366,10 +343,10 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor[] {rawCandidates[0]}; } - else if (kotlinPrimaryConstructor != null) { + else if (primaryConstructor != null) { candidateConstructors = (defaultConstructor != null ? - new Constructor[] {kotlinPrimaryConstructor, defaultConstructor} : - new Constructor[] {kotlinPrimaryConstructor}); + new Constructor[] {primaryConstructor, defaultConstructor} : + new Constructor[] {primaryConstructor}); } else { candidateConstructors = new Constructor[0]; @@ -381,15 +358,6 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean return (candidateConstructors.length > 0 ? candidateConstructors : null); } - /** - * Return true if Kotlin is present and if the specified class is a Kotlin one. - */ - @SuppressWarnings("unchecked") - private static boolean useKotlinSupport(Class clazz) { - return (kotlinMetadata != null && - clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); - } - @Override public PropertyValues postProcessPropertyValues( PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException { @@ -771,32 +739,4 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean } } - /** - * Inner class to avoid a hard dependency on Kotlin at runtime. - */ - private static class KotlinDelegate { - - /** - * Return the Java constructor corresponding to the Kotlin primary constructor if any. - * @param clazz the {@link Class} of the Kotlin class - * @see http://kotlinlang.org/docs/reference/classes.html#constructors - */ - @Nullable - public static Constructor findPrimaryConstructor(Class clazz) { - try { - KFunction primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); - if (primaryConstructor == null) { - return null; - } - Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); - Assert.notNull(constructor, "Can't get the Java constructor corresponding to the Kotlin primary constructor of " + clazz.getName()); - return constructor; - } - catch (UnsupportedOperationException ex) { - return null; - } - } - - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 4da1bb2ca29..995c0906c98 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -34,11 +34,11 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.core.GenericTypeResolver; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * Descriptor for a specific dependency that is about to be injected. @@ -51,22 +51,6 @@ import org.springframework.util.ClassUtils; @SuppressWarnings("serial") public class DependencyDescriptor extends InjectionPoint implements Serializable { - @Nullable - private static final Class kotlinMetadata; - - static { - Class metadata; - try { - metadata = ClassUtils.forName("kotlin.Metadata", DependencyDescriptor.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Kotlin API not available - no Kotlin support - metadata = null; - } - kotlinMetadata = metadata; - } - - private final Class declaringClass; @Nullable @@ -183,22 +167,14 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable if (this.field != null) { return !(this.field.getType() == Optional.class || hasNullableAnnotation() || - (useKotlinSupport(this.field.getDeclaringClass()) && KotlinDelegate.isNullable(this.field))); + (KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && + KotlinDelegate.isNullable(this.field))); } else { return !obtainMethodParameter().isOptional(); } } - /** - * Return true if Kotlin is present and if the specified class is a Kotlin one. - */ - @SuppressWarnings("unchecked") - private static boolean useKotlinSupport(Class clazz) { - return (kotlinMetadata != null && - clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); - } - /** * Check whether the underlying field is annotated with any variant of a * {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index 17526e763fa..090d3725185 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -17,7 +17,6 @@ package org.springframework.beans.factory.support; import java.beans.ConstructorProperties; -import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; @@ -32,11 +31,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; - -import kotlin.reflect.KFunction; -import kotlin.reflect.KParameter; -import kotlin.reflect.jvm.ReflectJvmMapping; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeanWrapper; @@ -82,22 +76,6 @@ class ConstructorResolver { private static final NamedThreadLocal currentInjectionPoint = new NamedThreadLocal<>("Current injection point"); - @Nullable - private static final Class kotlinMetadata; - - static { - Class metadata; - try { - metadata = ClassUtils.forName("kotlin.Metadata", ConstructorResolver.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Kotlin API not available - no Kotlin support - metadata = null; - } - kotlinMetadata = metadata; - } - - private final AbstractAutowireCapableBeanFactory beanFactory; @@ -818,8 +796,8 @@ class ConstructorResolver { * Template method for resolving the specified argument which is supposed to be autowired. */ @Nullable - protected Object resolveAutowiredArgument( - MethodParameter param, String beanName, @Nullable Set autowiredBeanNames, TypeConverter typeConverter) { + protected Object resolveAutowiredArgument(MethodParameter param, String beanName, + @Nullable Set autowiredBeanNames, TypeConverter typeConverter) { if (InjectionPoint.class.isAssignableFrom(param.getParameterType())) { InjectionPoint injectionPoint = currentInjectionPoint.get(); @@ -828,18 +806,8 @@ class ConstructorResolver { } return injectionPoint; } - boolean required = !(useKotlinSupport(param.getContainingClass()) && KotlinDelegate.isOptional(param)); return this.beanFactory.resolveDependency( - new DependencyDescriptor(param, required), beanName, autowiredBeanNames, typeConverter); - } - - /** - * Return true if Kotlin is present and if the specified class is a Kotlin one. - */ - @SuppressWarnings("unchecked") - private static boolean useKotlinSupport(Class clazz) { - return (kotlinMetadata != null && - clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); + new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter); } static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) { @@ -947,37 +915,4 @@ class ConstructorResolver { } } - /** - * Inner class to avoid a hard dependency on Kotlin at runtime. - */ - private static class KotlinDelegate { - - /** - * Check whether the specified {@link MethodParameter} represents an optional Kotlin parameter or not. - */ - public static boolean isOptional(MethodParameter param) { - Method method = param.getMethod(); - Constructor ctor = param.getConstructor(); - int index = param.getParameterIndex(); - KFunction function = null; - if (method != null) { - function = ReflectJvmMapping.getKotlinFunction(method); - } - else if (ctor != null) { - function = ReflectJvmMapping.getKotlinFunction(ctor); - } - if (function != null) { - List parameters = function.getParameters(); - return parameters - .stream() - .filter(p -> KParameter.Kind.VALUE.equals(p.getKind())) - .collect(Collectors.toList()) - .get(index) - .isOptional(); - } - return false; - } - - } - } 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 3afba43a532..0909ad36400 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-2013 the original author or authors. + * Copyright 2002-2017 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,7 +18,6 @@ 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; @@ -275,23 +274,6 @@ public class BeanUtilsTests { } } } - - @Test - public void testFindDefaultConstructorAndInstantiate() { - Constructor ctor = BeanUtils.findPrimaryConstructor(Bean.class); - assertNotNull(ctor); - Bean bean = BeanUtils.instantiateClass(ctor); - assertNotNull(bean); - } - - @Test - public void testFindSingleNonDefaultConstructorAndInstantiate() { - Constructor ctor = BeanUtils.findPrimaryConstructor(BeanWithSingleNonDefaultConstructor.class); - assertNotNull(ctor); - BeanWithSingleNonDefaultConstructor bean = BeanUtils.instantiateClass(ctor, "foo"); - assertNotNull(bean); - assertEquals("foo", bean.getName()); - } private void assertSignatureEquals(Method desiredMethod, String signature) { assertEquals(desiredMethod, BeanUtils.resolveSignature(signature, MethodSignatureBean.class)); 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 dd19bce123a..c2b64d5d820 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt @@ -66,26 +66,6 @@ class BeanUtilsKotlinTests { assertEquals(12, baz.param2) } - @Test - fun `2 constructors with default one`() { - assertEquals(TwoConstructorsWithDefaultOne::class.java.getDeclaredConstructor(), BeanUtils.findPrimaryConstructor(TwoConstructorsWithDefaultOne::class.java)) - } - - @Test - fun `2 constructors without default one`() { - assertNull(BeanUtils.findPrimaryConstructor(TwoConstructorsWithoutDefaultOne::class.java)) - } - - @Test - fun `1 constructor with default one`() { - assertEquals(OneConstructorWithDefaultOne::class.java.getDeclaredConstructor(), BeanUtils.findPrimaryConstructor(OneConstructorWithDefaultOne::class.java)) - } - - @Test - fun `1 constructor without default one`() { - assertEquals(OneConstructorWithoutDefaultOne::class.java.getDeclaredConstructor(String::class.java), BeanUtils.findPrimaryConstructor(OneConstructorWithoutDefaultOne::class.java)) - } - class Foo(val param1: String, val param2: Int) class Bar(val param1: String, val param2: Int = 12) diff --git a/spring-context/src/main/java/org/springframework/validation/FieldError.java b/spring-context/src/main/java/org/springframework/validation/FieldError.java index 1f498448b96..c72529a0f48 100644 --- a/spring-context/src/main/java/org/springframework/validation/FieldError.java +++ b/spring-context/src/main/java/org/springframework/validation/FieldError.java @@ -102,7 +102,8 @@ public class FieldError extends ObjectError { @Override public String toString() { return "Field error in object '" + getObjectName() + "' on field '" + this.field + - "': rejected value [" + this.rejectedValue + "]; " + resolvableToString(); + "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " + + resolvableToString(); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java new file mode 100644 index 00000000000..1cc03edddf0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.core; + +import java.lang.annotation.Annotation; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * A common delegate for detecting Kotlin's presence and for identifying Kotlin types. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 5.0 + */ +@SuppressWarnings("unchecked") +public abstract class KotlinDetector { + + @Nullable + private static final Class kotlinMetadata; + + static { + Class metadata; + try { + metadata = ClassUtils.forName("kotlin.Metadata", KotlinDetector.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Kotlin API not available - no Kotlin support + metadata = null; + } + kotlinMetadata = (Class) metadata; + } + + + /** + * Determine whether Kotlin is present in general. + */ + public static boolean isKotlinPresent() { + return (kotlinMetadata != null); + } + + /** + * Determine whether the given {@code Class} is a Kotlin type + * (with Kotlin metadata present on it). + */ + public static boolean isKotlinType(Class clazz) { + return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java index b3178630453..f368a72b4e0 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java @@ -16,7 +16,6 @@ package org.springframework.core; -import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.List; @@ -27,7 +26,6 @@ import kotlin.reflect.KParameter; import kotlin.reflect.jvm.ReflectJvmMapping; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities @@ -41,27 +39,13 @@ import org.springframework.util.ClassUtils; */ public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { - @Nullable - private static final Class kotlinMetadata; - - static { - Class metadata; - try { - metadata = ClassUtils.forName("kotlin.Metadata", KotlinReflectionParameterNameDiscoverer.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Kotlin API not available - no special support for Kotlin class instantiation - metadata = null; - } - kotlinMetadata = metadata; - } - @Override @Nullable public String[] getParameterNames(Method method) { - if (!useKotlinSupport(method.getDeclaringClass())) { + if (!KotlinDetector.isKotlinType(method.getDeclaringClass())) { return null; } + try { KFunction function = ReflectJvmMapping.getKotlinFunction(method); return (function != null ? getParameterNames(function.getParameters()) : null); @@ -74,9 +58,10 @@ public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDis @Override @Nullable public String[] getParameterNames(Constructor ctor) { - if (!useKotlinSupport(ctor.getDeclaringClass())) { + if (!KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { return null; } + try { KFunction function = ReflectJvmMapping.getKotlinFunction(ctor); return (function != null ? getParameterNames(function.getParameters()) : null); @@ -103,13 +88,4 @@ public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDis return parameterNames; } - /** - * Return true if Kotlin is present and if the specified class is a Kotlin one. - */ - @SuppressWarnings("unchecked") - private static boolean useKotlinSupport(Class clazz) { - return (kotlinMetadata != null && - clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); - } - } diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 6660b56f5f3..6676d503289 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -37,7 +37,6 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method} @@ -58,22 +57,6 @@ import org.springframework.util.ClassUtils; */ public class MethodParameter { - @Nullable - private static final Class kotlinMetadata; - - static { - Class metadata; - try { - metadata = ClassUtils.forName("kotlin.Metadata", MethodParameter.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Kotlin API not available - no Kotlin support - metadata = null; - } - kotlinMetadata = metadata; - } - - private final Executable executable; private final int parameterIndex; @@ -353,16 +336,7 @@ public class MethodParameter { */ public boolean isOptional() { return (getParameterType() == Optional.class || hasNullableAnnotation() || - (useKotlinSupport(this.getContainingClass()) && KotlinDelegate.isNullable(this))); - } - - /** - * Return true if Kotlin is present and if the specified class is a Kotlin one. - */ - @SuppressWarnings("unchecked") - private static boolean useKotlinSupport(Class clazz) { - return (kotlinMetadata != null && - clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); + (KotlinDetector.isKotlinType(getContainingClass()) && KotlinDelegate.isOptional(this))); } /** @@ -754,9 +728,10 @@ public class MethodParameter { private static class KotlinDelegate { /** - * Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not. + * Check whether the specified {@link MethodParameter} represents a nullable Kotlin type + * or an optional parameter (with a default value in the Kotlin declaration). */ - public static boolean isNullable(MethodParameter param) { + public static boolean isOptional(MethodParameter param) { Method method = param.getMethod(); Constructor ctor = param.getConstructor(); int index = param.getParameterIndex(); @@ -774,13 +749,12 @@ public class MethodParameter { } if (function != null) { List parameters = function.getParameters(); - return parameters + KParameter parameter = parameters .stream() .filter(p -> KParameter.Kind.VALUE.equals(p.getKind())) .collect(Collectors.toList()) - .get(index) - .getType() - .isMarkedNullable(); + .get(index); + return (parameter.getType().isMarkedNullable() || parameter.isOptional()); } } return false; diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index ae680150c05..ac96f637645 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -57,6 +57,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.FatalBeanException; import org.springframework.context.ApplicationContext; +import org.springframework.core.KotlinDetector; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -762,7 +763,7 @@ public class Jackson2ObjectMapperBuilder { } // Kotlin present? - if (ClassUtils.isPresent("kotlin.Metadata", this.moduleClassLoader)) { + if (KotlinDetector.isKotlinPresent()) { try { Class kotlinModule = (Class) ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader); diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index 1b5872d8fe3..cb91a3cdeec 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -198,11 +198,22 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception { MethodParameter nestedParameter = parameter.nestedIfOptional(); - Class type = nestedParameter.getNestedParameterType(); + Class clazz = nestedParameter.getNestedParameterType(); - Constructor ctor = BeanUtils.findPrimaryConstructor(type); + Constructor ctor = BeanUtils.findPrimaryConstructor(clazz); if (ctor == null) { - throw new IllegalStateException("No primary constructor found for " + type.getName()); + Constructor[] ctors = clazz.getConstructors(); + if (ctors.length == 1) { + ctor = ctors[0]; + } + else { + try { + ctor = clazz.getDeclaredConstructor(); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("No primary or default constructor found for " + clazz, ex); + } + } } Object attribute = constructAttribute(ctor, attributeName, binderFactory, webRequest); @@ -263,8 +274,13 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol } } try { - args[i] = (value != null ? - binder.convertIfNecessary(value, paramType, new MethodParameter(ctor, i)) : null); + MethodParameter methodParam = new MethodParameter(ctor, i); + if (value == null && methodParam.isOptional()) { + args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null); + } + else { + args[i] = binder.convertIfNecessary(value, paramType, methodParam); + } } catch (TypeMismatchException ex) { bindingFailure = true; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 2848cae2e0f..38e862b05f7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -21,6 +21,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.util.List; import java.util.Map; +import java.util.Optional; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; @@ -196,15 +197,29 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR } private Mono createAttribute( - String attributeName, Class attributeType, BindingContext context, ServerWebExchange exchange) { + String attributeName, Class clazz, BindingContext context, ServerWebExchange exchange) { - Constructor[] ctors = attributeType.getConstructors(); - if (ctors.length != 1) { - // No standard data class or standard JavaBeans arrangement -> - // defensively go with default constructor, expecting regular bean property bindings. - return Mono.just(BeanUtils.instantiateClass(attributeType)); + Constructor ctor = BeanUtils.findPrimaryConstructor(clazz); + if (ctor == null) { + Constructor[] ctors = clazz.getConstructors(); + if (ctors.length == 1) { + ctor = ctors[0]; + } + else { + try { + ctor = clazz.getDeclaredConstructor(); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("No primary or default constructor found for " + clazz, ex); + } + } } - Constructor ctor = ctors[0]; + return constructAttribute(ctor, attributeName, context, exchange); + } + + private Mono constructAttribute(Constructor ctor, String attributeName, + BindingContext context, ServerWebExchange exchange) { + if (ctor.getParameterCount() == 0) { // A single default constructor -> clearly a standard JavaBeans arrangement. return Mono.just(BeanUtils.instantiateClass(ctor)); @@ -237,7 +252,13 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR } } value = (value instanceof List ? ((List) value).toArray() : value); - args[i] = binder.convertIfNecessary(value, paramTypes[i], new MethodParameter(ctor, i)); + MethodParameter methodParam = new MethodParameter(ctor, i); + if (value == null && methodParam.isOptional()) { + args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null); + } + else { + args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam); + } } return BeanUtils.instantiateClass(ctor, args); }); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 88f5a6bfe6a..2255515330e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -69,6 +69,7 @@ import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.MethodParameter; import org.springframework.core.convert.converter.Converter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionServiceFactoryBean; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -99,6 +100,7 @@ import org.springframework.tests.sample.beans.TestBean; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.SerializationTestUtils; import org.springframework.util.StringUtils; @@ -1761,6 +1763,30 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals("value1-true-3", response.getContentAsString()); } + @Test + public void dataClassBindingWithOptionalParameter() throws Exception { + initServletWithControllers(ValidatedDataClassController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + request.addParameter("param2", "true"); + request.addParameter("optionalParam", "8"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals("value1-true-8", response.getContentAsString()); + } + + @Test + public void dataClassBindingWithMissingParameter() throws Exception { + initServletWithControllers(ValidatedDataClassController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertTrue(response.getContentAsString().contains("field 'param2'")); + } + @Test public void dataClassBindingWithConversionError() throws Exception { initServletWithControllers(ValidatedDataClassController.class); @@ -3390,10 +3416,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl public int param3; - @ConstructorProperties({"param1", "param2"}) - public DataClass(String param1, boolean p2) { + @ConstructorProperties({"param1", "param2", "optionalParam"}) + public DataClass(String param1, boolean p2, Optional optionalParam) { this.param1 = param1; this.param2 = p2; + Assert.notNull(optionalParam, "Optional must not be null"); + optionalParam.ifPresent(integer -> this.param3 = integer); } public void setParam3(int param3) { @@ -3418,6 +3446,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean(); vf.afterPropertiesSet(); binder.setValidator(vf); + binder.setConversionService(new DefaultFormattingConversionService()); } @RequestMapping("/bind")