Czytanie własnego Manifestu Jar

136

Muszę przeczytać Manifestplik, który dostarczył moje zajęcia, ale kiedy używam:

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

Otrzymuję MANIFESTod pierwszego .jarzaładowanego do środowiska wykonawczego Java.
Moja aplikacja będzie działać z apletu lub startera internetowego,
więc myślę, że nie będę miał dostępu do własnego .jarpliku.

W rzeczywistości chcę przeczytać Export-packageatrybut, z .jarktórego uruchomiono Felix OSGi, aby móc udostępnić te pakiety Felixowi. Jakieś pomysły?

Houtman
źródło
3
Myślę, że poniższa odpowiedź FrameworkUtil.getBundle () jest najlepsza. Odpowiada, co faktycznie chcesz zrobić (pobrać eksport pakietu), a nie o to, o co prosiłeś (przeczytaj manifest).
Chris Dolan,

Odpowiedzi:

117

Możesz zrobić jedną z dwóch rzeczy:

  1. Wywołaj getResources()i iteruj po zwróconej kolekcji adresów URL, odczytując je jako manifesty, aż znajdziesz swój:

    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. Możesz spróbować sprawdzić, czy getClass().getClassLoader()jest to instancja java.net.URLClassLoader. Większość programów ładujących klasy Sun to, w tym AppletClassLoader. Następnie możesz go rzucić i wywołać, findResource()który jest znany - przynajmniej w przypadku apletów - aby bezpośrednio zwrócić potrzebny manifest:

    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
źródło
5
Idealny! Nigdy nie wiedziałem, że możesz iterować po zasobach o tej samej nazwie.
Houtman
Skąd wiesz, że program ładujący klasy rozpoznaje tylko jeden plik .jar? (chyba w wielu przypadkach prawda) Wolałbym raczej użyć czegoś związanego bezpośrednio z daną klasą.
Jason S
7
jest to dobra praktyka , aby oddzielne odpowiedzi dla każdego z nich, zamiast w tym 2 poprawki w jednej odpowiedzi. Oddzielne odpowiedzi można głosować niezależnie.
Alba Mendez
uwaga: potrzebowałem czegoś podobnego, ale jestem w WOJNIE na JBoss, więc drugie podejście nie działa dla mnie. Skończyło się na wariancie stackoverflow.com/a/1283496/160799
Gregor
1
Pierwsza opcja nie działała dla mnie. Mam manifesty moich 62 słoików zależności, ale nie ten, w którym zdefiniowano aktualną klasę ...
Jolta
121

Najpierw możesz znaleźć adres URL swojej klasy. Jeśli jest to plik JAR, ładujesz stamtąd manifest. Na przykład,

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
źródło
Podoba mi się to rozwiązanie, ponieważ bezpośrednio pobiera własny manifest, zamiast szukać go.
Jay
1
można nieco poprawić, usuwając sprawdzanie stanuclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Jay
2
Kto zamyka strumień?
ceving
1
To nie działa w klasach wewnętrznych, ponieważ getSimpleNameusuwa nazwę klasy zewnętrznej. To będzie pracować dla klas wewnętrznych: clazz.getName().replace (".", "/") + ".class".
ceving
3
Musisz zamknąć strumień, konstruktor manifestu tego nie robi.
BrianT.
21

Możesz użyć Manifestsz manifestów jcabi i odczytać dowolny atrybut z dowolnego z dostępnych plików MANIFEST.MF za pomocą tylko jednej linii:

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

Jedyna zależność, której potrzebujesz, to:

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

Więcej informacji można znaleźć w tym poście na blogu: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

yegor256
źródło
Bardzo ładne biblioteki. Czy istnieje sposób kontrolowania poziomu dziennika?
assylias
1
Wszystkie biblioteki jcabi logują się przez SLF4J. Możesz wysyłać komunikaty dziennika za pomocą dowolnego narzędzia, na przykład log4j lub
logback
jeśli używasz logback.xml, linia, którą musisz dodać, wygląda następująco<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher
1
Wiele manifestów z tego samego
modułu ładującego klas
16

Przyznam się z góry, że ta odpowiedź nie odpowiada na pierwotne pytanie, jakim jest ogólnie dostęp do Manifestu. Jeśli jednak naprawdę trzeba przeczytać jeden z wielu „standardowych” atrybutów Manifestu, poniższe rozwiązanie jest znacznie prostsze niż te zamieszczone powyżej. Mam więc nadzieję, że moderator na to pozwoli. Zauważ, że to rozwiązanie jest w Kotlinie, a nie w Javie, ale spodziewałbym się, że przeniesienie do Javy byłoby trywialne. (Chociaż przyznaję, że nie znam odpowiednika Java ".`package`".

W moim przypadku chciałem odczytać atrybut „Implementation-Version”, więc zacząłem od rozwiązań podanych powyżej, aby uzyskać strumień, a następnie przeczytałem go, aby uzyskać wartość. Chociaż to rozwiązanie zadziałało, współpracownik przeglądający mój kod pokazał mi łatwiejszy sposób zrobienia tego, co chciałem. Zauważ, że to rozwiązanie jest w Kotlinie, a nie w Javie.

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

Jeszcze raz zauważ, że nie jest to odpowiedź na pierwotne pytanie, w szczególności „Pakiet eksportowy” nie wydaje się być jednym z obsługiwanych atrybutów. To powiedziawszy, istnieje myPackage.name, który zwraca wartość. Być może ktoś, kto rozumie to bardziej niż ja, mogę skomentować, czy zwraca to wartość, o którą prosi oryginalny plakat.

Steven W. Klassen
źródło
6
Rzeczywiście, port Java jest prosty:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson
To jest to, czego szukałem. Cieszę się też, że Java ma również odpowiednik.
Aleksander Stelmaczonek
12

Uważam, że najwłaściwszym sposobem uzyskania manifestu dla dowolnego pakietu (w tym pakietu, który załadował daną klasę) jest użycie obiektu Bundle lub 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();

Zauważ, że obiekt Bundle umożliwia również getEntry(String path)wyszukiwanie zasobów zawartych w konkretnym pakiecie, zamiast przeszukiwania całej ścieżki klas tego pakietu.

Ogólnie rzecz biorąc, jeśli potrzebujesz informacji specyficznych dla pakietu, nie polegaj na założeniach dotyczących programów ładujących klasy, po prostu użyj bezpośrednio interfejsów API OSGi.

Anthony Juckel
źródło
10

Najłatwiej jest użyć klasy 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);

Ponieważ w niektórych przypadkach ...class.getProtectionDomain().getCodeSource().getLocation();podaje ścieżkę z vfs:/, więc należy to dodatkowo załatwić.

ayurchuk
źródło
To zdecydowanie najłatwiejszy i najczystszy sposób na zrobienie tego.
walen
9

Poniższy kod działa z wieloma typami archiwów (jar, war) i wieloma typami klas ładujących (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
źródło
może wynikać z clz.getResource(resource).toString()ukośników odwrotnych?
dorzecze
6

Możesz użyć getProtectionDomain (). GetCodeSource () w następujący sposób:

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();
}
Ty też
źródło
1
getCodeSourcemoże wrócić null. Jakie są kryteria, aby to zadziałało? Dokumentacja nie wyjaśnić.
ceving
4
Skąd jest DataUtilitiesimportowany? Wygląda na to, że nie ma go w JDK.
Jolta,
2

Dlaczego dołączasz krok getClassLoader? Jeśli powiesz „this.getClass (). GetResource ()”, powinieneś otrzymywać zasoby w stosunku do klasy wywołującej. Nigdy nie korzystałem z ClassLoader.getResource (), chociaż z szybkiego spojrzenia na dokumentację Java wygląda na to, że dostaniesz pierwszy zasób o tej nazwie znaleziony w dowolnej bieżącej ścieżce klas.

Sójka
źródło
Jeśli twoja klasa nosi nazwę „com.mypackage.MyClass”, wywołanie class.getResource("myresource.txt")spróbuje załadować ten zasób z com/mypackage/myresource.txt. Jak dokładnie zamierzasz użyć tego podejścia, aby uzyskać manifest?
ChssPly76
1
Ok, muszę się cofnąć. To wynika z braku testowania. Myślałem, że możesz powiedzieć this.getClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Jakkolwiek wiele ".." jest potrzebnych, biorąc pod uwagę nazwę twojego pakietu.) Ale podczas gdy który działa dla plików klas w katalogu, aby przejść w górę drzewa katalogów, najwyraźniej nie działa dla plików JAR. Nie rozumiem, dlaczego nie, ale tak właśnie jest. Nie działa też this.getClass (). GetResource ("/ META-INF / MANIFEST.MF") - dzięki temu otrzymuję manifest dla rt.jar. (Ciąg dalszy ...)
Jay
Możesz użyć getResource, aby znaleźć ścieżkę do własnego pliku klasy, a następnie usunąć wszystko po znaku „!” aby uzyskać ścieżkę do jar, a następnie dodaj „/META-INF/MANIFEST.MF”. Jak zasugerował Zhihong, więc głosuję na jego.
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
źródło
W tej odpowiedzi zastosowano bardzo złożony i podatny na błędy sposób ładowania manifestu. znacznie prostszym rozwiązaniem jest użycie cl.getResourceAsStream("META-INF/MANIFEST.MF").
Robert
Próbowałeś tego? Jaki manifest słoika otrzyma, jeśli masz wiele słoików w ścieżce klas? To zajmie pierwszy, który nie jest tym, czego potrzebujesz. Mój kod rozwiązuje ten problem i naprawdę działa.
Alex Konshin
Nie krytykowałem sposobu, w jaki używasz modułu ładującego klasy do ładowania określonego zasobu. Zwracałem uwagę, że cały kod między classLoader.getResource(..)i url.openStream()jest całkowicie nieistotny i podatny na błędy, ponieważ próbuje zrobić to samo, co classLoader.getResourceAsStream(..)robi.
Robert
Nie. To jest inne. Mój kod przyjmuje manifest z konkretnego pliku jar, w którym znajduje się klasa, a nie z pierwszego pliku jar w ścieżce klas.
Alex Konshin
Twój „kod ładowania specyficzny dla słoika” jest odpowiednikiem następujących dwóch wierszy:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert,
0

Użyłem rozwiązania Anthony'ego Juckela, ale w pliku MANIFEST.MF klucz musi zaczynać się od wielkich liter.

Więc mój plik MANIFEST.MF zawiera klucz taki jak:

Mykey: wartość

Następnie w aktywatorze lub innej klasie możesz użyć kodu Anthony'ego do odczytania pliku MANIFEST.MF i potrzebnej wartości.

// 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
źródło
0

Mam to dziwne rozwiązanie, które uruchamia aplikacje wojenne na wbudowanym serwerze Jetty, ale te aplikacje muszą również działać na standardowych serwerach Tomcat, a my mamy kilka specjalnych właściwości w manfestie.

Problem polegał na tym, że w Tomcat manifest można było odczytać, ale gdy był na molo, został odebrany losowy manifest (który pomijał specjalne właściwości)

Opierając się na odpowiedzi Alexa Konshina, wymyśliłem następujące rozwiązanie (strumień wejściowy jest następnie używany w klasie 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
źródło