Як перелічити файли всередині файлу 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.