Skip to content

Commit 7c534ee

Browse files
committed
Merge branch '6.1.x'
# Conflicts: # framework-platform/framework-platform.gradle
2 parents 716e7de + 70886e3 commit 7c534ee

File tree

6 files changed

+98
-26
lines changed

6 files changed

+98
-26
lines changed

framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ Kotlin::
3737
all controller methods. This is the effect of `@RestController`, which is nothing more
3838
than a meta-annotation marked with `@Controller` and `@ResponseBody`.
3939

40+
A `Resource` object can be returned for file content, copying the `InputStream`
41+
content of the provided resource to the response `OutputStream`. Note that the
42+
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
43+
close it after it has been copied to the response. If you are using `InputStreamResource`
44+
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
45+
(e.g. through a lambda expression that retrieves the actual `InputStream`).
46+
4047
You can use `@ResponseBody` with reactive types.
4148
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.
4249

framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc

+10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ Kotlin::
3232
----
3333
======
3434

35+
The body will usually be provided as a value object to be rendered to a corresponding
36+
response representation (e.g. JSON) by one of the registered `HttpMessageConverters`.
37+
38+
A `ResponseEntity<Resource>` can be returned for file content, copying the `InputStream`
39+
content of the provided resource to the response `OutputStream`. Note that the
40+
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
41+
close it after it has been copied to the response. If you are using `InputStreamResource`
42+
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
43+
(e.g. through a lambda expression that retrieves the actual `InputStream`).
44+
3545
Spring MVC supports using a single value xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive type]
3646
to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive
3747
types for the body. This allows the following types of async responses:

framework-platform/framework-platform.gradle

+10-10
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ dependencies {
1616
api(platform("org.apache.groovy:groovy-bom:4.0.21"))
1717
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
1818
api(platform("org.assertj:assertj-bom:3.25.3"))
19-
api(platform("org.eclipse.jetty:jetty-bom:12.0.8"))
20-
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.8"))
19+
api(platform("org.eclipse.jetty:jetty-bom:12.0.9"))
20+
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.9"))
2121
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
2222
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0"))
2323
api(platform("org.junit:junit-bom:5.10.2"))
24-
api(platform("org.mockito:mockito-bom:5.11.0"))
24+
api(platform("org.mockito:mockito-bom:5.12.0"))
2525

2626
constraints {
2727
api("com.fasterxml:aalto-xml:1.3.2")
@@ -102,13 +102,13 @@ dependencies {
102102
api("org.apache.httpcomponents.client5:httpclient5:5.3.1")
103103
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.4")
104104
api("org.apache.poi:poi-ooxml:5.2.5")
105-
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.23")
106-
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.23")
107-
api("org.apache.tomcat:tomcat-util:10.1.23")
108-
api("org.apache.tomcat:tomcat-websocket:10.1.23")
109-
api("org.aspectj:aspectjrt:1.9.22")
110-
api("org.aspectj:aspectjtools:1.9.22")
111-
api("org.aspectj:aspectjweaver:1.9.22")
105+
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.24")
106+
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.24")
107+
api("org.apache.tomcat:tomcat-util:10.1.24")
108+
api("org.apache.tomcat:tomcat-websocket:10.1.24")
109+
api("org.aspectj:aspectjrt:1.9.22.1")
110+
api("org.aspectj:aspectjtools:1.9.22.1")
111+
api("org.aspectj:aspectjweaver:1.9.22.1")
112112
api("org.awaitility:awaitility:4.2.0")
113113
api("org.bouncycastle:bcpkix-jdk18on:1.72")
114114
api("org.codehaus.jettison:jettison:1.5.4")

spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java

+51-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,16 +23,22 @@
2323
import org.springframework.util.Assert;
2424

2525
/**
26-
* {@link Resource} implementation for a given {@link InputStream}.
26+
* {@link Resource} implementation for a given {@link InputStream} or a given
27+
* {@link InputStreamSource} (which can be supplied as a lambda expression)
28+
* for a lazy {@link InputStream} on demand.
29+
*
2730
* <p>Should only be used if no other specific {@code Resource} implementation
2831
* is applicable. In particular, prefer {@link ByteArrayResource} or any of the
29-
* file-based {@code Resource} implementations where possible.
32+
* file-based {@code Resource} implementations if possible. If you need to obtain
33+
* a custom stream multiple times, use a custom {@link AbstractResource} subclass
34+
* with a corresponding {@code getInputStream()} implementation.
3035
*
3136
* <p>In contrast to other {@code Resource} implementations, this is a descriptor
3237
* for an <i>already opened</i> resource - therefore returning {@code true} from
33-
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to
34-
* keep the resource descriptor somewhere, or if you need to read from a stream
35-
* multiple times.
38+
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to keep
39+
* the resource descriptor somewhere, or if you need to read from a stream multiple
40+
* times. This also applies when constructed with an {@code InputStreamSource}
41+
* which lazily obtains the stream but only allows for single access as well.
3642
*
3743
* @author Juergen Hoeller
3844
* @author Sam Brannen
@@ -44,30 +50,62 @@
4450
*/
4551
public class InputStreamResource extends AbstractResource {
4652

47-
private final InputStream inputStream;
53+
private final InputStreamSource inputStreamSource;
4854

4955
private final String description;
5056

57+
private final Object equality;
58+
5159
private boolean read = false;
5260

5361

5462
/**
55-
* Create a new InputStreamResource.
63+
* Create a new {@code InputStreamResource} with a lazy {@code InputStream}
64+
* for single use.
65+
* @param inputStreamSource an on-demand source for a single-use InputStream
66+
* @since 6.1.7
67+
*/
68+
public InputStreamResource(InputStreamSource inputStreamSource) {
69+
this(inputStreamSource, "resource loaded from InputStreamSource");
70+
}
71+
72+
/**
73+
* Create a new {@code InputStreamResource} with a lazy {@code InputStream}
74+
* for single use.
75+
* @param inputStreamSource an on-demand source for a single-use InputStream
76+
* @param description where the InputStream comes from
77+
* @since 6.1.7
78+
*/
79+
public InputStreamResource(InputStreamSource inputStreamSource, @Nullable String description) {
80+
Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
81+
this.inputStreamSource = inputStreamSource;
82+
this.description = (description != null ? description : "");
83+
this.equality = inputStreamSource;
84+
}
85+
86+
/**
87+
* Create a new {@code InputStreamResource} for an existing {@code InputStream}.
88+
* <p>Consider retrieving the InputStream on demand if possible, reducing its
89+
* lifetime and reliably opening it and closing it through regular
90+
* {@link InputStreamSource#getInputStream()} usage.
5691
* @param inputStream the InputStream to use
92+
* @see #InputStreamResource(InputStreamSource)
5793
*/
5894
public InputStreamResource(InputStream inputStream) {
5995
this(inputStream, "resource loaded through InputStream");
6096
}
6197

6298
/**
63-
* Create a new InputStreamResource.
99+
* Create a new {@code InputStreamResource} for an existing {@code InputStream}.
64100
* @param inputStream the InputStream to use
65101
* @param description where the InputStream comes from
102+
* @see #InputStreamResource(InputStreamSource, String)
66103
*/
67104
public InputStreamResource(InputStream inputStream, @Nullable String description) {
68105
Assert.notNull(inputStream, "InputStream must not be null");
69-
this.inputStream = inputStream;
106+
this.inputStreamSource = () -> inputStream;
70107
this.description = (description != null ? description : "");
108+
this.equality = inputStream;
71109
}
72110

73111

@@ -98,7 +136,7 @@ public InputStream getInputStream() throws IOException, IllegalStateException {
98136
"do not use InputStreamResource if a stream needs to be read multiple times");
99137
}
100138
this.read = true;
101-
return this.inputStream;
139+
return this.inputStreamSource.getInputStream();
102140
}
103141

104142
/**
@@ -117,15 +155,15 @@ public String getDescription() {
117155
@Override
118156
public boolean equals(@Nullable Object other) {
119157
return (this == other || (other instanceof InputStreamResource that &&
120-
this.inputStream.equals(that.inputStream)));
158+
this.equality.equals(that.equality)));
121159
}
122160

123161
/**
124162
* This implementation returns the hash code of the underlying InputStream.
125163
*/
126164
@Override
127165
public int hashCode() {
128-
return this.inputStream.hashCode();
166+
return this.equality.hashCode();
129167
}
130168

131169
}

spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,11 +38,12 @@
3838
* @see InputStreamResource
3939
* @see ByteArrayResource
4040
*/
41+
@FunctionalInterface
4142
public interface InputStreamSource {
4243

4344
/**
4445
* Return an {@link InputStream} for the content of an underlying resource.
45-
* <p>It is expected that each call creates a <i>fresh</i> stream.
46+
* <p>It is usually expected that every such call creates a <i>fresh</i> stream.
4647
* <p>This requirement is particularly important when you consider an API such
4748
* as JavaMail, which needs to be able to read the stream multiple times when
4849
* creating mail attachments. For such a use case, it is <i>required</i>
@@ -51,6 +52,7 @@ public interface InputStreamSource {
5152
* @throws java.io.FileNotFoundException if the underlying resource does not exist
5253
* @throws IOException if the content stream could not be opened
5354
* @see Resource#isReadable()
55+
* @see Resource#isOpen()
5456
*/
5557
InputStream getInputStream() throws IOException;
5658

spring-core/src/test/java/org/springframework/core/io/ResourceTests.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.nio.file.Path;
3535
import java.nio.file.Paths;
3636
import java.util.Base64;
37+
import java.util.concurrent.atomic.AtomicBoolean;
3738
import java.util.stream.Stream;
3839

3940
import okhttp3.mockwebserver.Dispatcher;
@@ -189,14 +190,21 @@ void hasContent() throws Exception {
189190
String content = FileCopyUtils.copyToString(new InputStreamReader(resource1.getInputStream()));
190191
assertThat(content).isEqualTo(testString);
191192
assertThat(new InputStreamResource(is)).isEqualTo(resource1);
193+
assertThat(new InputStreamResource(() -> is)).isNotEqualTo(resource1);
192194
assertThatIllegalStateException().isThrownBy(resource1::getInputStream);
193195

194196
Resource resource2 = new InputStreamResource(new ByteArrayInputStream(testBytes));
195197
assertThat(resource2.getContentAsByteArray()).containsExactly(testBytes);
196198
assertThatIllegalStateException().isThrownBy(resource2::getContentAsByteArray);
197199

198-
Resource resource3 = new InputStreamResource(new ByteArrayInputStream(testBytes));
200+
AtomicBoolean obtained = new AtomicBoolean();
201+
Resource resource3 = new InputStreamResource(() -> {
202+
obtained.set(true);
203+
return new ByteArrayInputStream(testBytes);
204+
});
205+
assertThat(obtained).isFalse();
199206
assertThat(resource3.getContentAsString(StandardCharsets.US_ASCII)).isEqualTo(testString);
207+
assertThat(obtained).isTrue();
200208
assertThatIllegalStateException().isThrownBy(() -> resource3.getContentAsString(StandardCharsets.US_ASCII));
201209
}
202210

@@ -206,13 +214,20 @@ void isOpen() {
206214
Resource resource = new InputStreamResource(is);
207215
assertThat(resource.exists()).isTrue();
208216
assertThat(resource.isOpen()).isTrue();
217+
218+
resource = new InputStreamResource(() -> is);
219+
assertThat(resource.exists()).isTrue();
220+
assertThat(resource.isOpen()).isTrue();
209221
}
210222

211223
@Test
212224
void hasDescription() {
213225
InputStream is = new ByteArrayInputStream("testString".getBytes());
214226
Resource resource = new InputStreamResource(is, "my description");
215227
assertThat(resource.getDescription()).contains("my description");
228+
229+
resource = new InputStreamResource(() -> is, "my description");
230+
assertThat(resource.getDescription()).contains("my description");
216231
}
217232
}
218233

0 commit comments

Comments
 (0)