使用org.springframework.util.FileSystemUtils#deleteRecursively
public abstract class FilesUtils {
public static void delFilesSpring(String filePath){
try {
Path path = Paths.get(filePath);
FileSystemUtils.deleteRecursively(path);
} catch (IOException e) {
e.printStackTrace();
}
}
}
发现过程小记
最近在开发过程中使用了一个功能需要删除一个本地的文件夹。最开始毫不犹豫的使用了这个jdk中的这个方法java.nio.file.Files#deleteIfExists
。很快写完了。
public abstract class FilesUtils {
public static void delFilesJdk(String filePath){
try {
Path path = Paths.get(filePath);
Files.deleteIfExists(path);
} catch (IOException e) {
e.printStackTrace();
}
}
}
但是很不幸被无情的打脸了。测试直接报错。
java.nio.file.DirectoryNotEmptyException: D:\test
at sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:266)
at sun.nio.fs.AbstractFileSystemProvider.deleteIfExists(AbstractFileSystemProvider.java:108)
at java.nio.file.Files.deleteIfExists(Files.java:1165)
at com.hyl.light.util.FilesUtils.delFilesJdk(FilesUtils.java:31)
at com.hyl.light.utils.FileDelTest.jdkDel(FileDelTest.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
然后又仔细看了一下源码
/**
* Deletes a file if it exists.
*
* <p> As with the {@link #delete(Path) delete(Path)} method, an
* implementation may need to examine the file to determine if the file is a
* directory. Consequently this method may not be atomic with respect to
* other file system operations. If the file is a symbolic link, then the
* symbolic link itself, not the final target of the link, is deleted.
*
* <p> If the file is a directory then the directory must be empty. In some
* implementations a directory has entries for special files or links that
* are created when the directory is created. In such implementations a
* directory is considered empty when only the special entries exist.
*
* <p> On some operating systems it may not be possible to remove a file when
* it is open and in use by this Java virtual machine or other programs.
*
* @param path
* the path to the file to delete
*
* @return {@code true} if the file was deleted by this method; {@code
* false} if the file could not be deleted because it did not
* exist
*
* @throws DirectoryNotEmptyException
* if the file is a directory and could not otherwise be deleted
* because the directory is not empty <i>(optional specific
* exception)</i>
* @throws IOException
* if an I/O error occurs
* @throws SecurityException
* In the case of the default provider, and a security manager is
* installed, the {@link SecurityManager#checkDelete(String)} method
* is invoked to check delete access to the file.
*/
public static boolean deleteIfExists(Path path) throws IOException {
return provider(path).deleteIfExists(path);
}
源码很明显的指出了这里会抛出一个异常DirectoryNotEmptyException
。大概意思就是如果是文件夹,同时文件夹内不是空的就会抛出这个异常。
GG。问题想的太简单了。
网上搜索了一波,想想有没有大佬已经遇见过类似的问题了。果然一波搜索就出来。大概的思路就是递归去查找文件夹下的目录一一删除。具体的代码网上有很多。不做说明了。
但是不死心的我,同时作为spring的脑残粉,我的直觉告诉我spring中肯定也有同样的问题。他们是怎么解决的呢?很开心,也很容易,我在spring找到了。
org.springframework.util.FileSystemUtils#deleteRecursively
/**
* Delete the supplied {@link File} - for directories,
* recursively delete any nested directories or files as well.
* @param root the root {@code File} to delete
* @return {@code true} if the {@code File} existed and was deleted,
* or {@code false} it it did not exist
* @throws IOException in the case of I/O errors
* @since 5.0
*/
public static boolean deleteRecursively(@Nullable Path root) throws IOException {
if (root == null) {
return false;
}
if (!Files.exists(root)) {
return false;
}
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
return true;
}
这里spring直接使用了jdk的Files.walkFileTree
同时重写了SimpleFileVisitor
的两个方法visitFile
、postVisitDirectory
。
原来是这样的:
/**
* A simple visitor of files with default behavior to visit all files and to
* re-throw I/O errors.
*
* <p> Methods in this class may be overridden subject to their general contract.
*
* @param <T> The type of reference to the files
*
* @since 1.7
*/
public class SimpleFileVisitor<T> implements FileVisitor<T> {
/**
* Initializes a new instance of this class.
*/
protected SimpleFileVisitor() {
}
/**
* Invoked for a directory before entries in the directory are visited.
*
* <p> Unless overridden, this method returns {@link FileVisitResult#CONTINUE
* CONTINUE}.
*/
@Override
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(dir);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
/**
* Invoked for a file in a directory.
*
* <p> Unless overridden, this method returns {@link FileVisitResult#CONTINUE
* CONTINUE}.
*/
@Override
public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(file);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
/**
* Invoked for a file that could not be visited.
*
* <p> Unless overridden, this method re-throws the I/O exception that prevented
* the file from being visited.
*/
@Override
public FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException
{
Objects.requireNonNull(file);
throw exc;
}
/**
* Invoked for a directory after entries in the directory, and all of their
* descendants, have been visited.
*
* <p> Unless overridden, this method returns {@link FileVisitResult#CONTINUE
* CONTINUE} if the directory iteration completes without an I/O exception;
* otherwise this method re-throws the I/O exception that caused the iteration
* of the directory to terminate prematurely.
*/
@Override
public FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException
{
Objects.requireNonNull(dir);
if (exc != null)
throw exc;
return FileVisitResult.CONTINUE;
}
}
原始的并没有任何操作。但是spring中的工具类重写过后提供了delete操作。
/**
* Walks a file tree.
*
* <p> This method works as if invoking it were equivalent to evaluating the
* expression:
* <blockquote><pre>
* walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor)
* </pre></blockquote>
* In other words, it does not follow symbolic links, and visits all levels
* of the file tree.
*
* @param start
* the starting file
* @param visitor
* the file visitor to invoke for each file
*
* @return the starting file
*
* @throws SecurityException
* If the security manager denies access to the starting file.
* In the case of the default provider, the {@link
* SecurityManager#checkRead(String) checkRead} method is invoked
* to check read access to the directory.
* @throws IOException
* if an I/O error is thrown by a visitor method
*/
public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
throws IOException
{
return walkFileTree(start,
EnumSet.noneOf(FileVisitOption.class),
Integer.MAX_VALUE,
visitor);
}
/**
* Walks a file tree.
*
* <p> This method walks a file tree rooted at a given starting file. The
* file tree traversal is <em>depth-first</em> with the given {@link
* FileVisitor} invoked for each file encountered. File tree traversal
* completes when all accessible files in the tree have been visited, or a
* visit method returns a result of {@link FileVisitResult#TERMINATE
* TERMINATE}. Where a visit method terminates due an {@code IOException},
* an uncaught error, or runtime exception, then the traversal is terminated
* and the error or exception is propagated to the caller of this method.
*
* <p> For each file encountered this method attempts to read its {@link
* java.nio.file.attribute.BasicFileAttributes}. If the file is not a
* directory then the {@link FileVisitor#visitFile visitFile} method is
* invoked with the file attributes. If the file attributes cannot be read,
* due to an I/O exception, then the {@link FileVisitor#visitFileFailed
* visitFileFailed} method is invoked with the I/O exception.
*
* <p> Where the file is a directory, and the directory could not be opened,
* then the {@code visitFileFailed} method is invoked with the I/O exception,
* after which, the file tree walk continues, by default, at the next
* <em>sibling</em> of the directory.
*
* <p> Where the directory is opened successfully, then the entries in the
* directory, and their <em>descendants</em> are visited. When all entries
* have been visited, or an I/O error occurs during iteration of the
* directory, then the directory is closed and the visitor's {@link
* FileVisitor#postVisitDirectory postVisitDirectory} method is invoked.
* The file tree walk then continues, by default, at the next <em>sibling</em>
* of the directory.
*
* <p> By default, symbolic links are not automatically followed by this
* method. If the {@code options} parameter contains the {@link
* FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then symbolic links are
* followed. When following links, and the attributes of the target cannot
* be read, then this method attempts to get the {@code BasicFileAttributes}
* of the link. If they can be read then the {@code visitFile} method is
* invoked with the attributes of the link (otherwise the {@code visitFileFailed}
* method is invoked as specified above).
*
* <p> If the {@code options} parameter contains the {@link
* FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then this method keeps
* track of directories visited so that cycles can be detected. A cycle
* arises when there is an entry in a directory that is an ancestor of the
* directory. Cycle detection is done by recording the {@link
* java.nio.file.attribute.BasicFileAttributes#fileKey file-key} of directories,
* or if file keys are not available, by invoking the {@link #isSameFile
* isSameFile} method to test if a directory is the same file as an
* ancestor. When a cycle is detected it is treated as an I/O error, and the
* {@link FileVisitor#visitFileFailed visitFileFailed} method is invoked with
* an instance of {@link FileSystemLoopException}.
*
* <p> The {@code maxDepth} parameter is the maximum number of levels of
* directories to visit. A value of {@code 0} means that only the starting
* file is visited, unless denied by the security manager. A value of
* {@link Integer#MAX_VALUE MAX_VALUE} may be used to indicate that all
* levels should be visited. The {@code visitFile} method is invoked for all
* files, including directories, encountered at {@code maxDepth}, unless the
* basic file attributes cannot be read, in which case the {@code
* visitFileFailed} method is invoked.
*
* <p> If a visitor returns a result of {@code null} then {@code
* NullPointerException} is thrown.
*
* <p> When a security manager is installed and it denies access to a file
* (or directory), then it is ignored and the visitor is not invoked for
* that file (or directory).
*
* @param start
* the starting file
* @param options
* options to configure the traversal
* @param maxDepth
* the maximum number of directory levels to visit
* @param visitor
* the file visitor to invoke for each file
*
* @return the starting file
*
* @throws IllegalArgumentException
* if the {@code maxDepth} parameter is negative
* @throws SecurityException
* If the security manager denies access to the starting file.
* In the case of the default provider, the {@link
* SecurityManager#checkRead(String) checkRead} method is invoked
* to check read access to the directory.
* @throws IOException
* if an I/O error is thrown by a visitor method
*/
public static Path walkFileTree(Path start,
Set<FileVisitOption> options,
int maxDepth,
FileVisitor<? super Path> visitor)
throws IOException
{
/**
* Create a FileTreeWalker to walk the file tree, invoking the visitor
* for each event.
*/
try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) {
FileTreeWalker.Event ev = walker.walk(start);
do {
FileVisitResult result;
switch (ev.type()) {
case ENTRY :
IOException ioe = ev.ioeException();
if (ioe == null) {
assert ev.attributes() != null;
result = visitor.visitFile(ev.file(), ev.attributes());
} else {
result = visitor.visitFileFailed(ev.file(), ioe);
}
break;
case START_DIRECTORY :
result = visitor.preVisitDirectory(ev.file(), ev.attributes());
// if SKIP_SIBLINGS and SKIP_SUBTREE is returned then
// there shouldn't be any more events for the current
// directory.
if (result == FileVisitResult.SKIP_SUBTREE ||
result == FileVisitResult.SKIP_SIBLINGS)
walker.pop();
break;
case END_DIRECTORY :
result = visitor.postVisitDirectory(ev.file(), ev.ioeException());
// SKIP_SIBLINGS is a no-op for postVisitDirectory
if (result == FileVisitResult.SKIP_SIBLINGS)
result = FileVisitResult.CONTINUE;
break;
default :
throw new AssertionError("Should not get here");
}
if (Objects.requireNonNull(result) != FileVisitResult.CONTINUE) {
if (result == FileVisitResult.TERMINATE) {
break;
} else if (result == FileVisitResult.SKIP_SIBLINGS) {
walker.skipRemainingSiblings();
}
}
ev = walker.next();
} while (ev != null);
}
return start;
}
这样Files#walkFileTree
就拥有了删除功能。就可以使用jdk中的这个方法去做文件的查找和删除。
迅速另外写了一个方法测试一波。
public abstract class FilesUtils {
public static void delFilesSpring(String filePath){
try {
Path path = Paths.get(filePath);
FileSystemUtils.deleteRecursively(path);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class FileDelTest {
String filePath = "D:\\test";
@Test
public void springDel(){
FilesUtils.delFilesSpring(filePath);
}
}
测试通过完美。果然‘spring’大佬还是很专业的。
ps:我使用的是Spring-5.1.6.RELEASE
版本。
补充一个 RocketMQ 中的做法,递归删除。
org.apache.rocketmq.common.UtilAll#deleteFile
public static void deleteFile(File file) {
if (!file.exists()) {
return;
}
if (file.isFile()) {
file.delete();
} else if (file.isDirectory()) {
File[] files = file.listFiles();
for (File file1 : files) {
deleteFile(file1);
}
file.delete();
}
}