Чтение манифеста моей собственной банки


135

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

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

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

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


3
Я думаю, что ответ FrameworkUtil.getBundle () ниже - лучший. Он отвечает на то, что вы действительно хотите сделать (получить экспорт пакета), а не на то, что вы просили (прочтите манифест).
Крис Долан,

Ответы:


117

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

  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
    }

5
Отлично! Я никогда не знал, что можно перебирать ресурсы с одинаковыми именами.
Houtman

Откуда вы знаете, что загрузчик классов знает только об одном файле .jar? (я полагаю, что это правда во многих случаях) Я бы предпочел использовать что-то, связанное непосредственно с рассматриваемым классом.
Jason S

7
это хорошая практика , чтобы сделать отдельные ответы для каждого из них, а в том числе 2 исправления в одном ответе. Отдельные ответы можно проголосовать независимо.
Альба Мендес,

просто примечание: мне нужно что-то подобное, но я нахожусь в WAR на JBoss, поэтому второй подход у меня не сработал. Я закончил с вариантом stackoverflow.com/a/1283496/160799
Грегор

1
Первый вариант мне не подошел. Я получил манифесты своих 62 jar-файлов зависимостей, но не тот, в котором был определен текущий класс ...
Джолта

120

Сначала вы можете найти 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");

Мне нравится это решение, так как оно напрямую получает ваш собственный манифест, а не ищет его.
Джей,

1
можно немного улучшить, убрав проверку условийclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Джей

2
Кто закрывает поток?
откр 03

1
Это не работает во внутренних классах, потому что getSimpleNameудаляет имя внешнего класса. Это будет работать для внутренних классов: clazz.getName().replace (".", "/") + ".class".
откр 03

3
Вам нужно закрыть поток, конструктор манифеста - нет.
BrianT.

21

Вы можете использовать Manifestsfrom jcabi-manifestests и прочитать любой атрибут из любого из доступных файлов 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


Очень хорошие библиотеки. Есть ли способ контролировать уровень журнала?
assylias

1
Все библиотеки jcabi регистрируются через SLF4J. Вы можете отправлять сообщения журнала, используя любое средство по вашему желанию, например log4j или logback
yegor256 01

при использовании logback.xml строка, которую вам нужно добавить, выглядит как<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher

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

13

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

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

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

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


4
Действительно, порт java прост:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ян Робертсон,

На самом деле это то, что я искал. Я также рад, что у Java есть аналог.
Александр Стельмацонек

12

Я считаю, что наиболее подходящий способ получить манифест для любого пакета (включая пакет, который загрузил данный класс) - использовать объект 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 напрямую.


10

Самый простой способ - использовать класс 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:/, с этим следует обращаться дополнительно.


Это, безусловно, самый простой и чистый способ сделать это.
walen 04

9

Следующий код работает с несколькими типами архивов (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);
    }
  }

может результат clz.getResource(resource).toString()иметь обратную косую черту?
бассейн

6

Вы можете использовать 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();
}

1
getCodeSourceможет вернуться null. По каким критериям это сработает? В документации это не объясняется.
откр. 03

4
Откуда DataUtilitiesпривозят? Похоже, этого нет в JDK.
Jolta

2

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


Если ваш класс назван "com.mypackage.MyClass", при вызове class.getResource("myresource.txt")будет предпринята попытка загрузить этот ресурс из com/mypackage/myresource.txt. Как именно вы собираетесь использовать этот подход для получения манифеста?
ChssPly76

1
Ладно, мне нужно отступить. Вот что получается из-за отсутствия тестирования. Я думал, что вы могли бы сказать this.getClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Однако, учитывая имя вашего пакета, необходимо много ".."). Но пока это работает для файлов классов в каталоге, чтобы продвигаться вверх по дереву каталогов, по-видимому, он не работает для JAR. Не понимаю, почему нет, но так оно и есть. Также не работает this.getClass (). GetResource ("/ META-INF / MANIFEST.MF") - это дает мне манифест для rt.jar. (Продолжение следует ...)
Джей

Что вы можете сделать, так это использовать getResource, чтобы найти путь к вашему собственному файлу класса, а затем удалить все после символа "!" чтобы получить путь к банке, добавьте "/META-INF/MANIFEST.MF". Как и предложил Чжихун, я голосую за него.
Джей,

1
  public static Manifest getManifest( 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 manifestUri = classUri.substring(0,separatorIndex+2)+"META-INF/MANIFEST.MF";
      URL url = new URL(manifestUri);
      inputStream = url.openStream();
      return new Manifest( inputStream );
    } catch ( Throwable e ) {
      // handle errors
      ...
      return null;
    } finally {
      if ( inputStream!=null ) {
        try {
          inputStream.close();
        } catch ( Throwable e ) {
          // ignore
        }
      }
    }
  }

В этом ответе используется очень сложный и подверженный ошибкам способ загрузки манифеста. гораздо более простое решение - использовать cl.getResourceAsStream("META-INF/MANIFEST.MF").
Роберт

Вы пробовали это? Какой манифест jar он получит, если у вас есть несколько jar-файлов в пути к классам? Потребуется первая, которая вам не нужна. Мой код решает эту проблему, и он действительно работает.
Alex Konshin

Я не критиковал то, как вы использовали загрузчик классов для загрузки определенного ресурса. Я указывал, что весь код между classLoader.getResource(..)и url.openStream()совершенно неуместен и подвержен ошибкам, поскольку он пытается делать то же самое, что и classLoader.getResourceAsStream(..)делает.
Роберт

Нет. Это отличается. Мой код берет манифест из конкретной банки, в которой расположен класс, а не из первой банки в пути к классам.
Alex Konshin

Ваш «код загрузки для конкретной банки» эквивалентен следующим двум строкам:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Роберт

0

Я использовал решение Anthony Juckel, но в 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();

0

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

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

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

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;
    }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.