From e509385eae29056621071b653e81fa8c0382b625 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 14 May 2024 21:59:42 +0200 Subject: [PATCH] Add InputStreamResource(InputStreamSource) constructor for lambda expressions Includes notes for reliable InputStream closing, in particular with Spring MVC. Closes gh-32802 --- .../ann-methods/responsebody.adoc | 7 ++ .../ann-methods/responseentity.adoc | 10 +++ .../core/io/InputStreamResource.java | 64 +++++++++++++++---- .../core/io/InputStreamSource.java | 6 +- .../core/io/ResourceTests.java | 17 ++++- 5 files changed, 88 insertions(+), 16 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc index 1cd1816e5d3c..4fb18b0002cf 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc @@ -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. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc index f311386cabb4..2baae5ae7677 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc @@ -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` 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: diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java index ab8e393b48d0..0615134e9e90 100644 --- a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java @@ -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.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. + * *

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

In contrast to other {@code Resource} implementations, this is a descriptor * for an already opened 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 @@ */ 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}. + *

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 InputStream getInputStream() throws IOException, IllegalStateException { "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 String getDescription() { @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 boolean equals(@Nullable Object other) { */ @Override public int hashCode() { - return this.inputStream.hashCode(); + return this.equality.hashCode(); } } diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java index 8d72c9cd8bbc..2f3eda75d793 100644 --- a/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java @@ -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 @@ * @see InputStreamResource * @see ByteArrayResource */ +@FunctionalInterface public interface InputStreamSource { /** * Return an {@link InputStream} for the content of an underlying resource. - *

It is expected that each call creates a fresh stream. + *

It is usually expected that every such call creates a fresh stream. *

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 required @@ -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; diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java index ccf54584acc1..591254e678ff 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java @@ -34,6 +34,7 @@ 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 @@ void hasContent() throws Exception { 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 @@ void isOpen() { 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 @@ void hasDescription() { 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"); } }