Чтение моего собственного Jar's Manifest

Мне нужно прочитать файл Manifest, который доставил мой класс, но когда я использую:

getClass().getClassLoader().getResources(...)

Я получаю MANIFEST из первого .jar, загруженного в Java Runtime.
Мое приложение будет запускаться из апплета или веб-сайта,
поэтому, полагаю, у меня не будет доступа к моему собственному .jar файлу.

На самом деле я хочу прочитать атрибут Export-package из .jar, который запустил Felix OSGi, чтобы я мог предоставить Felix эти пакеты. Любые идеи?


person Houtman    schedule 13.08.2009    source источник
comment
Я думаю, что ответ FrameworkUtil.getBundle() ниже является лучшим. Он отвечает на то, что вы на самом деле хотите сделать (получить экспорт пакета), а не на то, что вы просили (прочитать манифест).   -  person Chris Dolan    schedule 13.09.2012


Ответы (13)


Вы можете сделать одно из двух:

  1. Вызовите getResources() и перебирайте возвращенную коллекцию URL-адресов, считывая их как манифесты, пока не найдете свой:

    Enumeration<URL> resources = getClass().getClassLoader()
      .getResources("META-INF/MANIFEST.MF");
    while (resources.hasMoreElements()) {
        try {
          Manifest manifest = new Manifest(resources.nextElement().openStream());
          // check that this is your manifest and do what you need or get the next one
          ...
        } catch (IOException E) {
          // handle
        }
    }
    
  2. Вы можете попробовать проверить, является ли getClass().getClassLoader() экземпляром java.net.URLClassLoader. Большинство загрузчиков классов Sun, включая AppletClassLoader. Затем вы можете привести его и вызвать findResource(), который был известен - по крайней мере, для апплетов - для прямого возврата необходимого манифеста:

    URLClassLoader cl = (URLClassLoader) getClass().getClassLoader();
    try {
      URL url = cl.findResource("META-INF/MANIFEST.MF");
      Manifest manifest = new Manifest(url.openStream());
      // do stuff with it
      ...
    } catch (IOException E) {
      // handle
    }
    
person ChssPly76    schedule 13.08.2009
comment
Идеально! Я никогда не знал, что можно перебирать ресурсы с одним и тем же именем. - person Houtman; 14.08.2009
comment
Откуда вы знаете, что загрузчик классов знает только об одном файле .jar? (правда, я полагаю, во многих случаях) Я бы предпочел использовать что-то, связанное непосредственно с рассматриваемым классом. - person Jason S; 20.08.2009
comment
рекомендуется создавать отдельные ответы для каждого из них, а не включать два исправления в один ответ. За отдельные ответы можно голосовать независимо. - person Alba Mendez; 20.04.2011
comment
просто примечание: мне нужно было что-то подобное, но я нахожусь внутри WAR на JBoss, поэтому второй подход у меня не сработал. В итоге я получил вариант stackoverflow.com/a/1283496/160799. - person Gregor; 30.12.2011
comment
@chris-dolan Дал правильный ответ на этот вопрос (см. комментарий выше). - person Petr Gladkikh; 24.07.2013
comment
+1; Кстати, я только что обнаружил, что EAR/META-INF/... не включен в путь к классам среды выполнения (в Weblogic 10.3.5), поэтому его нельзя проверить как ресурс. - person robermann; 10.03.2014
comment
Спасибо за добавление проводки вокруг java.util.jar.Manifest. Я не знал об этом классе, пока не прочитал этот пост. Вероятно, я бы сам тупо/вручную проанализировал файл манифеста... - person kevinarpe; 29.05.2015
comment
Манифест дает мне довольно бесполезную пустую карту, поэтому мне все еще приходится анализировать ее вручную. Тем не менее, все еще полезный код для получения ресурса сначала. - person Gordon; 02.09.2015
comment
Первый вариант мне не подошел. Я получил манифесты моих 62 банок с зависимостями, но не тот, в котором был определен текущий класс... - person Jolta; 28.10.2016
comment
Вариант 2, похоже, перестал работать с Java 11 (возможно, Java 9). - person Matthew; 04.06.2020

Сначала вы можете найти URL своего класса. Если это JAR, вы загружаете манифест оттуда. Например,

Class clazz = MyClass.class;
String className = clazz.getSimpleName() + ".class";
String classPath = clazz.getResource(className).toString();
if (!classPath.startsWith("jar")) {
  // Class not from JAR
  return;
}
String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + 
    "/META-INF/MANIFEST.MF";
Manifest manifest = new Manifest(new URL(manifestPath).openStream());
Attributes attr = manifest.getMainAttributes();
String value = attr.getValue("Manifest-Version");
person ZZ Coder    schedule 13.08.2009
comment
Мне нравится это решение, так как оно получает ваш собственный манифест напрямую, а не ищет его. - person Jay; 14.08.2009
comment
можно немного улучшить, удалив проверку условия classPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF" - person Jay; 26.03.2013
comment
Кто закрывает поток? - person ceving; 03.12.2013
comment
Это не работает во внутренних классах, потому что getSimpleName удаляет имя внешнего класса. Это будет работать для внутренних классов: clazz.getName().replace (".", "/") + ".class". - person ceving; 03.12.2013
comment
Вам нужно закрыть поток, конструктор манифеста этого не делает. - person BrianT.; 01.07.2015
comment
Обратите внимание, что функции attr.containsKey(Manifest-Version) и attr.get(Manifest-Version) вернут значение null, поскольку этим функциям требуются аргументы внутреннего класса Attribute.Name. К сожалению, функции принимают Object... поэтому нет ошибки времени компиляции при присвоении ему String, но он вернет нулевое значение. Поэтому придерживайтесь Attribute.getValue(), как показано в примере выше. Attributes.getValue() явно принимает строку, а затем оборачивает ее в новый атрибут Attributes.Name для вас. (Я знаю, что это потому, что Attributes реализует Map, но, на мой взгляд, это просто небезопасное кодирование API.) - person Chris Janicki; 07.01.2018

Вы можете использовать Manifests из jcabi-manifests и прочитать любой атрибут из любого доступного файла MANIFEST.MF всего одной строкой:

String value = Manifests.read("My-Attribute");

Единственная зависимость, которая вам нужна:

<dependency>
  <groupId>com.jcabi</groupId>
  <artifactId>jcabi-manifests</artifactId>
  <version>0.7.5</version>
</dependency>

Кроме того, дополнительные сведения см. в этом сообщении в блоге: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

person yegor256    schedule 30.12.2012
comment
Очень хорошие библиотеки. Есть ли способ контролировать уровень журнала? - person assylias; 31.08.2013
comment
Все библиотеки jcabi регистрируются через SLF4J. Вы можете отправлять сообщения журнала, используя любое средство, которое пожелаете, например log4j или logback. - person yegor256; 01.09.2013
comment
если вы используете logback.xml, вам нужно добавить строку, похожую на <logger name="com.jcabi.manifests" level="OFF"/> - person driftcatcher; 31.07.2015
comment
Несколько манифестов из одного и того же загрузчика классов перекрываются и перезаписывают друг друга - person guai; 26.04.2018

Я сразу признаю, что этот ответ не отвечает на первоначальный вопрос о возможности доступа к манифесту. Однако, если действительно требуется прочитать один из ряда «стандартных» атрибутов манифеста, следующее решение намного проще, чем те, которые были опубликованы выше. Так что надеюсь модератор разрешит. Обратите внимание, что это решение находится в Kotlin, а не в Java, но я ожидаю, что перенос на Java будет тривиальным. (Хотя я признаю, что не знаю Java-эквивалент «.`package`».

В моем случае я хотел прочитать атрибут «Версия реализации», поэтому я начал с приведенных выше решений, чтобы получить поток, а затем прочитал его, чтобы получить значение. Хотя это решение работало, коллега, просматривавший мой код, показал мне более простой способ сделать то, что я хотел. Обратите внимание, что это решение находится в Kotlin, а не в Java.

val myPackage = MyApplication::class.java.`package`
val implementationVersion = myPackage.implementationVersion

Еще раз обратите внимание, что это не отвечает на исходный вопрос, в частности, «Экспорт-пакет», похоже, не является одним из поддерживаемых атрибутов. Тем не менее, есть myPackage.name, который возвращает значение. Возможно, кто-то, кто понимает это больше, чем я, может прокомментировать, возвращает ли это значение, которое запрашивает исходный постер.

person Steven W. Klassen    schedule 23.03.2018
comment
Действительно, порт java прост: String implementationVersion = MyApplication.class.getPackage().getImplementationVersion(); - person Ian Robertson; 29.11.2018
comment
На самом деле это то, что я искал. Я также рад, что у Java есть эквивалент. - person Aleksander Stelmaczonek; 15.01.2019

Я считаю, что наиболее подходящий способ получить манифест для любого пакета (включая пакет, который загрузил данный класс) — это использовать объект Bundle или BundleContext.

// If you have a BundleContext
Dictionary headers = bundleContext.getBundle().getHeaders();

// If you don't have a context, and are running in 4.2
Bundle bundle = FrameworkUtil.getBundle(this.getClass());
bundle.getHeaders();

Обратите внимание, что объект Bundle также предоставляет getEntry(String path) для поиска ресурсов, содержащихся в определенном пакете, вместо поиска по всему пути к классам этого пакета.

В общем, если вам нужна информация по конкретному пакету, не полагайтесь на предположения о загрузчиках классов, просто используйте API-интерфейсы OSGi напрямую.

person Anthony Juckel    schedule 16.08.2009

Самый простой способ — использовать класс JarURLConnection:

String className = getClass().getSimpleName() + ".class";
String classPath = getClass().getResource(className).toString();
if (!classPath.startsWith("jar")) {
    return DEFAULT_PROPERTY_VALUE;
}

URL url = new URL(classPath);
JarURLConnection jarConnection = (JarURLConnection) url.openConnection();
Manifest manifest = jarConnection.getManifest();
Attributes attributes = manifest.getMainAttributes();
return attributes.getValue(PROPERTY_NAME);

Поскольку в некоторых случаях ...class.getProtectionDomain().getCodeSource().getLocation(); дает путь с vfs:/, это нужно обрабатывать дополнительно.

person ayurchuk    schedule 17.02.2016
comment
Это, безусловно, самый простой и чистый способ сделать это. - person walen; 04.07.2019

Следующий код работает с несколькими типами архивов (jar, war) и несколькими типами загрузчиков классов (jar, url, vfs,...)

  public static Manifest getManifest(Class<?> clz) {
    String resource = "/" + clz.getName().replace(".", "/") + ".class";
    String fullPath = clz.getResource(resource).toString();
    String archivePath = fullPath.substring(0, fullPath.length() - resource.length());
    if (archivePath.endsWith("\\WEB-INF\\classes") || archivePath.endsWith("/WEB-INF/classes")) {
      archivePath = archivePath.substring(0, archivePath.length() - "/WEB-INF/classes".length()); // Required for wars
    }

    try (InputStream input = new URL(archivePath + "/META-INF/MANIFEST.MF").openStream()) {
      return new Manifest(input);
    } catch (Exception e) {
      throw new RuntimeException("Loading MANIFEST for class " + clz + " failed!", e);
    }
  }
person muellair    schedule 17.03.2015
comment
может ли результат clz.getResource(resource).toString() иметь обратную косую черту? - person basin; 06.02.2018

Вы можете использовать getProtectionDomain().getCodeSource() следующим образом:

URL url = Menu.class.getProtectionDomain().getCodeSource().getLocation();
File file = DataUtilities.urlToFile(url);
JarFile jar = null;
try {
    jar = new JarFile(file);
    Manifest manifest = jar.getManifest();
    Attributes attributes = manifest.getMainAttributes();
    return attributes.getValue("Built-By");
} finally {
    jar.close();
}
person Uto    schedule 29.03.2013
comment
getCodeSource может вернуть null. Каковы критерии, что это будет работать? документация не объясняет этого. - person ceving; 03.12.2013
comment
Откуда импортируется DataUtilities? Кажется, его нет в JDK. - person Jolta; 28.10.2016

Почему вы включаете шаг getClassLoader? Если вы скажете «this.getClass().getResource()», вы должны получать ресурсы относительно вызывающего класса. Я никогда не использовал ClassLoader.getResource(), хотя при беглом просмотре документов Java кажется, что это даст вам первый ресурс с таким именем, найденный в любом текущем пути к классам.

person Jay    schedule 13.08.2009
comment
Если ваш класс называется com.mypackage.MyClass, вызов class.getResource("myresource.txt") попытается загрузить этот ресурс из com/mypackage/myresource.txt. Как именно вы собираетесь использовать этот подход для получения манифеста? - person ChssPly76; 13.08.2009
comment
Ладно, я должен вернуться. Вот что получается, если не тестировать. Я подумал, что вы могли бы сказать this.getClass().getResource(../../META-INF/MANIFEST.MF) (однако много .. необходимо, учитывая имя вашего пакета.) Но пока это работает для класса файлов в каталоге, чтобы подняться вверх по дереву каталогов, это, по-видимому, не работает для JAR. Не понимаю, почему нет, но это так. Также не работает this.getClass().getResource(/META-INF/MANIFEST.MF) — это дает мне манифест для rt.jar. (Продолжение следует ...) - person Jay; 14.08.2009
comment
Что вы можете сделать, так это использовать getResource, чтобы найти путь к вашему собственному файлу класса, а затем удалить все после ! чтобы получить путь к банке, добавьте /META-INF/MANIFEST.MF. Как предложил Чжихун, так что я голосую за него. - person Jay; 14.08.2009

Более простой способ сделать это — использовать getPackage(). Например, чтобы получить версию реализации:

Application.class.getPackage().getImplementationVersion()
person Dave B    schedule 18.11.2020

Я использовал решение Энтони Юкеля, но в MANIFEST.MF ключ должен начинаться с прописных букв.

Итак, мой файл MANIFEST.MF содержит такой ключ, как:

Mykey: значение

Затем в активаторе или другом классе вы можете использовать код от Энтони, чтобы прочитать файл MANIFEST.MF и значение, которое вам нужно.

// If you have a BundleContext 
Dictionary headers = bundleContext.getBundle().getHeaders();

// If you don't have a context, and are running in 4.2 
Bundle bundle = `FrameworkUtil.getBundle(this.getClass()); 
bundle.getHeaders();
person user2935659    schedule 02.01.2014

У меня есть это странное решение, которое запускает военные приложения на встроенном сервере Jetty, но эти приложения также должны работать на стандартных серверах Tomcat, и у нас есть некоторые специальные свойства в манфесте.

Проблема заключалась в том, что в Tomcat манифест можно было прочитать, а в причале подхватывался случайный манифест (который пропускал специальные свойства)

Основываясь на ответе Алекса Коншина, я придумал следующее решение (затем входной поток используется в классе манифеста):

private static InputStream getWarManifestInputStreamFromClassJar(Class<?> cl ) {
    InputStream inputStream = null;
    try {
        URLClassLoader classLoader = (URLClassLoader)cl.getClassLoader();
        String classFilePath = cl.getName().replace('.','/')+".class";
        URL classUrl = classLoader.getResource(classFilePath);
        if ( classUrl==null ) return null;
        String classUri = classUrl.toString();
        if ( !classUri.startsWith("jar:") ) return null;
        int separatorIndex = classUri.lastIndexOf('!');
        if ( separatorIndex<=0 ) return null;
        String jarManifestUri = classUri.substring(0,separatorIndex+2);
        String containingWarManifestUri = jarManifestUri.substring(0,jarManifestUri.indexOf("WEB-INF")).replace("jar:file:/","file:///") + MANIFEST_FILE_PATH;
        URL url = new URL(containingWarManifestUri);
        inputStream = url.openStream();
        return inputStream;
    } catch ( Throwable e ) {
        // handle errors
        LOGGER.warn("No manifest file found in war file",e);
        return null;
    }
}
person GriffoGoes    schedule 03.08.2018

person    schedule
comment
В этом ответе используется очень сложный и подверженный ошибкам способ загрузки манифеста. гораздо более простым решением является использование cl.getResourceAsStream("META-INF/MANIFEST.MF"). - person Robert; 26.07.2017
comment
Вы пробовали? Какой манифест jar он получит, если у вас есть несколько jar в пути к классам? Он возьмет первое, что не то, что вам нужно. Мой код решает эту проблему, и он действительно работает. - person Alex Konshin; 27.07.2017
comment
Я не критиковал то, как вы используете загрузчик классов для загрузки определенного ресурса. Я указывал, что весь код между classLoader.getResource(..) и url.openStream() совершенно неуместен и подвержен ошибкам, поскольку пытается делать то же самое, что и classLoader.getResourceAsStream(..). - person Robert; 27.07.2017
comment
Неа. Это отличается. Мой код принимает манифест из конкретной банки, в которой находится класс, а не из первой банки в пути к классам. - person Alex Konshin; 29.07.2017
comment
Ваш специальный код загрузки jar эквивалентен следующим двум строкам: ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF")); - person Robert; 29.07.2017
comment
действительно, это решение можно реорганизовать, но оно работает. Решение с двумя вкладышами, предложенное Робертом, заканчивается NullPointerException в весенних загрузочных приложениях. - person aprodan; 11.10.2017