Đọc bản kê khai Jar của riêng tôi


133

Tôi cần đọc Manifesttệp, phân phối lớp của tôi, nhưng khi tôi sử dụng:

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

Tôi nhận được MANIFESTtừ lần đầu tiên được .jartải vào Java Runtime.
Ứng dụng của tôi sẽ chạy từ applet hoặc webstart,
vì vậy tôi sẽ không có quyền truy cập vào .jartệp của riêng mình , tôi đoán vậy.

Tôi thực sự muốn đọc Export-packagethuộc tính từ cái .jarđã khởi động Felix OSGi, vì vậy tôi có thể đưa các gói đó ra cho Felix. Có ý kiến ​​gì không?


3
Tôi nghĩ rằng câu trả lời FrameworkUtil.getBundle () dưới đây là tốt nhất. Nó trả lời những gì bạn thực sự muốn làm (nhận xuất khẩu của gói) thay vì những gì bạn yêu cầu (đọc bản kê khai).
Chris Dolan

Câu trả lời:


117

Bạn có thể làm một trong hai việc:

  1. Gọi getResources()và lặp qua bộ sưu tập URL được trả lại, đọc chúng dưới dạng bảng kê khai cho đến khi bạn tìm thấy URL của mình:

    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. Bạn có thể thử kiểm tra xem có phải getClass().getClassLoader()là một ví dụ hay không java.net.URLClassLoader. Đa số các lớp tải mặt trời là, bao gồm AppletClassLoader. Sau đó, bạn có thể truyền nó và gọi findResource()những gì đã được biết - ít nhất là cho các applet - để trả lại trực tiếp bảng kê khai cần thiết:

    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
Hoàn hảo! Tôi không bao giờ biết bạn có thể lặp qua các tài nguyên có cùng tên.
Houtman

Làm thế nào để bạn biết trình nạp lớp chỉ biết một tệp .jar duy nhất? (đúng trong nhiều trường hợp tôi cho rằng) Tôi muốn sử dụng một cái gì đó liên quan trực tiếp đến lớp trong câu hỏi.
Jason S

7
đó là một cách thực hành tốt để đưa ra câu trả lời riêng cho từng người, thay vì bao gồm 2 cách khắc phục trong một câu trả lời. Câu trả lời riêng biệt có thể được bỏ phiếu độc lập.
Alba Mendez

chỉ là một lưu ý: Tôi cần một cái gì đó tương tự nhưng tôi đang ở trong WAR trên JBoss, vì vậy cách tiếp cận thứ hai không hiệu quả với tôi. Tôi đã kết thúc với một biến thể của stackoverflow.com/a/1283496/160799
Gregor

1
Tùy chọn đầu tiên không phù hợp với tôi. Tôi đã nhận được các biểu hiện của 62 lọ phụ thuộc của mình, nhưng không phải là nơi mà lớp hiện tại được xác định ...
Jolta

119

Bạn có thể tìm thấy URL cho lớp học của bạn đầu tiên. Nếu đó là JAR, thì bạn tải tệp kê khai từ đó. Ví dụ,

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

Tôi thích giải pháp này vì nó được hiển thị trực tiếp của riêng bạn thay vì phải tìm kiếm nó.
Jay

1
có thể được cải thiện một chút bằng cách xóa kiểm tra điều kiệnclassPath.replace("org/example/MyClass.class", "META-INF/MANIFEST.MF"
Jay

2
Ai đóng luồng?
ceving

1
Điều này không hoạt động trong các lớp bên trong, vì getSimpleNameloại bỏ tên lớp bên ngoài. Điều này sẽ làm việc cho các lớp bên trong : clazz.getName().replace (".", "/") + ".class".
ceving

3
Bạn cần đóng luồng, hàm tạo tệp kê khai không.
BrianT.

21

Bạn có thể sử dụng Manifeststừ jcabi-manifest và đọc bất kỳ thuộc tính nào từ bất kỳ tệp MANIFEST.MF có sẵn nào chỉ bằng một dòng:

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

Sự phụ thuộc duy nhất bạn cần là:

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

Ngoài ra, hãy xem bài đăng trên blog này để biết thêm chi tiết: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html


Thư viện rất đẹp. Có cách nào để kiểm soát mức độ đăng nhập?
assylias

1
Tất cả libs jcabi đăng nhập thông qua SLF4J. Bạn có thể gửi tin nhắn nhật ký bằng bất kỳ cơ sở nào bạn muốn, ví dụ log4j hoặc logback
yegor256

nếu sử dụng logback.xml, dòng bạn cần thêm giống như<logger name="com.jcabi.manifests" level="OFF"/>
driftcatcher

1
Nhiều biểu hiện từ cùng một trình nạp lớp trùng nhau và ghi đè lên nhau
guai

13

Tôi sẽ thừa nhận rằng câu trả lời này không trả lời câu hỏi ban đầu, nói chung là có thể truy cập vào Bản kê khai. Tuy nhiên, nếu những gì thực sự cần thiết là đọc một trong số các thuộc tính Manifest "tiêu chuẩn", thì giải pháp sau đây đơn giản hơn nhiều so với các thuộc tính được đăng ở trên. Vì vậy, tôi hy vọng rằng người điều hành sẽ cho phép nó. Lưu ý rằng giải pháp này là trong Kotlin, không phải Java, nhưng tôi hy vọng rằng một cổng tới Java sẽ không đáng kể. (Mặc dù tôi thừa nhận tôi không biết Java tương đương với ".`package`".

Trong trường hợp của tôi, tôi muốn đọc thuộc tính "Phiên bản triển khai" vì vậy tôi đã bắt đầu với các giải pháp được đưa ra ở trên để có được luồng và sau đó đọc nó để lấy giá trị. Trong khi giải pháp này hiệu quả, một đồng nghiệp xem xét mã của tôi đã chỉ cho tôi một cách dễ dàng hơn để làm những gì tôi muốn. Lưu ý rằng giải pháp này là trong Kotlin, không phải Java.

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

Một lần nữa lưu ý rằng điều này không trả lời câu hỏi ban đầu, cụ thể là "Gói xuất khẩu" dường như không phải là một trong những thuộc tính được hỗ trợ. Điều đó nói rằng, có một myPackage.name trả về một giá trị. Có lẽ ai đó hiểu điều này nhiều hơn tôi có thể nhận xét về việc liệu điều đó có trả về giá trị mà poster ban đầu đang yêu cầu hay không.


4
Thật vậy, cổng java rất đơn giản:String implementationVersion = MyApplication.class.getPackage().getImplementationVersion();
Ian Robertson

Thực tế đây là những gì tôi đang tìm kiếm. Tôi cũng rất vui khi Java cũng có một tương đương.
Aleksander Stelmaczonek

12

Tôi tin rằng cách thích hợp nhất để có được bảng kê khai cho bất kỳ gói nào (bao gồm cả gói đã tải một lớp nhất định) là sử dụng đối tượng Bundle hoặc 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();

Lưu ý rằng đối tượng Bundle cũng cung cấp getEntry(String path)để tra cứu các tài nguyên có trong một gói cụ thể, thay vì tìm kiếm toàn bộ đường dẫn của gói đó.

Nói chung, nếu bạn muốn thông tin cụ thể theo gói, đừng dựa vào các giả định về trình nạp lớp, chỉ cần sử dụng API OSGi trực tiếp.


9

Đoạn mã sau hoạt động với nhiều loại tài liệu lưu trữ (jar, war) và nhiều loại trình nạp lớp (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);
    }
  }

kết quả có thể clz.getResource(resource).toString()có dấu gạch chéo ngược?
lưu vực

9

Cách dễ nhất là sử dụng lớp 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);

Bởi vì trong một số trường hợp ...class.getProtectionDomain().getCodeSource().getLocation();đưa ra đường dẫn với vfs:/, vì vậy điều này nên được xử lý bổ sung.


Đây là cách dễ nhất và sạch nhất để làm điều đó.
walen

6

Bạn có thể sử dụng getProtectionDomain (). GetCodeSource () như thế này:

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
getCodeSourcecó thể trở lại null. Các tiêu chí, điều này sẽ làm việc là gì? Các tài liệu không giải thích điều này.
ceving

4
Trong trường hợp được DataUtilitiesnhập khẩu từ đâu? Nó dường như không có trong JDK.
Jolta

2

Tại sao bạn bao gồm bước getClassLoader? Nếu bạn nói "this.getClass (). GetResource ()", bạn sẽ nhận được tài nguyên liên quan đến lớp gọi. Tôi chưa bao giờ sử dụng ClassLoader.getResource (), mặc dù nhìn nhanh vào Tài liệu Java, có vẻ như điều đó sẽ giúp bạn có được tài nguyên đầu tiên của tên đó được tìm thấy trong bất kỳ đường dẫn hiện tại nào.


Nếu lớp của bạn được đặt tên là "com.mypackage.MyClass", việc gọi class.getResource("myresource.txt")sẽ cố gắng tải tài nguyên đó từ đó com/mypackage/myresource.txt. Làm thế nào chính xác là bạn sẽ sử dụng phương pháp này để có được bảng kê khai?
ChssPly76

1
Được rồi, tôi phải quay lại. Đó là những gì không đến thử nghiệm. Tôi đã nghĩ rằng bạn có thể nói điều này.getClass (). GetResource ("../../ META-INF / MANIFEST.MF") (Tuy nhiên, rất nhiều ".." là cần thiết cho tên gói của bạn.) Nhưng trong khi hoạt động cho các tệp lớp trong một thư mục để làm việc theo cách của bạn lên cây thư mục, dường như nó không hoạt động đối với các tệp JAR. Tôi không thấy lý do tại sao không, nhưng đó là như thế. Cũng không phải cái này.getClass (). GetResource ("/ META-INF / MANIFEST.MF") - điều đó giúp tôi có được bảng kê khai cho rt.jar. (Sẽ được tiếp tục ...)
Jay

Những gì bạn có thể làm là sử dụng getResource để tìm đường dẫn đến tệp lớp của riêng bạn, sau đó loại bỏ mọi thứ sau dấu "!" để có được đường dẫn đến jar, sau đó thêm "/META-INF/MANIFEST.MF". Giống như Zhihong đề nghị, vì vậy tôi đang bỏ phiếu cho anh ấy.
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
        }
      }
    }
  }

Câu trả lời này sử dụng một cách rất phức tạp và dễ bị lỗi khi tải Bản kê khai. giải pháp đơn giản hơn nhiều là sử dụng cl.getResourceAsStream("META-INF/MANIFEST.MF").
Robert

Bạn đã thử à? Cái biểu hiện nào nó sẽ nhận được nếu bạn có nhiều lọ trong đường dẫn? Nó sẽ lấy cái đầu tiên không phải là thứ bạn cần. Mã của tôi giải quyết vấn đề này và nó thực sự hoạt động.
Alex Konshin

Tôi đã không chỉ trích cách bạn sử dụng trình tải lớp để tải một tài nguyên cụ thể. Tôi đã chỉ ra rằng tất cả các mã giữa classLoader.getResource(..)url.openStream()hoàn toàn không liên quan và dễ bị lỗi khi nó cố gắng làm tương tự như classLoader.getResourceAsStream(..)vậy.
Robert

Không. Nó là khác nhau. Mã của tôi lấy tệp kê khai từ tệp cụ thể nơi lớp được đặt thay vì từ tệp đầu tiên trong đường dẫn lớp.
Alex Konshin

"Mã tải cụ thể" của bạn tương đương với hai dòng sau:ClassLoader classLoader = cl.getClassLoader(); return new Manifest(classLoader.getResourceAsStream("/META-INF/MANIFEST.MF"));
Robert

0

Tôi đã sử dụng giải pháp từ Anthony Juckel nhưng trong MANIFEST.MF, khóa phải bắt đầu bằng chữ hoa.

Vì vậy, tệp MANIFEST.MF của tôi chứa một khóa như:

Mykey: giá trị

Sau đó, trong trình kích hoạt hoặc một lớp khác, bạn có thể sử dụng mã từ Anthony để đọc tệp MANIFEST.MF và giá trị mà bạn cần.

// 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

Tôi có giải pháp kỳ lạ này chạy các ứng dụng chiến tranh trong máy chủ Jetty nhúng nhưng những ứng dụng này cũng cần chạy trên các máy chủ Tomcat tiêu chuẩn và chúng tôi có một số thuộc tính đặc biệt trong manfest.

Vấn đề là khi ở Tomcat, bản kê khai có thể được đọc, nhưng khi ở cầu cảng, một bản kê khai ngẫu nhiên đã được chọn (bỏ qua các thuộc tính đặc biệt)

Dựa trên câu trả lời của Alex Konshin, tôi đã đưa ra giải pháp sau (dòng đầu vào sau đó được sử dụng trong lớp 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;
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.