【问题标题】:Reading my own Jar's Manifest阅读我自己的罐子清单
【发布时间】:2010-11-19 08:21:53
【问题描述】:

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

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

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

我实际上想从开始的.jar 中读取Export-package 属性 Felix OSGi,所以我可以将这些包暴露给 Felix。有什么想法吗?

【问题讨论】:

  • 我认为下面的 FrameworkUtil.getBundle() 答案是最好的。它回答了您实际想要做什么(获取捆绑包的导出),而不是您询问的内容(阅读清单)。

标签: java osgi manifest.mf apache-felix


【解决方案1】:

您可以做以下两件事之一:

  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
    }
    

【讨论】:

  • 完美!我从来不知道你可以遍历同名的资源。
  • 你怎么知道类加载器只知道一个 .jar 文件? (我想在很多情况下都是如此)我更愿意使用与相关类直接相关的东西。
  • 良好做法为每个答案制作单独答案,而不是在一个答案中包含 2 个修复。单独的答案可以独立投票。
  • 请注意:我需要类似的东西,但我在 JBoss 上的 WAR 中,所以第二种方法对我不起作用。我最终得到了stackoverflow.com/a/1283496/160799 的变体
  • 第一个选项对我不起作用。我得到了我的 62 个依赖项 jar 的清单,但不是定义当前类的那个...
【解决方案2】:

为什么要包含 getClassLoader 步骤?如果你说“this.getClass().getResource()”,你应该得到相对于调用类的资源。我从来没有使用过 ClassLoader.getResource(),不过快速浏览一下 Java 文档,听起来这会让你在任何当前类路径中找到第一个同名的资源。

【讨论】:

  • 如果您的类名为“com.mypackage.MyClass”,调用class.getResource("myresource.txt") 将尝试从com/mypackage/myresource.txt 加载该资源。您将如何使用这种方法来获取清单?
  • 好吧,我得回溯了。这就是不测试的结果。我在想你可以说 this.getClass().getResource("../../META-INF/MANIFEST.MF") (但是考虑到你的包名,很多“..”是必要的。)但是虽然它适用于目录中的类文件以在目录树中向上工作,但显然不适用于 JAR。我不明白为什么不,但就是这样。 this.getClass().getResource("/META-INF/MANIFEST.MF") 也不起作用——这让我得到了 rt.jar 的清单。 (待续……)
  • 你可以做的是使用getResource找到你自己的类文件的路径,然后去掉“!”后面的所有内容。获取 jar 的路径,然后附加“/META-INF/MANIFEST.MF”。就像志宏建议的那样,所以我投票给他。
【解决方案3】:

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

【讨论】:

  • 我喜欢这个解决方案,因为它直接获取您自己的清单,而不必搜索它。
  • 可以通过移除条件检查来稍微改进classPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
  • 谁关闭了流?
  • 这在内部类中不起作用,因为getSimpleName 删除了外部类名称。这适用于内部类:clazz.getName().replace (".", "/") + ".class".
  • 需要关闭流,manifest构造函数不需要。
【解决方案4】:

我认为获取任何包(包括加载给定类的包)的清单的最合适方法是使用 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。

【讨论】:

    【解决方案5】:

    您可以使用 jcabi-manifests 中的 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

    【讨论】:

    • 非常好的库。有没有办法控制日志级别?
    • 所有 jcabi 库都通过 SLF4J 记录。您可以使用任何您希望的工具发送日志消息,例如 log4j 或 logback
    • 如果使用 logback.xml 您需要添加的行类似于&lt;logger name="com.jcabi.manifests" level="OFF"/&gt;
    • 来自同一个类加载器的多个清单重叠并相互覆盖
    【解决方案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();
    }
    

    【讨论】:

    • getCodeSource 可能会返回 null。什么是标准,这将起作用? documentation 没有解释这一点。
    • DataUtilities 是从哪里导入的?它似乎不在 JDK 中。
    【解决方案7】:

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

    所以我的 MANIFEST.MF 文件包含如下键:

    Mykey:

    然后在激活器或其他类中,您可以使用 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();
    

    【讨论】:

      【解决方案8】:
        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 清单?它将采取第一个不是您需要的。我的代码解决了这个问题,它确实有效。
      • 我没有批评您使用类加载器加载特定资源的方式。我指出classLoader.getResource(..)url.openStream() 之间的所有代码都完全不相关且容易出错,因为它试图与classLoader.getResourceAsStream(..) 做同样的事情。
      • 不。它是不同的。我的代码从类所在的特定 jar 中获取清单,而不是从类路径中的第一个 jar 中获取清单。
      • 你的“jar具体加载代码”相当于下面两行:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
      【解决方案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() 的结果可以有反斜杠吗?
      【解决方案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:/ 的路径,所以应该另外处理。

      【讨论】:

      • 这是迄今为止最简单、最干净的方法。
      【解决方案11】:

      我会预先承认,这个答案并没有回答最初的问题,即通常能够访问清单的问题。但是,如果真正需要的是读取多个“标准”清单属性之一,则以下解决方案比上面发布的解决方案要简单得多。所以希望版主允许。请注意,此解决方案使用 Kotlin,而不是 Java,但我希望移植到 Java 将是微不足道的。 (虽然我承认我不知道“.`package`”的Java等价物。

      在我的例子中,我想读取属性“Implementation-Version”,所以我从上面给出的解决方案开始获取流,然后读取它以获取值。虽然这个解决方案有效,但审查我的代码的同事向我展示了一种更简单的方法来做我想做的事。请注意,此解决方案使用 Kotlin,而不是 Java。

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

      再次注意,这并没有回答原始问题,特别是“导出包”似乎不是受支持的属性之一。也就是说,有一个返回值的 myPackage.name。也许比我更了解这一点的人可以评论这是否会返回原始发布者要求的值。

      【讨论】:

      • 确实,java端口很简单:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
      • 事实上这就是我想要的。我也很高兴 Java 也有等价物。
      【解决方案12】:

      我有一个奇怪的解决方案,它在嵌入式 Jetty 服务器中运行战争应用程序,但这些应用程序还需要在标准 Tomcat 服务器上运行,而且我们在 manfest 中有一些特殊属性。

      问题是在Tomcat中,manifest可以被读取,但是在jetty中,一个随机manifest被拾取(错过了特殊属性)

      根据 Alex Konshin 的回答,我想出了以下解决方案(输入流随后用于 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;
          }
      }
      

      【讨论】:

        【解决方案13】:

        执行此操作的更简单方法是使用 getPackage()。 例如,要获取 Implementation-Version:

        Application.class.getPackage().getImplementationVersion()
        

        【讨论】:

          猜你喜欢
          • 2016-09-15
          • 2016-11-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-08-29
          • 1970-01-01
          • 2016-11-30
          • 2022-01-13
          相关资源
          最近更新 更多