从jar文件复制目录

39

我最近开发了一个应用程序并创建了jar文件。

我的其中一个类会创建一个输出目录,并从资源中填充它的文件。

我的代码大致如下:

// Copy files from dir "template" in this class resource to output.
private void createOutput(File output) throws IOException {

    File template = new File(FileHelper.URL2Path(getClass().getResource("template")));
    FileHelper.copyDirectory(template, output);
}

不幸的是,这并不起作用。

我尝试了以下方法,但没有成功:

在写这段话的时候,我在考虑把模板目录放在资源路径中的zip文件中。这样做可以将文件作为InputStream获取,并在需要时解压缩它。但我不确定这是否是正确的方法。


请重置正确答案,谢谢。 - 4F2E4A2E
13个回答

19

感谢您的解决方案!对于其他人,以下解决方案不使用辅助类(除了StringUtils)

/我为这个解决方案添加了额外的信息,请查看代码结尾,Zegor V/

public class FileUtils {
  public static boolean copyFile(final File toCopy, final File destFile) {
    try {
      return FileUtils.copyStream(new FileInputStream(toCopy),
          new FileOutputStream(destFile));
    } catch (final FileNotFoundException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean copyFilesRecusively(final File toCopy,
      final File destDir) {
    assert destDir.isDirectory();

    if (!toCopy.isDirectory()) {
      return FileUtils.copyFile(toCopy, new File(destDir, toCopy.getName()));
    } else {
      final File newDestDir = new File(destDir, toCopy.getName());
      if (!newDestDir.exists() && !newDestDir.mkdir()) {
        return false;
      }
      for (final File child : toCopy.listFiles()) {
        if (!FileUtils.copyFilesRecusively(child, newDestDir)) {
          return false;
        }
      }
    }
    return true;
  }

  public static boolean copyJarResourcesRecursively(final File destDir,
      final JarURLConnection jarConnection) throws IOException {

    final JarFile jarFile = jarConnection.getJarFile();

    for (final Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
      final JarEntry entry = e.nextElement();
      if (entry.getName().startsWith(jarConnection.getEntryName())) {
        final String filename = StringUtils.removeStart(entry.getName(), //
            jarConnection.getEntryName());

        final File f = new File(destDir, filename);
        if (!entry.isDirectory()) {
          final InputStream entryInputStream = jarFile.getInputStream(entry);
          if(!FileUtils.copyStream(entryInputStream, f)){
            return false;
          }
          entryInputStream.close();
        } else {
          if (!FileUtils.ensureDirectoryExists(f)) {
            throw new IOException("Could not create directory: "
                + f.getAbsolutePath());
          }
        }
      }
    }
    return true;
  }

  public static boolean copyResourcesRecursively( //
      final URL originUrl, final File destination) {
    try {
      final URLConnection urlConnection = originUrl.openConnection();
      if (urlConnection instanceof JarURLConnection) {
        return FileUtils.copyJarResourcesRecursively(destination,
            (JarURLConnection) urlConnection);
      } else {
        return FileUtils.copyFilesRecusively(new File(originUrl.getPath()),
            destination);
      }
    } catch (final IOException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean copyStream(final InputStream is, final File f) {
    try {
      return FileUtils.copyStream(is, new FileOutputStream(f));
    } catch (final FileNotFoundException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean copyStream(final InputStream is, final OutputStream os) {
    try {
      final byte[] buf = new byte[1024];

      int len = 0;
      while ((len = is.read(buf)) > 0) {
        os.write(buf, 0, len);
      }
      is.close();
      os.close();
      return true;
    } catch (final IOException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean ensureDirectoryExists(final File f) {
    return f.exists() || f.mkdir();
  }
}

它仅使用来自Apache Software Foundation的一个外部库,但是所使用的函数仅限于:

  public static String removeStart(String str, String remove) {
      if (isEmpty(str) || isEmpty(remove)) {
          return str;
      }
      if (str.startsWith(remove)){
          return str.substring(remove.length());
      }
      return str;
  }
  public static boolean isEmpty(CharSequence cs) {
      return cs == null || cs.length() == 0;
  }

我对Apache许可证的了解有限,但是您可以在代码中使用这些方法而无需使用库。然而,如果出现许可问题,我不负责。


1
在我提出了这个问题之后,我发现这个答案非常有帮助:http://stackoverflow.com/questions/17928722/custom-plugin-cant-copy-files-from-jar-because-protocol-is-jar - James Dunn

18

使用Java7+,可以通过创建FileSystem,然后使用walkFileTree递归地复制文件来实现。

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


    final Path jarPath = fileSystem.getPath(source);

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

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

    });
}

这种方法可以这样使用:

copyFromJar("/path/to/the/template/in/jar", Paths.get("/tmp/from-jar"))

1
很棒的答案,Java 7有很多好东西。看起来你的实用程序应该有3个参数:(1)要从其中复制的jar文件中的虚拟路径,(2)文件系统中的目标目录,以及(3)要从中提取的实际.jar文件归档的路径。我认为你的参数#1和#2与我的说法相匹配,但是不确定参数#3?也许您假设它是我们正在运行的jar,或者以某种方式让JVM类加载器在类路径上的一个jar中找到资源?只是检查一下,谢谢! - Mark Bennett
11
这个答案不起作用。我很想尝试使用(相对)较新的nio API,但好像无法使其工作:java.lang.IllegalArgumentException: Path component should be '/'。 - RockMeetHardplace
1
好的解决方案!我能够从jar /resources复制一个包含所有文件的文件夹,而不需要外部库。谢谢! - Vasile Bors
@RockMeetHardplace,我得到了你描述的错误“路径组件应该是'/'”,当资源恰好在类路径中时会出现这种情况。在这种情况下,url解析为文件url而不是jar url。尝试使用Duc提供的答案修改这个解决方案。 - sushantsha
这是最简单、最简洁的答案。绝对值得排名第一。它还使用了nio,这可能更快,对吧? - spierce7

15

我认为你使用zip文件的方法是有道理的。假设你将使用getResourceAsStream获取zip文件的内部内容,这些内容在逻辑上看起来像是一个目录树。

一个基本的实现方法:

InputStream is = getClass().getResourceAsStream("my_embedded_file.zip");
ZipInputStream zis = new ZipInputStream(is);
ZipEntry entry;

while ((entry = zis.getNextEntry()) != null) {
    // do something with the entry - for example, extract the data 
}

最后我做了这件事。将所有内容压缩并将zip文件作为资源获取后,需要在需要的地方进行解压缩。 - Macarse

8

我不喜欢之前发布的使用ZIP文件方法,所以想出了以下解决方案。

public void copyResourcesRecursively(URL originUrl, File destination) throws Exception {
    URLConnection urlConnection = originUrl.openConnection();
    if (urlConnection instanceof JarURLConnection) {
        copyJarResourcesRecursively(destination, (JarURLConnection) urlConnection);
    } else if (urlConnection instanceof FileURLConnection) {
        FileUtils.copyFilesRecursively(new File(originUrl.getPath()), destination);
    } else {
        throw new Exception("URLConnection[" + urlConnection.getClass().getSimpleName() +
                "] is not a recognized/implemented connection type.");
    }
}

public void copyJarResourcesRecursively(File destination, JarURLConnection jarConnection ) throws IOException {
    JarFile jarFile = jarConnection.getJarFile();
    for (JarEntry entry : CollectionUtils.iterable(jarFile.entries())) {
        if (entry.getName().startsWith(jarConnection.getEntryName())) {
            String fileName = StringUtils.removeStart(entry.getName(), jarConnection.getEntryName());
            if (!entry.isDirectory()) {
                InputStream entryInputStream = null;
                try {
                    entryInputStream = jarFile.getInputStream(entry);
                    FileUtils.copyStream(entryInputStream, new File(destination, fileName));
                } finally {
                    FileUtils.safeClose(entryInputStream);
                }
            } else {
                FileUtils.ensureDirectoryExists(new File(destination, fileName));
            }
        }
    }
}

例子用法(从类路径资源“config”复制所有文件到“${homeDirectory}/config”):

File configHome = new File(homeDirectory, "config/");
//noinspection ResultOfMethodCallIgnored
configHome.mkdirs();
copyResourcesRecursively(super.getClass().getResource("/config"), configHome);

这应该适用于从普通文件和Jar文件中复制的内容。

注意:上面的代码使用了一些自定义的实用程序类(FileUtils,CollectionUtils),以及一些来自Apache commons-lang的类(StringUtils),但函数名称应该相当明显。


2
你正在使用哪个库?(FileUtils,CollectionUtils,...) - BrainStone

6
lpiepiora的答案是正确的!但有一个小问题,源路径应该是jar的URL。当源路径为文件系统路径时,上述代码将无法正常工作。要解决这个问题,你应该使用ReferencePath,你可以从以下链接中获取代码:通过FileSystem对象从文件系统中读取 copyFromJar的新代码应该像这样:
public class ResourcesUtils {
public static void copyFromJar(final String sourcePath, final Path target) throws URISyntaxException,
        IOException {
    final PathReference pathReference = PathReference.getPath(new URI(sourcePath));
    final Path jarPath = pathReference.getPath();

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

        private Path currentTarget;

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

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

    });
}

public static void main(final String[] args) throws MalformedURLException, URISyntaxException, IOException {
    final String sourcePath = "jar:file:/c:/temp/example.jar!/src/main/resources";
    ResourcesUtils.copyFromJar(sourcePath, Paths.get("c:/temp/resources"));
}

1
您提供的链接并未包含完整的PathReference实现。 - Toldry

3

我不确定FileHelper是什么或者它的作用,但你无法直接从JAR复制文件(或目录)。使用InputStream就像你提到的那样是正确的方式(无论是来自jar还是zip):

InputStream is = getClass().getResourceAsStream("file_in_jar");
OutputStream os = new FileOutputStream("dest_file");
byte[] buffer = new byte[4096];
int length;
while ((length = is.read(buffer)) > 0) {
    os.write(buffer, 0, length);
}
os.close();
is.close();

你需要对每个文件进行上述操作(当然要适当处理异常)。根据你的部署配置,你可能能够像JarFile一样读取相关的jar文件(例如,如果作为未展开的Web应用程序的一部分部署,则实际上可能不可用作为文件)。如果你可以读取它,那么你应该能够遍历JarEntry实例列表,从而重建目录结构;否则,你可能需要将其存储在其他地方(例如文本或XML资源中)。
你可能想看一下Commons IO库 - 它提供了许多常用的流/文件功能,包括复制。

3
这里有一个来自tess4j项目的可用版本:链接。它涉及到IT技术。
 /**
 * This method will copy resources from the jar file of the current thread and extract it to the destination folder.
 * 
 * @param jarConnection
 * @param destDir
 * @throws IOException
 */
public void copyJarResourceToFolder(JarURLConnection jarConnection, File destDir) {

    try {
        JarFile jarFile = jarConnection.getJarFile();

        /**
         * Iterate all entries in the jar file.
         */
        for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {

            JarEntry jarEntry = e.nextElement();
            String jarEntryName = jarEntry.getName();
            String jarConnectionEntryName = jarConnection.getEntryName();

            /**
             * Extract files only if they match the path.
             */
            if (jarEntryName.startsWith(jarConnectionEntryName)) {

                String filename = jarEntryName.startsWith(jarConnectionEntryName) ? jarEntryName.substring(jarConnectionEntryName.length()) : jarEntryName;
                File currentFile = new File(destDir, filename);

                if (jarEntry.isDirectory()) {
                    currentFile.mkdirs();
                } else {
                    InputStream is = jarFile.getInputStream(jarEntry);
                    OutputStream out = FileUtils.openOutputStream(currentFile);
                    IOUtils.copy(is, out);
                    is.close();
                    out.close();
                }
            }
        }
    } catch (IOException e) {
        // TODO add logger
        e.printStackTrace();
    }

}

1
非常好的答案,正是我在寻找的! - RockMeetHardplace
1
有趣的事情,我正在寻找复制tesseract训练数据的方法。很幸运他们有这个! - Semyon Danilov
是的,你说得对,我当时已经添加了它。 - 4F2E4A2E

3

我知道这个问题现在有点过时了,但在尝试了一些不起作用的答案和其他需要整个库才能使用一个方法的答案后,我决定编写一个类。它不需要第三方库,并且已经测试过可以在Java 8中使用。有四个公共方法:copyResourcesToTempDircopyResourcesToDircopyResourceDirectoryjar

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.util.Enumeration;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * A helper to copy resources from a JAR file into a directory.
 */
public final class ResourceCopy {

    /**
     * URI prefix for JAR files.
     */
    private static final String JAR_URI_PREFIX = "jar:file:";

    /**
     * The default buffer size.
     */
    private static final int BUFFER_SIZE = 8 * 1024;

    /**
     * Copies a set of resources into a temporal directory, optionally preserving
     * the paths of the resources.
     * @param preserve Whether the files should be placed directly in the
     *  directory or the source path should be kept
     * @param paths The paths to the resources
     * @return The temporal directory
     * @throws IOException If there is an I/O error
     */
    public File copyResourcesToTempDir(final boolean preserve,
        final String... paths)
        throws IOException {
        final File parent = new File(System.getProperty("java.io.tmpdir"));
        File directory;
        do {
            directory = new File(parent, String.valueOf(System.nanoTime()));
        } while (!directory.mkdir());
        return this.copyResourcesToDir(directory, preserve, paths);
    }

    /**
     * Copies a set of resources into a directory, preserving the paths
     * and names of the resources.
     * @param directory The target directory
     * @param preserve Whether the files should be placed directly in the
     *  directory or the source path should be kept
     * @param paths The paths to the resources
     * @return The temporal directory
     * @throws IOException If there is an I/O error
     */
    public File copyResourcesToDir(final File directory, final boolean preserve,
        final String... paths) throws IOException {
        for (final String path : paths) {
            final File target;
            if (preserve) {
                target = new File(directory, path);
                target.getParentFile().mkdirs();
            } else {
                target = new File(directory, new File(path).getName());
            }
            this.writeToFile(
                Thread.currentThread()
                    .getContextClassLoader()
                    .getResourceAsStream(path),
                target
            );
        }
        return directory;
    }

    /**
     * Copies a resource directory from inside a JAR file to a target directory.
     * @param source The JAR file
     * @param path The path to the directory inside the JAR file
     * @param target The target directory
     * @throws IOException If there is an I/O error
     */
    public void copyResourceDirectory(final JarFile source, final String path,
        final File target) throws IOException {
        final Enumeration<JarEntry> entries = source.entries();
        final String newpath = String.format("%s/", path);
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            if (entry.getName().startsWith(newpath) && !entry.isDirectory()) {
                final File dest =
                    new File(target, entry.getName().substring(newpath.length()));
                final File parent = dest.getParentFile();
                if (parent != null) {
                    parent.mkdirs();
                }
                this.writeToFile(source.getInputStream(entry), dest);
            }
        }
    }

    /**
     * The JAR file containing the given class.
     * @param clazz The class
     * @return The JAR file or null
     * @throws IOException If there is an I/O error
     */
    public Optional<JarFile> jar(final Class<?> clazz) throws IOException {
        final String path =
            String.format("/%s.class", clazz.getName().replace('.', '/'));
        final URL url = clazz.getResource(path);
        Optional<JarFile> optional = Optional.empty();
        if (url != null) {
            final String jar = url.toString();
            final int bang = jar.indexOf('!');
            if (jar.startsWith(ResourceCopy.JAR_URI_PREFIX) && bang != -1) {
                optional = Optional.of(
                    new JarFile(
                        jar.substring(ResourceCopy.JAR_URI_PREFIX.length(), bang)
                    )
                );
            }
        }
        return optional;
    }

    /**
     * Writes an input stream to a file.
     * @param input The input stream
     * @param target The target file
     * @throws IOException If there is an I/O error
     */
    private void writeToFile(final InputStream input, final File target)
        throws IOException {
        final OutputStream output = Files.newOutputStream(target.toPath());
        final byte[] buffer = new byte[ResourceCopy.BUFFER_SIZE];
        int length = input.read(buffer);
        while (length > 0) {
            output.write(buffer, 0, length);
            length = input.read(buffer);
        }
        input.close();
        output.close();
    }

}

1
您的答案应该注意,它只有在资源在 jar 文件中时才能工作。类加载器可能会从其他内容中加载资源。 - toolforger
@toolforger 感谢您的澄清。欢迎您的贡献。 - Miguel Jiménez
1
这段代码救了我的一天,并将我的工作转换成了一个与JNA兼容的精美多平台JAR。点赞。另外+1是因为它是一个合适的类。 - LoneWanderer

2

最近我遇到了类似的问题。我尝试从Java资源中提取文件夹。所以我使用了Spring的PathMatchingResourcePatternResolver解决了这个问题。

以下代码可以获取指定资源中的所有文件和目录:

        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                + resourceFolder + "/**");

以下是将所有文件和目录从资源复制到磁盘路径的类。

public class ResourceExtractor {

public static final Logger logger = 
Logger.getLogger(ResourceExtractor.class);

public void extract(String resourceFolder, String destinationFolder){
    try {
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                + resourceFolder + "/**");
        URI inJarUri  = new DefaultResourceLoader().getResource("classpath:" + resourceFolder).getURI();

        for (Resource resource : resources){
            String relativePath = resource
                        .getURI()
                        .getRawSchemeSpecificPart()
                        .replace(inJarUri.getRawSchemeSpecificPart(), "");
            if (relativePath.isEmpty()){
                continue;
            }
            if (relativePath.endsWith("/") || relativePath.endsWith("\\")) {
                File dirFile = new File(destinationFolder + relativePath);
                if (!dirFile.exists()) {
                    dirFile.mkdir();
                }
            }
            else{
                copyResourceToFilePath(resource, destinationFolder + relativePath);
            }
        }
    }
    catch (IOException e){
        logger.debug("Extraction failed!", e );
    }
}

private void copyResourceToFilePath(Resource resource, String filePath) throws IOException{
    InputStream resourceInputStream = resource.getInputStream();
    File file = new File(filePath);
    if (!file.exists()) {
        FileUtils.copyInputStreamToFile(resourceInputStream, file);
    }
}

}


非常感谢您的分享。我已经在基于Spring Shell的原型命令行应用程序中使用了您的类,以从Jar中提取一些Thymeleaf模板。效果非常好! - opncow
在Spring Boot的上下文中,良好的解决方案 - sge

2
您可以使用 ClassLoader 来获取资源的 。一旦您获得了 InputStream,就可以将流的内容读取并写入 OutputStream 中。
在您的情况下,您需要创建多个 OutputStream 实例,每个实例对应一个要复制到目标位置的文件。这当然需要您事先知道文件名。
为此任务,最好使用 getResourceAsStream,而不是 getResource 或 getResources()。

Vineet:我不喜欢“需要预先知道文件名”的要求。我应该有一个静态列表,其中包含模板目录中的每个文件,并在更改时更新它。 :( - Macarse
1
是的,不幸的是在JAR文件内部无法获取目录列表。您可以参考Stack Overflow上的问题https://dev59.com/FkfRa4cB1Zd3GeqP_rsM - Vineet Reynolds
1
还有一个@https://dev59.com/yEXRa4cB1Zd3GeqPqk0t。如果你仔细阅读,你会注意到OP确实考虑了维护一个静态文件列表来读取。 - Vineet Reynolds
1
如果这个方法行不通,另一个提供的答案——从ZIP流中读取等——肯定会起作用。 - Vineet Reynolds

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