Як перелічити файли всередині файлу JAR?


114

У мене є цей код, який читає всі файли з каталогу.

    File textFolder = new File("text_directory");

    File [] texFiles = textFolder.listFiles( new FileFilter() {
           public boolean accept( File file ) {
               return file.getName().endsWith(".txt");
           }
    });

Це чудово працює. Він заповнює масив усіма файлами, які закінчуються ".txt" з каталогу "text_directory".

Як я можу прочитати вміст каталогу аналогічним чином в файлі JAR?

Тому я дійсно хочу зробити це перерахувати всі зображення всередині мого файлу JAR, щоб я міг їх завантажити:

ImageIO.read(this.getClass().getResource("CompanyLogo.png"));

(Це працює, тому що "CompanyLogo" є "жорстким кодом", але кількість зображень у файлі JAR може бути від 10 до 200 змінної довжини.)

EDIT

Тож я здогадуюсь, що моєю основною проблемою буде: Як дізнатися ім'я файлу JAR, де живе мій основний клас?

Звичайно, я міг прочитати це за допомогою java.util.Zip.

Моя структура така:

Вони схожі на:

my.jar!/Main.class
my.jar!/Aux.class
my.jar!/Other.class
my.jar!/images/image01.png
my.jar!/images/image02a.png
my.jar!/images/imwge034.png
my.jar!/images/imagAe01q.png
my.jar!/META-INF/manifest 

Зараз я можу завантажити, наприклад, "images / image01.png", використовуючи:

    ImageIO.read(this.getClass().getResource("images/image01.png));

Але тільки тому, що я знаю ім'я файлів, для решти мені доведеться їх динамічно завантажувати.


Просто думка - чому б не поштові зображення / jar розмістити в окремому файлі і не прочитати записи в ньому з вашого класу в іншій банці?
— Vineet Reynolds

3
Тому що для розповсюдження / встановлення знадобиться "додатковий" крок. :( Ви знаєте, кінцеві користувачі.
— OscarRyz

Зважаючи на те, що ви створили банку, ви можете також включити список файлів всередині неї, а не намагатися будь-які хитрощі.
— Том Хотін - тайклін

Ну, я можу помилятися, але банки можна вбудовувати і в інші банки. На цій основі працює одноразове (TM) упаковкове рішення ibm.com/developerworks/java/library/j-onejar . Крім того, у вашому випадку вам не потрібні класи завантаження здібностей.
— Vineet Reynolds

Відповіді:


91
CodeSource src = MyClass.class.getProtectionDomain().getCodeSource();
if (src != null) {
  URL jar = src.getLocation();
  ZipInputStream zip = new ZipInputStream(jar.openStream());
  while(true) {
    ZipEntry e = zip.getNextEntry();
    if (e == null)
      break;
    String name = e.getName();
    if (name.startsWith("path/to/your/dir/")) {
      /* Do something with this entry. */
      ...
    }
  }
} 
else {
  /* Fail... */
}

Зауважте, що в Java 7 ви можете створити файл FileSystemз файлу JAR (zip), а потім використовувати механізми ходіння та фільтрування каталогу NIO для пошуку по ньому. Це полегшило б написання коду, який обробляє JAR та "вибухали" каталоги.


ей спасибі ... шукав спосіб зробити це вже кілька годин !!
— ньютопський

9
Так, цей код працює, якщо ми хочемо перерахувати всі записи цього файлу jar. Але якщо я просто хочу вказати підкаталог всередині jar, наприклад, example.jar / dir1 / dir2 / , як я можу безпосередньо перелічити всі файли в цьому підкаталозі? Або мені потрібно розпакувати цей файл jar? Я високо ціную вашу допомогу!
— Ensom Hodder

Згаданий підхід Java 7 вказаний у відповіді @ acheron55 .
— Вадим

@Vadzim Ви впевнені, що відповідь acheron55 відповідає Java 7? Я не знайшов Files.walk () або java.util.Stream в Java 7, але в Java 8: docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
— Bruce Нд

@BruceSun, в Java 7 ви можете використовувати Files.walkFileTree (...) замість цього.
— Вадим

80

Код, який працює як для файлів IDE, так і для .jar:

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class ResourceWalker {
    public static void main(String[] args) throws URISyntaxException, IOException {
        URI uri = ResourceWalker.class.getResource("/resources").toURI();
        Path myPath;
        if (uri.getScheme().equals("jar")) {
            FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap());
            myPath = fileSystem.getPath("/resources");
        } else {
            myPath = Paths.get(uri);
        }
        Stream<Path> walk = Files.walk(myPath, 1);
        for (Iterator<Path> it = walk.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    }
}

5
FileSystems.newFileSystem()приймає а Map<String, ?>, тому вам потрібно вказати, Collections.emptyMap()що йому потрібно повернути відповідний тип. Це працює: Collections.<String, Object>emptyMap().
— Zero3

6
Фантастичний !!! але URI uri = MyClass.class.getResource ("/ ресурси"). toURI (); повинен мати MyClass.class.getClassLoader (). getResource ("/ ресурси"). toURI (); тобто, getClassLoader (). Інакше для мене це не працювало.
— EMM

8
Не забудьте закрити fileSystem!
— gmjonker

3
Це має бути першою відповіддю на 1.8 ( walkметод у Filesдоступний лише у 1.8). Єдина проблема полягає в тому, що каталог ресурсів з’являється у Files.walk(myPath, 1)файлах, а не лише у файлах. Я думаю, перший елемент можна просто проігнорувати
— toto_tico

4
myPath = fileSystem.getPath("/resources");не працює для мене; він нічого не знаходить. У моєму випадку це повинні бути "образи", а довідник "образи", безумовно, включений до моєї банки!
— phip1611

21

Еріксона відповідь працював відмінно:

Ось робочий код.

CodeSource src = MyClass.class.getProtectionDomain().getCodeSource();
List<String> list = new ArrayList<String>();

if( src != null ) {
    URL jar = src.getLocation();
    ZipInputStream zip = new ZipInputStream( jar.openStream());
    ZipEntry ze = null;

    while( ( ze = zip.getNextEntry() ) != null ) {
        String entryName = ze.getName();
        if( entryName.startsWith("images") &&  entryName.endsWith(".png") ) {
            list.add( entryName  );
        }
    }

 }
 webimages = list.toArray( new String[ list.size() ] );

І я лише змінив метод завантаження з цього:

File[] webimages = ... 
BufferedImage image = ImageIO.read(this.getClass().getResource(webimages[nextIndex].getName() ));

До цього:

String  [] webimages = ...

BufferedImage image = ImageIO.read(this.getClass().getResource(webimages[nextIndex]));

9

Я хотів би розповісти про відповідь acheron55 , оскільки це дуже безпечне рішення з кількох причин:

  1. Він не закриває FileSystemоб'єкт.
  2. Він не перевіряє, чи FileSystemоб'єкт вже існує.
  3. Це не безпечно для ниток.

Це дещо безпечніше рішення:

private static ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();

public void walk(String path) throws Exception {

    URI uri = getClass().getResource(path).toURI();
    if ("jar".equals(uri.getScheme()) {
        safeWalkJar(path, uri);
    } else {
        Files.walk(Paths.get(path));
    }
}

private void safeWalkJar(String path, URI uri) throws Exception {

    synchronized (getLock(uri)) {    
        // this'll close the FileSystem object at the end
        try (FileSystem fs = getFileSystem(uri)) {
            Files.walk(fs.getPath(path));
        }
    }
}

private Object getLock(URI uri) {

    String fileName = parseFileName(uri);  
    locks.computeIfAbsent(fileName, s -> new Object());
    return locks.get(fileName);
}

private String parseFileName(URI uri) {

    String schemeSpecificPart = uri.getSchemeSpecificPart();
    return schemeSpecificPart.substring(0, schemeSpecificPart.indexOf("!"));
}

private FileSystem getFileSystem(URI uri) throws IOException {

    try {
        return FileSystems.getFileSystem(uri);
    } catch (FileSystemNotFoundException e) {
        return FileSystems.newFileSystem(uri, Collections.<String, String>emptyMap());
    }
}   

Немає реальної потреби в синхронізації над ім'ям файлу; можна просто синхронізувати один і той же об'єкт кожен раз (або зробити метод synchronized), це суто оптимізація.

Я б сказав, що це все-таки проблематичне рішення, оскільки в коді можуть бути інші частини, які використовують FileSystemінтерфейс над одними і тими ж файлами, і це може заважати їм (навіть в одному потоковому додатку).
Крім того, він не перевіряє наявність nulls (наприклад, на getClass().getResource().

Цей особливий інтерфейс Java NIO - жахливий, оскільки він представляє глобальний / однотонний ресурс, не безпечний для потоків, а його документація надзвичайно розпливчаста (безліч невідомих через конкретні реалізації постачальника). Результати можуть відрізнятися від іншихFileSystem постачальників (не JAR). Можливо, є вагома причина, щоб він був таким; Я не знаю, я не досліджував реалізації.


1
Синхронізація зовнішніх ресурсів, як і ФС, не має особливого сенсу в межах однієї VM. Можуть бути й інші додатки, які отримують доступ до нього за межами вашої вітчизни. Окрім навіть усередині вашого власного додатку, ваш замок на основі імен можна легко обійти. З цим речами краще покластися на механізми синхронізації ОС, як-от блокування файлів.
— Еспіноса

@Espinosa Механізм блокування імен файлів цілком можна обійти; моя відповідь також недостатньо безпечна, але я вважаю, що це найбільше, що ви можете отримати з Java NIO за мінімальних зусиль. Покладатися на ОС, щоб керувати блокуваннями або не контролювати, які програми отримують доступ до файлів, є поганою практикою IMHO, якщо ви не створюєте додаток на основі клієнтів - скажімо, текстовий редактор. Якщо самостійно не керувати блокуваннями, це може призвести до викидання винятків, або призведе до блокування програми - обох слід уникати.
— Ейал Рот

8

Тож я здогадуюсь, що моєю основною проблемою буде те, як знати назву баночки, де живе мій основний клас.

Якщо припустити, що ваш проект упакований в Jar (не обов'язково правда!), Ви можете використовувати ClassLoader.getResource () або findResource () з назвою класу (далі - .class), щоб отримати банку, що містить заданий клас. Вам доведеться розібрати назву банку з поверненої URL-адреси (не така вже й жорстка), яку я залишу читачеві як вправу :-)

Обов’язково перевіряйте на випадок, коли клас не є частиною банку.


1
так - цікаво, що це було б відмовлено без коментарів ... Ми постійно користуємось вищеописаною технікою, і вона працює чудово.
— День Кевіна

Старе питання, але мені це здається прекрасним злом. Повернено до нуля :)
— Tuukka Mustonen

Оновлено, оскільки це єдине перелічене тут рішення для випадку, коли клас не має а CodeSource.
— Поновіть Моніку 2331977

7

Я переніс відповідь acheron55 на Java 7 і закрив FileSystemоб'єкт. Цей код працює в IDE, в файлах jar і в банці всередині війни на Tomcat 7; але зауважте, що він не працює в банці всередині війни на JBoss 7 (це дає FileSystemNotFoundException: Provider "vfs" not installed, див. також цей пост ). Крім того, як і оригінальний код, він не є безпечним для потоків, як пропонує помилка . З цих причин я відмовився від цього рішення; однак, якщо ви можете прийняти ці проблеми, ось мій готовий код:

import java.io.IOException;
import java.net.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;

public class ResourceWalker {

    public static void main(String[] args) throws URISyntaxException, IOException {
        URI uri = ResourceWalker.class.getResource("/resources").toURI();
        System.out.println("Starting from: " + uri);
        try (FileSystem fileSystem = (uri.getScheme().equals("jar") ? FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap()) : null)) {
            Path myPath = Paths.get(uri);
            Files.walkFileTree(myPath, new SimpleFileVisitor<Path>() { 
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println(file);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }
}

5

Ось метод, який я написав для "запуску всіх JUnits під пакетом". Ви повинні мати можливість адаптувати його до своїх потреб.

private static void findClassesInJar(List<String> classFiles, String path) throws IOException {
    final String[] parts = path.split("\\Q.jar\\\\E");
    if (parts.length == 2) {
        String jarFilename = parts[0] + ".jar";
        String relativePath = parts[1].replace(File.separatorChar, '/');
        JarFile jarFile = new JarFile(jarFilename);
        final Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            final String entryName = entry.getName();
            if (entryName.startsWith(relativePath)) {
                classFiles.add(entryName.replace('/', File.separatorChar));
            }
        }
    }
}

Редагувати: Ага, у такому випадку ви можете також захотіти цей фрагмент (той самий варіант використання :))

private static File findClassesDir(Class<?> clazz) {
    try {
        String path = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
        final String codeSourcePath = URLDecoder.decode(path, "UTF-8");
        final String thisClassPath = new File(codeSourcePath, clazz.getPackage().getName().repalce('.', File.separatorChar));
    } catch (UnsupportedEncodingException e) {
        throw new AssertionError("impossible", e);
    }
}

1
Я думаю, що велика проблема полягає в першу чергу знати ім'я файлу jar. Це баночка, де живе головний клас.
— OscarRyz

5

Ось приклад використання бібліотеки Reflections для рекурсивного сканування classpath за допомогою шаблону імен regex, доповненого парою Guava perks для отримання вмісту ресурсів:

Reflections reflections = new Reflections("com.example.package", new ResourcesScanner());
Set<String> paths = reflections.getResources(Pattern.compile(".*\\.template$"));

Map<String, String> templates = new LinkedHashMap<>();
for (String path : paths) {
    log.info("Found " + path);
    String templateName = Files.getNameWithoutExtension(path);
    URL resource = getClass().getClassLoader().getResource(path);
    String text = Resources.toString(resource, StandardCharsets.UTF_8);
    templates.put(templateName, text);
}

Це працює як з банками, так і з вибухаючими класами.


Будьте уважні, що роздуми все ще не підтримують Java 9 і вище: github.com/ronmamo/reflections/isissue/186 . Там є посилання на конкуруючі бібліотеки.
— Вадим

3

Файл jar - це лише поштовий файл зі структурованим маніфестом. Ви можете відкрити jar-файл за допомогою звичайних інструментів java zip та сканувати вміст файлу таким чином, надути потоки тощо. Потім використовуйте це у виклику getResourceAsStream, і він повинен бути всім фактом.

EDIT / після уточнення

Знадобилося мені хвилину, щоб запам'ятати всі шматочки та шматки, і я впевнений, що є більш чисті способи зробити це, але я хотів побачити, що я не божевільний. У моєму проекті image.jpg - це файл у деякій частині основного файлу jar. Я отримую завантажувач класу основного класу (SomeClass - точка входу) і використовую його для виявлення ресурсу image.jpg. Потім якась магія потоку, щоб увійти в цю Image Image InputStream і все в порядку.

InputStream inputStream = SomeClass.class.getClassLoader().getResourceAsStream("image.jpg");
JPEGImageReaderSpi imageReaderSpi = new JPEGImageReaderSpi();
ImageReader ir = imageReaderSpi.createReaderInstance();
ImageInputStream iis = new MemoryCacheImageInputStream(inputStream);
ir.setInput(iis);
....
ir.read(0); //will hand us a buffered image

Ця баночка містить основну програму та ресурси. Як я відношусь до самої банку? зсередини файлу jar?
— OscarRyz

Для посилання на файл JAR просто використовуйте "blah.JAR" як "Рядок". Ви можете, наприклад, new File("blah.JAR")створити об'єкт File, який представляє JAR. Просто замініть "bla.JAR" на ім'я вашого JAR.
— Томас Оуенс

Якщо це та сама баночка, яку ви вже закінчили, завантажувач класів повинен мати можливість бачити речі всередині банки ... Я неправильно зрозумів, що ви намагалися зробити спочатку.
— Майк

2
Ну так, у мене це вже є, проблема полягає в тому, що мені потрібно щось на зразок: "... getResourceAsStream (" *. Jpg "); ..." Тобто, динамічно, перераховуйте файли, що містяться.
— OscarRyz

3

З огляду на фактичний файл JAR, ви можете перелічити вміст, використовуючи JarFile.entries(). Вам потрібно знати місце розташування файлу JAR - ви не можете просто попросити завантажувач перерахувати все, до чого він може потрапити.

Ви маєте змогу опрацювати розташування файлу JAR на основі URL-адреси, повернутої з нього ThisClassName.class.getResource("ThisClassName.class"), але це може бути крихітно ненависне.


Читаючи свою відповідь, інше поставлене питання. Що призведе до виклику: this.getClass (). GetResource ("/ my_directory"); Він повинен повертати URL-адресу, яка в свою чергу може бути .... використана як каталог? Ну ... дозвольте спробувати.
— OscarRyz

Ви завжди знаєте місце JAR - воно знаходиться в "". Поки ім’я JAR є чимось, ви можете десь використовувати константу String. Тепер, якщо люди підуть змінювати назву JAR ...
— Томас Оуенс

@Thomas: Це припущення, що ви запускаєте додаток із поточного каталогу. Що не так з "java -jar foo / bar / baz.jar"?
— Джон Скіт

Я вважаю (і повинен був би підтвердити), що якби у вас був код Jar new File("baz.jar), об’єкт File представляв би ваш файл JAR.
— Томас Оуенс

@Thomas: Я не вірю в це. Я вважаю, що це буде відносно поточного робочого каталогу процесу. Я теж повинен перевірити :)
— Джон Скіт

3

Деякий час тому я зробив функцію, яка отримує класифікацію зсередини JAR:

public static Class[] getClasses(String packageName) 
throws ClassNotFoundException{
    ArrayList<Class> classes = new ArrayList<Class> ();

    packageName = packageName.replaceAll("\\." , "/");
    File f = new File(jarName);
    if(f.exists()){
        try{
            JarInputStream jarFile = new JarInputStream(
                    new FileInputStream (jarName));
            JarEntry jarEntry;

            while(true) {
                jarEntry=jarFile.getNextJarEntry ();
                if(jarEntry == null){
                    break;
                }
                if((jarEntry.getName ().startsWith (packageName)) &&
                        (jarEntry.getName ().endsWith (".class")) ) {
                    classes.add(Class.forName(jarEntry.getName().
                            replaceAll("/", "\\.").
                            substring(0, jarEntry.getName().length() - 6)));
                }
            }
        }
        catch( Exception e){
            e.printStackTrace ();
        }
        Class[] classesA = new Class[classes.size()];
        classes.toArray(classesA);
        return classesA;
    }else
        return null;
}

2
public static ArrayList<String> listItems(String path) throws Exception{
    InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(path);
    byte[] b = new byte[in.available()];
    in.read(b);
    String data = new String(b);
    String[] s = data.split("\n");
    List<String> a = Arrays.asList(s);
    ArrayList<String> m = new ArrayList<>(a);
    return m;
}

3
Хоча цей фрагмент коду може вирішити проблему, він не пояснює, чому або як він відповідає на питання. Додайте пояснення до свого коду , оскільки це дійсно допомагає покращити якість вашої публікації. Пам'ятайте, що ви відповідаєте на запитання читачів у майбутньому, і ці люди можуть не знати причини вашої пропозиції щодо коду.
— Самуель Філіп

дані порожні, коли ми виконуємо код з файлу jar.
— Aguid


1

Найбільш надійний механізм для перерахування всіх ресурсів на classpath - це використання даного шаблону з ClassGraph , оскільки він обробляє максимально широкий масив механізмів специфікації classpath , включаючи нову модульну систему JPMS. (Я автор ClassGraph.)

Як дізнатися ім'я файлу JAR, де живе мій основний клас?

URI mainClasspathElementURI;
try (ScanResult scanResult = new ClassGraph().whitelistPackages("x.y.z")
        .enableClassInfo().scan()) {
    mainClasspathElementURI =
            scanResult.getClassInfo("x.y.z.MainClass").getClasspathElementURI();
}

Як я можу аналогічно читати вміст каталогу у файлі JAR?

List<String> classpathElementResourcePaths;
try (ScanResult scanResult = new ClassGraph().overrideClasspath(mainClasspathElementURI)
        .scan()) {
    classpathElementResourcePaths = scanResult.getAllResources().getPaths();
}

Існує маса інших способів поводження з ресурсами .


1
Дуже приємний пакет, який легко використовувати в моєму проекті Scala, дякую.
— zslim

0

Просто інший спосіб лістингу / читання файлів з URL-адреси jar, і це робиться рекурсивно для вкладених банок

https://gist.github.com/trung/2cd90faab7f75b3bcbaa

URL urlResource = Thead.currentThread().getContextClassLoader().getResource("foo");
JarReader.read(urlResource, new InputStreamCallback() {
    @Override
    public void onFile(String name, InputStream is) throws IOException {
        // got file name and content stream 
    }
});

0

Ще один на дорогу:

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;

import static java.nio.file.FileSystems.newFileSystem;
import static java.util.Collections.emptyMap;

public class ResourceWalker {
  private static final PathMatcher FILE_MATCHER =
      FileSystems.getDefault().getPathMatcher( "glob:**.ttf" );

  public static List<Path> walk( final String directory )
      throws URISyntaxException, IOException {
    final List<Path> filenames = new ArrayList<>();
    final var resource = ResourceWalker.class.getResource( directory );

    if( resource != null ) {
      final var uri = resource.toURI();
      final var path = uri.getScheme().equals( "jar" )
          ? newFileSystem( uri, emptyMap() ).getPath( directory )
          : Paths.get( uri );
      final var walk = Files.walk( path, 10 );

      for( final var it = walk.iterator(); it.hasNext(); ) {
        final Path p = it.next();
        if( FILE_MATCHER.matches( p ) ) {
          filenames.add( p );
        }
      }
    }

    return filenames;
  }
}

Це трохи гнучкіше для підбору конкретних імен файлів, оскільки він використовує шаблони підстановки.


Більш функціональний стиль:

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.function.Consumer;

import static java.nio.file.FileSystems.newFileSystem;
import static java.util.Collections.emptyMap;

/**
 * Responsible for finding file resources.
 */
public class ResourceWalker {
  private static final PathMatcher FILE_MATCHER =
      FileSystems.getDefault().getPathMatcher( "glob:**.ttf" );

  public static void walk( final String dirName, final Consumer<Path> f )
      throws URISyntaxException, IOException {
    final var resource = ResourceWalker.class.getResource( dirName );

    if( resource != null ) {
      final var uri = resource.toURI();
      final var path = uri.getScheme().equals( "jar" )
          ? newFileSystem( uri, emptyMap() ).getPath( dirName )
          : Paths.get( uri );
      final var walk = Files.walk( path, 10 );

      for( final var it = walk.iterator(); it.hasNext(); ) {
        final Path p = it.next();
        if( FILE_MATCHER.matches( p ) ) {
          f.accept( p );
        }
      }
    }
  }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.