Leyendo mi propio Manifiesto de Jar

134

Necesito leer el Manifestarchivo, que entregó mi clase, pero cuando uso:

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

Obtengo el MANIFESTprimero .jarcargado en Java Runtime. Supongo que
mi aplicación se ejecutará desde un applet o un inicio web,
por lo que no tendré acceso a mi propio .jararchivo.

De hecho, quiero leer el Export-packageatributo del .jarque inició Felix OSGi, para poder exponer esos paquetes a Felix. ¿Algunas ideas?

Houtman
fuente
3
Creo que la respuesta FrameworkUtil.getBundle () a continuación es la mejor. Responde lo que realmente quieres hacer (obtener las exportaciones del paquete) en lugar de lo que pediste (lee el manifiesto).
Chris Dolan

Respuestas:

117

Puedes hacer una de las dos cosas:

  1. Llame getResources()e itere a través de la colección devuelta de URL, leyéndolas como manifiestos hasta que encuentre la suya:

    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. Puede intentar verificar si getClass().getClassLoader()es una instancia de java.net.URLClassLoader. La mayoría de los cargadores de clase Sun son, incluso AppletClassLoader. Luego puede emitirlo y llamar a lo findResource()que se conoce, al menos para los applets, para devolver el manifiesto necesario directamente:

    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
    }
    
ChssPly76
fuente
55
¡Perfecto! Nunca supe que podrías recorrer los recursos con el mismo nombre.
Houtman
¿Cómo sabes que el cargador de clases solo conoce un único archivo .jar? (cierto en muchos casos, supongo) Preferiría usar algo asociado directamente con la clase en cuestión.
Jason S
77
Es una buena práctica hacer respuestas separadas para cada una, en lugar de incluir las 2 soluciones en una sola respuesta. Las respuestas separadas se pueden votar de forma independiente.
Alba Mendez
solo una nota: necesitaba algo similar pero estoy dentro de un WAR en JBoss, por lo que el segundo enfoque no funcionó para mí. Terminé con una variante de stackoverflow.com/a/1283496/160799
Gregor
1
La primera opción no funcionó para mí. Obtuve los manifiestos de mis 62 frascos de dependencia, pero no el que definió la clase actual ...
Jolta
120

Puedes encontrar la URL de tu clase primero. Si es un JAR, entonces carga el manifiesto desde allí. Por ejemplo,

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");
Codificador ZZ
fuente
Me gusta esta solución, ya que obtiene su propio manifiesto directamente en lugar de tener que buscarlo.
Jay
1
puede mejorarse un poco eliminando la verificación de condiciónclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Jay
2
¿Quién cierra la corriente?
abandonando el
1
Esto no funciona en las clases internas, porque getSimpleNameelimina el nombre de la clase externa. Esto funcionará para las clases internas: clazz.getName().replace (".", "/") + ".class".
abandono el
3
Necesita cerrar la secuencia, el constructor de manifiesto no.
BrianT.
21

Se puede utilizar Manifestsdesde jcabi manifiestos y leer cualquier atributo de cualquiera de los archivos disponibles MANIFEST.MF con una sola línea:

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

La única dependencia que necesita es:

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

Además, consulte esta publicación de blog para obtener más detalles: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

yegor256
fuente
Muy buenas bibliotecas. ¿Hay alguna manera de controlar el nivel de registro?
Assylias
1
Todas las bibliotecas jcabi se registran a través de SLF4J. Puede enviar mensajes de registro utilizando cualquier recurso que desee, por ejemplo log4j o logback
yegor256
si usa logback.xml, la línea que necesita agregar es como<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher
1
Múltiples manifiestos de la misma superposición cargador de clases y sobrescribir el uno al otro
guai
13

Admitiré de antemano que esta respuesta no responde a la pregunta original, la de poder acceder en general al Manifiesto. Sin embargo, si lo que realmente se requiere es leer uno de varios atributos de Manifiesto "estándar", la siguiente solución es mucho más simple que las publicadas anteriormente. Así que espero que el moderador lo permita. Tenga en cuenta que esta solución está en Kotlin, no en Java, pero esperaría que un puerto a Java fuera trivial. (Aunque admito que no sé el equivalente Java de ".`package".

En mi caso, quería leer el atributo "Implementation-Version", así que comencé con las soluciones dadas anteriormente para obtener el flujo y luego lo leí para obtener el valor. Si bien esta solución funcionó, un compañero de trabajo que revisó mi código me mostró una manera más fácil de hacer lo que quería. Tenga en cuenta que esta solución está en Kotlin, no en Java.

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

Una vez más, tenga en cuenta que esto no responde a la pregunta original, en particular "Export-package" no parece ser uno de los atributos admitidos. Dicho esto, hay un myPackage.name que devuelve un valor. Quizás alguien que entiende esto más de lo que puedo comentar si eso devuelve el valor que solicita el póster original.

Steven W. Klassen
fuente
44
De hecho, el puerto de Java es sencillo:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson el
De hecho, esto es lo que estaba buscando. También estoy feliz de que Java también tenga un equivalente.
Aleksander Stelmaczonek
12

Creo que la forma más adecuada de obtener el manifiesto para cualquier paquete (incluido el paquete que cargó una clase determinada) es usar el objeto Bundle o 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();

Tenga en cuenta que el objeto Bundle también proporciona la getEntry(String path)búsqueda de recursos contenidos dentro de un paquete específico, en lugar de buscar en todo el classpath de ese paquete.

En general, si desea información específica del paquete, no confíe en suposiciones sobre los cargadores de clases, solo use las API de OSGi directamente.

Anthony Juckel
fuente
9

El siguiente código funciona con múltiples tipos de archivos (jar, war) y múltiples tipos de cargadores de clases (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);
    }
  }
muellair
fuente
puede resultar de clz.getResource(resource).toString()tener barras invertidas?
Cuenca
9

La forma más fácil es usar la clase 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);

Porque en algunos casos ...class.getProtectionDomain().getCodeSource().getLocation();da camino con vfs:/, por lo que esto debe manejarse adicionalmente.

ayurchuk
fuente
Esta es, de lejos, la forma más fácil y limpia de hacerlo.
walen
6

Puede usar getProtectionDomain (). GetCodeSource () de esta manera:

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();
}
Uto
fuente
1
getCodeSourcepuede devolver null. ¿Cuáles son los criterios para que esto funcione? La documentación no explica esto.
abandonando el
44
¿De dónde se DataUtilitiesimporta? No parece estar en el JDK.
Jolta
2

¿Por qué incluye el paso getClassLoader? Si dice "this.getClass (). GetResource ()", debería obtener recursos relativos a la clase que realiza la llamada. Nunca he usado ClassLoader.getResource (), aunque de un vistazo rápido a los Documentos de Java parece que eso te dará el primer recurso de ese nombre encontrado en cualquier classpath actual.

Arrendajo
fuente
Si su clase se llama "com.mypackage.MyClass", la llamada class.getResource("myresource.txt")intentará cargar ese recurso desde com/mypackage/myresource.txt. ¿Cómo exactamente utilizará este enfoque para obtener el manifiesto?
ChssPly76
1
De acuerdo, tengo que retroceder. Eso es lo que viene de no probar. Estaba pensando que podría decir this.getClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Sin embargo, se necesitan muchos ".." para el nombre de su paquete). que funciona para los archivos de clase en un directorio para trabajar en un árbol de directorios, aparentemente no funciona para JAR. No veo por qué no, pero así es como es. Tampoco funciona this.getClass (). GetResource ("/ META-INF / MANIFEST.MF"), eso me da el manifiesto para rt.jar. (Continuará ...)
Jay
Lo que puede hacer es usar getResource para encontrar la ruta a su propio archivo de clase, luego quitar todo después del "!" para obtener la ruta al jar, luego agregue "/META-INF/MANIFEST.MF". Como sugirió Zhihong, así que votaré por él.
Jay
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
        }
      }
    }
  }
Alex Konshin
fuente
Esta respuesta utiliza una forma muy compleja y propensa a errores de cargar el Manifiesto. la solución mucho más simple es usar cl.getResourceAsStream("META-INF/MANIFEST.MF").
Robert
¿Lo intentaste? ¿Qué manifiesto de jar obtendrá si tiene varios jar en classpath? Tomará el primero que no sea lo que necesita. Mi código resuelve este problema y realmente funciona.
Alex Konshin
No critiqué la forma en que usas el cargador de clases para cargar un recurso específico. Estaba señalando que todo el código entre classLoader.getResource(..)y url.openStream()es totalmente irrelevante y propenso a errores, ya que intenta hacer lo mismo que lo classLoader.getResourceAsStream(..)hace.
Robert
No Es diferente. Mi código toma manifiesto del jar específico donde se encuentra la clase en lugar del primer jar del classpath.
Alex Konshin
Su "código de carga específico de jarra" es equivalente a las dos líneas siguientes:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert
0

He usado la solución de Anthony Juckel pero en MANIFEST.MF la clave tiene que comenzar con mayúsculas.

Entonces mi archivo MANIFEST.MF contiene una clave como:

Mykey: valor

Luego, en el activador u otra clase, puede usar el código de Anthony para leer el archivo MANIFEST.MF y el valor que necesita.

// 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();
usuario2935659
fuente
0

Tengo esta extraña solución que ejecuta aplicaciones de guerra en un servidor Jetty incorporado, pero estas aplicaciones también deben ejecutarse en servidores Tomcat estándar, y tenemos algunas propiedades especiales en el Manfest.

El problema era que cuando estaba en Tomcat, el manifiesto podía leerse, pero cuando estaba en el embarcadero, se recogía un manifiesto aleatorio (que no tenía las propiedades especiales)

Basado en la respuesta de Alex Konshin, se me ocurrió la siguiente solución (el flujo de entrada se usa en una clase de Manifiesto):

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;
    }
}
GriffoGoes
fuente