在图像中加载后无法删除外部 JAR 文件

Cannot delete external JAR file after loading in Image

我目前正忙于构建一个简单的 javafx 应用程序。在此应用程序中,您可以在应用程序中添加和删除 .jar 文件,我正在从中收集一些信息。我从这些 .jar 文件中使用的其中一件事是存储为图像的纹理。

每当我使用 jar 文件中的路径加载图像时,我都无法在以后删除此 .jar 文件。这是非常合乎逻辑的,因为 .jar 文件现在正被此图像使用,但出于某种原因,即使删除对此图像的所有引用并调用垃圾收集器,我也无法从应用程序中删除 .jar 文件。

我目前尝试删除文件的方式如下:

File file = new File("mods/file.jar");
file.delete();

在应用程序的某个时刻,我按如下方式初始化图像:

Image block = new Image(pathToJar);

我尝试了以下方法来解决我的问题:

目前,我不太清楚为什么我无法真正删除 .jar 文件。谁能向我解释为什么会这样以及如何解决?非常感谢!

编辑:申请的目的不够明确。我正在制作一个颜色选择器,它从 .jar 文件(Minecraft mods)中获取纹理的平均颜色。这些.jar 文件将以灵活的方式从应用程序中添加和删除。一旦您不想再考虑某个 .jar 文件的纹理,您只需将其从应用程序中删除并完成处理即可。为了便于使用,我制作了 .jar 文件的副本,以便我可以相对地访问它们。完成 .jar 文件后,我想删除此副本,因为它只会占用不必要的存储空间。

我已将问题(到目前为止)缩小到图像的初始化。根据要求,这里是 MRE:

public class example extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        // Read image, needed at some point in my code
        Image block = new Image("jar:File:./mods/botania.jar!/assets/botania/textures/blocks/alfheim_portal.png");

        // Make it so that jar is not used anymore
        block.cancel();
        block = null;
        System.gc();

        // Try to delete the file after being done with it
        File delete = new File("mods/botania.jar");
        Files.delete(delete.toPath());
    }
}

在初始化图像并删除对它的引用后,它应该由垃圾收集器清理(至少据我所知)。相反,现在它只给出以下错误:

Caused by: java.nio.file.FileSystemException: mods\botania.jar: The process cannot access the file because it is being used by another process.

我无法在我的系统上重现该问题(Java 14 on Mac OS X);但是 运行 它在 Windows 10 上具有相同的 JDK/JavaFX 版本会产生您描述的异常。

问题似乎是(请参阅 , and hat-tip to @matt 来挖掘)默认情况下,jar: URL 的 URL 处理程序缓存对底层 JarFile 对象(并且不调用 close(),即使从中获得的 InputStream 已关闭)。由于 Windows 在这些情况下保留文件句柄,因此底层 OS 无法删除 jar 文件。

我认为解决这个问题最自然的方法是使用 java.util.jar API(或者只是简单的 java.util.zip API),这会让你走得更远比生成 URL 并将 URL 传递给 Image 构造函数更多地控制关闭资源。

这是一个使用这种方法的独立示例,它适用于我的 Mac 和我的 Windows 10 虚拟机:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import javax.imageio.ImageIO;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class App extends Application {
    
    @Override
    public void init() throws Exception {
        // create an image in a jar file:
        
        BufferedImage image = new BufferedImage(100, 100, ColorSpace.TYPE_RGB);
        Graphics graphics = image.getGraphics();
        graphics.setColor(new Color(0xd5, 0x5e, 0x00));
        graphics.fillRect(0, 0, 100, 100);
        graphics.setColor(new Color(0x35, 0x9b, 0x73));
        graphics.fillOval(25, 25, 50, 50);

        JarOutputStream out = new JarOutputStream(new FileOutputStream("test.jar"));
        ZipEntry entry = new ZipEntry("test.png");
        out.putNextEntry(entry);
        ImageIO.write(image, "png", out);
        out.close();
    }

    @Override
    public void start(Stage stage) throws Exception {
                
        assert Files.exists(Paths.get("test.jar")) : "Jar file not created";

        JarFile jarFile = new JarFile("test.jar");
        JarEntry imgEntry = jarFile.getJarEntry("test.png");
        InputStream inputStream = jarFile.getInputStream(imgEntry);
        Image img = new Image(inputStream);
        jarFile.close();
        
//      The following line (instead of the previous five lines) works on Mac OS X, 
//      but not on Windows
//      
//      Image img = new Image("jar:file:test.jar!/test.png");
        
        Files.delete(Paths.get("test.jar"));
        
        assert ! Files.exists(Paths.get("test.jar")) : "Jar file not deleted";
        
        ImageView iview = new ImageView(img);
        Scene scene = new Scene(new BorderPane(iview), 200, 200);
        stage.setScene(scene);
        stage.show();
        
    }

    public static void main(String[] args) {
        launch();
    }

}