Add Kotlin body advices
This commit introduces KotlinRequestBodyAdvice and KotlinResponseBodyAdvice in order to set a KType hint when relevant. Closes gh-34923
This commit is contained in:
@@ -17,27 +17,20 @@
|
||||
package org.springframework.http.converter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KType;
|
||||
import kotlin.reflect.full.KCallables;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
import kotlinx.serialization.KSerializer;
|
||||
import kotlinx.serialization.SerialFormat;
|
||||
import kotlinx.serialization.SerializersKt;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
|
||||
@@ -84,12 +77,12 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return serializer(ResolvableType.forClass(clazz)) != null;
|
||||
return serializer(ResolvableType.forClass(clazz), null) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
|
||||
if (!ResolvableType.NONE.equals(type) && serializer(type) != null) {
|
||||
if (!ResolvableType.NONE.equals(type) && serializer(type, null) != null) {
|
||||
return canRead(mediaType);
|
||||
}
|
||||
else {
|
||||
@@ -99,7 +92,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
|
||||
|
||||
@Override
|
||||
public boolean canWrite(ResolvableType type, Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
if (!ResolvableType.NONE.equals(type) && serializer(type) != null) {
|
||||
if (!ResolvableType.NONE.equals(type) && serializer(type, null) != null) {
|
||||
return canWrite(mediaType);
|
||||
}
|
||||
else {
|
||||
@@ -111,7 +104,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
|
||||
public final Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
KSerializer<Object> serializer = serializer(type);
|
||||
KSerializer<Object> serializer = serializer(type, hints);
|
||||
if (serializer == null) {
|
||||
throw new HttpMessageNotReadableException("Could not find KSerializer for " + type, inputMessage);
|
||||
}
|
||||
@@ -129,7 +122,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
|
||||
@Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
ResolvableType resolvableType = (ResolvableType.NONE.equals(type) ? ResolvableType.forInstance(object) : type);
|
||||
KSerializer<Object> serializer = serializer(resolvableType);
|
||||
KSerializer<Object> serializer = serializer(resolvableType, hints);
|
||||
if (serializer == null) {
|
||||
throw new HttpMessageNotWritableException("Could not find KSerializer for " + resolvableType);
|
||||
}
|
||||
@@ -149,29 +142,21 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
|
||||
* @param resolvableType the type to find a serializer for
|
||||
* @return a resolved serializer for the given type, or {@code null}
|
||||
*/
|
||||
private @Nullable KSerializer<Object> serializer(ResolvableType resolvableType) {
|
||||
if (resolvableType.getSource() instanceof MethodParameter parameter) {
|
||||
Method method = parameter.getMethod();
|
||||
Assert.notNull(method, "Method must not be null");
|
||||
if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
|
||||
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
|
||||
if (function != null) {
|
||||
KType type = (parameter.getParameterIndex() == -1 ? function.getReturnType() :
|
||||
KCallables.getValueParameters(function).get(parameter.getParameterIndex()).getType());
|
||||
KSerializer<Object> serializer = this.kTypeSerializerCache.get(type);
|
||||
if (serializer == null) {
|
||||
try {
|
||||
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
|
||||
}
|
||||
catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
if (serializer != null) {
|
||||
this.kTypeSerializerCache.put(type, serializer);
|
||||
}
|
||||
}
|
||||
return serializer;
|
||||
private @Nullable KSerializer<Object> serializer(ResolvableType resolvableType, @Nullable Map<String, Object> hints) {
|
||||
if (hints != null && hints.containsKey(KType.class.getName())) {
|
||||
KType type = (KType) hints.get(KType.class.getName());
|
||||
KSerializer<Object> serializer = this.kTypeSerializerCache.get(type);
|
||||
if (serializer == null) {
|
||||
try {
|
||||
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
|
||||
}
|
||||
catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
if (serializer != null) {
|
||||
this.kTypeSerializerCache.put(type, serializer);
|
||||
}
|
||||
}
|
||||
return serializer;
|
||||
}
|
||||
Type type = resolvableType.getType();
|
||||
KSerializer<Object> serializer = this.typeSerializerCache.get(type);
|
||||
|
||||
@@ -33,8 +33,10 @@ import org.springframework.web.testfixture.http.MockHttpOutputMessage
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.javaType
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
@@ -246,7 +248,10 @@ class KotlinSerializationJsonHttpMessageConverterTests {
|
||||
val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_8))
|
||||
inputMessage.headers.contentType = MediaType.APPLICATION_JSON
|
||||
val methodParameter = MethodParameter.forExecutable(::handleMapWithNullable::javaMethod.get()!!, 0)
|
||||
val result = converter.read(ResolvableType.forMethodParameter(methodParameter), inputMessage, null) as Map<String, String?>
|
||||
val hints = mapOf(KType::class.jvmName to typeOf<Map<String, String?>>())
|
||||
|
||||
val result = converter.read(ResolvableType.forMethodParameter(methodParameter), inputMessage,
|
||||
hints) as Map<String, String?>
|
||||
|
||||
assertThat(result).containsExactlyEntriesOf(mapOf("value" to null))
|
||||
}
|
||||
@@ -400,9 +405,10 @@ class KotlinSerializationJsonHttpMessageConverterTests {
|
||||
val serializableBean = mapOf<String, String?>("value" to null)
|
||||
val expectedJson = """{"value":null}"""
|
||||
val methodParameter = MethodParameter.forExecutable(::handleMapWithNullable::javaMethod.get()!!, -1)
|
||||
val hints = mapOf(KType::class.jvmName to typeOf<Map<String, String?>>())
|
||||
|
||||
this.converter.write(serializableBean, ResolvableType.forMethodParameter(methodParameter), null,
|
||||
outputMessage, null)
|
||||
outputMessage, hints)
|
||||
|
||||
val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user