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:
Sébastien Deleuze
2024-07-01 14:51:46 +02:00
parent 0717748f58
commit 4555384528
14 changed files with 513 additions and 48 deletions

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2002-2023 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.http.converter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.lang.Nullable;
/**
* Abstract base class for most {@link SmartHttpMessageConverter} implementations.
*
* @author Sebastien Deleuze
* @since 6.2
* @param <T> the converted object type
*/
public abstract class AbstractSmartHttpMessageConverter<T> extends AbstractHttpMessageConverter<T>
implements SmartHttpMessageConverter<T> {
/**
* Construct an {@code AbstractSmartHttpMessageConverter} with no supported media types.
* @see #setSupportedMediaTypes
*/
protected AbstractSmartHttpMessageConverter() {
}
/**
* Construct an {@code AbstractSmartHttpMessageConverter} with one supported media type.
* @param supportedMediaType the supported media type
*/
protected AbstractSmartHttpMessageConverter(MediaType supportedMediaType) {
super(supportedMediaType);
}
/**
* Construct an {@code AbstractSmartHttpMessageConverter} with multiple supported media type.
* @param supportedMediaTypes the supported media types
*/
protected AbstractSmartHttpMessageConverter(MediaType... supportedMediaTypes) {
super(supportedMediaTypes);
}
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
Class<?> clazz = type.resolve();
return (clazz != null ? canRead(clazz, mediaType) : canRead(mediaType));
}
@Override
public boolean canWrite(ResolvableType type, Class<?> clazz, @Nullable MediaType mediaType) {
return canWrite(clazz, mediaType);
}
/**
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
@Override
public final void write(T t, ResolvableType type, @Nullable MediaType contentType,
HttpOutputMessage outputMessage, @Nullable Map<String, Object> hints)
throws IOException, HttpMessageNotWritableException {
HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}, hints);
}
@Override
public boolean repeatable() {
return supportsRepeatableWrites(t);
}
});
}
else {
writeInternal(t, type, outputMessage, hints);
outputMessage.getBody().flush();
}
}
@Override
protected void writeInternal(T t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
writeInternal(t, ResolvableType.NONE, outputMessage, null);
}
/**
* Abstract template method that writes the actual body. Invoked from
* {@link #write(Object, ResolvableType, MediaType, HttpOutputMessage, Map)}.
* @param t the object to write to the output message
* @param type the type of object to write
* @param outputMessage the HTTP output message to write to
* @param hints additional information about how to encode
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
protected abstract void writeInternal(T t, ResolvableType type, HttpOutputMessage outputMessage,
@Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException;
@Override
protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return read(ResolvableType.forClass(clazz), inputMessage, null);
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.lang.Nullable;
* @since 3.2
* @param <T> the converted object type
* @see org.springframework.core.ParameterizedTypeReference
* @see SmartHttpMessageConverter
*/
public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
@@ -53,7 +54,7 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType);
/**
* Read an object of the given type form the given input message, and returns it.
* Read an object of the given type from the given input message, and returns it.
* @param type the (potentially generic) type of object to return. This type must have
* previously been passed to the {@link #canRead canRead} method of this interface,
* which must have returned {@code true}.

View File

@@ -33,6 +33,7 @@ import org.springframework.lang.Nullable;
* @author Rossen Stoyanchev
* @since 3.0
* @param <T> the converted object type
* @see SmartHttpMessageConverter
*/
public interface HttpMessageConverter<T> {

View File

@@ -0,0 +1,132 @@
/*
* 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.
* 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.http.converter;
import java.io.IOException;
import java.util.Map;
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;
/**
* A specialization of {@link HttpMessageConverter} that can convert an HTTP request
* into a target object of a specified {@link ResolvableType} and a source object of
* a specified {@link ResolvableType} into an HTTP response with optional hints.
*
* <p>It provides default methods for {@link HttpMessageConverter} in order to allow
* subclasses to only have to implement the smart APIs.
*
* @author Sebastien Deleuze
* @since 6.2
* @param <T> the converted object type
*/
public interface SmartHttpMessageConverter<T> extends HttpMessageConverter<T> {
/**
* Indicates whether the given type can be read by this converter.
* This method should perform the same checks as
* {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones
* related to the generic type.
* @param type the (potentially generic) type to test for readability. The
* {@linkplain ResolvableType#getSource() type source} may be used for retrieving
* additional information (the related method signature for example) when relevant.
* @param mediaType the media type to read, can be {@code null} if not specified.
* Typically, the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise
*/
boolean canRead(ResolvableType type, @Nullable MediaType mediaType);
@Override
default boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
return canRead(ResolvableType.forClass(clazz), mediaType);
}
/**
* Read an object of the given type from the given input message, and returns it.
* @param type the (potentially generic) type of object to return. This type must have
* previously been passed to the {@link #canRead(ResolvableType, MediaType) canRead}
* method of this interface, which must have returned {@code true}. The
* {@linkplain ResolvableType#getSource() type source} may be used for retrieving
* additional information (the related method signature for example) when relevant.
* @param inputMessage the HTTP input message to read from
* @param hints additional information about how to encode
* @return the converted object
* @throws IOException in case of I/O errors
* @throws HttpMessageNotReadableException in case of conversion errors
*/
T read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
throws IOException, HttpMessageNotReadableException;
@Override
default T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return read(ResolvableType.forClass(clazz), inputMessage, null);
}
/**
* Indicates whether the given class can be written by this converter.
* <p>This method should perform the same checks as
* {@link HttpMessageConverter#canWrite(Class, MediaType)} with additional ones
* related to the generic type.
* @param targetType the (potentially generic) target type to test for writability
* (can be {@link ResolvableType#NONE} if not specified). The {@linkplain ResolvableType#getSource() type source}
* may be used for retrieving additional information (the related method signature for example) when relevant.
* @param valueClass the source object class to test for writability
* @param mediaType the media type to write (can be {@code null} if not specified);
* typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(ResolvableType targetType, Class<?> valueClass, @Nullable MediaType mediaType);
@Override
default boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
return canWrite(ResolvableType.forClass(clazz), clazz, mediaType);
}
/**
* Write a given object to the given output message.
* @param t the object to write to the output message. The type of this object must
* have previously been passed to the {@link #canWrite canWrite} method of this
* interface, which must have returned {@code true}.
* @param type the (potentially generic) type of object to write. This type must have
* previously been passed to the {@link #canWrite canWrite} method of this interface,
* which must have returned {@code true}. Can be {@link ResolvableType#NONE} if not specified.
* The {@linkplain ResolvableType#getSource() type source} may be used for retrieving additional
* information (the related method signature for example) when relevant.
* @param contentType the content type to use when writing. May be {@code null} to
* indicate that the default content type of the converter must be used. If not
* {@code null}, this media type must have previously been passed to the
* {@link #canWrite canWrite} method of this interface, which must have returned
* {@code true}.
* @param outputMessage the message to write to
* @param hints additional information about how to encode
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
void write(T t, ResolvableType type, @Nullable MediaType contentType, HttpOutputMessage outputMessage,
@Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException;
@Override
default void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
write(t, ResolvableType.forInstance(t), contentType, outputMessage, null);
}
}

View File

@@ -60,6 +60,7 @@ import org.springframework.http.client.observation.DefaultClientRequestObservati
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.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -212,15 +213,24 @@ final class DefaultRestClient implements RestClient {
}
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
if (genericHttpMessageConverter.canRead(bodyType, null, contentType)) {
if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) {
if (genericMessageConverter.canRead(bodyType, null, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
}
return (T) genericHttpMessageConverter.read(bodyType, null, responseWrapper);
return (T) genericMessageConverter.read(bodyType, null, responseWrapper);
}
}
if (messageConverter.canRead(bodyClass, contentType)) {
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
ResolvableType resolvableType = ResolvableType.forType(bodyType);
if (smartMessageConverter.canRead(resolvableType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + resolvableType + "]");
}
return (T) smartMessageConverter.read(resolvableType, responseWrapper, null);
}
}
else if (messageConverter.canRead(bodyClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
}
@@ -453,7 +463,15 @@ final class DefaultRestClient implements RestClient {
return;
}
}
if (messageConverter.canWrite(bodyClass, contentType)) {
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
ResolvableType resolvableType = ResolvableType.forType(bodyType);
if (smartMessageConverter.canWrite(resolvableType, bodyClass, contentType)) {
logBody(body, contentType, smartMessageConverter);
smartMessageConverter.write(body, resolvableType, contentType, clientRequest, null);
return;
}
}
else if (messageConverter.canWrite(bodyClass, contentType)) {
logBody(body, contentType, messageConverter);
messageConverter.write(body, contentType, clientRequest);
return;

View File

@@ -29,6 +29,7 @@ import org.springframework.http.client.ClientHttpResponse;
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.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
@@ -104,15 +105,22 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
}
}
if (this.responseClass != null) {
if (messageConverter.canRead(this.responseClass, contentType)) {
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
ResolvableType resolvableType = ResolvableType.forType(this.responseType);
if (smartMessageConverter.canRead(resolvableType, contentType)) {
if (logger.isDebugEnabled()) {
String className = this.responseClass.getName();
logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
logger.debug("Reading to [" + resolvableType + "]");
}
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
return (T) smartMessageConverter.read(resolvableType, responseWrapper, null);
}
}
else if (this.responseClass != null && messageConverter.canRead(this.responseClass, contentType)) {
if (logger.isDebugEnabled()) {
String className = this.responseClass.getName();
logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
}
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
}
}
}
catch (IOException | HttpMessageNotReadableException ex) {

View File

@@ -33,6 +33,7 @@ import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -52,6 +53,7 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
@@ -1030,13 +1032,15 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
private boolean canReadResponse(Type responseType, HttpMessageConverter<?> converter) {
Class<?> responseClass = (responseType instanceof Class<?> clazz ? clazz : null);
if (responseClass != null) {
return converter.canRead(responseClass, null);
}
else if (converter instanceof GenericHttpMessageConverter<?> genericConverter) {
if (converter instanceof GenericHttpMessageConverter<?> genericConverter) {
return genericConverter.canRead(responseType, null, null);
}
else if (converter instanceof SmartHttpMessageConverter<?> smartConverter) {
return smartConverter.canRead(ResolvableType.forType(responseType), null);
}
else if (responseType instanceof Class<?> responseClass) {
return converter.canRead(responseClass, null);
}
return false;
}
@@ -1114,6 +1118,17 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
return;
}
}
else if (messageConverter instanceof SmartHttpMessageConverter smartConverter) {
ResolvableType resolvableType = ResolvableType.forType(requestBodyType);
if (smartConverter.canWrite(resolvableType, requestBodyClass, requestContentType)) {
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new ArrayList<>(values)));
}
logBody(requestBody, requestContentType, smartConverter);
smartConverter.write(requestBody, resolvableType, requestContentType, httpRequest, null);
return;
}
}
else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new ArrayList<>(values)));