仅使用纯 java 从 jar 文件复制目录

Copy directory from a jar file using only pure java

在我的资源文件夹中,我有一个名为 init 的文件夹。我想将该文件夹及其内部的所有内容复制到名为 ready 的文件夹中的 jar 外部。我想在不使用任何外部库的情况下做到这一点,只是纯粹的 java.

我试过以下方法

public static void copyFromJar(String source, final Path target)
throws
URISyntaxException,
IOException
{
    URI        resource   = ServerInitializer.class.getResource("").toURI();
    FileSystem fileSystem = FileSystems.newFileSystem(resource, Collections.<String, String>emptyMap());

    final Path jarPath = fileSystem.getPath(source);

    Files.walkFileTree(jarPath, new SimpleFileVisitor<>()
    {
        private Path currentTarget;

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
        throws
        IOException
        {
            currentTarget = target.resolve(jarPath.relativize(dir).toString());
            Files.createDirectories(currentTarget);
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
        throws
        IOException
        {
            Files.copy(file, target.resolve(jarPath.relativize(file).toString()),
                       StandardCopyOption.REPLACE_EXISTING);
            return FileVisitResult.CONTINUE;
        }
    });
}

但是我的应用程序已经在第

行死掉了
FileSystem fileSystem = FileSystems.newFileSystem(resource, Collections.<String, String>emptyMap());

有例外

java.lang.IllegalArgumentException: Path component should be '/'

当我打电话时

copyFromJar("/init", Paths.get("ready");

知道我做错了什么吗?或者有人可以提供代码将目录从 jar 复制到外部而不使用任何外部库吗?

仅供参考 我已经看过 this 解决方案,但它太旧并且使用了 apache 库,但我需要适用于 windows 和 [=41] 的纯 java 解决方案=].

这是一种非常复杂的方法。它还完全取决于您的应用程序是否在 jar 中,这使得测试、部署到运行时模块化加载器等变得棘手。

有更简单的方法可以做到这一点。具体来说,SomeClass.class.getResourceAsStream("/foo/bar") 将为您提供条目 /foo/barInputStream 在与表示 SomeClass 的 class 文件所在的相同 class 路径根目录中- 无论是 jar 文件、普通的旧目录还是更奇特的东西。

这就是您应该 'copy' 处理文件的方式。这个简单的代码:

String theResource = "com/foo/quillionsapp/toCopy/example.txt";
Path targetPath = ....;

try (var in = YourClass.class.getResourceAsStream("/" + theResource)) {
  Path tgt = targetPath.resolve(theResource);
  Files.createDirectories(tgt.getParent());
  try (var out = Files.newOutputStream(tgt)) {
    in.transferTo(out);
  }
}

现在您只需要一个要复制的所有文件的列表。 class路径抽象只是不支持列表。所以,任何破解它的尝试都只是:黑客。当你例如时它会失败有模块化的装载机等。您可以这样做 - 如果您的代码在 jar 文件中 而不是 ,那么您已经在编写代码了。编写一个方法为您提供基于 'dir on the file system' 的 class 路径以及基于 'jar file' 的路径的所有内容的列表并不难,但是有一个现成的替代方法:制作文本具有已知名称的文件,该文件在编译时 列出了所有资源。您可以自己编写,也可以编写脚本。可以像 ls src/resources/* > filesToCopy.txt 或诸如此类的东西一样微不足道。您还可以使用注释处理器来生成这样的文件。

一旦你知道文件存在(它与你要复制的文件在同一个 jar 中),用 getResourceAsStream 以同样的方式读取它,现在你有一个资源列表要写出来使用上面的代码。这个技巧意味着您的代码与 ClassLoader 的 API 完全兼容:您只是依赖 'get me an inputstream with the full data of this named resource' 的保证功能,没有别的。

注意@rzwitserloot 的回答和警告,我也不推荐这种方法,因为它不会 运行 在所有情况下 - 仅来自特定的 jar 文件系统,因此不会通过 IDE 调试器,将来可能无法使用。

话虽如此,您所做的只是从 ZIP 文件系统中解压缩。这需要一个简单的辅助方法来 copy 任何目标资源:

static boolean copy(Path from, BasicFileAttributes a, Path target) {
    System.out.println("Copy "+(a.isDirectory() ? "DIR " : "FILE")+" => "+target);
    try {
        if (a.isDirectory())
            Files.createDirectories(target);
        else if (a.isRegularFile())
            Files.copy(from, target, StandardCopyOption.REPLACE_EXISTING);
    }
    catch (IOException e) {
        throw new UncheckedIOException(e);
    }
    return true;
}

unzip 的区别是从类加载器创建文件系统而不是 Path

static void copyDir(ClassLoader classLoader, String resPath, Path target) throws IOException, URISyntaxException {
    System.out.println("copyDir("+resPath+", "+target+")");

    URI uri = classLoader.getResource(resPath).toURI();

    BiPredicate<Path, BasicFileAttributes> foreach = (p,a) -> copy(p,a, Path.of(target.toString(), p.toString())) && false;

    try(var fs = FileSystems.newFileSystem(uri, Map.of())) {
        final Path subdir = fs.getPath(resPath);
        for (Path root : fs.getRootDirectories()) {
            System.out.println("root: "+root);
            try (Stream<Path> stream = Files.find(subdir, Integer.MAX_VALUE, foreach)) {
                stream.count();
            }
        }
    }
}

然后你可以调用合适的类加载器和路径:

copyDir(yourapp.getClass().getClassLoader(), "some/path/in/jar", Path.of("copied.resource.dir"));