Membaca Manifest Jar saya sendiri

134

Saya perlu membaca Manifestfile, yang mengirimkan kelas saya, tetapi ketika saya menggunakan:

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

Saya mendapatkan MANIFESTdari yang pertama .jardimuat ke Java Runtime.
Aplikasi saya akan berjalan dari applet atau webstart,
jadi saya tidak akan memiliki akses ke .jarfile saya sendiri , saya kira.

Saya sebenarnya ingin membaca Export-packageatribut dari .jarmana memulai Felix OSGi, jadi saya bisa mengekspos paket-paket itu ke Felix. Ada ide?

Houtman
sumber
3
Saya pikir jawaban FrameworkUtil.getBundle () di bawah ini adalah yang terbaik. Ini menjawab apa yang sebenarnya ingin Anda lakukan (dapatkan ekspor bundel) daripada apa yang Anda minta (baca manifes).
Chris Dolan

Jawaban:

117

Anda dapat melakukan satu dari dua hal:

  1. Panggil getResources()dan ulangi melalui koleksi URL yang dikembalikan, bacalah sebagai manifes sampai Anda menemukan milik Anda:

    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. Anda dapat mencoba memeriksa apakah getClass().getClassLoader()merupakan instance dari java.net.URLClassLoader. Mayoritas classloader Sun adalah, termasuk AppletClassLoader. Anda kemudian dapat melemparkannya dan menelepon findResource()yang telah dikenal - untuk applet, setidaknya - untuk mengembalikan manifes yang diperlukan secara langsung:

    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
sumber
5
Sempurna! Saya tidak pernah tahu Anda bisa beralih melalui sumber daya dengan nama yang sama.
Houtman
Bagaimana Anda tahu classloader hanya mengetahui satu file .jar? (benar dalam banyak kasus saya kira) Saya lebih suka menggunakan sesuatu yang berhubungan langsung dengan kelas yang bersangkutan.
Jason S
7
ini adalah praktik yang baik untuk membuat jawaban terpisah untuk masing-masing, alih-alih menyertakan 2 perbaikan dalam satu jawaban. Jawaban terpisah dapat dipilih secara independen.
Alba Mendez
hanya sebuah catatan: Saya membutuhkan sesuatu yang serupa tetapi saya berada di dalam PERANG di JBoss, jadi pendekatan kedua tidak bekerja untuk saya. Saya berakhir dengan varian stackoverflow.com/a/1283496/160799
Gregor
1
Opsi pertama tidak berhasil untuk saya. Saya mendapatkan manifes dari 62 toples ketergantungan saya, tetapi bukan yang mana kelas saat ini didefinisikan ...
Jolta
120

Anda dapat menemukan URL untuk kelas Anda terlebih dahulu. Jika itu JAR, maka Anda memuat manifes dari sana. Sebagai contoh,

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
sumber
Saya suka solusi ini karena mendapat manifes Anda sendiri secara langsung daripada harus mencarinya.
Jay
1
dapat ditingkatkan sedikit dengan menghapus pemeriksaan kondisiclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Jay
2
Siapa yang menutup sungai?
ceving
1
Ini tidak berfungsi di kelas dalam, karena getSimpleNamemenghapus nama kelas luar. Ini akan bekerja untuk kelas batin: clazz.getName().replace (".", "/") + ".class".
ceving
3
Anda harus menutup aliran, konstruktor manifes tidak.
BrianT.
21

Anda dapat menggunakan Manifestsdari manifes jcabi dan membaca atribut apa pun dari file MANIFEST.MF yang tersedia hanya dengan satu baris:

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

Satu-satunya ketergantungan yang Anda butuhkan adalah:

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

Juga, lihat posting blog ini untuk detail lebih lanjut: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

yegor256
sumber
Perpustakaan yang sangat bagus. Apakah ada cara untuk mengontrol level log?
assylias
1
Semua lib jcabi masuk melalui SLF4J. Anda dapat mengirim pesan log menggunakan fasilitas apa pun yang Anda inginkan, misalnya log4j atau logback
yegor256
jika menggunakan logback.xml baris yang perlu Anda tambahkan adalah seperti<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher
1
Beberapa manifes dari classloader yang sama saling tumpang tindih dan saling menimpa
guai
13

Saya akui di muka bahwa jawaban ini tidak menjawab pertanyaan awal, yaitu bahwa pada umumnya dapat mengakses Manifest. Namun jika yang benar-benar diperlukan adalah membaca salah satu dari sejumlah atribut Manifes "standar", solusi berikut ini jauh lebih sederhana daripada yang diposting di atas. Jadi saya berharap moderator akan mengizinkannya. Perhatikan bahwa solusi ini ada di Kotlin, bukan Java, tetapi saya berharap bahwa port ke Java akan sepele. (Meskipun saya akui saya tidak tahu bahasa Jawa yang setara dengan ".`package`".

Dalam kasus saya, saya ingin membaca atribut "Implementasi-Versi" jadi saya mulai dengan solusi yang diberikan di atas untuk mendapatkan aliran dan kemudian membacanya untuk mendapatkan nilai. Sementara solusi ini berhasil, seorang rekan kerja yang meninjau kode saya menunjukkan kepada saya cara yang lebih mudah untuk melakukan apa yang saya inginkan. Perhatikan bahwa solusi ini ada di Kotlin, bukan Jawa.

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

Sekali lagi perhatikan bahwa ini tidak menjawab pertanyaan awal, khususnya "Paket ekspor" tampaknya tidak menjadi salah satu atribut yang didukung. Yang mengatakan, ada myPackage.name yang mengembalikan nilai. Mungkin seseorang yang memahami hal ini lebih dari yang dapat saya komentari apakah itu mengembalikan nilai yang diminta oleh pengirim aslinya.

Steven W. Klassen
sumber
4
Memang, port java sangat mudah:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson
Faktanya inilah yang saya cari. Saya juga senang bahwa Java juga memiliki padanan.
Aleksander Stelmaczonek
12

Saya percaya cara paling tepat untuk mendapatkan manifes untuk bundel apa pun (termasuk bundel yang memuat kelas tertentu) adalah dengan menggunakan objek Bundle atau 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();

Perhatikan bahwa objek Bundle juga menyediakan getEntry(String path)untuk mencari sumber daya yang terkandung dalam bundel tertentu, daripada mencari seluruh classpath bundel itu.

Secara umum, jika Anda menginginkan informasi bundel spesifik, jangan mengandalkan asumsi tentang classloader, cukup gunakan API OSGi secara langsung.

Anthony Juckel
sumber
9

Kode berikut ini berfungsi dengan beberapa jenis arsip (jar, perang) dan beberapa jenis classloader (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
sumber
dapat hasil clz.getResource(resource).toString()memiliki garis miring terbalik?
baskom
9

Cara termudah adalah dengan menggunakan kelas 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);

Karena dalam beberapa kasus ...class.getProtectionDomain().getCodeSource().getLocation();memberi jalan vfs:/, jadi ini harus ditangani tambahan.

ayurchuk
sumber
Sejauh ini, ini adalah cara termudah dan terbersih untuk melakukannya.
walen
6

Anda dapat menggunakan getProtectionDomain (). GetCodeSource () seperti ini:

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();
}
Kamu juga
sumber
1
getCodeSourcedapat kembali null. Apa kriterianya, bahwa ini akan berhasil? The dokumentasi tidak menjelaskan hal ini.
ceving
4
Dari mana DataUtilitiesdiimpor? Sepertinya tidak ada di JDK.
Jolta
2

Mengapa Anda termasuk langkah getClassLoader? Jika Anda mengatakan "this.getClass (). GetResource ()" Anda harus mendapatkan sumber daya relatif terhadap kelas panggilan. Saya tidak pernah menggunakan ClassLoader.getResource (), meskipun dari melihat cepat pada Java Docs sepertinya itu akan membuat Anda sumber daya pertama dari nama yang ditemukan di classpath saat ini.

Jay
sumber
Jika kelas Anda bernama "com.mypackage.MyClass", panggilan class.getResource("myresource.txt")akan mencoba memuat sumber daya itu com/mypackage/myresource.txt. Bagaimana tepatnya Anda akan menggunakan pendekatan ini untuk mendapatkan manifes?
ChssPly76
1
Oke, saya harus mundur. Itu yang datang dari tidak menguji. Saya berpikir bahwa Anda dapat mengatakan this.getClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Namun banyak ".." yang perlu diberikan nama paket Anda.) Tetapi sementara yang berfungsi untuk file kelas di direktori untuk bekerja dengan cara Anda naik pohon direktori, itu tampaknya tidak berfungsi untuk JAR. Saya tidak mengerti mengapa tidak, tapi memang begitu. Tidak juga this.getClass (). GetResource ("/ META-INF / MANIFEST.MF") - yang membuat saya manifes untuk rt.jar. (Bersambung ...)
Jay
Yang dapat Anda lakukan adalah menggunakan getResource untuk menemukan path ke file kelas Anda sendiri, lalu menanggalkan semuanya setelah "!" untuk mendapatkan jalur ke toples, lalu tambahkan "/META-INF/MANIFEST.MF". Seperti yang disarankan Zhihong, jadi saya memilihnya.
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
sumber
Jawaban ini menggunakan cara pemuatan Manifes yang sangat kompleks dan rawan kesalahan. solusi yang lebih sederhana adalah menggunakan cl.getResourceAsStream("META-INF/MANIFEST.MF").
Robert
Apakah kamu sudah mencobanya? Manifes jar apa yang akan didapat jika Anda memiliki beberapa stoples di classpath? Ini akan mengambil yang pertama bukan yang Anda butuhkan. Kode saya memecahkan masalah ini dan itu benar-benar berfungsi.
Alex Konshin
Saya tidak mengkritik cara Anda menggunakan classloader karena memuat sumber daya tertentu. Saya menunjukkan bahwa semua kode antara classLoader.getResource(..)dan url.openStream()benar-benar tidak relevan dan rawan kesalahan karena mencoba melakukan hal yang sama classLoader.getResourceAsStream(..).
Robert
Nggak. Itu berbeda. Kode saya mengambil manifes dari toples khusus tempat kelas berada daripada dari toples pertama di classpath.
Alex Konshin
"Kode pemuatan spesifik jar" Anda setara dengan dua baris berikut:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert
0

Saya telah menggunakan solusi dari Anthony Juckel tetapi dalam MANIFEST.MF kuncinya harus dimulai dengan huruf besar.

Jadi file MANIFEST.MF saya mengandung kunci seperti:

Mykey: value

Kemudian di aktivator atau kelas lain Anda dapat menggunakan kode dari Anthony untuk membaca file MANIFEST.MF dan nilai yang Anda butuhkan.

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

Saya punya solusi aneh ini yang menjalankan aplikasi perang di server Jetty tertanam tetapi aplikasi ini juga harus berjalan di server Tomcat standar, dan kami memiliki beberapa properti khusus di manfest.

Masalahnya adalah ketika di Tomcat, manifes dapat dibaca, tetapi ketika di dermaga, manifes acak diambil (yang melewatkan properti khusus)

Berdasarkan jawaban Alex Konshin, saya datang dengan solusi berikut (inputstream kemudian digunakan dalam kelas 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
sumber