From 39d2769bd0cee82e15104b7d10201a324e81656f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 13 Dec 2016 17:38:58 +0100 Subject: [PATCH] Autodetect Kotlin nullability for optional injection points (analogous to java.util.Optional) Built-in support in MethodParameter and DependencyDescriptor supersedes our separate KotlinUtils helper. Issue: SPR-14951 --- build.gradle | 8 ++ .../factory/config/DependencyDescriptor.java | 43 ++++++++- .../annotation/KotlinAutowiredTests.kt | 87 +++++++++++++++++++ .../springframework/core/MethodParameter.java | 56 +++++++++++- .../org/springframework/util/KotlinUtils.java | 84 ------------------ .../core/KotlinMethodParameterTests.kt | 67 ++++++++++++++ .../springframework/util/KotlinUtilsTests.kt | 62 ------------- 7 files changed, 257 insertions(+), 150 deletions(-) create mode 100644 spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt delete mode 100644 spring-core/src/main/java/org/springframework/util/KotlinUtils.java create mode 100644 spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt delete mode 100644 spring-core/src/test/kotlin/org/springframework/util/KotlinUtilsTests.kt diff --git a/build.gradle b/build.gradle index 8ace9a7022..53b2472dca 100644 --- a/build.gradle +++ b/build.gradle @@ -312,6 +312,7 @@ configure(subprojects - project(":spring-build-src")) { subproject -> project("spring-build-src") { description = "Exposes gradle buildSrc for IDE support" + apply plugin: "groovy" dependencies { @@ -427,11 +428,16 @@ project("spring-core") { project("spring-beans") { description = "Spring Beans" + // Disabled since Kotlin compiler does not support JDK 9 yet + //apply plugin: "kotlin" + dependencies { compile(project(":spring-core")) compile(files(project(":spring-core").cglibRepackJar)) optional("javax.inject:javax.inject:1") optional("javax.el:javax.el-api:${elApiVersion}") + optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") + optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") optional("org.yaml:snakeyaml:${snakeyamlVersion}") testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") } @@ -439,6 +445,7 @@ project("spring-beans") { project("spring-beans-groovy") { description "Groovy Bean Definitions" + merge.into = project(":spring-beans") apply plugin: "groovy" @@ -501,6 +508,7 @@ project("spring-instrument") { project("spring-context") { description = "Spring Context" + apply plugin: "groovy" dependencies { 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 82c6883cc1..13a469be01 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 @@ -23,6 +23,11 @@ import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Map; +import java.util.Optional; + +import kotlin.Metadata; +import kotlin.reflect.KProperty; +import kotlin.reflect.jvm.ReflectJvmMapping; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -33,6 +38,7 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; /** * Descriptor for a specific dependency that is about to be injected. @@ -45,6 +51,10 @@ import org.springframework.core.ResolvableType; @SuppressWarnings("serial") public class DependencyDescriptor extends InjectionPoint implements Serializable { + private static final boolean kotlinPresent = + ClassUtils.isPresent("kotlin.Unit", DependencyDescriptor.class.getClassLoader()); + + private final Class declaringClass; private String methodName; @@ -83,6 +93,7 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable */ public DependencyDescriptor(MethodParameter methodParameter, boolean required, boolean eager) { super(methodParameter); + this.declaringClass = methodParameter.getDeclaringClass(); if (this.methodParameter.getMethod() != null) { this.methodName = methodParameter.getMethod().getName(); @@ -116,6 +127,7 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable */ public DependencyDescriptor(Field field, boolean required, boolean eager) { super(field); + this.declaringClass = field.getDeclaringClass(); this.fieldName = field.getName(); this.required = required; @@ -128,6 +140,7 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable */ public DependencyDescriptor(DependencyDescriptor original) { super(original); + this.declaringClass = original.declaringClass; this.methodName = original.methodName; this.parameterTypes = original.parameterTypes; @@ -144,7 +157,17 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable * Return whether this dependency is required. */ public boolean isRequired() { - return this.required; + if (!this.required) { + return false; + } + + if (this.field != null) { + return !(this.field.getType() == Optional.class || + (kotlinPresent && KotlinDelegate.isNullable(this.field))); + } + else { + return !this.methodParameter.isOptional(); + } } /** @@ -398,4 +421,22 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable } } + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Check whether the specified {@link Field} represents a nullable Kotlin type or not. + */ + public static boolean isNullable(Field field) { + if (field.getDeclaringClass().isAnnotationPresent(Metadata.class)) { + KProperty property = ReflectJvmMapping.getKotlinProperty(field); + return (property != null && property.getReturnType().isMarkedNullable()); + } + return false; + } + } + } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt new file mode 100644 index 0000000000..0292030911 --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2016 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.beans.factory.annotation + +import java.lang.reflect.Method + +import org.junit.Before +import org.junit.Test + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.tests.sample.beans.TestBean + +import org.junit.Assert.* + +/** + * Tests for Kotlin support with [@Autowired]. + * + * @author Juergen Hoeller + */ +class KotlinAutowiredTests { + + @Test + fun autowiringWithTarget() { + var bf = DefaultListableBeanFactory() + var bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + var bd = RootBeanDefinition(KotlinBean::class.java) + bd.setScope(RootBeanDefinition.SCOPE_PROTOTYPE) + bf.registerBeanDefinition("annotatedBean", bd) + var tb = TestBean() + bf.registerSingleton("testBean", tb) + + var kb = bf.getBean("annotatedBean", KotlinBean::class.java) + assertSame(tb, kb.injectedFromConstructor) + assertSame(tb, kb.injectedFromMethod) + assertSame(tb, kb.injectedField) + } + + @Test + fun autowiringWithoutTarget() { + var bf = DefaultListableBeanFactory() + var bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + var bd = RootBeanDefinition(KotlinBean::class.java) + bd.setScope(RootBeanDefinition.SCOPE_PROTOTYPE) + bf.registerBeanDefinition("annotatedBean", bd) + + var kb = bf.getBean("annotatedBean", KotlinBean::class.java) + assertNull(kb.injectedFromConstructor) + assertNull(kb.injectedFromMethod) + assertNull(kb.injectedField) + } + + + class KotlinBean(val injectedFromConstructor: TestBean?) { + + var injectedFromMethod: TestBean? = null + + @Autowired + var injectedField: TestBean? = null + + @Autowired + fun injectedMethod(p1: TestBean?) { + injectedFromMethod = p1 + } + } + +} 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 03de8085f4..924f5aafbf 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -26,11 +26,18 @@ import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; + +import kotlin.Metadata; +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; import org.springframework.util.Assert; -import org.springframework.util.KotlinUtils; +import org.springframework.util.ClassUtils; /** * Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method} @@ -45,12 +52,17 @@ import org.springframework.util.KotlinUtils; * @author Rob Harrop * @author Andy Clement * @author Sam Brannen + * @author Sebastien Deleuze * @since 2.0 * @see GenericCollectionTypeResolver * @see org.springframework.core.annotation.SynthesizingMethodParameter */ public class MethodParameter { + private static final boolean kotlinPresent = + ClassUtils.isPresent("kotlin.Unit", MethodParameter.class.getClassLoader()); + + private final Method method; private final Constructor constructor; @@ -311,11 +323,13 @@ public class MethodParameter { /** * Return whether this method indicates a parameter which is not required - * (either in the form of Java 8's {@link java.util.Optional} or Kotlin nullable type). + * (either in the form of Java 8's {@link java.util.Optional} or Kotlin's + * nullable type). * @since 4.3 */ public boolean isOptional() { - return (getParameterType() == Optional.class || KotlinUtils.isNullable(this)); + return (getParameterType() == Optional.class || + (kotlinPresent && KotlinDelegate.isNullable(this))); } /** @@ -672,4 +686,40 @@ public class MethodParameter { return parameterIndex; } + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not. + */ + public static boolean isNullable(MethodParameter param) { + if (param.getContainingClass().isAnnotationPresent(Metadata.class)) { + int parameterIndex = param.getParameterIndex(); + if (parameterIndex == -1) { + KFunction function = ReflectJvmMapping.getKotlinFunction(param.getMethod()); + return (function != null && function.getReturnType().isMarkedNullable()); + } + else { + KFunction function = (param.getMethod() != null ? + ReflectJvmMapping.getKotlinFunction(param.getMethod()) : + ReflectJvmMapping.getKotlinFunction(param.getConstructor())); + if (function != null) { + List parameters = function.getParameters(); + return parameters + .stream() + .filter(p -> KParameter.Kind.VALUE.equals(p.getKind())) + .collect(Collectors.toList()) + .get(parameterIndex) + .getType() + .isMarkedNullable(); + } + } + } + return false; + } + } + } diff --git a/spring-core/src/main/java/org/springframework/util/KotlinUtils.java b/spring-core/src/main/java/org/springframework/util/KotlinUtils.java deleted file mode 100644 index d47ab71a66..0000000000 --- a/spring-core/src/main/java/org/springframework/util/KotlinUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2002-2016 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.util; - -import kotlin.Metadata; -import kotlin.reflect.KFunction; -import kotlin.reflect.KParameter; -import kotlin.reflect.jvm.ReflectJvmMapping; -import org.springframework.core.MethodParameter; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Miscellaneous Kotlin utility methods. - * - * @author Raman Gupta - * @author Sebastien Deleuze - * @since 5.0 - */ -public abstract class KotlinUtils { - - private static final boolean kotlinPresent = ClassUtils.isPresent("kotlin.Unit", KotlinUtils.class.getClassLoader()); - - /** - * Return whether Kotlin is available on the classpath or not. - */ - public static boolean isKotlinPresent() { - return kotlinPresent; - } - - /** - * Return whether the specified type is a Kotlin class or not. - */ - public static boolean isKotlinClass(Class type) { - Assert.notNull(type, "Type must not be null"); - return isKotlinPresent() && type.getDeclaredAnnotation(Metadata.class) != null; - } - - /** - * Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not. - */ - public static boolean isNullable(MethodParameter methodParameter) { - Method method = methodParameter.getMethod(); - int parameterIndex = methodParameter.getParameterIndex(); - if (isKotlinClass(methodParameter.getContainingClass())) { - if (parameterIndex < 0) { - KFunction function = ReflectJvmMapping.getKotlinFunction(method); - return function != null && function.getReturnType().isMarkedNullable(); - } - else { - KFunction function = (method != null ? ReflectJvmMapping.getKotlinFunction(method) : - ReflectJvmMapping.getKotlinFunction(methodParameter.getConstructor())); - if (function != null) { - List parameters = function.getParameters(); - return parameters - .stream() - .filter(p -> KParameter.Kind.VALUE.equals(p.getKind())) - .collect(Collectors.toList()) - .get(parameterIndex) - .getType() - .isMarkedNullable(); - } - } - } - return false; - } - -} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt new file mode 100644 index 0000000000..2157099a4c --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2016 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.reflect.Method + +import org.junit.Before +import org.junit.Test + +import org.junit.Assert.* + +/** + * Tests for Kotlin support in [MethodParameter]. + * + * @author Raman Gupta + * @author Sebastien Deleuze + * @author Juergen Hoeller + */ +class KotlinMethodParameterTests { + + lateinit var nullableMethod: Method + + lateinit var nonNullableMethod: Method + + + @Before + @Throws(NoSuchMethodException::class) + fun setup() { + nullableMethod = javaClass.getMethod("nullable", String::class.java) + nonNullableMethod = javaClass.getMethod("nonNullable", String::class.java) + } + + + @Test + fun `Method parameter nullability`() { + assertTrue(MethodParameter(nullableMethod, 0).isOptional()) + assertFalse(MethodParameter(nonNullableMethod, 0).isOptional()) + } + + @Test + fun `Method return type nullability`() { + assertTrue(MethodParameter(nullableMethod, -1).isOptional()) + assertFalse(MethodParameter(nonNullableMethod, -1).isOptional()) + } + + + @Suppress("unused", "unused_parameter") + fun nullable(p1: String?): Int? = 42 + + @Suppress("unused", "unused_parameter") + fun nonNullable(p1: String): Int = 42 + +} diff --git a/spring-core/src/test/kotlin/org/springframework/util/KotlinUtilsTests.kt b/spring-core/src/test/kotlin/org/springframework/util/KotlinUtilsTests.kt deleted file mode 100644 index 10eb4786e1..0000000000 --- a/spring-core/src/test/kotlin/org/springframework/util/KotlinUtilsTests.kt +++ /dev/null @@ -1,62 +0,0 @@ -package org.springframework.util - -import org.junit.Before -import org.junit.Test - -import java.lang.reflect.Method - -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.springframework.core.MethodParameter -import org.springframework.util.KotlinUtils.* - -/** - * Test fixture for [KotlinUtils]. - * - * @author Raman Gupta - * @author Sebastien Deleuze - */ -class KotlinUtilsTests { - - lateinit var nullableMethod: Method - - lateinit var nonNullableMethod: Method - - @Before - @Throws(NoSuchMethodException::class) - fun setup() { - nullableMethod = javaClass.getMethod("nullable", String::class.java) - nonNullableMethod = javaClass.getMethod("nonNullable", String::class.java) - } - - @Test - fun `Is kotlin present`() { - // we'd have to change the build to test the opposite - assertTrue(isKotlinPresent()) - } - - @Test - fun `Are kotlin classes detected`() { - assertFalse(isKotlinClass(MethodParameter::class.java)) - assertTrue(isKotlinClass(javaClass)) - } - - @Test - fun `Obtains method return type nullability`() { - assertTrue(isNullable(MethodParameter(nullableMethod, -1))) - assertFalse(isNullable(MethodParameter(nonNullableMethod, -1))) - } - - @Test - fun `Obtains method parameter nullability`() { - assertTrue(isNullable(MethodParameter(nullableMethod, 0))) - assertFalse(isNullable(MethodParameter(nonNullableMethod, 0))) - } - - @Suppress("unused", "unused_parameter") - fun nullable(p1: String?): Int? = 42 - - @Suppress("unused", "unused_parameter") - fun nonNullable(p1: String): Int = 42 - -}