From 27221581a125341fc3e77b460eacdd9cb54159b2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 8 Sep 2025 17:03:07 +0200 Subject: [PATCH] Expose getFilePath() on Resource interface for consistent NIO support Closes gh-35435 --- .../beans/propertyeditors/PathEditor.java | 2 +- .../core/io/FileSystemResource.java | 8 +++++++ .../springframework/core/io/PathResource.java | 19 ++++++++------- .../org/springframework/core/io/Resource.java | 24 +++++++++++++++---- .../core/io/buffer/DataBufferUtils.java | 7 +++--- .../core/io/PathResourceTests.java | 15 ++++++------ .../core/io/ResourceTests.java | 12 ++++++++++ .../http/codec/ResourceHttpMessageWriter.java | 15 ++++++------ .../resource/EncodedResourceResolver.java | 6 +++++ .../resource/VersionResourceResolver.java | 6 +++++ .../resource/EncodedResourceResolver.java | 6 +++++ .../resource/VersionResourceResolver.java | 6 +++++ 12 files changed, 94 insertions(+), 32 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 7b53e70ff3a..de26e7b7067 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -108,7 +108,7 @@ public class PathEditor extends PropertyEditorSupport { } else { try { - setValue(resource.getFile().toPath()); + setValue(resource.getFilePath()); } catch (IOException ex) { String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " + diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java index cae57ac8ce4..ec51f4cefd3 100644 --- a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java @@ -292,6 +292,14 @@ public class FileSystemResource extends AbstractResource implements WritableReso return (this.file != null ? this.file : this.filePath.toFile()); } + /** + * This implementation returns the underlying NIO Path reference. + */ + @Override + public Path getFilePath() { + return this.filePath; + } + /** * This implementation opens a FileChannel for the underlying file. * @see java.nio.channels.FileChannel diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java index 7a6bd448823..9b8eaaefe83 100644 --- a/spring-core/src/main/java/org/springframework/core/io/PathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.java @@ -219,15 +219,16 @@ public class PathResource extends AbstractResource implements WritableResource { * This implementation returns the underlying {@link File} reference. */ @Override - public File getFile() throws IOException { - try { - return this.path.toFile(); - } - catch (UnsupportedOperationException ex) { - // Only paths on the default file system can be converted to a File: - // Do exception translation for cases where conversion is not possible. - throw new FileNotFoundException(this.path + " cannot be resolved to absolute file path"); - } + public File getFile() { + return this.path.toFile(); + } + + /** + * This implementation returns the underlying {@link Path} reference. + */ + @Override + public Path getFilePath() { + return this.path; } /** diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index 0ae3750f8d5..b7e39449407 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -25,6 +25,7 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import org.jspecify.annotations.Nullable; @@ -91,11 +92,13 @@ public interface Resource extends InputStreamSource { /** * Determine whether this resource represents a file in a file system. - *

A value of {@code true} strongly suggests (but does not guarantee) - * that a {@link #getFile()} call will succeed. + *

A value of {@code true} suggests (but does not guarantee) that a + * {@link #getFile()} call will succeed. For non-default file systems, + * {@link #getFilePath()} is the more reliable follow-up call. *

This is conservatively {@code false} by default. * @since 5.0 * @see #getFile() + * @see #getFilePath() */ default boolean isFile() { return false; @@ -118,13 +121,26 @@ public interface Resource extends InputStreamSource { /** * Return a File handle for this resource. - * @throws java.io.FileNotFoundException if the resource cannot be resolved as - * absolute file path, i.e. if the resource is not available in a file system + *

Note: This only works for files in the default file system. + * @throws UnsupportedOperationException if the resource is a file but cannot be + * exposed as a {@code java.io.File}; try {@link #getFilePath()} instead + * @throws java.io.FileNotFoundException if the resource cannot be resolved as a file * @throws IOException in case of general resolution/reading failures * @see #getInputStream() */ File getFile() throws IOException; + /** + * Return an NIO Path handle for this resource. + *

Note: This works for files in non-default file systems as well. + * @throws java.io.FileNotFoundException if the resource cannot be resolved as a file + * @throws IOException in case of general resolution/reading failures + * @since 7.0 + */ + default Path getFilePath() throws IOException { + return getFile().toPath(); + } + /** * Return a {@link ReadableByteChannel}. *

It is expected that each call creates a fresh channel. diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 4c3c430e954..5875b3c0d6a 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -16,7 +16,6 @@ package org.springframework.core.io.buffer; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -223,9 +222,9 @@ public abstract class DataBufferUtils { try { if (resource.isFile()) { - File file = resource.getFile(); + Path filePath = resource.getFilePath(); return readAsynchronousFileChannel( - () -> AsynchronousFileChannel.open(file.toPath(), StandardOpenOption.READ), + () -> AsynchronousFileChannel.open(filePath, StandardOpenOption.READ), position, bufferFactory, bufferSize); } } @@ -233,7 +232,7 @@ public abstract class DataBufferUtils { // fallback to resource.readableChannel(), below } Flux result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize); - return position == 0 ? result : skipUntilByteCount(result, position); + return (position == 0 ? result : skipUntilByteCount(result, position)); } diff --git a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java index af9daeee0cb..5823195032f 100644 --- a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java @@ -186,10 +186,11 @@ class PathResourceTests { } @Test - void getFile() throws IOException { + void getFile() { PathResource resource = new PathResource(TEST_FILE); File file = new File(TEST_FILE); assertThat(resource.getFile().getAbsoluteFile()).isEqualTo(file.getAbsoluteFile()); + assertThat(resource.getFilePath()).isEqualTo(file.toPath()); } @Test @@ -198,7 +199,7 @@ class PathResourceTests { given(path.normalize()).willReturn(path); given(path.toFile()).willThrow(new UnsupportedOperationException()); PathResource resource = new PathResource(path); - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::getFile); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(resource::getFile); } @Test @@ -236,13 +237,13 @@ class PathResourceTests { @Test void filename() { - Resource resource = new PathResource(TEST_FILE); + PathResource resource = new PathResource(TEST_FILE); assertThat(resource.getFilename()).isEqualTo("example.properties"); } @Test void description() { - Resource resource = new PathResource(TEST_FILE); + PathResource resource = new PathResource(TEST_FILE); assertThat(resource.getDescription()).contains("path ["); assertThat(resource.getDescription()).contains(TEST_FILE); } @@ -261,9 +262,9 @@ class PathResourceTests { @Test void equalsAndHashCode() { - Resource resource1 = new PathResource(TEST_FILE); - Resource resource2 = new PathResource(TEST_FILE); - Resource resource3 = new PathResource(TEST_DIR); + PathResource resource1 = new PathResource(TEST_FILE); + PathResource resource2 = new PathResource(TEST_FILE); + PathResource resource3 = new PathResource(TEST_DIR); assertThat(resource1).isEqualTo(resource1); assertThat(resource1).isEqualTo(resource2); assertThat(resource2).isEqualTo(resource1); 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 ffb08bc50c3..9385a210f99 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 @@ -55,6 +55,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.junit.jupiter.params.provider.Arguments.argumentSet; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for various {@link Resource} implementations. @@ -268,6 +270,16 @@ class ResourceTests { assertThat(relative).isEqualTo(new FileSystemResource("dir/subdir")); } + @Test + void getFilePath() throws Exception { + Path path = mock(); + given(path.normalize()).willReturn(path); + given(path.toFile()).willThrow(new UnsupportedOperationException()); + Resource resource = new FileSystemResource(path); + assertThat(resource.getFilePath()).isSameAs(path); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(resource::getFile); + } + @Test void readableChannelProvidesContent() throws Exception { Resource resource = new FileSystemResource(getClass().getResource("ResourceTests.class").getFile()); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java index a464a12108f..5e56fe1e482 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java @@ -16,8 +16,9 @@ package org.springframework.http.codec; -import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -193,17 +194,17 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter { if (message instanceof ZeroCopyHttpOutputMessage zeroCopyHttpOutputMessage && resource.isFile()) { try { - File file = resource.getFile(); - long pos = region != null ? region.getPosition() : 0; - long count = region != null ? region.getCount() : file.length(); + Path filePath = resource.getFilePath(); + long pos = (region != null ? region.getPosition() : 0); + long count = (region != null ? region.getCount() : Files.size(filePath)); if (logger.isDebugEnabled()) { String formatted = region != null ? "region " + pos + "-" + (count) + " of " : ""; logger.debug(Hints.getLogPrefix(hints) + "Zero-copy " + formatted + "[" + resource + "]"); } - return zeroCopyHttpOutputMessage.writeWith(file, pos, count); + return zeroCopyHttpOutputMessage.writeWith(filePath, pos, count); } - catch (IOException ex) { - // should not happen + catch (IOException ignore) { + // returning null below leads to fallback code path } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java index 2b12943e3a2..07c9345695b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java @@ -23,6 +23,7 @@ import java.net.URI; import java.net.URL; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -240,6 +241,11 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return this.encoded.getFile(); } + @Override + public Path getFilePath() throws IOException { + return this.encoded.getFilePath(); + } + @Override public InputStream getInputStream() throws IOException { return this.encoded.getInputStream(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java index 24f2f34efb6..860d055a0a0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java @@ -23,6 +23,7 @@ import java.net.URI; import java.net.URL; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -283,6 +284,11 @@ public class VersionResourceResolver extends AbstractResourceResolver { return this.original.getFile(); } + @Override + public Path getFilePath() throws IOException { + return this.original.getFilePath(); + } + @Override public InputStream getInputStream() throws IOException { return this.original.getInputStream(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java index 24779113504..0b80b3b40d8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java @@ -23,6 +23,7 @@ import java.net.URI; import java.net.URL; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -236,6 +237,11 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return this.encoded.getFile(); } + @Override + public Path getFilePath() throws IOException { + return this.encoded.getFilePath(); + } + @Override public InputStream getInputStream() throws IOException { return this.encoded.getInputStream(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index d521889f2f0..9786ffe9954 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -23,6 +23,7 @@ import java.net.URI; import java.net.URL; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -279,6 +280,11 @@ public class VersionResourceResolver extends AbstractResourceResolver { return this.original.getFile(); } + @Override + public Path getFilePath() throws IOException { + return this.original.getFilePath(); + } + @Override public InputStream getInputStream() throws IOException { return this.original.getInputStream();