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:
Juergen Hoeller 2024-05-14 21:59:42 +02:00
parent b7aafda872
commit e509385eae
5 changed files with 88 additions and 16 deletions

View File

@ -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.

View File

@ -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:

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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");
}
}