读我自己的Jar清单


133

我需要阅读该Manifest文件,该文件提供了我的课程,但是当我使用时:

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

MANIFEST从第一个.jar加载到Java运行时中就得到了。
我的应用程序将通过applet或Webstart运行,
所以我无法访问自己的.jar文件。

我实际上是想Export-package.jar启动Felix OSGi的中读取属性,因此可以将这些包公开给Felix。有任何想法吗?


3
我认为下面的FrameworkUtil.getBundle()答案是最好的。它回答的是您实际要执行的操作(获取捆绑包的导出信息),而不是您的要求(阅读清单)。
克里斯·多兰

Answers:


117

您可以执行以下两项操作之一:

  1. 调用getResources()并遍历返回的URL集合,将它们作为清单读取,直到找到您的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()已知的(至少对于applet而言)直接返回所需的清单:

    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
完善!我不知道您可以遍历具有相同名称的资源。
豪特曼

您如何知道类加载器仅知道单个.jar文件?(我想在很多情况下是正确的)我宁愿使用直接与所讨论的类相关联的东西。
杰森S

7
这是一个很好的做法,使不同的答案,为每一个,而不是包括一个答案的2个修复。单独的答案可以独立投票。
阿尔巴·门德斯

只是一个注释:我需要类似的东西,但是我在JBoss的WAR中,所以第二种方法对我不起作用。我最终得到了stackoverflow.com/a/1283496/160799的
Gregor,

1
第一种选择对我不起作用。我得到了62个依赖关系jar的清单,但没有定义当前类的
那个

119

您可以先找到课程的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"
Jay

2
谁关闭了流?
ceving 2013年

1
这在内部类中不起作用,因为getSimpleName删除了外部类名称。这将适用于内部类:clazz.getName().replace (".", "/") + ".class"
ceving 2013年

3
您需要关闭流,而清单构造器则不需要。
BrianT。

21

您可以Manifestsjcabi-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


非常好的图书馆。有没有办法控制日志级别?
assylias

1
所有jcabi库都通过SLF4J记录。您可以使用任何所需的功能来分发日志消息,例如log4j或logback
yegor256

如果使用logback.xml您需要添加行就像是<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher

1
来自同一个类加载器的多个清单重叠并且彼此覆盖
guai

13

我将首先承认这个答案不能回答最初的问题,即通常能够访问清单的问题。但是,如果真正需要读取许多“标准”清单属性之一,则以下解决方案比上面发布的解决方案简单得多。所以我希望主持人允许。请注意,此解决方案是在Kotlin中而不是Java中进行的,但是我希望Java的移植是微不足道的。(尽管我承认我不知道Java的“ .`package`”。

在我的情况下,我想读取属性“ Implementation-Version”,因此我从上面给出的解决方案开始获取流,然后对其进行读取以获取值。在此解决方案有效的同时,一位同事检查了我的代码,向我展示了一种更轻松的方法来完成我想要的事情。请注意,此解决方案位于Kotlin中,而不是Java中。

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

再次注意,这并不能回答原始问题,特别是“ Export-package”似乎不是受支持的属性之一。也就是说,有一个myPackage.name返回一个值。也许有人比我理解得更多,所以我无法评论这是否返回了原始发布者所要求的值。


4
实际上,java端口非常简单:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson

这就是我一直在寻找的事实。我也很高兴Java也具有等效功能。
Aleksander Stelmaczonek

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)了查找特定捆绑软件中包含的资源的功能,而不是搜索该捆绑软件的整个类路径。

通常,如果您想要特定于包的信息,则不要依赖于有关类加载器的假设,只需直接使用OSGi API。


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()反斜线吗?
盆地

9

最简单的方法是使用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:/,所以应该另外处理。


到目前为止,这是最简单,最干净的方法。
沃伦

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。有什么标准可以起作用?该文档没有对此进行解释。
ceving 2013年

4
从哪里DataUtilities进口?它似乎不在JDK中。
Jolta

2

为什么要包括getClassLoader步骤?如果您说“ this.getClass()。getResource()”,则应该获取相对于调用类的资源。我从未使用过ClassLoader.getResource(),尽管快速浏览一下Java Docs听起来会为您提供在任何当前类路径中找到的具有该名称的第一个资源。


如果您的类名为“ 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的清单。(待续...)
周杰伦2009年

您可以做的是使用getResource查找到您自己的类文件的路径,然后删除“!”之后的所有内容。获取jar的路径,然后附加“ /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")
罗伯特

你试过了吗?如果您在classpath中有多个jar,它将得到什么jar清单?它将采用您不需要的第一个。我的代码解决了这个问题,并且确实有效。
Alex Konshin

我没有批评您如何使用类加载器加载特定资源。我指出的是,classLoader.getResource(..)和之间的所有代码url.openStream()都是完全不相关的,并且容易出错,因为它试图做到与之相同classLoader.getResourceAsStream(..)
罗伯特

不。它是不同的。我的代码从类所在的特定jar中提取清单,而不是从classpath中的第一个jar中提取清单。
Alex Konshin

您的“特定于jar的加载代码”等效于以下两行:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
罗伯特·罗伯特(Robert Robert)

0

我使用了Anthony Juckel的解决方案,但在MANIFEST.MF中,密钥必须以大写字母开头。

所以我的MANIFEST.MF文件包含一个像这样的密钥:

我的钥匙:价值

然后,在激活器或其他类中,您可以使用Anthony的代码来读取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服务器中运行war应用程序,但是这些应用程序也需要在标准Tomcat服务器上运行,并且清单中有一些特殊的属性。

问题是,在Tomcat中时,可以读取清单,但是在码头中时,会拾取随机清单(错过了特殊属性)

根据Alex Konshin的回答,我提出了以下解决方案(然后在Manifest类中使用inputstream):

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;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.