阅读自己的Jar文件清单

152

我需要读取Manifest文件,该文件包含我的类信息。但是当我使用以下代码:

getClass().getClassLoader().getResources(...)
我从Java Runtime中加载第一个.jar文件获取MANIFEST。 我的应用将会在applet或者webstart中运行, 所以我猜我无法访问自己的.jar文件。 我实际上想要读取启动Felix OSGi的.jar文件中的Export-package属性,这样我就可以将这些包暴露给Felix。有什么想法吗?

3
我认为下面的FrameworkUtil.getBundle()答案是最好的。它回答了你实际想要做的事情(获取bundle的exports),而不是你提出的问题(读取清单)。 - Chris Dolan
14个回答

133

你可以选择以下两种方式之一:

  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
        }
    }
    
    你可以尝试检查 getClass().getClassLoader() 是否是 java.net.URLClassLoader 的实例。大多数 Sun 的类加载器都是,包括 AppletClassLoader。然后你可以将其强制转换并调用 findResource() 方法,这个方法已经被知道 - 至少对于 applets 来说 - 直接返回所需的清单:
  2. 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
9
将两个修复方案分别列出来,而不是将它们放在同一个答案中,这是一个好的做法。独立的答案可以被单独投票。 - Alba Mendez
+1;顺便说一句,我刚刚发现 EAR/META-INF/... 没有包含在运行时类路径中(在Weblogic 10.3.5上),因此无法作为资源进行检查。 - robermann
感谢在 java.util.jar.Manifest 周围添加连线。在阅读了那篇文章之前,我不知道这个类。我可能会愚蠢地/手动解析 Manifest 文件... - kevinarpe
1
第一种选项对我没用。我得到了62个依赖jar包的清单,但没有定义当前类的那个... - Jolta
显示剩余5条评论

125

首先你需要找到你的类所在的URL地址。如果是一个JAR文件,那么你可以从中加载manifest文件。例如:

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");

我喜欢这个解决方案,因为它可以直接获取你自己的清单,而不需要搜索它。 - Jay
3
可以通过删除条件检查 classPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF" 来稍微改进一下。 - Jay
2
谁关闭了流? - ceving
1
这在内部类中不起作用,因为 getSimpleName 会删除外部类的名称。下面的方法可用于内部类:clazz.getName().replace(".", "/") + ".class" - ceving
3
你需要关闭流,而清单构造函数不会这样做。 - BrianT.
请注意,函数attr.containsKey("Manifest-Version")和attr.get("Manifest-Version")将返回null,因为这些函数需要内部类Attribute.Name的参数。不幸的是,这些函数接受Object...,所以当给它一个String时,没有编译时错误,但它将返回一个null值。因此,请使用上面示例中显示的Attribute.getValue()。Attributes.getValue()明确接受一个String,然后为您将其包装在新的Attributes.Name中。(我知道这是因为Attributes实现了Map,但在我看来,这不是安全的API编码。) - Chris Janicki

22
你可以使用来自jcabi-manifest的Manifests,并且只需一行代码即可读取任何可用MANIFEST.MF文件中的任何属性: Manifestsjcabi-manifests
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"/> - floer32
1
同一类加载器中的多个清单重叠并互相覆盖。 - guai

21
我要提前承认,这个答案并没有回答原始问题,即通常如何访问清单。然而,如果真正需要的是读取一些“标准”清单属性中的一个,那么下面的解决方案比上面发布的解决方案要简单得多。请注意,这个解决方案是用Kotlin编写的,而不是Java,但我预计将其转换为Java应该很简单。(尽管我承认我不知道Java中“.`package`”的等效方式。)
在我的情况下,我想要读取属性“Implementation-Version”,所以我首先使用上面给出的解决方案获取流,然后读取它以获取值。虽然这个解决方案有效,但是我的一个同事在审查我的代码时向我展示了一个更简单的方法来实现我想要的功能。请注意,这个解决方案是用Kotlin编写的,而不是Java。
val myPackage = MyApplication::class.java.`package`
val implementationVersion = myPackage.implementationVersion

再次注意,这并没有回答原始问题,特别是"Export-package"似乎不是受支持的属性之一。话虽如此,有一个返回值的myPackage.name。也许比我更了解这个问题的人可以评论一下,这是否返回了原始帖子所请求的值。

10
确实,Java端口很简单:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion(); - Ian Robertson
事实上,这正是我正在寻找的。我也很高兴Java也有一个相当的等价物。 - Aleksander Stelmaczonek

14

最简单的方法是使用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

12
以下代码可与多种类型的档案文件(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() 的结果可能会有反斜杠吗? - basin

11

我认为获取任何捆绑包的清单(包括加载给定类的捆绑包)的最合适方法是使用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)方法来查找特定bundle中包含的资源,而不是搜索该bundle的整个类路径。
通常情况下,如果您想获取特定bundle的信息,请不要依赖于类加载器的假设,直接使用OSGi API。

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
4
DataUtilities 是从哪里导入的?它似乎不在 JDK 中。 - Jolta

6
更简单的方法是使用 getPackage()。例如,要获取 Implementation-Version:
Application.class.getPackage().getImplementationVersion()

1
这并没有具体回答原问题,但却正是我所寻找的。 - Greg Brown

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") - Robert
你试过了吗?如果类路径中有多个JAR文件,它会获取哪个JAR清单?它将获取第一个不符合您需求的。我的代码解决了这个问题,而且确实有效。 - Alex Konshin
我并没有批评你使用类加载器来加载特定资源的方式。我只是指出,在classLoader.getResource(..)url.openStream()之间的所有代码都是完全无关紧要且容易出错的,因为它试图做与classLoader.getResourceAsStream(..)相同的事情。 - Robert
不是的。我的代码会从包含该类的特定jar中获取清单,而不是从类路径中的第一个jar获取。 - Alex Konshin
你的“特定于jar加载代码”等同于以下两行代码:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF")); - Robert
确实,这个解决方案可以重构,但它是有效的。Robert提出的两行代码解决方案在Spring Boot应用程序中最终会导致NullPointerException。 - aprodan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接