如何将jar包中的资源文件复制到程序文件中

4

我花了数个小时寻找答案,但仍无法弄清楚。我正在尝试将包含我正在开发的游戏的所有图像和数据文件的资源文件夹从运行的jar文件中复制到E:/Program Files/mtd/中。当我在eclipse中运行它时,一切正常,但当我导出jar文件并尝试运行时,会出现NoSuchFileException错误。

`JAR
Installing...
file:///C:/Users/Cam/Desktop/mtd.jar/resources to file:///E:/Program%20Files/mtd
/resources
java.nio.file.NoSuchFileException: C:\Users\Cam\Desktop\mtd.jar\resources
        at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
        at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
        at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
        at sun.nio.fs.WindowsFileAttributeViews$Basic.readAttributes(Unknown Sou
rce)
        at sun.nio.fs.WindowsFileAttributeViews$Basic.readAttributes(Unknown Sou
rce)
        at sun.nio.fs.WindowsFileSystemProvider.readAttributes(Unknown Source)
        at java.nio.file.Files.readAttributes(Unknown Source)
        at java.nio.file.FileTreeWalker.walk(Unknown Source)
        at java.nio.file.FileTreeWalker.walk(Unknown Source)
        at java.nio.file.Files.walkFileTree(Unknown Source)
        at java.nio.file.Files.walkFileTree(Unknown Source)
        at me.Zacx.mtd.main.Game.<init>(Game.java:94)
        at me.Zacx.mtd.main.Game.main(Game.java:301)`

这是我使用的代码:
    if (!pfFolder.exists()) {
    pfFolder.mkdir();
    try {

        URL url = getClass().getResource("/resources/");
        URI uri = null;

        if (url.getProtocol().equals("jar")) {
            System.out.println("JAR");
            JarURLConnection connect = (JarURLConnection) url.openConnection();
            uri = new URI(connect.getJarFileURL().toURI().toString() + "/resources/");

        } else if (url.getProtocol().equals("file")) {
            System.out.println("FILE");
            uri = url.toURI();
        }

        final Path src = Paths.get(uri);
        final Path tar = Paths.get(System.getenv("ProgramFiles") + "/mtd/resources/");

        System.out.println("Installing...");
        System.out.println(src.toUri() + " to " + tar.toUri());

        Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
            public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) throws IOException {
                return copy(file);
            }
            public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs ) throws IOException {
                return copy(dir);
            }
            private FileVisitResult copy( Path fileOrDir ) throws IOException {
                System.out.println("Copying " + fileOrDir.toUri() + " to " + tar.resolve( src.relativize( fileOrDir ) ).toUri());
                Files.copy( fileOrDir, tar.resolve( src.relativize( fileOrDir ) ) );
                return FileVisitResult.CONTINUE;
            }
        });
        System.out.println("Done!");
    } catch (IOException e) {
        e.printStackTrace();
    } catch (URISyntaxException e) {
        e.printStackTrace();
    }
}

%20是HTML转义空格,这可能是由于.toUri()的URL编码引起的问题。第94行具体在哪里?(Game.java:94C:\Users\Cam\Desktop\mtd.jar\resources是一个文件夹还是一个文件? - Maximilian Gerhardt
@MaximilianGerhardt 使用.replaceAll(...)去除%20并不能修复它,Game.java的第94行是Files.walkFileTree(src, new SimpleFileVisitor<Path>() {,而resources文件夹包含了我的游戏中的所有图像等资源。抱歉没有包括这些:/ - Zacx
那么,当Java说java.nio.file.NoSuchFileException: C:\Users\Cam\Desktop\mtd.jar\resources时,它不是正确的吗?因为\resources确实是一个文件夹,而不是目录?在遍历文件树的方式中一定存在某些错误。也许尝试先创建文件夹?你的copy()函数似乎没有被调用,否则我们会看到Copying .. to ...输出行,或者它正在.resolve()relativize()函数中的确切System.out.println()行崩溃。添加更多的调试输出以确保。 - Maximilian Gerhardt
@MaximilianGerhardt 问题在于它无法看到位于jar内的资源文件夹,该文件夹完全存在,但java似乎无法看到它,我不知道为什么。是否需要进行一些不同的操作,因为它是一个文件夹而不是一个文件? - Zacx
它在Eclipse中能够工作的原因是所有资源都作为独立文件存在于文件系统中,因此您始终遵循file协议路径。当所有内容打包到jar文件中时,您无法简单地进行树形遍历;标准的树形遍历只会将jar文件报告为常规文件并继续处理下一个条目。您必须"打开"jar文件作为其自己的文件系统,以便在其中进行树形遍历。 - AJNeufeld
当我尝试使用您的解决方案时,我在这一行上收到了“java.nio.file.ProviderNotFoundException:未找到提供程序”的错误消息:“try(FileSystem fs = FileSystems.newFileSystem(Paths.get(System.getProperty(“java.class.path”)),null)){”。 - Zacx
3个回答

1
问题在于不同的文件系统。 C:/Users/Cam/Desktop/mtd.jarWindowsFileSystem 中的一个 File。由于它是一个文件,而不是目录,您无法访问文件中的子目录;如果 mtd.jar 实际上是一个目录而不是一个文件,则 C:/Users/Cam/Desktop/mtd.jar/resources 才是有效的 Path
为了访问位于不同文件系统上的内容,必须使用该文件系统根目录下的路径。例如,如果您有一个文件位于 D:\dir1\dir2\file,您不能使用以 C:\ 开头的路径(符号链接除外)来访问它;您必须使用从该文件系统根目录开始的路径 D:\
一个jar文件就是一个文件。它可以位于文件系统中的任何位置,并且可以像任何常规文件一样移动、复制或删除。但是,它本身包含了自己的文件系统。没有可以用来引用jar文件系统内部任何文件的Windows路径,就像没有以C:\开头的路径可以引用D:\文件系统内部的任何文件一样。
为了访问jar的内容,您必须将jar作为ZipFileSystem打开。
// Autoclose the file system at end of try { ... } block.
try(FileSystem zip_fs = FileSystems.newFileSystem(pathToZipFile, null)) {
}

一旦您拥有了zip_fs,您可以使用zip_fs.getPath("/path/in/zip");来获取其中一个文件的Path。这个Path对象实际上是一个ZipFileSystemProvider路径对象,而不是一个WindowsFileSystemProvider路径对象,但它是一个可以打开、读取等的Path对象,至少在关闭ZipFileSystem之前是这样的。最大的区别在于path.getFileSystem()将返回ZipFileSystem,而resolve()relativize()不能使用getFileSystem()返回不同文件系统的路径对象。
当您的项目从Eclipse运行时,所有资源都在WindowsFileSystem中,因此遍历文件系统树并复制资源很直接。当您的项目从一个jar包中运行时,默认文件系统中没有这些资源。
以下是一个Java类,它将资源复制到安装目录中。它适用于Eclipse(所有资源都作为单独的文件),以及当应用程序打包成一个jar包时。
public class Installer extends SimpleFileVisitor<Path> {

    public static void installResources(Path dst, Class<?> cls, String root) throws URISyntaxException, IOException {
        URL location = cls.getProtectionDomain().getCodeSource().getLocation();
        if (location.getProtocol().equals("file")) {
            Path path = Paths.get(location.toURI());
            if (location.getPath().endsWith(".jar")) {
                try (FileSystem fs = FileSystems.newFileSystem(path, null)) {
                    installResources(dst, fs.getPath("/" + root));
                }
            } else {
                installResources(dst, path.resolve(root));
            }
        } else {
            throw new IllegalArgumentException("Not supported: " + location);
        }
    }

    private static void installResources(Path dst, Path src) throws IOException {
        Files.walkFileTree(src, new Installer(dst, src));
    }

    private final Path target, source;

    private Installer(Path dst, Path src) {
        target = dst;
        source = src;
    }

    private Path resolve(Path path) {
        return target.resolve(source.relativize(path).toString());
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        Path dst = resolve(dir);
        Files.createDirectories(dst);
        return super.preVisitDirectory(dir, attrs);
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Path dst = resolve(file);
        Files.copy(Files.newInputStream(file), dst, StandardCopyOption.REPLACE_EXISTING);
        return super.visitFile(file, attrs);
    }
}

Called as:

    Path dst = Paths.get("C:\\Program Files\\mtd");
    Installer.installResources(dst, Game.class, "resources");

当我尝试这个解决方案时,我得到了这个输出 file:///C:/Users/Cam/Desktop/eclipse/Work/Meme%20Tower%20Defence/bin/ to file:///E:/Program%20Files/mtd/resources file:/C:/Users/Cam/Desktop/eclipse/Work/Meme%20Tower%20Defence/bin/ Exception in thread "main" java.nio.file.ProviderNotFoundException: Provider not found 当前代码:`System.out.println(src.toUri() + " to " + tar.toUri()); System.out.println(uri); try (FileSystem fs = FileSystems.newFileSystem(Paths.get(uri), null)) { Path path = fs.getPath("/resources/");Files.walkFileTree(path, new SimpleFileVisitor() {` - Zacx
你有指向 jar 的 uri 吗?试试使用调用的 FileSystems.newFileSystem(uri, new HashMap<>()); 版本。 - AJNeufeld
我不确定你的意思,请你能否再详细说明一下? 再次抱歉,我对文件系统还比较新。 - Zacx
@Zacx 我已经扩展了我的示例以展示一个例子。你需要添加资源复制。 - AJNeufeld

1
这比我想象的要困难,但以下是如何操作的。
这是我的复制方法参考 https://examples.javacodegeeks.com/core-java/io/file/4-ways-to-copy-file-in-java/
public void copyFile(String inputPath, String outputPath ) throws IOException
{

    InputStream inputStream = null;

    OutputStream outputStream = null;
    try {

        inputStream =  getClass().getResourceAsStream(inputPath);
        outputStream = new FileOutputStream(outputPath);

        byte[] buf = new byte[1024];

        int bytesRead;

        while ((bytesRead = inputStream.read(buf)) > 0) {

            outputStream.write(buf, 0, bytesRead);

       }

    }
    finally {


        inputStream.close();

        outputStream.close();

    }

请注意此图片中Jar文件的项目结构 项目结构

现在我需要读取Jar文件。这是对此解决方案的变化 如何从我的Jar文件中获取资源“文件夹”?。这两种方法共同产生结果。我已经测试过它,它可以工作。

public class Main {

public static void main(String[] args) throws IOException {
    // TODO Auto-generated method stub

    final String pathPartOne = "test/com";
    final String pathPartTwo = "/MyResources";
    String pathName = "C:\\Users\\Jonathan\\Desktop\\test.jar";

    JarTest test = new JarTest();

    final File jarFile = new File(pathName);

    if(jarFile.isFile()) {  // Run with JAR file
        final JarFile jar = new JarFile(jarFile);
        final Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
        while(entries.hasMoreElements()) {
            final String name = entries.nextElement().getName();

            if (name.startsWith(pathPartOne+pathPartTwo + "/")) { //filter according to the path
                if(name.contains("."))//has extension
                {
                    String relavtivePath = name.substring(pathPartOne.length()+1);
                    String fileName = name.substring(name.lastIndexOf('/')+1);
                    System.out.println(relavtivePath);
                    System.out.println(fileName);
                    test.copyFile(relavtivePath, "C:\\Users\\Jonathan\\Desktop\\" + fileName);
                }

            }
        }
        jar.close();
    } 


}

}

希望这有所帮助。

这将读取一个文件,我已经对图像等进行了相同的操作,但是我正在尝试从我的jar内部复制一个文件夹到jar外部的目录。 - Zacx
是的,但我认为相同的原则也适用于这里。您首先需要通过获取路径来获取要复制的文件。一旦您拥有了路径,您就应该可以解决问题了。我不认为“C:\ Users \ Cam \ Desktop \ mtd.jar \ resources”是有效路径。 - Jonathan.Hickey
理论上来说,这应该是一个有效的路径,因为mtd.jar内部有一个名为resources的文件夹。如果我完全错了,很抱歉,因为我对文件系统库还很陌生,而且我只有14岁,没有多年的经验可以用来解决这个问题:( - Zacx
请查看https://dev59.com/f2gu5IYBdhLWcg3w3qx7,一旦您获得了文件夹,其余部分应该很容易。 - Jonathan.Hickey

-2

我终于找到了答案。我不想打出一个冗长的解释,但是对于任何正在寻找答案的人,这就是它。

 `  
              //on startup
               installDir("");
                for (int i = 0; i < toInstall.size(); i++) {
                    File f = toInstall.get(i);
                    String deepPath = f.getPath().replace(f.getPath().substring(0, f.getPath().lastIndexOf("resources") + "resources".length() + 1), "");
                    System.out.println(deepPath);
                    System.out.println("INSTALLING: " + deepPath);
                    installDir(deepPath);
                    System.out.println("INDEX: " + i);
                }

public void installDir(String path) {
            System.out.println(path);
            final URL url = getClass().getResource("/resources/" + path);
            if (url != null) {
                try {
                    final File apps = new File(url.toURI());
                    for (File app : apps.listFiles()) {
                        System.out.println(app);
                            System.out.println("copying..." + app.getPath() + " to " + pfFolder.getPath());
                            String deepPath = app.getPath().replace(app.getPath().substring(0, app.getPath().lastIndexOf("resources") + "resources".length() + 1), "");
                            System.out.println(deepPath);

                        try {

                                File f = new File(resources.getPath() + "/" + deepPath);
                                if (getExtention(app) != null) {
                                FileOutputStream resourceOS = new FileOutputStream(f);
                                byte[] byteArray = new byte[1024];
                                int i;
                                InputStream classIS = getClass().getClassLoader().getResourceAsStream("resources/" + deepPath);
                        //While the input stream has bytes
                                while ((i = classIS.read(byteArray)) > 0) 
                                {
                        //Write the bytes to the output stream
                                    resourceOS.write(byteArray, 0, i);
                                }
                        //Close streams to prevent errors
                                classIS.close();
                                resourceOS.close();
                                } else {
                                    System.out.println("new dir: " + f.getPath() + " (" + toInstall.size() + ")");
                                    f.mkdir();
                                    toInstall.add(f);
                                    System.out.println(toInstall.size());
                                }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                } catch (URISyntaxException ex) {
                    // never happens
                }
            }

        }`

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接