Ich lese mein eigenes Jar's Manifest

133

Ich muss die ManifestDatei lesen , die meine Klasse geliefert hat, aber wenn ich benutze:

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

Ich bekomme das MANIFESTvon der ersten .jarin die Java Runtime geladen.
Meine App wird über ein Applet oder einen Webstart ausgeführt,
sodass ich vermutlich keinen Zugriff auf meine eigene .jarDatei habe.

Ich möchte eigentlich das Export-packageAttribut lesen , von dem aus .jarFelix OSGi gestartet wurde, damit ich diese Pakete Felix zugänglich machen kann. Irgendwelche Ideen?

Houtman
quelle
3
Ich denke, die unten stehende Antwort von FrameworkUtil.getBundle () ist die beste. Es beantwortet eher, was Sie tatsächlich tun möchten (die Exporte des Bundles abrufen), als was Sie gefragt haben (lesen Sie das Manifest).
Chris Dolan

Antworten:

117

Sie können eines von zwei Dingen tun:

  1. Rufen Sie getResources()die zurückgegebene URL-Sammlung auf, durchlaufen Sie sie und lesen Sie sie als Manifest, bis Sie Ihre finden:

    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. Sie können versuchen zu überprüfen, ob getClass().getClassLoader()es sich um eine Instanz von handelt java.net.URLClassLoader. Die Mehrheit der Sun-Klassenlader sind, einschließlich AppletClassLoader. Sie können es dann umwandeln und findResource()das bekannte - zumindest für Applets - aufrufen , um das benötigte Manifest direkt zurückzugeben:

    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
quelle
5
Perfekt! Ich wusste nie, dass Sie Ressourcen mit demselben Namen durchlaufen können.
Houtman
Woher wissen Sie, dass dem Klassenladeprogramm nur eine einzige JAR-Datei bekannt ist? (in vielen Fällen wahr, nehme ich an) Ich würde viel lieber etwas verwenden, das direkt mit der betreffenden Klasse verbunden ist.
Jason S
7
Es ist eine gute Praxis , für jede Antwort eine separate Antwort zu geben, anstatt die beiden Korrekturen in eine Antwort aufzunehmen. Separate Antworten können unabhängig gewählt werden.
Alba Mendez
Nur eine Anmerkung: Ich brauchte etwas Ähnliches, aber ich bin in einem WAR auf JBoss, also hat der zweite Ansatz für mich nicht funktioniert. Am
Gregor
1
Die erste Option hat bei mir nicht funktioniert. Ich habe die Manifeste meiner 62 Abhängigkeitsgläser erhalten, aber nicht die, in denen die aktuelle Klasse definiert wurde ...
Jolta
119

Sie finden zuerst die URL für Ihre Klasse. Wenn es sich um eine JAR handelt, laden Sie das Manifest von dort. Beispielsweise,

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");
ZZ Coder
quelle
Ich mag diese Lösung, da sie direkt Ihr eigenes Manifest erhält, anstatt danach suchen zu müssen.
Jay
1
kann ein wenig verbessert werden, indem die Zustandsprüfung entfernt wirdclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Jay
2
Wer schließt den Stream?
Ceving
1
Dies funktioniert in inneren Klassen nicht, da getSimpleNameder Name der äußeren Klasse entfernt wird. Dies funktioniert für innere Klassen : clazz.getName().replace (".", "/") + ".class".
Ceving
3
Sie müssen den Stream schließen, der Manifest-Konstruktor nicht.
BrianT.
21

Sie können Manifestsaus jcabi-Manifesten verwenden und jedes Attribut aus jeder verfügbaren MANIFEST.MF-Datei mit nur einer Zeile lesen:

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

Die einzige Abhängigkeit, die Sie benötigen, ist:

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

Weitere Informationen finden Sie in diesem Blogbeitrag: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

yegor256
quelle
Sehr schöne Bibliotheken. Gibt es eine Möglichkeit, die Protokollstufe zu steuern?
Assylias
1
Alle jcabi-Bibliotheken melden sich über SLF4J an. Sie können Protokollnachrichten mit jeder gewünschten Funktion
versenden
Wenn Sie logback.xml verwenden, ist die Zeile, die Sie hinzufügen müssen, wie<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher
1
Mehrere Manifeste desselben Klassenladers überlappen sich und überschreiben sich gegenseitig
Guai
13

Ich gebe vorab zu, dass diese Antwort nicht die ursprüngliche Frage beantwortet, nämlich allgemein auf das Manifest zugreifen zu können. Wenn jedoch wirklich eines von mehreren "Standard" -Manifestattributen gelesen werden muss, ist die folgende Lösung viel einfacher als die oben genannten. Ich hoffe also, dass der Moderator dies zulässt. Beachten Sie, dass sich diese Lösung in Kotlin und nicht in Java befindet, aber ich würde erwarten, dass ein Port nach Java trivial ist. (Obwohl ich zugebe, dass ich das Java-Äquivalent von ".`package`" nicht kenne.

In meinem Fall wollte ich das Attribut "Implementation-Version" lesen, also begann ich mit den oben angegebenen Lösungen, um den Stream zu erhalten, und las ihn dann, um den Wert zu erhalten. Während diese Lösung funktionierte, zeigte mir ein Mitarbeiter, der meinen Code überprüfte, einen einfacheren Weg, um das zu tun, was ich wollte. Beachten Sie, dass sich diese Lösung in Kotlin und nicht in Java befindet.

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

Beachten Sie erneut, dass dies nicht die ursprüngliche Frage beantwortet, insbesondere "Export-Paket" scheint nicht eines der unterstützten Attribute zu sein. Es gibt jedoch einen myPackage.name, der einen Wert zurückgibt. Vielleicht kann jemand, der dies besser versteht als ich, kommentieren, ob dies den Wert zurückgibt, den das Originalplakat anfordert.

Steven W. Klassen
quelle
4
In der Tat ist der Java-Port unkompliziert:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson
Es ist die Tatsache, dass ich danach gesucht habe. Ich bin auch froh, dass Java auch ein Äquivalent hat.
Aleksander Stelmaczonek
12

Ich glaube, der am besten geeignete Weg, um das Manifest für ein Bundle (einschließlich des Bundles, das eine bestimmte Klasse geladen hat) zu erhalten, ist die Verwendung des Bundle- oder BundleContext-Objekts.

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

Beachten Sie, dass das Bundle-Objekt auch das getEntry(String path)Nachschlagen von Ressourcen in einem bestimmten Bundle bietet, anstatt den gesamten Klassenpfad dieses Bundles zu durchsuchen.

Wenn Sie bündelspezifische Informationen wünschen, verlassen Sie sich im Allgemeinen nicht auf Annahmen über die Klassenladeprogramme, sondern verwenden Sie einfach die OSGi-APIs direkt.

Anthony Juckel
quelle
9

Der folgende Code funktioniert mit mehreren Arten von Archiven (jar, war) und mehreren Arten von Klassenladeprogrammen (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);
    }
  }
Müllair
quelle
kann das Ergebnis von clz.getResource(resource).toString()Backslashes sein?
Becken
9

Der einfachste Weg ist die Verwendung der JarURLConnection-Klasse:

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

Da in einigen Fällen ...class.getProtectionDomain().getCodeSource().getLocation();Pfad mit angibt vfs:/, sollte dies zusätzlich behandelt werden.

Ayurchuk
quelle
Dies ist bei weitem der einfachste und sauberste Weg, dies zu tun.
Wal
6

Sie können getProtectionDomain (). GetCodeSource () folgendermaßen verwenden:

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
quelle
1
getCodeSourcekann zurückkehren null. Was sind die Kriterien, damit dies funktioniert? Die Dokumentation erklärt dies nicht.
Ceving
4
Woher wird DataUtilitiesimportiert? Es scheint nicht im JDK zu sein.
Jolta
2

Warum schließen Sie den Schritt getClassLoader ein? Wenn Sie "this.getClass (). GetResource ()" sagen, sollten Sie Ressourcen relativ zur aufrufenden Klasse abrufen. Ich habe ClassLoader.getResource () noch nie verwendet. Wenn Sie sich jedoch die Java-Dokumente ansehen, erhalten Sie die erste Ressource dieses Namens, die in einem aktuellen Klassenpfad gefunden wird.

Jay
quelle
Wenn Ihre Klasse "com.mypackage.MyClass" heißt, class.getResource("myresource.txt")wird beim Aufruf versucht, diese Ressource zu laden com/mypackage/myresource.txt. Wie genau werden Sie diesen Ansatz verwenden, um das Manifest zu erhalten?
ChssPly76
1
Okay, ich muss zurückgehen. Das kommt davon, nicht zu testen. Ich dachte, Sie könnten dies sagen. GetClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Wie viele ".." auch immer erforderlich sind, wenn Sie Ihren Paketnamen angeben.) Aber während Das funktioniert für Klassendateien in einem Verzeichnis, um sich in einem Verzeichnisbaum nach oben zu arbeiten. Es funktioniert anscheinend nicht für JARs. Ich verstehe nicht warum nicht, aber so ist es. Auch this.getClass (). GetResource ("/ META-INF / MANIFEST.MF") funktioniert nicht - das bringt mir das Manifest für rt.jar. (Fortsetzung
Jay
Sie können getResource verwenden, um den Pfad zu Ihrer eigenen Klassendatei zu finden, und dann nach dem "!" Um den Pfad zum Glas zu erhalten, fügen Sie "/META-INF/MANIFEST.MF" hinzu. Wie Zhihong vorgeschlagen hat, stimme ich ihm zu.
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
quelle
Diese Antwort verwendet eine sehr komplexe und fehleranfällige Methode zum Laden des Manifests. Die viel einfachere Lösung ist die Verwendung cl.getResourceAsStream("META-INF/MANIFEST.MF").
Robert
Hast Du es versucht? Welches Glasmanifest wird es erhalten, wenn Sie mehrere Gläser im Klassenpfad haben? Es wird den ersten brauchen, der nicht das ist, was Sie brauchen. Mein Code löst dieses Problem und es funktioniert wirklich.
Alex Konshin
Ich habe nicht kritisiert, wie Sie den Klassenladeprogramm zum Laden einer bestimmten Ressource verwenden. Ich habe darauf hingewiesen, dass der gesamte Code zwischen classLoader.getResource(..)und url.openStream()völlig irrelevant und fehleranfällig ist, da er versucht, dasselbe zu tun wie er classLoader.getResourceAsStream(..).
Robert
Nee. Es ist anders. Mein Code stammt aus dem spezifischen Jar, in dem sich die Klasse befindet, und nicht aus dem ersten Jar im Klassenpfad.
Alex Konshin
Ihr "jar spezifischer Ladecode" entspricht den folgenden zwei Zeilen:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert
0

Ich habe die Lösung von Anthony Juckel verwendet, aber in MANIFEST.MF muss der Schlüssel mit Großbuchstaben beginnen.

Meine MANIFEST.MF-Datei enthält also einen Schlüssel wie:

Mykey: Wert

Dann können Sie im Aktivator oder einer anderen Klasse den Code von Anthony verwenden, um die Datei MANIFEST.MF und den Wert zu lesen, den Sie benötigen.

// 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();
user2935659
quelle
0

Ich habe diese seltsame Lösung, die Kriegsanwendungen auf einem eingebetteten Jetty-Server ausführt, aber diese Apps müssen auch auf Standard-Tomcat-Servern ausgeführt werden, und wir haben einige spezielle Eigenschaften im Manfest.

Das Problem war, dass in Tomcat das Manifest gelesen werden konnte, aber in der Anlegestelle ein zufälliges Manifest aufgenommen wurde (das die besonderen Eigenschaften verfehlte).

Basierend auf der Antwort von Alex Konshin habe ich die folgende Lösung gefunden (der Inputstream wird dann in einer Manifest-Klasse verwendet):

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
quelle