首页 > 解决方案 > 无法删除文件,因为它已被另一个进程使用,即使 Stream 已关闭

问题描述

我有一个负责序列化/加密和解密/反序列化给定对象的类。

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class SerializingUtils {
    public static void serialize(User user, String password, String filePath) {
        SecretKey key = makeKey(password);

        try {
            Cipher cipher = Cipher.getInstance("Blowfish");
            cipher.init(Cipher.ENCRYPT_MODE, key);

            SealedObject sealedObject = new SealedObject(user, cipher);

            ObjectOutputStream stream = createOutputStream(filePath, cipher);

            stream.writeObject(sealedObject);
            stream.close();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    public static User deserialize(String filePath, String password) throws StreamCorruptedException, Exception {
        SecretKey key = makeKey(password);

        Cipher cipher = Cipher.getInstance("Blowfish");
        cipher.init(Cipher.DECRYPT_MODE, key);

        ObjectInputStream stream = createInputStream(filePath, cipher);

        SealedObject sealedObject = (SealedObject) stream.readObject();

        stream.close();

        return (User) sealedObject.getObject(cipher);
    }

    private static SecretKey makeKey(String password) {
        return new SecretKeySpec(password.getBytes(StandardCharsets.UTF_8), "Blowfish");
    }

    private static ObjectOutputStream createOutputStream(String filePath, Cipher cipher) throws IOException {
        FileOutputStream fileStream = new FileOutputStream(filePath);
        BufferedOutputStream bufferedStream = new BufferedOutputStream(fileStream);
        CipherOutputStream cipherStream = new CipherOutputStream(bufferedStream, cipher);
        return new ObjectOutputStream(cipherStream);
    }

    private static ObjectInputStream createInputStream(String filePath, Cipher cipher) throws Exception {
        FileInputStream fileStream = new FileInputStream(filePath);
        BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
        CipherInputStream cipherStream = new CipherInputStream(bufferedStream, cipher);
        return new ObjectInputStream(cipherStream);
    }
}

它工作得很好。我尝试为它编写一些测试:

public class SerializingUtilsTest {
    private String path;
    private User user;
    private String password;

    @Before
    public void setUp() {
        path = FileUtils.getAppFolder() + File.separator + "test_file.pman";
        user = new User("Josh");
        password = "drAkE";

        SerializingUtils.serialize(user, password, path);
    }

    @Test
    public void shouldThrowExceptionIfPasswordIsWrong() {
        assertThrows(StreamCorruptedException.class, () -> {
            SerializingUtils.deserialize(path, "drake");
        });
    }

    @Test
    public void shouldReturnAUserWithCorrectName() throws Exception {
        User obtainedUser = SerializingUtils.deserialize(path, password);

        assertEquals(user.getName(), obtainedUser.getName());
    }
}

这几乎是一个集成测试:它调用实际方法并测试用户输入正确/错误密码时会发生什么。测试也运行良好。但是后来我尝试实现一个拆卸方法来在测试完成后删除创建的文件:

@After
public void tearDown() {
    Path filePath = Paths.get(path);

    try {
        Files.delete(filePath);
    } catch (IOException exception) {
        exception.printStackTrace();
    }
}

但是,tear down 方法在被调用时会抛出此异常(2 次):

java.nio.file.FileSystemException: C:\dev\passman\test_file.pman: The file is already being used by another process
    at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
    at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
    at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
    at java.base/java.nio.file.Files.delete(Files.java:1146)
    at com.passman.utils.SerializingUtilsTest.tearDown(SerializingUtilsTest.java:49)

也许FileStreams 用于构建ObjectOutputStream并且ObjectInputStream没有被关闭,即使我呼吁close他们。我想知道是否bufferedStreamcipherStream保持开放。无论如何,这里真正的问题是,我怎样才能确保里面的所有流在createOutputStream()createInputStream()不再需要它们时关闭?我可以在将流作为参数传递给另一种类型的流后立即关闭它,还是它仍然被引用使用?

我的代码的依赖关系,如果有人想测试整个事情:

// User.java
public class User implements Serializable {
    private final String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
// FileUtils.java
public class FileUtils {
    public static String getAppFolder() {
        try {
            return new File(".").getCanonicalPath();
        } catch (IOException exception) {
            return null;
        }
    }
}

标签: java

解决方案


我猜如果在打开和关闭之间抛出异常,您的流将保持打开状态。

 public static User deserialize(String filePath, String password) throws StreamCorruptedException, Exception {
            SecretKey key = makeKey(password);
    
            Cipher cipher = Cipher.getInstance("Blowfish");
            cipher.init(Cipher.DECRYPT_MODE, key);
    
// stream opens here
            ObjectInputStream stream = createInputStream(filePath, cipher);
// something goes wrong here  and exception is thrown  
            SealedObject sealedObject = (SealedObject) stream.readObject();
// this doesn't happen...    
            stream.close();
    
            return (User) sealedObject.getObject(cipher);
}

- - - 尝试这个 - - -

try(var stream = createInputStream(filePath, cipher)) {
// read your object and process it. this is a try-with-resources clause so it auto-closes your stream.
} catch (Exception ex) {
// handle exceptions here (or re-throw them)
}

推荐阅读