Merge branch '7.0.x'

This commit is contained in:
Sébastien Deleuze
2026-04-28 11:16:42 +02:00
9 changed files with 116 additions and 22 deletions
@@ -134,9 +134,10 @@ public abstract class CoroutinesUtils {
Object arg = args[index];
if (!(parameter.isOptional() && arg == null)) {
KType type = parameter.getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass)) &&
!JvmClassMappingKt.getJavaClass(kClass).isInstance(arg)) {
arg = box(kClass, arg);
}
argMap.put(parameter, arg);
@@ -166,9 +167,10 @@ public abstract class CoroutinesUtils {
private static Object box(KClass<?> kClass, @Nullable Object arg) {
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
KType type = constructor.getParameters().get(0).getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> parameterClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass)) &&
!JvmClassMappingKt.getJavaClass(parameterClass).isInstance(arg)) {
arg = box(parameterClass, arg);
}
if (!KCallablesJvm.isAccessible(constructor)) {
@@ -71,6 +71,8 @@ public class ReactiveAdapterRegistry {
private static final boolean MUTINY_PRESENT;
private static final boolean CONTEXT_PROPAGATION_PRESENT;
static {
ClassLoader classLoader = ReactiveAdapterRegistry.class.getClassLoader();
REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent("org.reactivestreams.Publisher", classLoader);
@@ -78,6 +80,7 @@ public class ReactiveAdapterRegistry {
RXJAVA_3_PRESENT = ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", classLoader);
COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", classLoader);
MUTINY_PRESENT = ClassUtils.isPresent("io.smallrye.mutiny.Multi", classLoader);
CONTEXT_PROPAGATION_PRESENT = ClassUtils.isPresent("io.micrometer.context.ContextSnapshotFactory", classLoader);
}
private final List<ReactiveAdapter> adapters = new ArrayList<>();
@@ -356,7 +359,9 @@ public class ReactiveAdapterRegistry {
registry.registerReactiveType(
ReactiveTypeDescriptor.multiValue(kotlinx.coroutines.flow.Flow.class, kotlinx.coroutines.flow.FlowKt::emptyFlow),
source -> kotlinx.coroutines.reactor.ReactorFlowKt.asFlux((kotlinx.coroutines.flow.Flow<?>) source),
CONTEXT_PROPAGATION_PRESENT ?
source -> kotlinx.coroutines.reactor.ReactorFlowKt.asFlux((kotlinx.coroutines.flow.Flow<?>) source, new PropagationContextElement()) :
source -> kotlinx.coroutines.reactor.ReactorFlowKt.asFlux((kotlinx.coroutines.flow.Flow<?>) source),
kotlinx.coroutines.reactive.ReactiveFlowKt::asFlow);
}
}
@@ -236,6 +236,13 @@ class CoroutinesUtilsTests {
Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo")
}
@Test
suspend fun invokeSuspendingFunctionWithNullableValueClassParameterAndUnderlyingValue() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono
Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo")
}
@Test
suspend fun invokeSuspendingFunctionWithNullableValueClassParameter() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
@@ -16,13 +16,18 @@
package org.springframework.core
import io.micrometer.observation.Observation
import io.micrometer.observation.tck.TestObservationRegistry
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.reactivestreams.Publisher
@@ -40,6 +45,8 @@ import kotlin.reflect.KClass
@OptIn(DelicateCoroutinesApi::class)
class ReactiveAdapterRegistryKotlinTests {
private val observationRegistry = TestObservationRegistry.create()
private val registry = ReactiveAdapterRegistry.getSharedInstance()
@Test
@@ -82,6 +89,23 @@ class ReactiveAdapterRegistryKotlinTests {
assertThat((target as Flow<*>).toList()).contains(1, 2, 3)
}
@Test
fun propagateMicrometerContextToFlow() {
val source = flow {
val currentObservation = observationRegistry.currentObservation
assertThat(currentObservation).isNotNull
emit(currentObservation?.context?.name)
}
val observation = Observation.createNotStarted("coroutine", observationRegistry)
observation.observe {
val target: Publisher<String> = getAdapter(Flow::class).toPublisher(source)
val result = runBlocking(Dispatchers.IO) {
target.awaitSingle()
}
assertThat(result).isEqualTo("coroutine")
}
}
private fun getAdapter(reactiveType: KClass<*>): ReactiveAdapter {
return this.registry.getAdapter(reactiveType.java)!!
}
@@ -316,9 +316,10 @@ public class InvocableHandlerMethod extends HandlerMethod {
Object arg = args[index];
if (!(parameter.isOptional() && arg == null)) {
KType type = parameter.getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass)) &&
!JvmClassMappingKt.getJavaClass(kClass).isInstance(arg)) {
arg = box(kClass, arg);
}
argMap.put(parameter, arg);
@@ -337,9 +338,10 @@ public class InvocableHandlerMethod extends HandlerMethod {
private static Object box(KClass<?> kClass, @Nullable Object arg) {
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
KType type = constructor.getParameters().get(0).getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> parameterClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass)) &&
!JvmClassMappingKt.getJavaClass(parameterClass).isInstance(arg)) {
arg = box(parameterClass, arg);
}
if (!KCallablesJvm.isAccessible(constructor)) {
@@ -155,6 +155,13 @@ class InvocableHandlerMethodKotlinTests {
Assertions.assertThat(value).isEqualTo(1L)
}
@Test
fun valueClassWithNullableAndUnderlyingValue() {
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, 1L))
val value = getInvocable(ValueClassHandler::valueClassWithNullable.javaMethod!!).invokeForRequest(request, null)
Assertions.assertThat(value).isEqualTo(1L)
}
@Test
fun valueClassWithNullable() {
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))
@@ -215,6 +222,14 @@ class InvocableHandlerMethodKotlinTests {
StepVerifier.create(value as Mono<Long>).verifyComplete()
}
@Test
fun suspendingValueClassWithNullableAndUnderlyingValue() {
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, 1L))
val value = getInvocable(SuspendingValueClassHandler::valueClassWithNullable.javaMethod!!).invokeForRequest(request, null)
StepVerifier.create(value as Mono<Long>).expectNext(1L).verifyComplete()
}
@Test
fun suspendingValueClassWithPrivateConstructor() {
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
@@ -356,9 +356,10 @@ public class InvocableHandlerMethod extends HandlerMethod {
Object arg = args[index];
if (!(parameter.isOptional() && arg == null)) {
KType type = parameter.getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass)) &&
!JvmClassMappingKt.getJavaClass(kClass).isInstance(arg)) {
arg = box(kClass, arg);
}
argMap.put(parameter, arg);
@@ -378,9 +379,10 @@ public class InvocableHandlerMethod extends HandlerMethod {
private static Object box(KClass<?> kClass, @Nullable Object arg) {
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
KType type = constructor.getParameters().get(0).getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> parameterClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass)) &&
!JvmClassMappingKt.getJavaClass(parameterClass).isInstance(arg)) {
arg = box(parameterClass, arg);
}
if (!KCallablesJvm.isAccessible(constructor)) {
@@ -258,6 +258,14 @@ class InvocableHandlerMethodKotlinTests {
assertHandlerResultValue(result, "1")
}
@Test
fun valueClassWithNullableAndUnderlyingValue() {
this.resolvers.add(stubResolver(1L, LongValueClass::class.java))
val method = ValueClassController::valueClassWithNullable.javaMethod!!
val result = invoke(ValueClassController(), method)
assertHandlerResultValue(result, "1")
}
@Test
fun valueClassWithNullable() {
this.resolvers.add(stubResolver(null, LongValueClass::class.java))
@@ -320,6 +328,14 @@ class InvocableHandlerMethodKotlinTests {
assertHandlerResultValue(result, "null")
}
@Test
fun suspendingValueClassWithNullableAndUnderlyingValue() {
this.resolvers.add(stubResolver(1L, LongValueClass::class.java))
val method = SuspendingValueClassController::valueClassWithNullable.javaMethod!!
val result = invoke(SuspendingValueClassController(), method)
assertHandlerResultValue(result, "1")
}
@Test
fun suspendingValueClassWithPrivateConstructor() {
this.resolvers.add(stubResolver(1L, Long::class.java))
@@ -590,4 +606,4 @@ class InvocableHandlerMethodKotlinTests {
}
class CustomException(message: String) : Throwable(message)
}
}
@@ -16,18 +16,16 @@
package org.springframework.web.reactive.result.method.annotation
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import io.micrometer.observation.ObservationRegistry
import io.micrometer.observation.tck.TestObservationRegistry
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.Assumptions.assumeFalse
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpHeaders
@@ -86,6 +84,15 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
assertThat(entity.body).isEqualTo("foobar")
}
@ParameterizedHttpServerTest
fun `Handler method returning Flow with observation`(httpServer: HttpServer) {
startServer(httpServer)
val entity = performGet("/flow-observation", HttpHeaders.EMPTY, String::class.java)
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).isEqualTo("http.server.requests")
}
@ParameterizedHttpServerTest
fun `Suspending handler method returning Flow`(httpServer: HttpServer) {
startServer(httpServer)
@@ -135,11 +142,16 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
@Configuration
@EnableWebFlux
@ComponentScan(resourcePattern = "**/CoroutinesIntegrationTests*")
open class WebConfig
open class WebConfig {
@Bean
open fun observationRegistry(): ObservationRegistry = TestObservationRegistry.create()
}
@OptIn(DelicateCoroutinesApi::class)
@RestController
class CoroutinesController {
class CoroutinesController(private val observationRegistry: ObservationRegistry) {
@GetMapping("/suspend")
suspend fun suspendingEndpoint(): String {
@@ -167,6 +179,15 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
delay(1)
}
@GetMapping("/flow-observation")
fun flowObservationEndpoint(): Flow<String?> {
return flow {
val currentObservation = observationRegistry.currentObservation
assertThat(currentObservation).isNotNull
emit(currentObservation?.context?.name)
}
}
@GetMapping("/suspending-flow")
suspend fun suspendingFlowEndpoint(): Flow<String> {
delay(1)