mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Merge branch '7.0.x'
This commit is contained in:
@@ -134,7 +134,7 @@ public abstract class CoroutinesUtils {
|
|||||||
Object arg = args[index];
|
Object arg = args[index];
|
||||||
if (!(parameter.isOptional() && arg == null)) {
|
if (!(parameter.isOptional() && arg == null)) {
|
||||||
KType type = parameter.getType();
|
KType type = parameter.getType();
|
||||||
if (!(type.isMarkedNullable() && arg == null) &&
|
if (!type.isMarkedNullable() &&
|
||||||
type.getClassifier() instanceof KClass<?> kClass &&
|
type.getClassifier() instanceof KClass<?> kClass &&
|
||||||
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
|
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
|
||||||
arg = box(kClass, arg);
|
arg = box(kClass, arg);
|
||||||
@@ -166,7 +166,7 @@ public abstract class CoroutinesUtils {
|
|||||||
private static Object box(KClass<?> kClass, @Nullable Object arg) {
|
private static Object box(KClass<?> kClass, @Nullable Object arg) {
|
||||||
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
|
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
|
||||||
KType type = constructor.getParameters().get(0).getType();
|
KType type = constructor.getParameters().get(0).getType();
|
||||||
if (!(type.isMarkedNullable() && arg == null) &&
|
if (!type.isMarkedNullable() &&
|
||||||
type.getClassifier() instanceof KClass<?> parameterClass &&
|
type.getClassifier() instanceof KClass<?> parameterClass &&
|
||||||
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
|
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
|
||||||
arg = box(parameterClass, arg);
|
arg = box(parameterClass, arg);
|
||||||
|
|||||||
@@ -229,6 +229,13 @@ class CoroutinesUtilsTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
suspend fun invokeSuspendingFunctionWithNullableValueClassParameterAndNonNullParameter() {
|
||||||
|
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
|
||||||
|
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, ValueClass("foo"), null) as Mono
|
||||||
|
Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
suspend fun invokeSuspendingFunctionWithNullableValueClassParameter() {
|
suspend fun invokeSuspendingFunctionWithNullableValueClassParameter() {
|
||||||
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
|
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
description = "Spring TestContext Framework"
|
description = "Spring TestContext Framework"
|
||||||
|
|
||||||
apply plugin: "kotlin"
|
apply plugin: "kotlin"
|
||||||
|
apply plugin: "kotlinx-serialization"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":spring-core"))
|
api(project(":spring-core"))
|
||||||
@@ -81,6 +82,7 @@ dependencies {
|
|||||||
testImplementation("org.hibernate.orm:hibernate-core")
|
testImplementation("org.hibernate.orm:hibernate-core")
|
||||||
testImplementation("org.hibernate.validator:hibernate-validator")
|
testImplementation("org.hibernate.validator:hibernate-validator")
|
||||||
testImplementation("org.hsqldb:hsqldb")
|
testImplementation("org.hsqldb:hsqldb")
|
||||||
|
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
||||||
testImplementation("org.junit.platform:junit-platform-testkit")
|
testImplementation("org.junit.platform:junit-platform-testkit")
|
||||||
testImplementation("tools.jackson.core:jackson-databind")
|
testImplementation("tools.jackson.core:jackson-databind")
|
||||||
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
|
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-present 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.test.web.reactive.server
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
class WebTestClientKotlinTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun expectBodyListKotlinSerialization() {
|
||||||
|
val client = WebTestClient.bindToController(TestController::class.java)
|
||||||
|
.configureClient()
|
||||||
|
.codecs {
|
||||||
|
it.registerDefaults(false)
|
||||||
|
it.customCodecs().register(KotlinSerializationJsonDecoder())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
client.get().uri("/test")
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.exchangeSuccessfully()
|
||||||
|
.expectBodyList<Response>()
|
||||||
|
.hasSize(2)
|
||||||
|
.contains(Response("Hello"), Response("World"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Response(val message: String)
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class TestController {
|
||||||
|
@GetMapping("test")
|
||||||
|
fun test(): List<Response> = listOf(Response("Hello"), Response("World"))
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -55,7 +55,7 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
|
|||||||
implements Decoder<Object> {
|
implements Decoder<Object> {
|
||||||
|
|
||||||
// String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
|
// String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
|
||||||
private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
|
protected final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,7 +158,7 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private CodecException processException(IllegalArgumentException ex) {
|
protected CodecException processException(IllegalArgumentException ex) {
|
||||||
return new DecodingException("Decoding error: " + ex.getMessage(), ex);
|
return new DecodingException("Decoding error: " + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
@@ -16,11 +16,22 @@
|
|||||||
|
|
||||||
package org.springframework.http.codec.json;
|
package org.springframework.http.codec.json;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer;
|
||||||
|
import kotlinx.serialization.builtins.BuiltinSerializersKt;
|
||||||
import kotlinx.serialization.json.Json;
|
import kotlinx.serialization.json.Json;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.DecodingException;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.KotlinSerializationStringDecoder;
|
import org.springframework.http.codec.KotlinSerializationStringDecoder;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
@@ -96,4 +107,37 @@ public class KotlinSerializationJsonDecoder extends KotlinSerializationStringDec
|
|||||||
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
|
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
return Flux.defer(() -> {
|
||||||
|
KSerializer<Object> serializer = serializer(elementType);
|
||||||
|
if (serializer == null) {
|
||||||
|
return Mono.error(new DecodingException("Could not find KSerializer for " + elementType));
|
||||||
|
}
|
||||||
|
return this.stringDecoder
|
||||||
|
.decode(inputStream, elementType, mimeType, hints)
|
||||||
|
.switchOnFirst((signal, flux) -> {
|
||||||
|
if (signal.hasValue()) {
|
||||||
|
String value = Objects.requireNonNull(signal.get());
|
||||||
|
if (value.stripLeading().startsWith("[") && !List.class.isAssignableFrom(elementType.toClass())) {
|
||||||
|
KSerializer<List<Object>> listSerializer = BuiltinSerializersKt.ListSerializer(serializer);
|
||||||
|
return flux
|
||||||
|
.flatMapIterable(string -> format().decodeFromString(listSerializer, string))
|
||||||
|
.onErrorMap(IllegalArgumentException.class, this::processException);
|
||||||
|
}
|
||||||
|
return flux.handle((string, sink) -> {
|
||||||
|
try {
|
||||||
|
sink.next(format().decodeFromString(serializer, string));
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException ex) {
|
||||||
|
sink.error(processException(ex));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return flux;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -316,7 +316,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||||||
Object arg = args[index];
|
Object arg = args[index];
|
||||||
if (!(parameter.isOptional() && arg == null)) {
|
if (!(parameter.isOptional() && arg == null)) {
|
||||||
KType type = parameter.getType();
|
KType type = parameter.getType();
|
||||||
if (!(type.isMarkedNullable() && arg == null) &&
|
if (!type.isMarkedNullable() &&
|
||||||
type.getClassifier() instanceof KClass<?> kClass &&
|
type.getClassifier() instanceof KClass<?> kClass &&
|
||||||
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
|
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
|
||||||
arg = box(kClass, arg);
|
arg = box(kClass, arg);
|
||||||
@@ -337,7 +337,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||||||
private static Object box(KClass<?> kClass, @Nullable Object arg) {
|
private static Object box(KClass<?> kClass, @Nullable Object arg) {
|
||||||
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
|
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
|
||||||
KType type = constructor.getParameters().get(0).getType();
|
KType type = constructor.getParameters().get(0).getType();
|
||||||
if (!(type.isMarkedNullable() && arg == null) &&
|
if (!type.isMarkedNullable() &&
|
||||||
type.getClassifier() instanceof KClass<?> parameterClass &&
|
type.getClassifier() instanceof KClass<?> parameterClass &&
|
||||||
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
|
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
|
||||||
arg = box(parameterClass, arg);
|
arg = box(parameterClass, arg);
|
||||||
|
|||||||
+28
@@ -197,6 +197,34 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
|
|||||||
}, null, null)
|
}, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeJsonArrayToFlux() {
|
||||||
|
val input = Flux.concat(
|
||||||
|
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
|
||||||
|
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"))
|
||||||
|
|
||||||
|
testDecodeAll(input, ResolvableType.forClass(Pojo::class.java), {
|
||||||
|
it.expectNext(Pojo("f1", "b1"))
|
||||||
|
.expectNext(Pojo("f2", "b2"))
|
||||||
|
.expectComplete()
|
||||||
|
.verify()
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeJsonArrayToFluxOfList() {
|
||||||
|
val input = Flux.concat(
|
||||||
|
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]\n"),
|
||||||
|
stringBuffer("[{\"bar\":\"b3\",\"foo\":\"f3\"},{\"bar\":\"b4\",\"foo\":\"f4\"}]"))
|
||||||
|
|
||||||
|
testDecodeAll(input, ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), {
|
||||||
|
it.expectNext(listOf(Pojo("f1", "b1"),Pojo("f2", "b2")))
|
||||||
|
.expectNext(listOf(Pojo("f3", "b3"),Pojo("f4", "b4")))
|
||||||
|
.expectComplete()
|
||||||
|
.verify()
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
override fun decodeToMono() {
|
override fun decodeToMono() {
|
||||||
val input = Flux.concat(
|
val input = Flux.concat(
|
||||||
|
|||||||
+7
@@ -148,6 +148,13 @@ class InvocableHandlerMethodKotlinTests {
|
|||||||
Assertions.assertThatIllegalArgumentException().isThrownBy { invocable.invokeForRequest(request, null) }
|
Assertions.assertThatIllegalArgumentException().isThrownBy { invocable.invokeForRequest(request, null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun valueClassWithNullableAndNonNullParameter() {
|
||||||
|
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, LongValueClass(1)))
|
||||||
|
val value = getInvocable(ValueClassHandler::valueClassWithNullable.javaMethod!!).invokeForRequest(request, null)
|
||||||
|
Assertions.assertThat(value).isEqualTo(1L)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun valueClassWithNullable() {
|
fun valueClassWithNullable() {
|
||||||
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))
|
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))
|
||||||
|
|||||||
+3
-3
@@ -329,7 +329,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||||||
private static final String COROUTINE_CONTEXT_ATTRIBUTE = "org.springframework.web.server.CoWebFilter.context";
|
private static final String COROUTINE_CONTEXT_ATTRIBUTE = "org.springframework.web.server.CoWebFilter.context";
|
||||||
|
|
||||||
@SuppressWarnings("DataFlowIssue")
|
@SuppressWarnings("DataFlowIssue")
|
||||||
public static @Nullable Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction,
|
public static @Nullable Object invokeFunction(Method method, Object target, @Nullable Object[] args, boolean isSuspendingFunction,
|
||||||
ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
|
ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
|
||||||
|
|
||||||
if (isSuspendingFunction) {
|
if (isSuspendingFunction) {
|
||||||
@@ -356,7 +356,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||||||
Object arg = args[index];
|
Object arg = args[index];
|
||||||
if (!(parameter.isOptional() && arg == null)) {
|
if (!(parameter.isOptional() && arg == null)) {
|
||||||
KType type = parameter.getType();
|
KType type = parameter.getType();
|
||||||
if (!(type.isMarkedNullable() && arg == null) &&
|
if (!type.isMarkedNullable() &&
|
||||||
type.getClassifier() instanceof KClass<?> kClass &&
|
type.getClassifier() instanceof KClass<?> kClass &&
|
||||||
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
|
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
|
||||||
arg = box(kClass, arg);
|
arg = box(kClass, arg);
|
||||||
@@ -378,7 +378,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||||||
private static Object box(KClass<?> kClass, @Nullable Object arg) {
|
private static Object box(KClass<?> kClass, @Nullable Object arg) {
|
||||||
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
|
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
|
||||||
KType type = constructor.getParameters().get(0).getType();
|
KType type = constructor.getParameters().get(0).getType();
|
||||||
if (!(type.isMarkedNullable() && arg == null) &&
|
if (!type.isMarkedNullable() &&
|
||||||
type.getClassifier() instanceof KClass<?> parameterClass &&
|
type.getClassifier() instanceof KClass<?> parameterClass &&
|
||||||
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
|
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
|
||||||
arg = box(parameterClass, arg);
|
arg = box(parameterClass, arg);
|
||||||
|
|||||||
+8
@@ -250,6 +250,14 @@ class InvocableHandlerMethodKotlinTests {
|
|||||||
assertExceptionThrown(result, IllegalArgumentException::class)
|
assertExceptionThrown(result, IllegalArgumentException::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun valueClassWithNullableAndNonNullParameter() {
|
||||||
|
this.resolvers.add(stubResolver(LongValueClass(1), LongValueClass::class.java))
|
||||||
|
val method = ValueClassController::valueClassWithNullable.javaMethod!!
|
||||||
|
val result = invoke(ValueClassController(), method)
|
||||||
|
assertHandlerResultValue(result, "1")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun valueClassWithNullable() {
|
fun valueClassWithNullable() {
|
||||||
this.resolvers.add(stubResolver(null, LongValueClass::class.java))
|
this.resolvers.add(stubResolver(null, LongValueClass::class.java))
|
||||||
|
|||||||
Reference in New Issue
Block a user