Java 向正在运行的Jar写入

Java 向正在运行的Jar写入,java,jar,Java,Jar,我正在尝试创建一个Jar文件,该文件包含Jar中的数据,该数据在程序执行之间持续存在。我知道还有其他保存数据的方法,但我选择的是一个完全自包含的Jar文件 我有一些似乎有效的方法,我希望你们能对我的方法中的任何漏洞给予反馈,因为我觉得有些地方很粗糙。我现在做的是: Jar包含一个文件saves.txt。此文件包含一位数字:开始时为1 当Jar运行时,它读取该文件(使用getResourceAsStream)并向用户显示该数字 然后,我使用ZipFile API将整个jar提取到一个临时目录中

我正在尝试创建一个Jar文件,该文件包含Jar中的数据,该数据在程序执行之间持续存在。我知道还有其他保存数据的方法,但我选择的是一个完全自包含的Jar文件

我有一些似乎有效的方法,我希望你们能对我的方法中的任何漏洞给予反馈,因为我觉得有些地方很粗糙。我现在做的是:

  • Jar包含一个文件saves.txt。此文件包含一位数字:开始时为1
  • 当Jar运行时,它读取该文件(使用getResourceAsStream)并向用户显示该数字
  • 然后,我使用ZipFile API将整个jar提取到一个临时目录中
  • 既然saves.txt是一个文件(不是资源),我就增加这个数字并覆盖临时目录中的文件
  • 然后我使用JarOutputStream将临时目录(包括更新的saves.txt文件)的内容重新压缩回同一个jar中
  • 最后,我删除临时目录,程序退出。当用户再次运行jar时,saves.txt文件已经更新
我做这些繁琐的工作是为了避开这个bug,在这个bug中,你不能只更新jar中的一个文件:

我没有使用jar-u命令,因为我不想要求最终用户拥有JDK

我可以看出这方面存在一些问题,但对我来说,没有一个是交易破坏者:

  • 这对签名JAR不起作用。这对我来说不是问题,因为这些罐子无论如何都不会签名
  • 如果jar位于一个没有写权限的目录中,这将不起作用。我可以警告用户这一点
  • 如果jar在运行程序时在存档资源管理器(如7zip)中打开,这将不起作用。我可以通过简单地向用户显示一条消息,要求他们关闭它来解决这个问题
  • 这使得部署新版本的jar变得很烦人,因为保存在jar中。用户要么以某种方式从旧jar加载数据,要么开始清理。我也同意
因此,我的问题是:上述任何一项看起来特别可怕吗?如果是,为什么

编辑:我应该注意,我并不是在问这种方法是标准的还是令人困惑的,我是在问代码。是否有任何原因导致代码无法工作

“在运行jar时重写jar的内容”这一步是最让我紧张的一步,但我找不到任何文档说明您不能,甚至不应该这样做。如果我在程序中间做了这件事,那么可能会发生坏事,但这只会发生在程序的末尾。覆盖jar是程序的最后一行

最重要的是,这一切似乎都起作用了!我已经在Windows7、Windows8和Mac上测试过了。因此,如果有人知道任何文档,或者能想到这种代码不起作用的极端情况,我一定会非常感激

这是一个MCVE,显示了上述所有内容。为了实现这一点,您必须将该类编译成一个可运行的jar文件以及一个包含单个数字的saves.txt文件

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.swing.JOptionPane;

public class JarTest {

    static List<File> extractedFiles = new ArrayList<File>();

    public static void unzipWholeJar() throws IOException{

        File jarFile = new File(JarTest.class.getProtectionDomain().getCodeSource().getLocation().getPath());
        ZipFile jarZipFile = new ZipFile(jarFile);

        Enumeration<? extends ZipEntry> entities = jarZipFile.entries();

        while (entities.hasMoreElements()) {
            ZipEntry entry = (ZipEntry)entities.nextElement();

            if(!entry.isDirectory()){

                InputStream in = jarZipFile.getInputStream(jarZipFile.getEntry(entry.getName()));

                File file = new File("extracted/" + entry.getName());

                extractedFiles.add(file);

                File parent = new File(file.getParent());
                parent.mkdirs();

                OutputStream out = new FileOutputStream(file);

                byte[] buffer = new byte[65536];
                int bufferSize;
                while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1){
                    out.write(buffer, 0, bufferSize);
                }

                in.close();
                out.close();
            }
        }

        jarZipFile.close();
    }

    public static void rezipExtractedFiles() throws FileNotFoundException, IOException{

        File jarFile = new File(JarTest.class.getProtectionDomain().getCodeSource().getLocation().getPath());
        JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(jarFile)));

        for(File f : extractedFiles){
            String absPath = f.getAbsolutePath().replaceAll("\\\\", "/");
            String fileInJar = absPath.substring(absPath.indexOf("extracted") + 10);
            addFile(jos, fileInJar, f);
        }

        jos.close();
    }

    public static void addFile(JarOutputStream jos, String fileInJar, File file) throws FileNotFoundException, IOException{

        InputStream in = new FileInputStream(file);

        jos.putNextEntry(new ZipEntry(fileInJar));

        int bufferSize;
        byte[] buffer = new byte[4096];

        while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1) {
            jos.write(buffer, 0, bufferSize);
        }

        in.close();
        jos.closeEntry();

    }

    public static void updateSavesFile(int newX) throws IOException{
        BufferedWriter writer = new BufferedWriter(new FileWriter("extracted/saves.txt"));
        writer.write(String.valueOf(newX));
        writer.close();
    }

    public static void deleteExtracted(File f){
        if (f.isDirectory()) {
            for (File c : f.listFiles())
                deleteExtracted(c);
        }
        f.delete();
    }

    public static void main(String... args) throws IOException{

        //read saves file in
        BufferedReader br = new BufferedReader(new InputStreamReader(JarTest.class.getClassLoader().getResourceAsStream("saves.txt")));
        String line = br.readLine();
        int x = Integer.parseInt(line);
        br.close();

        //show the message
        JOptionPane.showMessageDialog(null, "Times opened: " + x);

        //unzip the whole jar
        //this will fail if the jar is in a folder without write privileges
        //but I can catch that and warn the user
        unzipWholeJar();

        //update the unzipped saves file
        updateSavesFile(x+1);

        //put the files back into the jar
        //this will fail if the jar is open in an archive explorer
        //but I can catch that and warn the user
        rezipExtractedFiles();

        //delete what we extracted
        deleteExtracted(new File("extracted"));
    }
}
import java.io.BufferedOutputStream;
导入java.io.BufferedReader;
导入java.io.BufferedWriter;
导入java.io.File;
导入java.io.FileInputStream;
导入java.io.FileNotFoundException;
导入java.io.FileOutputStream;
导入java.io.FileWriter;
导入java.io.IOException;
导入java.io.InputStream;
导入java.io.InputStreamReader;
导入java.io.OutputStream;
导入java.util.ArrayList;
导入java.util.Enumeration;
导入java.util.List;
导入java.util.jar.JarOutputStream;
导入java.util.zip.ZipEntry;
导入java.util.zip.ZipFile;
导入javax.swing.JOptionPane;
公共类测试{
静态列表extractedFiles=new ArrayList();
public static void unzipWholeJar()引发IOException{
File jarFile=新文件(JarTest.class.getProtectionDomain().getCodeSource().getLocation().getPath());
ZipFile jarZipFile=新ZipFile(jarFile);

枚举概念

这有点可怕

  • 与在jar之外使用配置文件相比,这是浪费
  • 用户通常不关心配置文件的位置
  • 它将状态保存在程序中。为什么这样不好?因为它们没有分开,用户无法在不复制整个程序的情况下轻松传输配置,或者在不共享其配置的情况下共享程序
  • 如果可以读取和重写jar,还可以将配置文件写入同一目录
  • 假设它确实可以完美地工作,它会使代码复杂化,可能是不必要的
  • 如果它不起作用,你的用户可能会留下一个坏掉的可执行文件。这不是一个好的用户体验
您不需要将它放在同一个jar中。能够将jar的快照(与配置捆绑在一起)导出到程序中可能是一项功能,但在默认情况下,它可能没有足够的价值

实施

当主机操作系统耗尽资源时会发生什么?文件描述符、空间。。。 您可能会在
java.io.File.
中得到
NullPointerException
,但是您在硬盘上留下了什么状态?您的程序可以处理现有文件吗?所有这些都是不必要的复杂性,您可以通过使用jar之外的文件来避免

由于外部原因,写入操作随时可能失败。而且由于您没有使用finally块来关闭流,我不确定更改是否会刷新

结论

面对你在处理过程中遇到的持久性问题,这是一个聪明的技巧,但仍然是一个技巧。它会在某个时候为某人爆炸。如果你能避免,就去做。另一方面,我可能会误解这个问题
Map<String, String> zipProperties = new HashMap<>();
// zipProperties.put("create", "true");
zipProperties.put("encoding", "UTF-8");
try (FileSystem zipFS = FileSystems.newFileSystem(jarUri, zipProperties)) {
    Path dataPath = zipFS.getPath("/data/file.txt");
    ...
    Files.copy(inputStream, dataPath, StandardCopyOption.REPLACE_EXISTING);
}