Support java.nio.file Paths and FileSystems with nested jars

Add a `NestedFileSystemProvider` implementation so that the JDK's
`ZipFileSystem` can load content from nested jars and nested
directory entries.

Creating a `ZipFileSystem` may be a relatively expensive operation as
zip structures need to be parsed and in the case of directory entries
a virtual datablock nees to be generated on the fly. As such, we
install the `ZipFileSystem` as late as possible since in a typical
application it may never be needed.

This commit also tweaks Gradle and Maven plugins to ensure that the
service loader file is written to repackaged jars.

Closes gh-7161
This commit is contained in:
Phillip Webb
2023-10-18 14:57:05 -07:00
parent 4b495ca2a9
commit 3c62defb9d
21 changed files with 2115 additions and 5 deletions

View File

@@ -75,6 +75,19 @@ public record NestedLocation(Path path, String nestedEntryName) {
return parse(UrlDecoder.decode(url.getPath()));
}
/**
* Create a new {@link NestedLocation} from the given URI.
* @param uri the nested URI
* @return a new {@link NestedLocation} instance
* @throws IllegalArgumentException if the URI is not valid
*/
public static NestedLocation fromUri(URI uri) {
if (uri == null || !"nested".equalsIgnoreCase(uri.getScheme())) {
throw new IllegalArgumentException("'uri' must not be null and must use 'nested' scheme");
}
return parse(uri.getSchemeSpecificPart());
}
static NestedLocation parse(String path) {
if (path == null || path.isEmpty()) {
throw new IllegalArgumentException("'path' must not be empty");

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.ref.Cleaner.Cleanable;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Path;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
import org.springframework.boot.loader.ref.Cleaner;
import org.springframework.boot.loader.zip.CloseableDataBlock;
import org.springframework.boot.loader.zip.DataBlock;
import org.springframework.boot.loader.zip.ZipContent;
/**
* {@link SeekableByteChannel} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
class NestedByteChannel implements SeekableByteChannel {
private long position;
private final Resources resources;
private final Cleanable cleanup;
private final long size;
private volatile boolean closed;
NestedByteChannel(Path path, String nestedEntryName) throws IOException {
this(path, nestedEntryName, Cleaner.instance);
}
NestedByteChannel(Path path, String nestedEntryName, Cleaner cleaner) throws IOException {
this.resources = new Resources(path, nestedEntryName);
this.cleanup = cleaner.register(this, this.resources);
this.size = this.resources.getData().size();
}
@Override
public boolean isOpen() {
return !this.closed;
}
@Override
public void close() throws IOException {
if (this.closed) {
return;
}
this.closed = true;
try {
this.cleanup.clean();
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
@Override
public int read(ByteBuffer dst) throws IOException {
assertNotClosed();
int count = this.resources.getData().read(dst, this.position);
if (count > 0) {
this.position += count;
}
return count;
}
@Override
public int write(ByteBuffer src) throws IOException {
throw new NonWritableChannelException();
}
@Override
public long position() throws IOException {
assertNotClosed();
return this.position;
}
@Override
public SeekableByteChannel position(long position) throws IOException {
assertNotClosed();
if (position < 0 || position >= this.size) {
throw new IllegalArgumentException("Position must be in bounds");
}
this.position = position;
return this;
}
@Override
public long size() throws IOException {
assertNotClosed();
return this.size;
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw new NonWritableChannelException();
}
private void assertNotClosed() throws ClosedChannelException {
if (this.closed) {
throw new ClosedChannelException();
}
}
/**
* Resources used by the channel and suitable for registration with a {@link Cleaner}.
*/
static class Resources implements Runnable {
private final ZipContent zipContent;
private final CloseableDataBlock data;
Resources(Path path, String nestedEntryName) throws IOException {
this.zipContent = ZipContent.open(path, nestedEntryName);
this.data = this.zipContent.openRawZipData();
}
DataBlock getData() {
return this.data;
}
@Override
public void run() {
releaseAll();
}
private void releaseAll() {
IOException exception = null;
try {
this.data.close();
}
catch (IOException ex) {
exception = ex;
}
try {
this.zipContent.close();
}
catch (IOException ex) {
if (exception != null) {
ex.addSuppressed(exception);
}
exception = ex;
}
if (exception != null) {
throw new UncheckedIOException(exception);
}
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileStoreAttributeView;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
/**
* {@link FileStore} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
class NestedFileStore extends FileStore {
private final NestedFileSystem fileSystem;
NestedFileStore(NestedFileSystem fileSystem) {
this.fileSystem = fileSystem;
}
@Override
public String name() {
return this.fileSystem.toString();
}
@Override
public String type() {
return "nestedfs";
}
@Override
public boolean isReadOnly() {
return this.fileSystem.isReadOnly();
}
@Override
public long getTotalSpace() throws IOException {
return 0;
}
@Override
public long getUsableSpace() throws IOException {
return 0;
}
@Override
public long getUnallocatedSpace() throws IOException {
return 0;
}
@Override
public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
return getJarPathFileStore().supportsFileAttributeView(type);
}
@Override
public boolean supportsFileAttributeView(String name) {
return getJarPathFileStore().supportsFileAttributeView(name);
}
@Override
public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
return getJarPathFileStore().getFileStoreAttributeView(type);
}
@Override
public Object getAttribute(String attribute) throws IOException {
try {
return getJarPathFileStore().getAttribute(attribute);
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
protected FileStore getJarPathFileStore() {
try {
return Files.getFileStore(this.fileSystem.getJarPath());
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.IOException;
import java.net.URI;
import java.nio.file.ClosedFileSystemException;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.WatchService;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
/**
* {@link FileSystem} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
class NestedFileSystem extends FileSystem {
private static final Set<String> SUPPORTED_FILE_ATTRIBUTE_VIEWS = Set.of("basic");
private static final String FILE_SYSTEMS_CLASS_NAME = FileSystems.class.getName();
private static final Object EXISTING_FILE_SYSTEM = new Object();
private final NestedFileSystemProvider provider;
private final Path jarPath;
private volatile boolean closed;
private final Map<String, Object> zipFileSystems = new HashMap<>();
NestedFileSystem(NestedFileSystemProvider provider, Path jarPath) {
if (provider == null || jarPath == null) {
throw new IllegalArgumentException("Provider and JarPath must not be null");
}
this.provider = provider;
this.jarPath = jarPath;
}
void installZipFileSystemIfNecessary(String nestedEntryName) {
try {
boolean seen;
synchronized (this.zipFileSystems) {
seen = this.zipFileSystems.putIfAbsent(nestedEntryName, EXISTING_FILE_SYSTEM) != null;
}
if (!seen) {
URI uri = new URI("jar:nested:" + this.jarPath.toUri().getPath() + "/!" + nestedEntryName);
if (!hasFileSystem(uri)) {
FileSystem zipFileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
synchronized (this.zipFileSystems) {
this.zipFileSystems.put(nestedEntryName, zipFileSystem);
}
}
}
}
catch (Exception ex) {
// Ignore
}
}
private boolean hasFileSystem(URI uri) {
try {
FileSystems.getFileSystem(uri);
return true;
}
catch (FileSystemNotFoundException ex) {
return isCreatingNewFileSystem();
}
}
private boolean isCreatingNewFileSystem() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
if (stack != null) {
for (StackTraceElement element : stack) {
if (FILE_SYSTEMS_CLASS_NAME.equals(element.getClassName())) {
return "newFileSystem".equals(element.getMethodName());
}
}
}
return false;
}
@Override
public FileSystemProvider provider() {
return this.provider;
}
Path getJarPath() {
return this.jarPath;
}
@Override
public void close() throws IOException {
if (this.closed) {
return;
}
this.closed = true;
synchronized (this.zipFileSystems) {
this.zipFileSystems.values()
.stream()
.filter(FileSystem.class::isInstance)
.map(FileSystem.class::cast)
.forEach(this::closeZipFileSystem);
}
this.provider.removeFileSystem(this);
}
private void closeZipFileSystem(FileSystem zipFileSystem) {
try {
zipFileSystem.close();
}
catch (Exception ex) {
}
}
@Override
public boolean isOpen() {
return !this.closed;
}
@Override
public boolean isReadOnly() {
return true;
}
@Override
public String getSeparator() {
return "/!";
}
@Override
public Iterable<Path> getRootDirectories() {
assertNotClosed();
return Collections.emptySet();
}
@Override
public Iterable<FileStore> getFileStores() {
assertNotClosed();
return Collections.emptySet();
}
@Override
public Set<String> supportedFileAttributeViews() {
assertNotClosed();
return SUPPORTED_FILE_ATTRIBUTE_VIEWS;
}
@Override
public Path getPath(String first, String... more) {
assertNotClosed();
if (first == null || first.isBlank() || more.length != 0) {
throw new IllegalArgumentException("Nested paths must contain a single element");
}
return new NestedPath(this, first);
}
@Override
public PathMatcher getPathMatcher(String syntaxAndPattern) {
throw new UnsupportedOperationException("Nested paths do not support path matchers");
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService() {
throw new UnsupportedOperationException("Nested paths do not have a user principal lookup service");
}
@Override
public WatchService newWatchService() throws IOException {
throw new UnsupportedOperationException("Nested paths do not support the WacherService");
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
NestedFileSystem other = (NestedFileSystem) obj;
return this.jarPath.equals(other.jarPath);
}
@Override
public int hashCode() {
return this.jarPath.hashCode();
}
@Override
public String toString() {
return this.jarPath.toAbsolutePath().toString();
}
private void assertNotClosed() {
if (this.closed) {
throw new ClosedFileSystemException();
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.IOException;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.LinkOption;
import java.nio.file.NotDirectoryException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.ReadOnlyFileSystemException;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
/**
* {@link FileSystemProvider} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @since 3.2.0
*/
public class NestedFileSystemProvider extends FileSystemProvider {
private Map<Path, NestedFileSystem> fileSystems = new HashMap<>();
@Override
public String getScheme() {
return "nested";
}
@Override
public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
NestedLocation location = NestedLocation.fromUri(uri);
Path jarPath = location.path();
synchronized (this.fileSystems) {
if (this.fileSystems.containsKey(jarPath)) {
throw new FileSystemAlreadyExistsException();
}
NestedFileSystem fileSystem = new NestedFileSystem(this, location.path());
this.fileSystems.put(location.path(), fileSystem);
return fileSystem;
}
}
@Override
public FileSystem getFileSystem(URI uri) {
NestedLocation location = NestedLocation.fromUri(uri);
synchronized (this.fileSystems) {
NestedFileSystem fileSystem = this.fileSystems.get(location.path());
if (fileSystem == null) {
throw new FileSystemNotFoundException();
}
return fileSystem;
}
}
@Override
public Path getPath(URI uri) {
NestedLocation location = NestedLocation.fromUri(uri);
synchronized (this.fileSystems) {
NestedFileSystem fileSystem = this.fileSystems.computeIfAbsent(location.path(),
(path) -> new NestedFileSystem(this, path));
fileSystem.installZipFileSystemIfNecessary(location.nestedEntryName());
return fileSystem.getPath(location.nestedEntryName());
}
}
void removeFileSystem(NestedFileSystem fileSystem) {
synchronized (this.fileSystems) {
this.fileSystems.remove(fileSystem.getJarPath());
}
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
throws IOException {
NestedPath nestedPath = NestedPath.cast(path);
return new NestedByteChannel(nestedPath.getJarPath(), nestedPath.getNestedEntryName());
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
throw new NotDirectoryException(NestedPath.cast(dir).toString());
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void delete(Path path) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
return path.equals(path2);
}
@Override
public boolean isHidden(Path path) throws IOException {
return false;
}
@Override
public FileStore getFileStore(Path path) throws IOException {
NestedPath nestedPath = NestedPath.cast(path);
nestedPath.assertExists();
return new NestedFileStore(nestedPath.getFileSystem());
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
Path jarPath = getJarPath(path);
jarPath.getFileSystem().provider().checkAccess(jarPath, modes);
}
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
Path jarPath = getJarPath(path);
return jarPath.getFileSystem().provider().getFileAttributeView(jarPath, type, options);
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
throws IOException {
Path jarPath = getJarPath(path);
return jarPath.getFileSystem().provider().readAttributes(jarPath, type, options);
}
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
Path jarPath = getJarPath(path);
return jarPath.getFileSystem().provider().readAttributes(jarPath, attributes, options);
}
protected Path getJarPath(Path path) {
return NestedPath.cast(path).getJarPath();
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.IOError;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Objects;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
import org.springframework.boot.loader.zip.ZipContent;
/**
* {@link Path} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
final class NestedPath implements Path {
private final NestedFileSystem fileSystem;
private final String nestedEntryName;
private volatile Boolean entryExists;
NestedPath(NestedFileSystem fileSystem, String nestedEntryName) {
if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) {
throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required");
}
this.fileSystem = fileSystem;
this.nestedEntryName = nestedEntryName;
}
Path getJarPath() {
return this.fileSystem.getJarPath();
}
String getNestedEntryName() {
return this.nestedEntryName;
}
@Override
public NestedFileSystem getFileSystem() {
return this.fileSystem;
}
@Override
public boolean isAbsolute() {
return true;
}
@Override
public Path getRoot() {
return null;
}
@Override
public Path getFileName() {
return this;
}
@Override
public Path getParent() {
return null;
}
@Override
public int getNameCount() {
return 1;
}
@Override
public Path getName(int index) {
if (index != 0) {
throw new IllegalArgumentException("Nested paths only have a single element");
}
return this;
}
@Override
public Path subpath(int beginIndex, int endIndex) {
if (beginIndex != 0 || endIndex != 1) {
throw new IllegalArgumentException("Nested paths only have a single element");
}
return this;
}
@Override
public boolean startsWith(Path other) {
return equals(other);
}
@Override
public boolean endsWith(Path other) {
return equals(other);
}
@Override
public Path normalize() {
return this;
}
@Override
public Path resolve(Path other) {
throw new UnsupportedOperationException("Unable to resolve nested path");
}
@Override
public Path relativize(Path other) {
throw new UnsupportedOperationException("Unable to relativize nested path");
}
@Override
public URI toUri() {
try {
String jarFilePath = this.fileSystem.getJarPath().toUri().getPath();
return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName);
}
catch (URISyntaxException ex) {
throw new IOError(ex);
}
}
@Override
public Path toAbsolutePath() {
return this;
}
@Override
public Path toRealPath(LinkOption... options) throws IOException {
return this;
}
@Override
public WatchKey register(WatchService watcher, Kind<?>[] events, Modifier... modifiers) throws IOException {
throw new UnsupportedOperationException("Nested paths cannot be watched");
}
@Override
public int compareTo(Path other) {
NestedPath otherNestedPath = cast(other);
return this.nestedEntryName.compareTo(otherNestedPath.nestedEntryName);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
NestedPath other = (NestedPath) obj;
return Objects.equals(this.fileSystem, other.fileSystem)
&& Objects.equals(this.nestedEntryName, other.nestedEntryName);
}
@Override
public int hashCode() {
return Objects.hash(this.fileSystem, this.nestedEntryName);
}
@Override
public String toString() {
return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName;
}
void assertExists() throws NoSuchFileException {
if (!Files.isRegularFile(getJarPath())) {
throw new NoSuchFileException(toString());
}
Boolean entryExists = this.entryExists;
if (entryExists == null) {
try {
try (ZipContent content = ZipContent.open(getJarPath(), this.nestedEntryName)) {
entryExists = true;
}
}
catch (IOException ex) {
entryExists = false;
}
this.entryExists = entryExists;
}
if (!entryExists) {
throw new NoSuchFileException(toString());
}
}
static NestedPath cast(Path path) {
if (path instanceof NestedPath nestedPath) {
return nestedPath;
}
throw new ProviderMismatchException();
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Non-blocking IO {@link java.nio.file.FileSystem} implementation for nested suppoprt.
*/
package org.springframework.boot.loader.nio.file;

View File

@@ -0,0 +1 @@
org.springframework.boot.loader.nio.file.NestedFileSystemProvider