Lire le manifeste de mon propre Jar

135

J'ai besoin de lire le Manifestfichier, qui a livré ma classe, mais quand j'utilise:

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

J'obtiens le MANIFESTpremier .jarchargé dans le Java Runtime.
Mon application fonctionnera à partir d'une applet ou d'un webstart,
donc je n'aurai pas accès à mon propre .jarfichier, je suppose.

Je veux en fait lire l' Export-packageattribut du .jarqui a démarré Felix OSGi, afin que je puisse exposer ces paquets à Felix. Des idées?

Houtman
la source
3
Je pense que la réponse FrameworkUtil.getBundle () ci-dessous est la meilleure. Il répond à ce que vous voulez réellement faire (obtenir les exportations du bundle) plutôt qu'à ce que vous avez demandé (lire le manifeste).
Chris Dolan

Réponses:

117

Vous pouvez faire l'une des deux choses suivantes:

  1. Appelez getResources()et parcourez la collection d'URL renvoyée, en les lisant comme des manifestes jusqu'à ce que vous trouviez le vôtre:

    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. Vous pouvez essayer de vérifier s'il getClass().getClassLoader()s'agit d'une instance de java.net.URLClassLoader. La majorité des chargeurs de classe Sun sont, y compris AppletClassLoader. Vous pouvez ensuite le lancer et appeler findResource()ce qui a été connu - pour les applets, au moins - pour renvoyer directement le manifeste nécessaire:

    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
la source
5
Parfait! Je n'ai jamais su que vous pouviez parcourir les ressources du même nom.
Houtman
Comment savez-vous que le chargeur de classe ne connaît qu'un seul fichier .jar? (vrai dans de nombreux cas, je suppose) Je préférerais de loin utiliser quelque chose associé directement à la classe en question.
Jason S
7
c'est une bonne pratique de faire des réponses séparées pour chacune, au lieu d'inclure les 2 correctifs dans une seule réponse. Des réponses séparées peuvent être votées indépendamment.
Alba Mendez
juste une note: j'avais besoin de quelque chose de similaire mais je suis dans un WAR sur JBoss, donc la deuxième approche n'a pas fonctionné pour moi. Je me suis retrouvé avec une variante de stackoverflow.com/a/1283496/160799
Gregor
1
La première option n'a pas fonctionné pour moi. J'ai eu les manifestes de mes 62 jar de dépendances, mais pas celui où la classe actuelle a été définie ...
Jolta
120

Vous pouvez d'abord trouver l'URL de votre classe. S'il s'agit d'un JAR, vous chargez le manifeste à partir de là. Par exemple,

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");
Codeur ZZ
la source
J'aime cette solution car elle obtient directement votre propre manifeste plutôt que de devoir le rechercher.
Jay
1
peut être amélioré un peu en supprimant la vérification de l'étatclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Jay
2
Qui ferme le flux?
ceving
1
Cela ne fonctionne pas dans les classes internes, car getSimpleNamesupprime le nom de classe externe. Cela fonctionne pour les classes internes: clazz.getName().replace (".", "/") + ".class".
ceving
3
Vous devez fermer le flux, ce n'est pas le cas du constructeur du manifeste.
BrianT.
21

Vous pouvez utiliser à Manifestspartir de jcabi-manifestes et lire n'importe quel attribut de l'un des fichiers MANIFEST.MF disponibles avec une seule ligne:

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

La seule dépendance dont vous avez besoin est:

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

Consultez également ce billet de blog pour plus de détails: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

yegor256
la source
Très belles bibliothèques. Existe-t-il un moyen de contrôler le niveau de journalisation?
assylias
1
Toutes les bibliothèques jcabi se connectent via SLF4J. Vous pouvez envoyer des messages de journal en utilisant n'importe quelle fonction de votre choix, par exemple log4j ou logback
yegor256
si vous utilisez logback.xml, la ligne que vous devez ajouter est comme<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher
1
Plusieurs manifestes du même chargeur de classe se chevauchent et se remplacent
guai
13

J'avoue d'emblée que cette réponse ne répond pas à la question initiale, celle de pouvoir généralement accéder au Manifeste. Cependant, si ce qui est vraiment nécessaire est de lire l'un des nombreux attributs Manifest "standard", la solution suivante est beaucoup plus simple que celles publiées ci-dessus. J'espère donc que le modérateur le permettra. Notez que cette solution est en Kotlin, pas en Java, mais je m'attendrais à ce qu'un portage vers Java soit trivial. (Bien que j'admette que je ne connais pas l'équivalent Java de ".`package`".

Dans mon cas, je voulais lire l'attribut "Implementation-Version" donc j'ai commencé avec les solutions données ci-dessus pour obtenir le flux puis le lire pour obtenir la valeur. Pendant que cette solution fonctionnait, un collègue examinant mon code m'a montré un moyen plus simple de faire ce que je voulais. Notez que cette solution est en Kotlin, pas en Java.

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

Notez encore une fois que cela ne répond pas à la question initiale, en particulier "Export-package" ne semble pas être l'un des attributs pris en charge. Cela dit, il existe un myPackage.name qui renvoie une valeur. Peut-être que quelqu'un qui comprend cela plus que moi peut dire si cela renvoie la valeur que l'affiche originale demande.

Steven W. Klassen
la source
4
En effet, le portage java est simple:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson
C'est en fait ce que je cherchais. Je suis également heureux que Java ait également un équivalent.
Aleksander Stelmaczonek
12

Je pense que le moyen le plus approprié d'obtenir le manifeste pour tout bundle (y compris le bundle qui a chargé une classe donnée) est d'utiliser l'objet Bundle ou 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();

Notez que l'objet Bundle permet également getEntry(String path)de rechercher des ressources contenues dans un bundle spécifique, plutôt que de rechercher le chemin de classe complet de ce bundle.

En général, si vous voulez des informations spécifiques aux bundles, ne vous fiez pas aux hypothèses concernant les chargeurs de classes, utilisez simplement les API OSGi directement.

Anthony Juckel
la source
9

Le code suivant fonctionne avec plusieurs types d'archives (jar, war) et plusieurs types de classloaders (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
la source
peut résulter d' clz.getResource(resource).toString()avoir des contre-obliques?
bassin
9

Le moyen le plus simple est d'utiliser la classe 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);

Parce que dans certains cas, ...class.getProtectionDomain().getCodeSource().getLocation();donne le chemin avec vfs:/, cela doit donc être traité en plus.

Ayurchuk
la source
C'est, de loin, le moyen le plus simple et le plus propre de le faire.
walen le
6

Vous pouvez utiliser getProtectionDomain (). GetCodeSource () comme ceci:

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
la source
1
getCodeSourcepeut revenir null. Quels sont les critères pour que cela fonctionne? La documentation n'explique pas cela.
ceving
4
D'où est DataUtilitiesimporté? Cela ne semble pas être dans le JDK.
Jolta
2

Pourquoi incluez-vous l'étape getClassLoader? Si vous dites "this.getClass (). GetResource ()", vous devriez obtenir des ressources relatives à la classe appelante. Je n'ai jamais utilisé ClassLoader.getResource (), bien que d'un rapide coup d'œil à la documentation Java, il semble que vous obtiendrez la première ressource de ce nom trouvée dans n'importe quel chemin de classe actuel.

Geai
la source
Si votre classe est nommée "com.mypackage.MyClass", l'appel class.getResource("myresource.txt")essaiera de charger cette ressource à partir de com/mypackage/myresource.txt. Comment allez-vous utiliser cette approche pour obtenir le manifeste?
ChssPly76
1
D'accord, je dois revenir en arrière. C'est ce qui vient de ne pas tester. Je pensais que vous pouviez dire this.getClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Cependant, de nombreux ".." sont nécessaires étant donné le nom de votre package.) qui fonctionne pour les fichiers de classe dans un répertoire pour monter dans une arborescence de répertoires, cela ne fonctionne apparemment pas pour les JAR. Je ne vois pas pourquoi, mais c'est comme ça. This.getClass (). GetResource ("/ META-INF / MANIFEST.MF") ne fonctionne pas non plus - cela me donne le manifeste pour rt.jar. (A suivre ...)
Jay
Ce que vous pouvez faire, c'est utiliser getResource pour trouver le chemin vers votre propre fichier de classe, puis supprimer tout ce qui suit le "!" pour obtenir le chemin d'accès au bocal, puis ajoutez "/META-INF/MANIFEST.MF". Comme Zhihong l'a suggéré, je vote donc pour lui.
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
la source
Cette réponse utilise une manière très complexe et sujette aux erreurs de charger le manifeste. la solution beaucoup plus simple consiste à utiliser cl.getResourceAsStream("META-INF/MANIFEST.MF").
Robert
Est-ce que tu l'as essayé? Quel fichier jar obtiendra-t-il si vous avez plusieurs fichiers jar dans classpath? Il faudra le premier qui n'est pas ce dont vous avez besoin. Mon code résout ce problème et cela fonctionne vraiment.
Alex Konshin
Je n'ai pas critiqué la façon dont vous utilisez le classloader pour charger une ressource spécifique. Je faisais remarquer que tout le code entre classLoader.getResource(..)et url.openStream()est totalement hors de propos et sujet aux erreurs car il essaie de faire la même chose que le classLoader.getResourceAsStream(..)fait.
Robert
Nan. C'est différent. Mon code prend le manifeste du jar spécifique où se trouve la classe plutôt que du premier jar du chemin de classe.
Alex Konshin le
Votre "code de chargement spécifique au bocal" équivaut aux deux lignes suivantes:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert
0

J'ai utilisé la solution d'Anthony Juckel mais dans le MANIFEST.MF, la clé doit commencer par des majuscules.

Donc mon fichier MANIFEST.MF contient une clé comme:

Mykey: valeur

Ensuite, dans l'activateur ou dans une autre classe, vous pouvez utiliser le code d'Anthony pour lire le fichier MANIFEST.MF et la valeur dont vous avez besoin.

// 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
la source
0

J'ai cette solution étrange qui exécute des applications de guerre dans un serveur Jetty intégré, mais ces applications doivent également fonctionner sur des serveurs Tomcat standard, et nous avons des propriétés spéciales dans le manfest.

Le problème était que quand dans Tomcat, le manifeste pouvait être lu, mais quand dans la jetée, un manifeste aléatoire était ramassé (qui manquait les propriétés spéciales)

Sur la base de la réponse d'Alex Konshin, j'ai proposé la solution suivante (le flux d'entrée est ensuite utilisé dans une classe 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;
    }
}
GriffoGoes
la source