Add InputStreamResource(InputStreamSource) constructor for lambda expressions
Includes notes for reliable InputStream closing, in particular with Spring MVC. Closes gh-32802
This commit is contained in:
parent
b7aafda872
commit
e509385eae
|
|
@ -37,6 +37,13 @@ Kotlin::
|
|||
all controller methods. This is the effect of `@RestController`, which is nothing more
|
||||
than a meta-annotation marked with `@Controller` and `@ResponseBody`.
|
||||
|
||||
A `Resource` object can be returned for file content, copying the `InputStream`
|
||||
content of the provided resource to the response `OutputStream`. Note that the
|
||||
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
|
||||
close it after it has been copied to the response. If you are using `InputStreamResource`
|
||||
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
|
||||
(e.g. through a lambda expression that retrieves the actual `InputStream`).
|
||||
|
||||
You can use `@ResponseBody` with reactive types.
|
||||
See xref:web/webmvc/mvc-ann-async.adoc[Asynchronous Requests] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] for more details.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,16 @@ Kotlin::
|
|||
----
|
||||
======
|
||||
|
||||
The body will usually be provided as a value object to be rendered to a corresponding
|
||||
response representation (e.g. JSON) by one of the registered `HttpMessageConverters`.
|
||||
|
||||
A `ResponseEntity<Resource>` can be returned for file content, copying the `InputStream`
|
||||
content of the provided resource to the response `OutputStream`. Note that the
|
||||
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
|
||||
close it after it has been copied to the response. If you are using `InputStreamResource`
|
||||
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
|
||||
(e.g. through a lambda expression that retrieves the actual `InputStream`).
|
||||
|
||||
Spring MVC supports using a single value xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive type]
|
||||
to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive
|
||||
types for the body. This allows the following types of async responses:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -23,16 +23,22 @@ import org.springframework.lang.Nullable;
|
|||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Resource} implementation for a given {@link InputStream}.
|
||||
* {@link Resource} implementation for a given {@link InputStream} or a given
|
||||
* {@link InputStreamSource} (which can be supplied as a lambda expression)
|
||||
* for a lazy {@link InputStream} on demand.
|
||||
*
|
||||
* <p>Should only be used if no other specific {@code Resource} implementation
|
||||
* is applicable. In particular, prefer {@link ByteArrayResource} or any of the
|
||||
* file-based {@code Resource} implementations where possible.
|
||||
* file-based {@code Resource} implementations if possible. If you need to obtain
|
||||
* a custom stream multiple times, use a custom {@link AbstractResource} subclass
|
||||
* with a corresponding {@code getInputStream()} implementation.
|
||||
*
|
||||
* <p>In contrast to other {@code Resource} implementations, this is a descriptor
|
||||
* for an <i>already opened</i> resource - therefore returning {@code true} from
|
||||
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to
|
||||
* keep the resource descriptor somewhere, or if you need to read from a stream
|
||||
* multiple times.
|
||||
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to keep
|
||||
* the resource descriptor somewhere, or if you need to read from a stream multiple
|
||||
* times. This also applies when constructed with an {@code InputStreamSource}
|
||||
* which lazily obtains the stream but only allows for single access as well.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
|
|
@ -44,30 +50,62 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
public class InputStreamResource extends AbstractResource {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final InputStreamSource inputStreamSource;
|
||||
|
||||
private final String description;
|
||||
|
||||
private final Object equality;
|
||||
|
||||
private boolean read = false;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new InputStreamResource.
|
||||
* Create a new {@code InputStreamResource} with a lazy {@code InputStream}
|
||||
* for single use.
|
||||
* @param inputStreamSource an on-demand source for a single-use InputStream
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public InputStreamResource(InputStreamSource inputStreamSource) {
|
||||
this(inputStreamSource, "resource loaded from InputStreamSource");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code InputStreamResource} with a lazy {@code InputStream}
|
||||
* for single use.
|
||||
* @param inputStreamSource an on-demand source for a single-use InputStream
|
||||
* @param description where the InputStream comes from
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public InputStreamResource(InputStreamSource inputStreamSource, @Nullable String description) {
|
||||
Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
|
||||
this.inputStreamSource = inputStreamSource;
|
||||
this.description = (description != null ? description : "");
|
||||
this.equality = inputStreamSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code InputStreamResource} for an existing {@code InputStream}.
|
||||
* <p>Consider retrieving the InputStream on demand if possible, reducing its
|
||||
* lifetime and reliably opening it and closing it through regular
|
||||
* {@link InputStreamSource#getInputStream()} usage.
|
||||
* @param inputStream the InputStream to use
|
||||
* @see #InputStreamResource(InputStreamSource)
|
||||
*/
|
||||
public InputStreamResource(InputStream inputStream) {
|
||||
this(inputStream, "resource loaded through InputStream");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new InputStreamResource.
|
||||
* Create a new {@code InputStreamResource} for an existing {@code InputStream}.
|
||||
* @param inputStream the InputStream to use
|
||||
* @param description where the InputStream comes from
|
||||
* @see #InputStreamResource(InputStreamSource, String)
|
||||
*/
|
||||
public InputStreamResource(InputStream inputStream, @Nullable String description) {
|
||||
Assert.notNull(inputStream, "InputStream must not be null");
|
||||
this.inputStream = inputStream;
|
||||
this.inputStreamSource = () -> inputStream;
|
||||
this.description = (description != null ? description : "");
|
||||
this.equality = inputStream;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -98,7 +136,7 @@ public class InputStreamResource extends AbstractResource {
|
|||
"do not use InputStreamResource if a stream needs to be read multiple times");
|
||||
}
|
||||
this.read = true;
|
||||
return this.inputStream;
|
||||
return this.inputStreamSource.getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,7 +155,7 @@ public class InputStreamResource extends AbstractResource {
|
|||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof InputStreamResource that &&
|
||||
this.inputStream.equals(that.inputStream)));
|
||||
this.equality.equals(that.equality)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,7 +163,7 @@ public class InputStreamResource extends AbstractResource {
|
|||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.inputStream.hashCode();
|
||||
return this.equality.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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.
|
||||
|
|
@ -38,11 +38,12 @@ import java.io.InputStream;
|
|||
* @see InputStreamResource
|
||||
* @see ByteArrayResource
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface InputStreamSource {
|
||||
|
||||
/**
|
||||
* Return an {@link InputStream} for the content of an underlying resource.
|
||||
* <p>It is expected that each call creates a <i>fresh</i> stream.
|
||||
* <p>It is usually expected that every such call creates a <i>fresh</i> stream.
|
||||
* <p>This requirement is particularly important when you consider an API such
|
||||
* as JavaMail, which needs to be able to read the stream multiple times when
|
||||
* creating mail attachments. For such a use case, it is <i>required</i>
|
||||
|
|
@ -51,6 +52,7 @@ public interface InputStreamSource {
|
|||
* @throws java.io.FileNotFoundException if the underlying resource does not exist
|
||||
* @throws IOException if the content stream could not be opened
|
||||
* @see Resource#isReadable()
|
||||
* @see Resource#isOpen()
|
||||
*/
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import okhttp3.mockwebserver.Dispatcher;
|
||||
|
|
@ -189,14 +190,21 @@ class ResourceTests {
|
|||
String content = FileCopyUtils.copyToString(new InputStreamReader(resource1.getInputStream()));
|
||||
assertThat(content).isEqualTo(testString);
|
||||
assertThat(new InputStreamResource(is)).isEqualTo(resource1);
|
||||
assertThat(new InputStreamResource(() -> is)).isNotEqualTo(resource1);
|
||||
assertThatIllegalStateException().isThrownBy(resource1::getInputStream);
|
||||
|
||||
Resource resource2 = new InputStreamResource(new ByteArrayInputStream(testBytes));
|
||||
assertThat(resource2.getContentAsByteArray()).containsExactly(testBytes);
|
||||
assertThatIllegalStateException().isThrownBy(resource2::getContentAsByteArray);
|
||||
|
||||
Resource resource3 = new InputStreamResource(new ByteArrayInputStream(testBytes));
|
||||
AtomicBoolean obtained = new AtomicBoolean();
|
||||
Resource resource3 = new InputStreamResource(() -> {
|
||||
obtained.set(true);
|
||||
return new ByteArrayInputStream(testBytes);
|
||||
});
|
||||
assertThat(obtained).isFalse();
|
||||
assertThat(resource3.getContentAsString(StandardCharsets.US_ASCII)).isEqualTo(testString);
|
||||
assertThat(obtained).isTrue();
|
||||
assertThatIllegalStateException().isThrownBy(() -> resource3.getContentAsString(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +214,10 @@ class ResourceTests {
|
|||
Resource resource = new InputStreamResource(is);
|
||||
assertThat(resource.exists()).isTrue();
|
||||
assertThat(resource.isOpen()).isTrue();
|
||||
|
||||
resource = new InputStreamResource(() -> is);
|
||||
assertThat(resource.exists()).isTrue();
|
||||
assertThat(resource.isOpen()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -213,6 +225,9 @@ class ResourceTests {
|
|||
InputStream is = new ByteArrayInputStream("testString".getBytes());
|
||||
Resource resource = new InputStreamResource(is, "my description");
|
||||
assertThat(resource.getDescription()).contains("my description");
|
||||
|
||||
resource = new InputStreamResource(() -> is, "my description");
|
||||
assertThat(resource.getDescription()).contains("my description");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue