Skip to content

Commit 4555384

Browse files
committed
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
1 parent 0717748 commit 4555384

14 files changed

+513
-48
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.util.Map;
22+
23+
import org.springframework.core.ResolvableType;
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.http.HttpInputMessage;
26+
import org.springframework.http.HttpOutputMessage;
27+
import org.springframework.http.MediaType;
28+
import org.springframework.http.StreamingHttpOutputMessage;
29+
import org.springframework.lang.Nullable;
30+
31+
/**
32+
* Abstract base class for most {@link SmartHttpMessageConverter} implementations.
33+
*
34+
* @author Sebastien Deleuze
35+
* @since 6.2
36+
* @param <T> the converted object type
37+
*/
38+
public abstract class AbstractSmartHttpMessageConverter<T> extends AbstractHttpMessageConverter<T>
39+
implements SmartHttpMessageConverter<T> {
40+
41+
/**
42+
* Construct an {@code AbstractSmartHttpMessageConverter} with no supported media types.
43+
* @see #setSupportedMediaTypes
44+
*/
45+
protected AbstractSmartHttpMessageConverter() {
46+
}
47+
48+
/**
49+
* Construct an {@code AbstractSmartHttpMessageConverter} with one supported media type.
50+
* @param supportedMediaType the supported media type
51+
*/
52+
protected AbstractSmartHttpMessageConverter(MediaType supportedMediaType) {
53+
super(supportedMediaType);
54+
}
55+
56+
/**
57+
* Construct an {@code AbstractSmartHttpMessageConverter} with multiple supported media type.
58+
* @param supportedMediaTypes the supported media types
59+
*/
60+
protected AbstractSmartHttpMessageConverter(MediaType... supportedMediaTypes) {
61+
super(supportedMediaTypes);
62+
}
63+
64+
65+
@Override
66+
protected boolean supports(Class<?> clazz) {
67+
return true;
68+
}
69+
70+
@Override
71+
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
72+
Class<?> clazz = type.resolve();
73+
return (clazz != null ? canRead(clazz, mediaType) : canRead(mediaType));
74+
}
75+
76+
@Override
77+
public boolean canWrite(ResolvableType type, Class<?> clazz, @Nullable MediaType mediaType) {
78+
return canWrite(clazz, mediaType);
79+
}
80+
81+
/**
82+
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
83+
* and then calls {@link #writeInternal}.
84+
*/
85+
@Override
86+
public final void write(T t, ResolvableType type, @Nullable MediaType contentType,
87+
HttpOutputMessage outputMessage, @Nullable Map<String, Object> hints)
88+
throws IOException, HttpMessageNotWritableException {
89+
90+
HttpHeaders headers = outputMessage.getHeaders();
91+
addDefaultHeaders(headers, t, contentType);
92+
93+
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
94+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
95+
@Override
96+
public void writeTo(OutputStream outputStream) throws IOException {
97+
writeInternal(t, type, new HttpOutputMessage() {
98+
@Override
99+
public OutputStream getBody() {
100+
return outputStream;
101+
}
102+
103+
@Override
104+
public HttpHeaders getHeaders() {
105+
return headers;
106+
}
107+
}, hints);
108+
}
109+
110+
@Override
111+
public boolean repeatable() {
112+
return supportsRepeatableWrites(t);
113+
}
114+
});
115+
}
116+
else {
117+
writeInternal(t, type, outputMessage, hints);
118+
outputMessage.getBody().flush();
119+
}
120+
}
121+
122+
@Override
123+
protected void writeInternal(T t, HttpOutputMessage outputMessage)
124+
throws IOException, HttpMessageNotWritableException {
125+
126+
writeInternal(t, ResolvableType.NONE, outputMessage, null);
127+
}
128+
129+
/**
130+
* Abstract template method that writes the actual body. Invoked from
131+
* {@link #write(Object, ResolvableType, MediaType, HttpOutputMessage, Map)}.
132+
* @param t the object to write to the output message
133+
* @param type the type of object to write
134+
* @param outputMessage the HTTP output message to write to
135+
* @param hints additional information about how to encode
136+
* @throws IOException in case of I/O errors
137+
* @throws HttpMessageNotWritableException in case of conversion errors
138+
*/
139+
protected abstract void writeInternal(T t, ResolvableType type, HttpOutputMessage outputMessage,
140+
@Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException;
141+
142+
@Override
143+
protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
144+
throws IOException, HttpMessageNotReadableException {
145+
146+
return read(ResolvableType.forClass(clazz), inputMessage, null);
147+
}
148+
}

spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* @since 3.2
3636
* @param <T> the converted object type
3737
* @see org.springframework.core.ParameterizedTypeReference
38+
* @see SmartHttpMessageConverter
3839
*/
3940
public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
4041

@@ -53,7 +54,7 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
5354
boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType);
5455

5556
/**
56-
* Read an object of the given type form the given input message, and returns it.
57+
* Read an object of the given type from the given input message, and returns it.
5758
* @param type the (potentially generic) type of object to return. This type must have
5859
* previously been passed to the {@link #canRead canRead} method of this interface,
5960
* which must have returned {@code true}.

spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @author Rossen Stoyanchev
3434
* @since 3.0
3535
* @param <T> the converted object type
36+
* @see SmartHttpMessageConverter
3637
*/
3738
public interface HttpMessageConverter<T> {
3839

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter;
18+
19+
import java.io.IOException;
20+
import java.util.Map;
21+
22+
import org.springframework.core.ResolvableType;
23+
import org.springframework.http.HttpInputMessage;
24+
import org.springframework.http.HttpOutputMessage;
25+
import org.springframework.http.MediaType;
26+
import org.springframework.lang.Nullable;
27+
28+
/**
29+
* A specialization of {@link HttpMessageConverter} that can convert an HTTP request
30+
* into a target object of a specified {@link ResolvableType} and a source object of
31+
* a specified {@link ResolvableType} into an HTTP response with optional hints.
32+
*
33+
* <p>It provides default methods for {@link HttpMessageConverter} in order to allow
34+
* subclasses to only have to implement the smart APIs.
35+
*
36+
* @author Sebastien Deleuze
37+
* @since 6.2
38+
* @param <T> the converted object type
39+
*/
40+
public interface SmartHttpMessageConverter<T> extends HttpMessageConverter<T> {
41+
42+
/**
43+
* Indicates whether the given type can be read by this converter.
44+
* This method should perform the same checks as
45+
* {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones
46+
* related to the generic type.
47+
* @param type the (potentially generic) type to test for readability. The
48+
* {@linkplain ResolvableType#getSource() type source} may be used for retrieving
49+
* additional information (the related method signature for example) when relevant.
50+
* @param mediaType the media type to read, can be {@code null} if not specified.
51+
* Typically, the value of a {@code Content-Type} header.
52+
* @return {@code true} if readable; {@code false} otherwise
53+
*/
54+
boolean canRead(ResolvableType type, @Nullable MediaType mediaType);
55+
56+
@Override
57+
default boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
58+
return canRead(ResolvableType.forClass(clazz), mediaType);
59+
}
60+
61+
/**
62+
* Read an object of the given type from the given input message, and returns it.
63+
* @param type the (potentially generic) type of object to return. This type must have
64+
* previously been passed to the {@link #canRead(ResolvableType, MediaType) canRead}
65+
* method of this interface, which must have returned {@code true}. The
66+
* {@linkplain ResolvableType#getSource() type source} may be used for retrieving
67+
* additional information (the related method signature for example) when relevant.
68+
* @param inputMessage the HTTP input message to read from
69+
* @param hints additional information about how to encode
70+
* @return the converted object
71+
* @throws IOException in case of I/O errors
72+
* @throws HttpMessageNotReadableException in case of conversion errors
73+
*/
74+
T read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
75+
throws IOException, HttpMessageNotReadableException;
76+
77+
@Override
78+
default T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
79+
throws IOException, HttpMessageNotReadableException {
80+
81+
return read(ResolvableType.forClass(clazz), inputMessage, null);
82+
}
83+
84+
/**
85+
* Indicates whether the given class can be written by this converter.
86+
* <p>This method should perform the same checks as
87+
* {@link HttpMessageConverter#canWrite(Class, MediaType)} with additional ones
88+
* related to the generic type.
89+
* @param targetType the (potentially generic) target type to test for writability
90+
* (can be {@link ResolvableType#NONE} if not specified). The {@linkplain ResolvableType#getSource() type source}
91+
* may be used for retrieving additional information (the related method signature for example) when relevant.
92+
* @param valueClass the source object class to test for writability
93+
* @param mediaType the media type to write (can be {@code null} if not specified);
94+
* typically the value of an {@code Accept} header.
95+
* @return {@code true} if writable; {@code false} otherwise
96+
*/
97+
boolean canWrite(ResolvableType targetType, Class<?> valueClass, @Nullable MediaType mediaType);
98+
99+
@Override
100+
default boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
101+
return canWrite(ResolvableType.forClass(clazz), clazz, mediaType);
102+
}
103+
104+
/**
105+
* Write a given object to the given output message.
106+
* @param t the object to write to the output message. The type of this object must
107+
* have previously been passed to the {@link #canWrite canWrite} method of this
108+
* interface, which must have returned {@code true}.
109+
* @param type the (potentially generic) type of object to write. This type must have
110+
* previously been passed to the {@link #canWrite canWrite} method of this interface,
111+
* which must have returned {@code true}. Can be {@link ResolvableType#NONE} if not specified.
112+
* The {@linkplain ResolvableType#getSource() type source} may be used for retrieving additional
113+
* information (the related method signature for example) when relevant.
114+
* @param contentType the content type to use when writing. May be {@code null} to
115+
* indicate that the default content type of the converter must be used. If not
116+
* {@code null}, this media type must have previously been passed to the
117+
* {@link #canWrite canWrite} method of this interface, which must have returned
118+
* {@code true}.
119+
* @param outputMessage the message to write to
120+
* @param hints additional information about how to encode
121+
* @throws IOException in case of I/O errors
122+
* @throws HttpMessageNotWritableException in case of conversion errors
123+
*/
124+
void write(T t, ResolvableType type, @Nullable MediaType contentType, HttpOutputMessage outputMessage,
125+
@Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException;
126+
127+
@Override
128+
default void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
129+
throws IOException, HttpMessageNotWritableException {
130+
write(t, ResolvableType.forInstance(t), contentType, outputMessage, null);
131+
}
132+
}

spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java

+23-5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.springframework.http.converter.GenericHttpMessageConverter;
6161
import org.springframework.http.converter.HttpMessageConverter;
6262
import org.springframework.http.converter.HttpMessageNotReadableException;
63+
import org.springframework.http.converter.SmartHttpMessageConverter;
6364
import org.springframework.lang.Nullable;
6465
import org.springframework.util.Assert;
6566
import org.springframework.util.CollectionUtils;
@@ -212,15 +213,24 @@ private <T> T readWithMessageConverters(ClientHttpResponse clientResponse, Runna
212213
}
213214

214215
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
215-
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
216-
if (genericHttpMessageConverter.canRead(bodyType, null, contentType)) {
216+
if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) {
217+
if (genericMessageConverter.canRead(bodyType, null, contentType)) {
217218
if (logger.isDebugEnabled()) {
218219
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
219220
}
220-
return (T) genericHttpMessageConverter.read(bodyType, null, responseWrapper);
221+
return (T) genericMessageConverter.read(bodyType, null, responseWrapper);
222+
}
223+
}
224+
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
225+
ResolvableType resolvableType = ResolvableType.forType(bodyType);
226+
if (smartMessageConverter.canRead(resolvableType, contentType)) {
227+
if (logger.isDebugEnabled()) {
228+
logger.debug("Reading to [" + resolvableType + "]");
229+
}
230+
return (T) smartMessageConverter.read(resolvableType, responseWrapper, null);
221231
}
222232
}
223-
if (messageConverter.canRead(bodyClass, contentType)) {
233+
else if (messageConverter.canRead(bodyClass, contentType)) {
224234
if (logger.isDebugEnabled()) {
225235
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
226236
}
@@ -453,7 +463,15 @@ private void writeWithMessageConverters(Object body, Type bodyType, ClientHttpRe
453463
return;
454464
}
455465
}
456-
if (messageConverter.canWrite(bodyClass, contentType)) {
466+
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
467+
ResolvableType resolvableType = ResolvableType.forType(bodyType);
468+
if (smartMessageConverter.canWrite(resolvableType, bodyClass, contentType)) {
469+
logBody(body, contentType, smartMessageConverter);
470+
smartMessageConverter.write(body, resolvableType, contentType, clientRequest, null);
471+
return;
472+
}
473+
}
474+
else if (messageConverter.canWrite(bodyClass, contentType)) {
457475
logBody(body, contentType, messageConverter);
458476
messageConverter.write(body, contentType, clientRequest);
459477
return;

spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java

+13-5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.http.converter.GenericHttpMessageConverter;
3030
import org.springframework.http.converter.HttpMessageConverter;
3131
import org.springframework.http.converter.HttpMessageNotReadableException;
32+
import org.springframework.http.converter.SmartHttpMessageConverter;
3233
import org.springframework.lang.Nullable;
3334
import org.springframework.util.Assert;
3435
import org.springframework.util.FileCopyUtils;
@@ -104,14 +105,21 @@ public T extractData(ClientHttpResponse response) throws IOException {
104105
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
105106
}
106107
}
107-
if (this.responseClass != null) {
108-
if (messageConverter.canRead(this.responseClass, contentType)) {
108+
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
109+
ResolvableType resolvableType = ResolvableType.forType(this.responseType);
110+
if (smartMessageConverter.canRead(resolvableType, contentType)) {
109111
if (logger.isDebugEnabled()) {
110-
String className = this.responseClass.getName();
111-
logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
112+
logger.debug("Reading to [" + resolvableType + "]");
112113
}
113-
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
114+
return (T) smartMessageConverter.read(resolvableType, responseWrapper, null);
115+
}
116+
}
117+
else if (this.responseClass != null && messageConverter.canRead(this.responseClass, contentType)) {
118+
if (logger.isDebugEnabled()) {
119+
String className = this.responseClass.getName();
120+
logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
114121
}
122+
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
115123
}
116124
}
117125
}

0 commit comments

Comments
 (0)