如何将jar包内的文件复制到jar包外部?

64

我想从一个jar文件中复制一个文件。被复制的文件将要被复制到工作目录之外。我尝试了多种方法,但所有的方法都会导致复制出来的文件大小为0字节。

编辑:我希望通过程序自动实现文件复制,而不是手动操作。


你必须以编程的方式完成这个任务吗?如果不需要,你可以将Foo.jar重命名为Foo.zip,然后正常解压缩即可。 - Jeffrey
展示给我们(测试)不起作用的代码。1)我们可能能够解释为什么它不起作用,2)这段代码可能帮助我们理解你在这里实际尝试做什么。 - Stephen C
投票关闭。这个问题仍然太模糊/写得太差,需要很多猜测才能回答,因此对其他读者没有用处。 - Stephen C
此外,这是一个重复的问题:https://dev59.com/aHE95IYBdhLWcg3wi-Uc - Stephen C
3
与那个问题不同,该问题涵盖的是复制而非阅读。 - Andy Thomas
9个回答

63

首先我想说之前发布的一些答案完全正确,但我想给出我的答案,因为有时我们不能在GPL下使用开源库,或者因为我们太懒了不想下载jar文件XD,或者其他任何原因,这里是一个独立的解决方案。

下面的函数将复制与Jar文件相邻的资源:

  /**
     * Export a resource embedded into a Jar file to the local file path.
     *
     * @param resourceName ie.: "/SmartLibrary.dll"
     * @return The path to the exported resource
     * @throws Exception
     */
    static public String ExportResource(String resourceName) throws Exception {
        InputStream stream = null;
        OutputStream resStreamOut = null;
        String jarFolder;
        try {
            stream = ExecutingClass.class.getResourceAsStream(resourceName);//note that each / is a directory down in the "jar tree" been the jar the root of the tree
            if(stream == null) {
                throw new Exception("Cannot get resource \"" + resourceName + "\" from Jar file.");
            }

            int readBytes;
            byte[] buffer = new byte[4096];
            jarFolder = new File(ExecutingClass.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile().getPath().replace('\\', '/');
            resStreamOut = new FileOutputStream(jarFolder + resourceName);
            while ((readBytes = stream.read(buffer)) > 0) {
                resStreamOut.write(buffer, 0, readBytes);
            }
        } catch (Exception ex) {
            throw ex;
        } finally {
            stream.close();
            resStreamOut.close();
        }

        return jarFolder + resourceName;
    }

将ExecutingClass更改为您的类的名称,然后像这样调用:

String fullPath = ExportResource("/myresource.ext");

为Java 7+编辑(方便起见)

正如GOXR3PLUS所回答并被Andy Thomas指出,您可以通过以下方式实现:

Files.copy( InputStream in, Path target, CopyOption... options)

详见GOXR3PLUS 的回答了解更多细节


1
有趣的是,我之前在另一个项目中也需要完成这个任务。这个方法可行。谢谢! - jtl999
1
谢谢Ordiel,但是为什么我们将缓冲区大小设置为4字节? - hackjutsu
1
4个字节?(我认为你指的是4千字节)很容易,这是大多数硬盘中非常标准的扇区大小,您的硬件按扇区读取,因此“理论上”(还涉及许多其他事情,我们可以谈论数小时),这有助于实现高效率水平,如果您将其降低,您将要求硬盘读取两次相同的扇区以获取数据,而您本可以通过一次物理读取操作在您的硬盘上获得该数据。 - Ordiel
2
在Java 7+中,可以用一个简单的调用Files.copy(InputStream in, Path target, CopyOption... options)来替换掉一些代码,正如下面的一个较新的答案所指出的那样。 - Andy Thomas
1
你的 'finally' 子句中可能会出现 NPE,因为你没有检查流或 resStreamOut 是否为空。 - Evvo

42

鉴于您对0字节文件的评论,我认为您正在尝试以编程方式执行此操作,并且根据您的标签,您是在Java中执行此操作。如果是这样,那么只需使用Class.getResource()获取指向JAR文件中该文件的URL,然后使用Apache Commons IO FileUtils.copyURLToFile()将其复制到文件系统中。例如:

URL inputUrl = getClass().getResource("/absolute/path/of/source/in/jar/file");
File dest = new File("/path/to/destination/file");
FileUtils.copyURLToFile(inputUrl, dest);

很可能,你现有的代码问题在于你(正确地)使用了缓冲输出流来写入文件,但是(错误地)未关闭它。

哦,而且你应该编辑你的问题以澄清你想要以什么方式进行此操作(程序化,非程序化,语言,...)


此答案假设JAR文件在类路径上。这可能并非如此。即使是这种情况,也可能有另一个JAR文件在类路径上隐藏了OP试图提取的特定文件。 - Stephen C
足够正确,但如果我的假设是正确的,这对我来说似乎是最有可能的情况。如果我错了,这里还有其他答案可以解决它。 - Ryan Stewart
在编辑之前,对问题的良好解释是非常重要的。 - Anuj Balan
寻找commons-io依赖的人可以访问以下链接:https://mvnrepository.com/artifact/commons-io/commons-io - prayagupa

19

使用 Java 7+ 更快的方法,以及获取当前目录的代码:

   /**
     * Copy a file from source to destination.
     *
     * @param source
     *        the source
     * @param destination
     *        the destination
     * @return True if succeeded , False if not
     */
    public static boolean copy(InputStream source , String destination) {
        boolean succeess = true;

        System.out.println("Copying ->" + source + "\n\tto ->" + destination);

        try {
            Files.copy(source, Paths.get(destination), StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException ex) {
            logger.log(Level.WARNING, "", ex);
            succeess = false;
        }

        return succeess;

    }

测试它(icon.png 是应用程序包图像中的一张图片):

copy(getClass().getResourceAsStream("/image/icon.png"),getBasePathForClass(Main.class)+"icon.png");
关于这行代码(getBasePathForClass(Main.class)):->请查看我在此处添加的答案 :) -> 在Java中获取当前工作目录

14

Java 8(实际上FileSystem自1.7就有了)带有一些很酷的新类/方法来处理这个问题。正如有人已经提到的,JAR基本上是ZIP文件,你可以使用

final URI jarFileUril = URI.create("jar:file:" + file.toURI().getPath());
final FileSystem fs = FileSystems.newFileSystem(jarFileUri, env);

(请查看 Zip File)
然后你可以使用其中一个方便的方法,比如:
fs.getPath("filename");

然后您可以使用 Files 类

try (final Stream<Path> sources = Files.walk(from)) {
     sources.forEach(src -> {
         final Path dest = to.resolve(from.relativize(src).toString());
         try {
            if (Files.isDirectory(from)) {
               if (Files.notExists(to)) {
                   log.trace("Creating directory {}", to);
                   Files.createDirectories(to);
               }
            } else {
                log.trace("Extracting file {} to {}", from, to);
                Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
            }
       } catch (IOException e) {
           throw new RuntimeException("Failed to unzip file.", e);
       }
     });
}

注意:我尝试解压JAR文件进行测试。


非常有趣,将来的项目会研究一下 :) - jtl999
我在这里添加了它,只是为了其他可能遇到此问题的人,为什么不使用新/更短的方法呢? - Lukino
"jar:file:"+file.toURI().getPath() looks somewhat nonsensical. Why not simply "jar:"+file.toURI()…  by the way, it’s toURI() rather than toUri() - Holger
@Holger 是的,这很重要,因为URI将使用toString()转换为字符串,其中包括'schema'(请参见文档)。如果您查看结果,URI.toString()具有'file:///foo/bar...',而getPath()仅具有'/foo/bar'。关于URI的观点很公正... toUri是Path类的一部分。现在我不确定我的变量'file'是File还是Path。如果它是File,则是toURI,但如果是Path,则无需使用toUri,因为我可以直接使用它。 - Lukino
也许你应该更仔细地看一下。你调用 getPath() 方法来移除模式,然后又添加了一个包含相同模式的字符串。我发布了一个替代方案,两者都被移除,因此结果将是 jar:file:/foo/bar,与你的变体完全相同。如果你不信,可以试试。 - Holger

8

强大的解决方案:

public static void copyResource(String res, String dest, Class c) throws IOException {
    InputStream src = c.getResourceAsStream(res);
    Files.copy(src, Paths.get(dest), StandardCopyOption.REPLACE_EXISTING);
}

您可以像这样使用它:
File tempFileGdalZip = File.createTempFile("temp_gdal", ".zip");
copyResource("/gdal.zip", tempFileGdalZip.getAbsolutePath(), this.getClass());

dest 必须是文件名或文件路径。 - Udo

2
使用 JarInputStream 类:
// assuming you already have an InputStream to the jar file..
JarInputStream jis = new JarInputStream( is );
// get the first entry
JarEntry entry = jis.getNextEntry();
// we will loop through all the entries in the jar file
while ( entry != null ) {
  // test the entry.getName() against whatever you are looking for, etc
  if ( matches ) {
    // read from the JarInputStream until the read method returns -1
    // ...
    // do what ever you want with the read output
    // ...
    // if you only care about one file, break here 
  }
  // get the next entry
  entry = jis.getNextEntry();
}
jis.close();

参见:

JarEntry


0

要从您的jar文件中复制文件到外部,您需要使用以下方法:

  1. 使用{{link1:getResourceAsStream()}}获取指向jar文件内部文件的InputStream
  2. 使用FileOutputStream打开目标文件
  3. 将字节从输入流复制到输出流
  4. 关闭流以防止资源泄漏

示例代码还包含一个变量,用于不替换现有值:

public File saveResource(String name) throws IOException {
    return saveResource(name, true);
}

public File saveResource(String name, boolean replace) throws IOException {
    return saveResource(new File("."), name, replace)
}

public File saveResource(File outputDirectory, String name) throws IOException {
    return saveResource(outputDirectory, name, true);
}

public File saveResource(File outputDirectory, String name, boolean replace)
       throws IOException {
    File out = new File(outputDirectory, name);
    if (!replace && out.exists()) 
        return out;
    // Step 1:
    InputStream resource = this.getClass().getResourceAsStream(name);
    if (resource == null)
       throw new FileNotFoundException(name + " (resource not found)");
    // Step 2 and automatic step 4
    try(InputStream in = resource;
        OutputStream writer = new BufferedOutputStream(
            new FileOutputStream(out))) {
         // Step 3
         byte[] buffer = new byte[1024 * 4];
         int length;
         while((length = in.read(buffer)) >= 0) {
             writer.write(buffer, 0, length);
         }
     }
     return out;
}

-2
一个jar文件只是一个zip文件。使用您熟悉的任何方法解压缩它,并正常复制文件即可。

-3
${JAVA_HOME}/bin/jar -cvf /path/to.jar

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