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

huangapple go评论82阅读模式
英文:

Cannot delete external JAR file after loading in Image

问题

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

每当我使用来自jar文件的路径加载图像时,后来我无法删除这个.jar文件。这是很合理的,因为.jar文件现在正在被这个图像使用,但由于某种原因,即使在删除对这个图像的所有引用并调用垃圾收集器后,我也无法从应用程序内部删除.jar文件。

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

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

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

Image block = new Image(pathToJar);

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

  • 设置 block = null; 并调用 System.gc();
  • 调用 Image.cancel() 并调用 System.gc();
  • 我还尝试了与 System.runFinalization(); 结合使用上述方法
  • 我尝试在舞台关闭时删除.jar文件,希望所有内容都会被卸载,但没有成功。

目前,我不太确定为什么无法实际删除.jar文件。有人能否解释一下为什么会这样,并如何解决这个问题?非常感谢!

英文:

I am currently busy with building a simple javafx application. In this application, you can add and remove .jar files from within the application, from which I am gathering some information. One of the things I use from these .jar files are textures stored as images.

Whenever I load an Image using a path from the jar file, I am unable to remove this .jar file later on. This is quite logical as the .jar file is now being used by this Image, but for some reason, I am unable to delete the .jar file from within the application even when removing all references to this Image and calling the garbage collector.

The way I am currently trying to delete the file is the following:

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

At some point in the application I initialize the image as follows:

Image block = new Image(pathToJar);

I have tried the following things to resolve my problem:

  • Set block = null; and call System.gc();
  • Call Image.cancel() and call System.gc();
  • I have also tried the above in combination with System.runFinalization();
  • I tried removing the .jar file when the stage is closing in hopes of everything being unloaded, but without success.

At this point in time, I am not quite sure why I am unable to actually delete the .jar file. Could anyone possibly explain to me why this is the case and how it could be resolved? Many thanks in advance!

EDIT: the purpose of the application was not clear enough. I am making a color picker that takes average colors of textures from .jar files (Minecraft mods). These .jar files are to be added and deleted from the application in a flexible way. Once you do not want to consider textures of a certain .jar file anymore, you just remove it from the application and be done with it. For ease of use, I made copies of the .jar files so I can access them relatively. Once you are done with the .jar file I want to remove this copy as it will just take in unnecessary storage.

I have narrowed the problem (thus far) down to the initialization of an Image. On request, here a 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());
    }
}

After initializing the image, and thereafter removing references to it, it should be cleaned by the garbage collector (at least to my knowledge). Instead, now it just gives the following error:

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

答案1

得分: 4

无法在我的系统上重现此问题(在Mac OS X上使用Java 14);然而,在Windows 10上运行相同的JDK/JavaFX版本会产生您描述的异常。

问题似乎是(请参阅https://stackoverflow.com/a/54777849/2067492,并向@matt表示感谢,他挖掘出了这一点),默认情况下,jar: URL的URL处理程序缓存对底层JarFile对象的访问(即使从中获取的InputStream已关闭,也不会调用close())。由于Windows在这些情况下保持文件句柄,底层操作系统无法删除jar文件。

我认为解决这个问题的最自然的方式是使用java.util.jar API(或者只是普通的java.util.zip API),它将允许您更好地控制关闭资源,而不是生成URL并将URL传递给Image构造函数。

以下是使用这种方法的自包含示例,在我的Mac和Windows 10虚拟VM上都有效:

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 {
        // 在一个jar文件中创建一张图片:

        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文件未创建";

        JarFile jarFile = new JarFile("test.jar");
        JarEntry imgEntry = jarFile.getJarEntry("test.png");
        InputStream inputStream = jarFile.getInputStream(imgEntry);
        Image img = new Image(inputStream);
        jarFile.close();

        // 以下行(而不是前面的五行)在Mac OS X上工作,但在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文件未删除";

        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();
    }

}
英文:

I can't reproduce the issue on my system (Java 14 on Mac OS X); however running it on Windows 10 with the same JDK/JavaFX versions produces the exception you describe.

The issue appears to be (see https://stackoverflow.com/a/54777849/2067492, and hat-tip to @matt for digging that out) that by default the URL handler for a jar: URL caches access to the underlying JarFile object (and doesn't call close(), even if an InputStream obtained from it is closed). Since Windows holds on to file handles in those situations, the underlying OS is unable to delete the jar file.

I think the most natural way to address this is to use the java.util.jar API (or just the plain java.util.zip API), which will give you far more control over closing resources than generating a URL and passing the URL to the Image constructor.

Here's a self-contained example using this approach, which works both on my Mac and on my Windows 10 virtual VM:

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();
}
}

huangapple
  • 本文由 发表于 2020年7月21日 18:46:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/63012824.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定