กำลังอ่าน Jar's Manifest ของฉันเอง


135

ฉันต้องการอ่านManifestไฟล์ซึ่งส่งมอบชั้นเรียนของฉัน แต่เมื่อฉันใช้:

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

ฉันได้รับMANIFESTจากการ.jarโหลดครั้งแรกใน Java Runtime
แอปของฉันจะทำงานจากแอพเพล็ตหรือเว็บสตาร์ต
ดังนั้นฉันจะไม่สามารถเข้าถึง.jarไฟล์ของตัวเองได้ฉันเดาว่า

จริงๆแล้วฉันต้องการอ่านExport-packageแอตทริบิวต์.jarที่เริ่มต้นของ Felix OSGi ดังนั้นฉันจึงสามารถเปิดเผยแพ็คเกจเหล่านั้นให้กับเฟลิกซ์ได้ ความคิดใด ๆ ?


3
ฉันคิดว่าคำตอบ FrameworkUtil.getBundle () ด้านล่างดีที่สุด มันตอบสิ่งที่คุณจริงต้องการที่จะทำ (ได้รับการส่งออกกำของ) มากกว่าสิ่งที่คุณถาม (อ่านอย่างชัดแจ้ง)
Chris Dolan

คำตอบ:


117

คุณสามารถทำอย่างใดอย่างหนึ่งจากสองสิ่ง:

  1. เรียกgetResources()และวนซ้ำผ่านคอลเล็กชัน URL ที่ส่งคืนโดยอ่านเป็นรายการจนกว่าคุณจะพบ URL ของคุณ:

    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. คุณสามารถลองตรวจสอบว่าgetClass().getClassLoader()เป็นอินสแตนซ์ของjava.net.URLClassLoaderไฟล์. ส่วนใหญ่ของ Sun classloaders ได้แก่AppletClassLoader. จากนั้นคุณสามารถส่งและเรียกfindResource()สิ่งที่เป็นที่รู้จัก - สำหรับแอพเพล็ตอย่างน้อย - เพื่อส่งคืนรายการที่ต้องการโดยตรง:

    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
    }

5
ที่สมบูรณ์แบบ! ฉันไม่เคยรู้เลยว่าคุณสามารถทำซ้ำผ่านแหล่งข้อมูลที่มีชื่อเดียวกันได้
Houtman

คุณรู้ได้อย่างไรว่า classloader รู้จักไฟล์. jar เพียงไฟล์เดียว (จริงในหลาย ๆ กรณีฉันคิดว่า) ฉันค่อนข้างจะใช้สิ่งที่เกี่ยวข้องโดยตรงกับชั้นเรียนที่เป็นปัญหา
Jason S

7
เป็นแนวทางปฏิบัติที่ดีในการสร้างคำตอบแยกกันสำหรับแต่ละคำตอบแทนที่จะรวมคำตอบ 2 ข้อไว้ในคำตอบเดียว คำตอบแยกกันสามารถโหวตได้โดยอิสระ
Alba Mendez

หมายเหตุ: ฉันต้องการบางสิ่งที่คล้ายกัน แต่ฉันอยู่ใน WAR บน JBoss ดังนั้นแนวทางที่สองจึงไม่ได้ผลสำหรับฉัน ฉันลงเอยด้วยstackoverflow.com/a/1283496/160799
Gregor

1
ตัวเลือกแรกไม่ได้ผลสำหรับฉัน ฉันได้รับข้อมูลจาก 62 ไหการพึ่งพาของฉัน แต่ไม่ใช่ขวดที่กำหนดคลาสปัจจุบัน ...
Jolta

120

คุณสามารถค้นหา URL สำหรับชั้นเรียนของคุณก่อน ถ้าเป็น JAR ให้โหลดไฟล์ Manifest จากที่นั่น ตัวอย่างเช่น,

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

ฉันชอบโซลูชันนี้เนื่องจากได้รับรายการของคุณเองโดยตรงแทนที่จะต้องค้นหา
เจ

1
สามารถปรับปรุงได้นิด ๆ หน่อย ๆ โดยการเอาเช็คสภาพclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
เจย์

2
ใครปิดสตรีม?
สิ้นสุด

1
สิ่งนี้ใช้ไม่ได้ในคลาสภายในเนื่องจากgetSimpleNameลบชื่อคลาสภายนอกออก clazz.getName().replace (".", "/") + ".class"นี้จะทำงานสำหรับการเรียนภายใน:
สิ้นสุด

3
คุณจำเป็นต้องปิดสตรีมตัวสร้างรายการไม่ได้
บจ.

21

คุณสามารถใช้Manifestsจากjcabi-manifestและอ่านแอตทริบิวต์ใด ๆ จากไฟล์ MANIFEST.MF ที่มีอยู่เพียงบรรทัดเดียว:

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

การพึ่งพาเพียงอย่างเดียวที่คุณต้องการคือ:

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

ดูรายละเอียดเพิ่มเติมได้จากบล็อกโพสต์นี้: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html


ห้องสมุดที่ดีมาก มีวิธีควบคุมระดับการบันทึกหรือไม่?
assylias

1
jcabi libs ทั้งหมดเข้าสู่ระบบผ่าน SLF4J คุณสามารถส่งข้อความบันทึกโดยใช้สิ่งอำนวยความสะดวกใด ๆ ที่คุณต้องการเช่น log4j หรือ logback
yegor256

หากใช้ logback.xml บรรทัดที่คุณต้องเพิ่มจะเป็นเช่น<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher

1
หลายรายการจากตัว
โหลดคลาส

13

ฉันจะยอมรับล่วงหน้าว่าคำตอบนี้ไม่ได้ตอบคำถามเดิมนั่นคือโดยทั่วไปสามารถเข้าถึง Manifest ได้ อย่างไรก็ตามหากสิ่งที่จำเป็นจริงๆคือการอ่านแอตทริบิวต์ Manifest "มาตรฐาน" จำนวนหนึ่งวิธีแก้ปัญหาต่อไปนี้จะง่ายกว่าที่โพสต์ไว้ด้านบนมาก ดังนั้นฉันหวังว่าผู้ดูแลจะอนุญาต โปรดทราบว่าโซลูชันนี้อยู่ใน Kotlin ไม่ใช่ Java แต่ฉันคาดว่าพอร์ตไปยัง Java จะไม่สำคัญ (แม้ว่าฉันยอมรับว่าฉันไม่รู้ว่า Java เทียบเท่ากับ ".`package`"

ในกรณีของฉันฉันต้องการอ่านแอตทริบิวต์ "Implementation-Version" ดังนั้นฉันจึงเริ่มต้นด้วยโซลูชันที่ระบุไว้ข้างต้นเพื่อรับสตรีมจากนั้นอ่านเพื่อรับค่า ในขณะที่วิธีนี้ได้ผลเพื่อนร่วมงานที่ตรวจสอบโค้ดของฉันแสดงให้ฉันเห็นวิธีที่ง่ายกว่าในการทำสิ่งที่ฉันต้องการ โปรดทราบว่าโซลูชันนี้อยู่ใน Kotlin ไม่ใช่ Java

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

โปรดทราบอีกครั้งว่าสิ่งนี้ไม่ได้ตอบคำถามเดิมโดยเฉพาะ "แพ็คเกจการส่งออก" ดูเหมือนจะไม่ใช่หนึ่งในแอตทริบิวต์ที่รองรับ ที่กล่าวว่ามี myPackage.name ที่ส่งคืนค่า บางทีอาจมีคนที่เข้าใจสิ่งนี้มากกว่าที่ฉันสามารถแสดงความคิดเห็นได้ว่าจะคืนค่าตามที่ผู้โพสต์ขอไว้หรือไม่


4
อันที่จริงพอร์ต java นั้นตรงไปตรงมา:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson

นี่คือสิ่งที่ฉันกำลังมองหา ฉันดีใจด้วยที่ Java ยังเทียบเท่า
Aleksander Stelmaczonek

12

ฉันเชื่อว่าวิธีที่เหมาะสมที่สุดในการรับรายการสำหรับบันเดิลใด ๆ (รวมถึงบันเดิลที่โหลดคลาสที่กำหนด) คือการใช้อ็อบเจ็กต์ Bundle หรือ 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();

โปรดทราบว่าอ็อบเจ็กต์ Bundle ยังให้getEntry(String path)เพื่อค้นหารีซอร์สที่อยู่ภายในบันเดิลที่ระบุแทนที่จะค้นหาคลาสพา ธ ทั้งหมดของบันเดิลนั้น

โดยทั่วไปหากคุณต้องการข้อมูลเฉพาะกลุ่มอย่าพึ่งพาสมมติฐานเกี่ยวกับตัวโหลดคลาสเพียงแค่ใช้ OSGi API โดยตรง


10

วิธีที่ง่ายที่สุดคือใช้คลาส 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);

เนื่องจากในบางกรณี...class.getProtectionDomain().getCodeSource().getLocation();ให้พา ธ ด้วยvfs:/ดังนั้นจึงควรจัดการเพิ่มเติม


นี่เป็นวิธีที่ง่ายและสะอาดที่สุดในการทำ
walen

9

รหัสต่อไปนี้ใช้งานได้กับไฟล์เก็บถาวรหลายประเภท (jar, war) และ 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);
    }
  }

อาจเป็นผลมาจากการclz.getResource(resource).toString()มีแบ็กสแลช?
อ่าง

6

คุณสามารถใช้ getProtectionDomain (). getCodeSource () ดังนี้:

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

1
getCodeSourcenullอาจจะกลับมา อะไรคือเกณฑ์ที่จะได้ผล? เอกสารไม่ได้อธิบายนี้
สิ้นสุด

4
ไหนจะDataUtilitiesนำเข้ามาจากไหน? ดูเหมือนไม่ได้อยู่ใน JDK
Jolta

2

เหตุใดคุณจึงรวม step getClassLoader ถ้าคุณพูดว่า "this.getClass (). getResource ()" คุณควรได้รับทรัพยากรที่สัมพันธ์กับคลาสการโทร ฉันไม่เคยใช้ ClassLoader.getResource () แม้ว่าจากการดูเอกสาร Java อย่างรวดเร็วดูเหมือนว่าจะทำให้คุณได้รับทรัพยากรแรกของชื่อนั้นที่พบในคลาสพา ธ ปัจจุบัน


ถ้าชั้นของคุณมีชื่อว่า "com.mypackage.MyClass" โทรจะพยายามโหลดทรัพยากรจากที่class.getResource("myresource.txt") com/mypackage/myresource.txtคุณจะใช้วิธีนี้ในการรับรายการได้อย่างไร?
ChssPly76

1
โอเคฉันต้องย้อนรอย นั่นคือสิ่งที่มาจากการไม่ทดสอบ ฉันคิดว่าคุณสามารถพูด this.getClass (). getResource ("../../ META-INF / MANIFEST.MF") (อย่างไรก็ตามมี ".. " จำนวนมากที่จำเป็นสำหรับชื่อแพ็คเกจของคุณ) แต่ในขณะที่ ที่ทำงานกับไฟล์คลาสในไดเร็กทอรีเพื่อหาทางขึ้นไดเร็กทอรีทรีดูเหมือนว่าจะใช้ไม่ได้กับ JAR ฉันไม่เห็นว่าทำไมไม่ แต่นั่นเป็นวิธีการ ไม่ทำงาน this.getClass (). getResource ("/ META-INF / MANIFEST.MF") ซึ่งทำให้ฉันได้รับรายการสำหรับ rt.jar (จะยังคง ... )
เจ

สิ่งที่คุณทำได้คือใช้ getResource เพื่อค้นหาเส้นทางไปยังไฟล์คลาสของคุณเองจากนั้นตัดทุกอย่างออกหลังจาก "!" เพื่อรับเส้นทางไปยัง jar จากนั้นต่อท้าย "/META-INF/MANIFEST.MF" เหมือนที่จื่อหงแนะนำดังนั้นฉันจึงโหวตให้เขา
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
        }
      }
    }
  }

คำตอบนี้ใช้วิธีการโหลดไฟล์ Manifest ที่ซับซ้อนและผิดพลาดได้ง่าย cl.getResourceAsStream("META-INF/MANIFEST.MF")วิธีการแก้ปัญหาที่ง่ายมากคือการใช้งาน
Robert

คุณลองหรือยัง? จะได้รับโถอะไรหากคุณมีหลายไหใน classpath? จะใช้ครั้งแรกที่ไม่ใช่สิ่งที่คุณต้องการ รหัสของฉันแก้ปัญหานี้ได้และใช้งานได้จริง
Alex Konshin

ฉันไม่ได้วิจารณ์วิธีที่คุณใช้ classloader สำหรับการโหลดทรัพยากรเฉพาะ ฉันชี้ให้เห็นว่ารหัสทั้งหมดระหว่างclassLoader.getResource(..)และurl.openStream()ไม่เกี่ยวข้องโดยสิ้นเชิงและเกิดข้อผิดพลาดได้ง่ายเนื่องจากพยายามทำเช่นเดียวกับที่classLoader.getResourceAsStream(..)ทำ
Robert

Nope มันมีความแตกต่างกัน รหัสของฉันรับรายการจาก jar เฉพาะที่คลาสตั้งอยู่แทนที่จะมาจาก jar แรกใน classpath
Alex Konshin

"รหัสการโหลดเฉพาะ jar" ของคุณเทียบเท่ากับสองบรรทัดต่อไปนี้:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert

0

ฉันใช้วิธีแก้ปัญหาจาก Anthony Juckel แต่ใน MANIFEST.MF คีย์ต้องขึ้นต้นด้วยตัวพิมพ์ใหญ่

ดังนั้นไฟล์ MANIFEST.MF ของฉันจึงมีคีย์เช่น:

Mykey:ค่า

จากนั้นในตัวกระตุ้นหรือคลาสอื่นคุณสามารถใช้รหัสจาก Anthony เพื่ออ่านไฟล์ MANIFEST.MF และค่าที่คุณต้องการ

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

0

ฉันมีวิธีแก้ปัญหาแปลก ๆ ที่เรียกใช้แอปพลิเคชันสงครามในเซิร์ฟเวอร์ Jetty ในตัว แต่แอพเหล่านี้จำเป็นต้องทำงานบนเซิร์ฟเวอร์ Tomcat มาตรฐานด้วยและเรามีคุณสมบัติพิเศษบางอย่างใน manfest

ปัญหาคือเมื่ออยู่ใน Tomcat รายการสามารถอ่านได้ แต่เมื่ออยู่ในท่าเทียบเรือจะมีการสุ่มรายการขึ้นมา (ซึ่งพลาดคุณสมบัติพิเศษ)

จากคำตอบของ Alex Konshin ฉันได้หาวิธีแก้ปัญหาต่อไปนี้ (จากนั้นจะใช้อินพุตสตรีมในคลาส 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;
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.