diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index bd446f0b53f..453981cae56 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -3,6 +3,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar description = "Spring Core" apply plugin: "kotlin" +apply plugin: "kotlinx-serialization" // spring-core includes asm, javapoet and repackages cglib, inlining all into the // spring-core jar. cglib itself depends on asm and is therefore further transformed by @@ -68,6 +69,7 @@ dependencies { testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor.tools:blockhound") testImplementation("org.skyscreamer:jsonassert") + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("org.junit.platform:junit-platform-launcher") testFixturesImplementation("org.junit.jupiter:junit-jupiter-api") diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/support/BindingReflectionHintsRegistrar.java index 58ad52b1bce..974a0d34e1a 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/BindingReflectionHintsRegistrar.java @@ -35,8 +35,10 @@ import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.TypeHint.Builder; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; /** * Register the necessary reflection hints so that the specified type can be @@ -55,6 +57,8 @@ public class BindingReflectionHintsRegistrar { private static final Consumer INVOKE = builder -> builder .withMode(ExecutableMode.INVOKE); + private static final String KOTLIN_COMPANION_SUFFIX = "$Companion"; + /** * Register the necessary reflection hints to bind the specified types. * @param hints the hints instance to use @@ -103,6 +107,14 @@ public class BindingReflectionHintsRegistrar { registerReflectionHints(hints, methodParameter.getGenericParameterType()); } } + String companionClassName = type.getCanonicalName() + KOTLIN_COMPANION_SUFFIX; + if (KotlinDetector.isKotlinType(type) && ClassUtils.isPresent(companionClassName, null)) { + Class companionClass = ClassUtils.resolveClassName(companionClassName, null); + Method serializerMethod = ClassUtils.getMethodIfAvailable(companionClass, "serializer"); + if (serializerMethod != null) { + hints.registerMethod(serializerMethod); + } + } } catch (IntrospectionException ex) { if (logger.isDebugEnabled()) { diff --git a/spring-core/src/test/kotlin/org/springframework/aot/hint/support/KotlinBindingReflectionHintsRegistrarTests.kt b/spring-core/src/test/kotlin/org/springframework/aot/hint/support/KotlinBindingReflectionHintsRegistrarTests.kt new file mode 100644 index 00000000000..17f1a63a9f0 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/aot/hint/support/KotlinBindingReflectionHintsRegistrarTests.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.hint.support + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.ThrowingConsumer +import org.junit.jupiter.api.Test +import org.springframework.aot.hint.* + +/** + * Tests for Kotlin support in [BindingReflectionHintsRegistrar]. + * + * @author Sebastien Deleuze + */ +class KotlinBindingReflectionHintsRegistrarTests { + + private val bindingRegistrar = BindingReflectionHintsRegistrar() + + private val hints = RuntimeHints() + + @Test + fun `Register type for Kotlinx serialization`() { + bindingRegistrar.registerReflectionHints(hints.reflection(), SampleSerializableClass::class.java) + assertThat(hints.reflection().typeHints()).satisfiesExactlyInAnyOrder( + ThrowingConsumer { typeHint: TypeHint -> + assertThat(typeHint.type).isEqualTo(TypeReference.of(String::class.java)) + assertThat(typeHint.memberCategories).isEmpty() + assertThat(typeHint.constructors()).isEmpty() + assertThat(typeHint.fields()).isEmpty() + assertThat(typeHint.methods()).isEmpty() + }, + ThrowingConsumer { typeHint: TypeHint -> + assertThat(typeHint.type).isEqualTo(TypeReference.of(SampleSerializableClass::class.java)) + assertThat(typeHint.methods()).singleElement() + .satisfies(ThrowingConsumer { methodHint: ExecutableHint -> + assertThat(methodHint.name).isEqualTo("getName") + assertThat(methodHint.modes) + .containsOnly(ExecutableMode.INVOKE) + }) + }, + ThrowingConsumer { typeHint: TypeHint -> + assertThat(typeHint.type).isEqualTo(TypeReference.of(SampleSerializableClass::class.qualifiedName + "\$Companion")) + assertThat(typeHint.methods()).singleElement() + .satisfies(ThrowingConsumer { methodHint: ExecutableHint -> + assertThat(methodHint.name).isEqualTo("serializer") + assertThat(methodHint.modes).containsOnly(ExecutableMode.INVOKE) + }) + }) + } +} + +@kotlinx.serialization.Serializable +class SampleSerializableClass(val name: String)