Introduce SmartHttpMessageConverter
SmartHttpMessageConverter is similar to GenericHttpMessageConverter, but more consistent with WebFlux Encoder and Decoder contracts, with the following differences: - A ResolvableType parameter is used instead of the Type one - The MethodParameter can be retrieved via the ResolvableType source - No contextClass parameter - `@Nullable Map<String, Object> hints` additional parameter for write and read methods This commit also refines RestTemplate#canReadResponse in order to use the most specific converter contract when possible. Closes gh-33118
This commit is contained in:
@@ -41,6 +41,7 @@ import org.reactivestreams.Subscription;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourceRegion;
|
||||
@@ -54,6 +55,7 @@ import org.springframework.http.InvalidMediaTypeException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.SmartHttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -309,7 +311,14 @@ final class DefaultEntityResponseBuilder<T> implements EntityResponse.Builder<T>
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (messageConverter.canWrite(entityClass, contentType)) {
|
||||
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(entityType);
|
||||
if (smartMessageConverter.canWrite(resolvableType, entityClass, contentType)) {
|
||||
smartMessageConverter.write(entity, resolvableType, contentType, serverResponse, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (messageConverter.canWrite(entityClass, contentType)) {
|
||||
((HttpMessageConverter<Object>) messageConverter).write(entity, contentType, serverResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.SmartHttpMessageConverter;
|
||||
import org.springframework.http.server.RequestPath;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -210,7 +211,13 @@ class DefaultServerRequest implements ServerRequest {
|
||||
return (T) genericMessageConverter.read(bodyType, bodyClass, this.serverHttpRequest);
|
||||
}
|
||||
}
|
||||
if (messageConverter.canRead(bodyClass, contentType)) {
|
||||
else if (messageConverter instanceof SmartHttpMessageConverter<?> smartMessageConverter) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(bodyType);
|
||||
if (smartMessageConverter.canRead(resolvableType, contentType)) {
|
||||
return (T) smartMessageConverter.read(resolvableType, this.serverHttpRequest, null);
|
||||
}
|
||||
}
|
||||
else if (messageConverter.canRead(bodyClass, contentType)) {
|
||||
HttpMessageConverter<T> theConverter =
|
||||
(HttpMessageConverter<T>) messageConverter;
|
||||
Class<? extends T> clazz = (Class<? extends T>) bodyClass;
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.SmartHttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
@@ -318,7 +319,13 @@ class DefaultServerRequestBuilder implements ServerRequest.Builder {
|
||||
return (T) genericMessageConverter.read(bodyType, bodyClass, inputMessage);
|
||||
}
|
||||
}
|
||||
if (messageConverter.canRead(bodyClass, contentType)) {
|
||||
else if (messageConverter instanceof SmartHttpMessageConverter<?> smartMessageConverter) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(bodyType);
|
||||
if (smartMessageConverter.canRead(resolvableType, contentType)) {
|
||||
return (T) smartMessageConverter.read(resolvableType, inputMessage, null);
|
||||
}
|
||||
}
|
||||
else if (messageConverter.canRead(bodyClass, contentType)) {
|
||||
HttpMessageConverter<T> theConverter =
|
||||
(HttpMessageConverter<T>) messageConverter;
|
||||
Class<? extends T> clazz = (Class<? extends T>) bodyClass;
|
||||
|
||||
@@ -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.
|
||||
@@ -36,6 +36,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -46,6 +47,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.SmartHttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -64,6 +66,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 3.1
|
||||
*/
|
||||
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
@@ -77,6 +80,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
|
||||
protected final List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
protected enum ConverterType { BASE, GENERIC, SMART };
|
||||
|
||||
private final RequestResponseBodyAdviceChain advice;
|
||||
|
||||
|
||||
@@ -99,7 +104,6 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@link RequestBodyAdvice} and
|
||||
* {@link RequestBodyAdvice} where each instance may be wrapped as a
|
||||
@@ -147,8 +151,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
|
||||
Class<?> contextClass = parameter.getContainingClass();
|
||||
Class<T> targetClass = (targetType instanceof Class clazz ? clazz : null);
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
|
||||
if (targetClass == null) {
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
|
||||
targetClass = (Class<T>) resolvableType.resolve();
|
||||
}
|
||||
|
||||
@@ -171,26 +175,46 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
|
||||
EmptyBodyCheckingHttpInputMessage message = null;
|
||||
try {
|
||||
ResolvableType targetResolvableType = null;
|
||||
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
|
||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
|
||||
GenericHttpMessageConverter<?> genericConverter =
|
||||
(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
|
||||
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
|
||||
(targetClass != null && converter.canRead(targetClass, contentType))) {
|
||||
Class<HttpMessageConverter<?>> converterClass = (Class<HttpMessageConverter<?>>) converter.getClass();
|
||||
ConverterType converterTypeToUse = null;
|
||||
if (converter instanceof GenericHttpMessageConverter<?> genericConverter) {
|
||||
if (genericConverter.canRead(targetType, contextClass, contentType)) {
|
||||
converterTypeToUse = ConverterType.GENERIC;
|
||||
}
|
||||
}
|
||||
else if (converter instanceof SmartHttpMessageConverter<?> smartConverter) {
|
||||
if (targetResolvableType == null) {
|
||||
targetResolvableType = getNestedTypeIfNeeded(resolvableType);
|
||||
}
|
||||
if (smartConverter.canRead(targetResolvableType, contentType)) {
|
||||
converterTypeToUse = ConverterType.SMART;
|
||||
}
|
||||
}
|
||||
else if (targetClass != null && converter.canRead(targetClass, contentType)) {
|
||||
converterTypeToUse = ConverterType.BASE;
|
||||
}
|
||||
if (converterTypeToUse != null) {
|
||||
if (message.hasBody()) {
|
||||
HttpInputMessage msgToUse =
|
||||
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
|
||||
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
|
||||
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
|
||||
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
|
||||
getAdvice().beforeBodyRead(message, parameter, targetType, converterClass);
|
||||
body = switch (converterTypeToUse) {
|
||||
case BASE -> ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse);
|
||||
case GENERIC -> ((GenericHttpMessageConverter<?>) converter).read(targetType, contextClass, msgToUse);
|
||||
case SMART -> ((SmartHttpMessageConverter<?>) converter).read(targetResolvableType, msgToUse, null);
|
||||
};
|
||||
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterClass);
|
||||
}
|
||||
else {
|
||||
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
|
||||
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterClass);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (body == NO_VALUE && noContentType && !message.hasBody()) {
|
||||
body = getAdvice().handleEmptyBody(
|
||||
null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class);
|
||||
@@ -223,6 +247,22 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the generic type of the {@code returnType} (or of the nested type
|
||||
* if it is an {@link HttpEntity} or/and an {@link Optional}).
|
||||
*/
|
||||
protected ResolvableType getNestedTypeIfNeeded(ResolvableType type) {
|
||||
ResolvableType genericType = type;
|
||||
if (Optional.class.isAssignableFrom(genericType.toClass())) {
|
||||
genericType = genericType.getNested(2);
|
||||
}
|
||||
if (HttpEntity.class.isAssignableFrom(genericType.toClass())) {
|
||||
genericType = genericType.getNested(2);
|
||||
}
|
||||
return genericType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
|
||||
* @param webRequest the web request to create an input message from
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.SmartHttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -74,6 +75,7 @@ import org.springframework.web.util.UrlPathHelper;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 3.1
|
||||
*/
|
||||
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
|
||||
@@ -202,7 +204,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
* be written by a converter, or if the content-type chosen by the server
|
||||
* has no compatible converter.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@SuppressWarnings({"rawtypes", "unchecked", "NullAway"})
|
||||
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
|
||||
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
|
||||
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
|
||||
@@ -312,25 +314,36 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
|
||||
if (selectedMediaType != null) {
|
||||
selectedMediaType = selectedMediaType.removeQualityValue();
|
||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||
GenericHttpMessageConverter genericConverter =
|
||||
(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
|
||||
if (genericConverter != null ?
|
||||
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
|
||||
converter.canWrite(valueType, selectedMediaType)) {
|
||||
|
||||
ResolvableType targetResolvableType = null;
|
||||
for (HttpMessageConverter converter : this.messageConverters) {
|
||||
ConverterType converterTypeToUse = null;
|
||||
if (converter instanceof GenericHttpMessageConverter genericConverter) {
|
||||
if (genericConverter.canWrite(targetType, valueType, selectedMediaType)) {
|
||||
converterTypeToUse = ConverterType.GENERIC;
|
||||
}
|
||||
}
|
||||
else if (converter instanceof SmartHttpMessageConverter smartConverter) {
|
||||
targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forMethodParameter(returnType));
|
||||
if (smartConverter.canWrite(targetResolvableType, valueType, selectedMediaType)) {
|
||||
converterTypeToUse = ConverterType.SMART;
|
||||
}
|
||||
}
|
||||
else if (converter.canWrite(valueType, selectedMediaType)){
|
||||
converterTypeToUse = ConverterType.BASE;
|
||||
}
|
||||
if (converterTypeToUse != null) {
|
||||
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
|
||||
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
|
||||
inputMessage, outputMessage);
|
||||
(Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
|
||||
if (body != null) {
|
||||
Object theBody = body;
|
||||
LogFormatUtils.traceDebug(logger, traceOn ->
|
||||
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
|
||||
addContentDispositionHeader(inputMessage, outputMessage);
|
||||
if (genericConverter != null) {
|
||||
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
|
||||
}
|
||||
else {
|
||||
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
|
||||
switch (converterTypeToUse) {
|
||||
case BASE -> converter.write(body, selectedMediaType, outputMessage);
|
||||
case GENERIC -> ((GenericHttpMessageConverter) converter).write(body, targetType, selectedMediaType, outputMessage);
|
||||
case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType, selectedMediaType, outputMessage, null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -416,8 +429,14 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
}
|
||||
Set<MediaType> result = new LinkedHashSet<>();
|
||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||
if (converter instanceof GenericHttpMessageConverter<?> ghmc && targetType != null) {
|
||||
if (ghmc.canWrite(targetType, valueClass, null)) {
|
||||
if (converter instanceof GenericHttpMessageConverter<?> genericConverter && targetType != null) {
|
||||
if (genericConverter.canWrite(targetType, valueClass, null)) {
|
||||
result.addAll(converter.getSupportedMediaTypes(valueClass));
|
||||
}
|
||||
}
|
||||
else if (converter instanceof SmartHttpMessageConverter<?> smartConverter && targetType != null) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(targetType);
|
||||
if (smartConverter.canWrite(resolvableType, valueClass, null)) {
|
||||
result.addAll(converter.getSupportedMediaTypes(valueClass));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user