Leverage KType in Kotlin Serialization WebMVC support

In order to take in account properly Kotlin null-safety with the
annotation programming model.

See gh-33016
This commit is contained in:
Sébastien Deleuze
2024-07-01 14:55:10 +02:00
parent 4555384528
commit 23dccc5977
4 changed files with 157 additions and 100 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -17,22 +17,30 @@
package org.springframework.http.converter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
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 kotlinx.serialization.descriptors.PolymorphicKind;
import kotlinx.serialization.descriptors.SerialDescriptor;
import org.springframework.core.GenericTypeResolver;
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.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
@@ -48,9 +56,11 @@ import org.springframework.util.ConcurrentReferenceHashMap;
* @since 6.0
* @param <T> the type of {@link SerialFormat}
*/
public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends SerialFormat> extends AbstractGenericHttpMessageConverter<Object> {
public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends SerialFormat> extends AbstractSmartHttpMessageConverter<Object> {
private final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Map<KType, KSerializer<Object>> kTypeSerializerCache = new ConcurrentReferenceHashMap<>();
private final Map<Type, KSerializer<Object>> typeSerializerCache = new ConcurrentReferenceHashMap<>();
private final T format;
@@ -66,15 +76,14 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
this.format = format;
}
@Override
protected boolean supports(Class<?> clazz) {
return serializer(clazz) != null;
return serializer(ResolvableType.forClass(clazz)) != null;
}
@Override
public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
if (serializer(GenericTypeResolver.resolveType(type, contextClass)) != null) {
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
if (!ResolvableType.NONE.equals(type) && serializer(type) != null) {
return canRead(mediaType);
}
else {
@@ -83,8 +92,8 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
}
@Override
public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
if (serializer(type != null ? GenericTypeResolver.resolveType(type, clazz) : clazz) != null) {
public boolean canWrite(ResolvableType type, Class<?> clazz, @Nullable MediaType mediaType) {
if (!ResolvableType.NONE.equals(type) && serializer(type) != null) {
return canWrite(mediaType);
}
else {
@@ -93,24 +102,12 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
}
@Override
public final Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
public final Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
throws IOException, HttpMessageNotReadableException {
Type resolvedType = GenericTypeResolver.resolveType(type, contextClass);
KSerializer<Object> serializer = serializer(resolvedType);
KSerializer<Object> serializer = serializer(type);
if (serializer == null) {
throw new HttpMessageNotReadableException("Could not find KSerializer for " + resolvedType, inputMessage);
}
return readInternal(serializer, this.format, inputMessage);
}
@Override
protected final Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
KSerializer<Object> serializer = serializer(clazz);
if (serializer == null) {
throw new HttpMessageNotReadableException("Could not find KSerializer for " + clazz, inputMessage);
throw new HttpMessageNotReadableException("Could not find KSerializer for " + type, inputMessage);
}
return readInternal(serializer, this.format, inputMessage);
}
@@ -122,13 +119,13 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
throws IOException, HttpMessageNotReadableException;
@Override
protected final void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
protected final void writeInternal(Object object, ResolvableType type, HttpOutputMessage outputMessage,
@Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException {
Type resolvedType = type != null ? type : object.getClass();
KSerializer<Object> serializer = serializer(resolvedType);
ResolvableType resolvableType = (ResolvableType.NONE.equals(type) ? ResolvableType.forInstance(object) : type);
KSerializer<Object> serializer = serializer(resolvableType);
if (serializer == null) {
throw new HttpMessageNotWritableException("Could not find KSerializer for " + resolvedType);
throw new HttpMessageNotWritableException("Could not find KSerializer for " + resolvableType);
}
writeInternal(object, serializer, this.format, outputMessage);
}
@@ -143,12 +140,38 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
* Tries to find a serializer that can marshall or unmarshall instances of the given type
* using kotlinx.serialization. If no serializer can be found, {@code null} is returned.
* <p>Resolved serializers are cached and cached results are returned on successive calls.
* @param type the type to find a serializer for
* @param resolvableType the type to find a serializer for
* @return a resolved serializer for the given type, or {@code null}
*/
@Nullable
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = this.serializerCache.get(type);
private 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);
Assert.notNull(function, "Kotlin function must not be 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) {
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
this.kTypeSerializerCache.put(type, serializer);
}
}
return serializer;
}
}
Type type = resolvableType.getType();
KSerializer<Object> serializer = this.typeSerializerCache.get(type);
if (serializer == null) {
try {
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
@@ -159,7 +182,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
this.serializerCache.put(type, serializer);
this.typeSerializerCache.put(type, serializer);
}
}
return serializer;