Allow repeatable writes in HttpMessageConverter

This commit ensures that the StreamingHttpOutputMessage.Body.repeatable
flag is set in message converters for bodies that can be written
repeatedly.

Closes gh-31516
See gh-31449
This commit is contained in:
Arjen Poutsma 2023-11-02 15:51:10 +01:00
parent ab316d9bc8
commit 6dd93d4d85
22 changed files with 206 additions and 25 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* 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.
@ -88,16 +88,27 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public OutputStream getBody() {
return outputStream;
public void writeTo(OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
@Override
public HttpHeaders getHeaders() {
return headers;
public boolean repeatable() {
return supportsRepeatableWrites(t);
}
}));
});
}
else {
writeInternal(t, type, outputMessage);

View File

@ -210,16 +210,27 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public OutputStream getBody() {
return outputStream;
public void writeTo(OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
@Override
public HttpHeaders getHeaders() {
return headers;
public boolean repeatable() {
return supportsRepeatableWrites(t);
}
}));
});
}
else {
writeInternal(t, outputMessage);
@ -289,6 +300,21 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
return null;
}
/**
* Indicates whether this message converter can
* {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the
* given object multiple times.
*
* <p>Default implementation returns {@code false}.
* @param t the object t
* @return {@code true} if {@code t} can be written repeatedly;
* {@code false} otherwise
* @since 6.1
*/
protected boolean supportsRepeatableWrites(T t) {
return false;
}
/**
* Indicates whether the given class is supported by this converter.

View File

@ -178,4 +178,9 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
}
return false;
}
@Override
protected boolean supportsRepeatableWrites(Object object) {
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -226,7 +226,17 @@ public class BufferedImageHttpMessageConverter implements HttpMessageConverter<B
outputMessage.getHeaders().setContentType(selectedContentType);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> writeInternal(image, selectedContentType, outputStream));
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
BufferedImageHttpMessageConverter.this.writeInternal(image, selectedContentType, outputStream);
}
@Override
public boolean repeatable() {
return true;
}
});
}
else {
writeInternal(image, selectedContentType, outputMessage.getBody());

View File

@ -67,4 +67,8 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
StreamUtils.copy(bytes, outputMessage.getBody());
}
@Override
protected boolean supportsRepeatableWrites(byte[] bytes) {
return true;
}
}

View File

@ -400,7 +400,17 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
StreamUtils.copy(bytes, outputStream);
}
@Override
public boolean repeatable() {
return true;
}
});
}
else {
StreamUtils.copy(bytes, outputMessage.getBody());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@ -135,4 +135,8 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve
return this.stringHttpMessageConverter.getContentLength(value, contentType);
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -166,4 +166,8 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
}
}
@Override
protected boolean supportsRepeatableWrites(Resource resource) {
return !(resource instanceof InputStreamResource);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -24,6 +24,7 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
@ -238,4 +239,24 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
os.write(buf.getBytes(StandardCharsets.US_ASCII));
}
@Override
@SuppressWarnings("unchecked")
protected boolean supportsRepeatableWrites(Object object) {
if (object instanceof ResourceRegion resourceRegion) {
return supportsRepeatableWrites(resourceRegion);
}
else {
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
for (ResourceRegion region : regions) {
if (!supportsRepeatableWrites(region)) {
return false;
}
}
return true;
}
}
private boolean supportsRepeatableWrites(ResourceRegion region) {
return !(region.getResource() instanceof InputStreamResource);
}
}

View File

@ -163,4 +163,8 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
return charset;
}
@Override
protected boolean supportsRepeatableWrites(String s) {
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -107,4 +107,8 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed>
}
}
@Override
protected boolean supportsRepeatableWrites(T t) {
return true;
}
}

View File

@ -568,4 +568,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return super.getContentLength(object, contentType);
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* 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.
@ -107,4 +107,8 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
}
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -110,4 +110,8 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter
}
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

View File

@ -247,6 +247,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName());
}
@Override
protected boolean supportsRepeatableWrites(Message message) {
return true;
}
/**
* Protobuf format support.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* 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.
@ -200,6 +200,11 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
}
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
(publicId, systemId) -> new InputSource(new StringReader(""));

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -137,4 +137,8 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve
this.marshaller.marshal(o, result);
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@ -269,6 +269,11 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
this.transformerFactory.newTransformer().transform(source, result);
}
@Override
protected boolean supportsRepeatableWrites(T t) {
return t instanceof DOMSource;
}
private static class CountingOutputStream extends OutputStream {

View File

@ -79,4 +79,18 @@ public class ByteArrayHttpMessageConverterTests {
assertThat(outputMessage.getHeaders().getContentLength()).isEqualTo(2);
}
@Test
public void repeatableWrites() throws IOException {
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage();
byte[] body = new byte[]{0x1, 0x2};
assertThat(converter.supportsRepeatableWrites(body)).isTrue();
converter.write(body, null, outputMessage1);
assertThat(outputMessage1.getBodyAsBytes()).isEqualTo(body);
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage();
converter.write(body, null, outputMessage2);
assertThat(outputMessage2.getBodyAsBytes()).isEqualTo(body);
}
}

View File

@ -171,4 +171,19 @@ class StringHttpMessageConverterTests {
assertThat(headers.getAcceptCharset()).isEmpty();
}
@Test
public void repeatableWrites() throws IOException {
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage();
String body = "Hello World";
assertThat(converter.supportsRepeatableWrites(body)).isTrue();
converter.write(body, TEXT_PLAIN_UTF_8, outputMessage1);
assertThat(outputMessage1.getBodyAsString()).isEqualTo(body);
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage();
converter.write(body, TEXT_PLAIN_UTF_8, outputMessage2);
assertThat(outputMessage2.getBodyAsString()).isEqualTo(body);
}
}

View File

@ -580,6 +580,22 @@ public class MappingJackson2HttpMessageConverterTests {
assertThat(result2).contains("\"property\":\"Value2\"");
}
@Test
public void repeatableWrites() throws IOException {
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage();
MyBean body = new MyBean();
body.setString("Foo");
converter.write(body, null, outputMessage1);
String result = outputMessage1.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage();
converter.write(body, null, outputMessage2);
result = outputMessage2.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
}
interface MyInterface {

View File

@ -290,6 +290,7 @@ public class SourceHttpMessageConverterTests {
DOMSource domSource = new DOMSource(document);
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThat(converter.supportsRepeatableWrites(domSource)).isTrue();
converter.write(domSource, null, outputMessage);
assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8)))
.isSimilarTo("<root>Hello World</root>");
@ -305,6 +306,7 @@ public class SourceHttpMessageConverterTests {
SAXSource saxSource = new SAXSource(new InputSource(new StringReader(xml)));
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThat(converter.supportsRepeatableWrites(saxSource)).isFalse();
converter.write(saxSource, null, outputMessage);
assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8)))
.isSimilarTo("<root>Hello World</root>");
@ -318,6 +320,7 @@ public class SourceHttpMessageConverterTests {
StreamSource streamSource = new StreamSource(new StringReader(xml));
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThat(converter.supportsRepeatableWrites(streamSource)).isFalse();
converter.write(streamSource, null, outputMessage);
assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8)))
.isSimilarTo("<root>Hello World</root>");