Browse Source

Introduce HttpMessageConverter#canWriteRepeatedly

The `AbstractHttpMessageConverter#supportsRepeatableWrites`
contract is a protected method that message converters can override.
This method tells whether the current converter can write several
times the payload given as a parameter. This is mainly useful on the
client side, where we need to know if we can send the same HTTP
message again, after receiving an HTTP redirect status.

Because this method is protected, this limits our ability to call
it from a different package; this is needed for gh-33263.

This commit promotes this method to the main `HttpMessageConverter`
interface and deprecates the former.

Closes gh-36252
pull/36549/head
Brian Clozel 4 days ago
parent
commit
051219c3c9
  1. 2
      spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java
  2. 4
      spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java
  3. 6
      spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java
  4. 6
      spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java
  5. 2
      spring-web/src/main/java/org/springframework/http/converter/AbstractSmartHttpMessageConverter.java
  6. 6
      spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java
  7. 9
      spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
  8. 18
      spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java
  9. 6
      spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java
  10. 8
      spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java
  11. 32
      spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java
  12. 6
      spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java
  13. 7
      spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java
  14. 6
      spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
  15. 6
      spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java
  16. 6
      spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java
  17. 7
      spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java
  18. 6
      spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java
  19. 6
      spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java
  20. 8
      spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java

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

@ -117,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt @@ -117,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
}
@Override
public boolean repeatable() {
return supportsRepeatableWrites(t);
return canWriteRepeatedly(t, contentType);
}
});
}

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

@ -304,9 +304,11 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv @@ -304,9 +304,11 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
* @return {@code true} if {@code t} can be written repeatedly;
* {@code false} otherwise
* @since 6.1
* @deprecated since 7.1 in favor of {@link #canWriteRepeatedly(Object, MediaType)}.
*/
@Deprecated(since = "7.1", forRemoval = true)
protected boolean supportsRepeatableWrites(T t) {
return false;
return canWriteRepeatedly(t, null);
}

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

@ -285,6 +285,11 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper @@ -285,6 +285,11 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper
return this.mapperRegistrations == null || selectMapper(valueClass, mediaType) != null;
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
/**
* Select an ObjectMapper to use, either the main ObjectMapper or another
* if the handling for the given Class has been customized through
@ -503,6 +508,7 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper @@ -503,6 +508,7 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

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

@ -120,6 +120,11 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends @@ -120,6 +120,11 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
return serializer(resolvableType, null) != null && canWrite(mediaType);
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
@Override
public final Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
throws IOException, HttpMessageNotReadableException {
@ -194,6 +199,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends @@ -194,6 +199,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object object) {
return true;
}

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

@ -108,7 +108,7 @@ public abstract class AbstractSmartHttpMessageConverter<T> extends AbstractHttpM @@ -108,7 +108,7 @@ public abstract class AbstractSmartHttpMessageConverter<T> extends AbstractHttpM
}
@Override
public boolean repeatable() {
return supportsRepeatableWrites(t);
return canWriteRepeatedly(t, contentType);
}
});
}

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

@ -50,6 +50,11 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< @@ -50,6 +50,11 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
return byte[].class == clazz;
}
@Override
public boolean canWriteRepeatedly(byte[] bytes, @Nullable MediaType contentType) {
return true;
}
@Override
public byte[] readInternal(Class<? extends byte[]> clazz, HttpInputMessage message) throws IOException {
long length = message.getHeaders().getContentLength();
@ -68,6 +73,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< @@ -68,6 +73,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(byte[] bytes) {
return true;
}

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

@ -486,7 +486,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue @@ -486,7 +486,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
outputMessage.getHeaders().setContentType(contentType);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
boolean repeatable = checkPartsRepeatable(parts);
boolean repeatable = checkPartsRepeatable(parts, contentType);
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
@ -506,7 +506,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue @@ -506,7 +506,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
}
@SuppressWarnings({"unchecked", "ConstantValue"})
private <T> boolean checkPartsRepeatable(MultiValueMap<String, Object> map) {
private <T> boolean checkPartsRepeatable(MultiValueMap<String, Object> map, MediaType contentType) {
return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> {
HttpHeaders headers = null;
Object body = part;
@ -515,9 +515,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue @@ -515,9 +515,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
body = entity.getBody();
Assert.state(body != null, "Empty body for part '" + e.getKey() + "': " + part);
}
HttpMessageConverter<?> converter = findConverterFor(e.getKey(), headers, body);
return (converter instanceof AbstractHttpMessageConverter<?> ahmc &&
((AbstractHttpMessageConverter<T>) ahmc).supportsRepeatableWrites((T) body));
HttpMessageConverter<T> converter = (HttpMessageConverter<T>) findConverterFor(e.getKey(), headers, body);
return converter != null && converter.canWriteRepeatedly((T) body, contentType);
}));
}

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

@ -108,4 +108,22 @@ public interface HttpMessageConverter<T> { @@ -108,4 +108,22 @@ public interface HttpMessageConverter<T> {
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
/**
* Indicates whether this message converter can
* {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the
* given payload multiple times.
* <p>This can be used by HTTP client libraries to know whether a message can be
* sent again, for example after an HTTP redirect. The default implementation
* returns {@code false}. This typically returns false if the payload can be read
* only once.
* @param t the object t
* @param contentType the content type to use when writing.
* @return {@code true} if {@code t} can be written repeatedly;
* {@code false} otherwise
* @since 7.1
*/
default boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) {
return false;
}
}

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

@ -99,6 +99,11 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve @@ -99,6 +99,11 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve
return canWrite(mediaType) && this.conversionService.canConvert(clazz, String.class);
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write
@ -137,6 +142,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve @@ -137,6 +142,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

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

@ -71,6 +71,11 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R @@ -71,6 +71,11 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
}
@Override
public boolean canWriteRepeatedly(Resource resource, @Nullable MediaType contentType) {
return !(resource instanceof InputStreamResource);
}
@Override
protected boolean supports(Class<?> clazz) {
return Resource.class.isAssignableFrom(clazz);
@ -141,8 +146,9 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R @@ -141,8 +146,9 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Resource resource) {
return !(resource instanceof InputStreamResource);
return canWriteRepeatedly(resource, null);
}

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

@ -104,6 +104,23 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa @@ -104,6 +104,23 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
return ResourceRegion.class.isAssignableFrom(typeArgumentClass);
}
@Override
public boolean canWriteRepeatedly(Object object, @Nullable MediaType contentType) {
if (object instanceof ResourceRegion resourceRegion) {
return supportsRepeatableWrites(resourceRegion);
}
else {
@SuppressWarnings("unchecked")
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
for (ResourceRegion region : regions) {
if (!supportsRepeatableWrites(region)) {
return false;
}
}
return true;
}
}
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
@ -140,20 +157,9 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa @@ -140,20 +157,9 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object object) {
if (object instanceof ResourceRegion resourceRegion) {
return supportsRepeatableWrites(resourceRegion);
}
else {
@SuppressWarnings("unchecked")
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
for (ResourceRegion region : regions) {
if (!supportsRepeatableWrites(region)) {
return false;
}
}
return true;
}
return canWriteRepeatedly(object, null);
}
private boolean supportsRepeatableWrites(ResourceRegion region) {

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

@ -89,6 +89,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str @@ -89,6 +89,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
return String.class == clazz;
}
@Override
public boolean canWriteRepeatedly(String s, @Nullable MediaType contentType) {
return true;
}
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
@ -161,6 +166,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str @@ -161,6 +166,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(String s) {
return true;
}

7
spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java

@ -29,6 +29,7 @@ import com.rometools.rome.feed.WireFeed; @@ -29,6 +29,7 @@ import com.rometools.rome.feed.WireFeed;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.WireFeedInput;
import com.rometools.rome.io.WireFeedOutput;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
@ -66,6 +67,11 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed> @@ -66,6 +67,11 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed>
}
@Override
public boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) {
return true;
}
@Override
@SuppressWarnings("unchecked")
protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
@ -108,6 +114,7 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed> @@ -108,6 +114,7 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed>
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(T t) {
return true;
}

6
spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java

@ -292,6 +292,11 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener @@ -292,6 +292,11 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return false;
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
/**
* Select an ObjectMapper to use, either the main ObjectMapper or another
* if the handling for the given Class has been customized through
@ -570,6 +575,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener @@ -570,6 +575,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

6
spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java

@ -24,6 +24,7 @@ import java.lang.reflect.Type; @@ -24,6 +24,7 @@ import java.lang.reflect.Type;
import com.google.gson.Gson;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
/**
@ -86,6 +87,10 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { @@ -86,6 +87,10 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
return this.gson;
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
@Override
protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
@ -109,6 +114,7 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { @@ -109,6 +114,7 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

6
spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java

@ -26,6 +26,7 @@ import jakarta.json.bind.JsonbBuilder; @@ -26,6 +26,7 @@ import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
/**
@ -94,6 +95,10 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter @@ -94,6 +95,10 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter
return this.jsonb;
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
@Override
protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
@ -111,6 +116,7 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter @@ -111,6 +116,7 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

7
spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java

@ -122,6 +122,12 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M @@ -122,6 +122,12 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
this(null, extensionRegistry);
}
@Override
public boolean canWriteRepeatedly(Message message, @Nullable MediaType contentType) {
return true;
}
/**
* Constructor for a subclass that supports additional formats.
* @param formatDelegate delegate to read and write additional formats
@ -255,6 +261,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M @@ -255,6 +261,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Message message) {
return true;
}

6
spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java

@ -126,6 +126,11 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa @@ -126,6 +126,11 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
return (supportedType && canWrite(mediaType));
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write
@ -235,6 +240,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa @@ -235,6 +240,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

6
spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java

@ -114,6 +114,11 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve @@ -114,6 +114,11 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve
return (canWrite(mediaType) && this.marshaller != null && this.marshaller.supports(clazz));
}
@Override
public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) {
return true;
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead()/canWrite()
@ -137,6 +142,7 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve @@ -137,6 +142,7 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(Object o) {
return true;
}

8
spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java

@ -148,6 +148,11 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe @@ -148,6 +148,11 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
return SUPPORTED_CLASSES.contains(clazz);
}
@Override
public boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) {
return t instanceof DOMSource;
}
@Override
@SuppressWarnings("unchecked")
protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
@ -293,8 +298,9 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe @@ -293,8 +298,9 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
}
@Override
@SuppressWarnings("removal")
protected boolean supportsRepeatableWrites(T t) {
return t instanceof DOMSource;
return canWriteRepeatedly(t, null);
}

Loading…
Cancel
Save