我能在运行时确定Java库的版本吗?

24

在运行时确定第三方 Java 库的版本是否可能?

3个回答

34

第三方Java库指的是一个Jar文件,该Jar文件清单具有专门用于指定库版本的属性。

请注意:并非所有的Jar文件都实际指定了版本,尽管它们应该这样做。

读取该信息的内置Java方法是使用反射,但您需要知道库中的某个类来查询。不太重要哪个类/接口。

示例

public class Test {
    public static void main(String[] args) {
        printVersion(org.apache.http.client.HttpClient.class);
        printVersion(com.fasterxml.jackson.databind.ObjectMapper.class);
        printVersion(com.google.gson.Gson.class);
    }
    public static void printVersion(Class<?> clazz) {
        Package p = clazz.getPackage();
        System.out.printf("%s%n  Title: %s%n  Version: %s%n  Vendor: %s%n",
                          clazz.getName(),
                          p.getImplementationTitle(),
                          p.getImplementationVersion(),
                          p.getImplementationVendor());
    }
}

输出

org.apache.http.client.HttpClient
  Title: HttpComponents Apache HttpClient
  Version: 4.3.6
  Vendor: The Apache Software Foundation
com.fasterxml.jackson.databind.ObjectMapper
  Title: jackson-databind
  Version: 2.7.0
  Vendor: FasterXML
com.google.gson.Gson
  Title: null
  Version: null
  Vendor: null

1
你说得对,这是更标准的方式。然而,Maven 的方式是由主要仓库的管理者强制执行的,因此更可靠。理想情况下,我想结合这两种方法。 - Sean Patrick Floyd
如果两个JAR文件包含相同的包,则此方法无法正常工作。例如:a-1.1.1.jar包含类com.abc.A,b-2.2.2.jar包含com.abc.B。那么对于包com.abc的输出对于类A和B是相同的。 - S. Doe

10
尽管没有普适标准,但有一种技巧适用于大多数开源库或通过Maven发布插件或兼容机制发布到Maven存储库的任何内容。由于JVM上的大多数其他构建系统都与Maven兼容,因此这也适用于通过Gradle或Ivy分发的库(可能还有其他的分发方式)。
Maven发布插件(及所有兼容的过程)会在发布的Jar文件中创建一个名为META-INF/${groupId}.${artifactId}/pom.properties的文件,其中包含groupIdartifactIdversion属性。
通过检查并解析此文件,我们可以检测到大多数库版本的版本信息。以下是示例代码(使用Java 8或更高版本):
/**
 * Reads a library's version if the library contains a Maven pom.properties
 * file. You probably want to cache the output or write it to a constant.
 *
 * @param referenceClass any class from the library to check
 * @return an Optional containing the version String, if present
 */
public static Optional<String> extractVersion(
    final Class<?> referenceClass) {
    return Optional.ofNullable(referenceClass)
                   .map(cls -> unthrow(cls::getProtectionDomain))
                   .map(ProtectionDomain::getCodeSource)
                   .map(CodeSource::getLocation)
                   .map(url -> unthrow(url::openStream))
                   .map(is -> unthrow(() -> new JarInputStream(is)))
                   .map(jis -> readPomProperties(jis, referenceClass))
                   .map(props -> props.getProperty("version"));
}

/**
 * Locate the pom.properties file in the Jar, if present, and return a
 * Properties object representing the properties in that file.
 *
 * @param jarInputStream the jar stream to read from
 * @param referenceClass the reference class, whose ClassLoader we'll be
 * using
 * @return the Properties object, if present, otherwise null
 */
private static Properties readPomProperties(
    final JarInputStream jarInputStream,
    final Class<?> referenceClass) {

    try {
        JarEntry jarEntry;
        while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
            String entryName = jarEntry.getName();
            if (entryName.startsWith("META-INF")
                && entryName.endsWith("pom.properties")) {

                Properties properties = new Properties();
                ClassLoader classLoader = referenceClass.getClassLoader();
                properties.load(classLoader.getResourceAsStream(entryName));
                return properties;
            }
        }
    } catch (IOException ignored) { }
    return null;
}

/**
 * Wrap a Callable with code that returns null when an exception occurs, so
 * it can be used in an Optional.map() chain.
 */
private static <T> T unthrow(final Callable<T> code) {
    try {
        return code.call();
    } catch (Exception ignored) { return null; }
}

为了测试这段代码,我将尝试使用3个类,一个来自VAVR,一个来自Guava,还有一个来自JDK。

public static void main(String[] args) {
    Stream.of(io.vavr.collection.LinkedHashMultimap.class,
              com.google.common.collect.LinkedHashMultimap.class,
              java.util.LinkedHashMap.class)
          .map(VersionExtractor::extractVersion)
          .forEach(System.out::println);
}

我的电脑上的输出如下:

Optional[0.9.2]
Optional[24.1-jre]
Optional.empty

最好打印出“class”名称而不是“Optional”。 - Scary Wombat
@ScaryWombat 是的,但如果没有版本存在,你会怎么做? - Sean Patrick Floyd

1
作为一名曾经负责许多非常老旧的Java项目的人,答案是“可以做到,但如何做取决于情况”。
首先,请检查您的JAR MANIFEST.MF文件。有时候你会很幸运。
其次,请扫描JAR文件以获取版本字段。有时你会很幸运,而有时候这个值会是错误的。
第三,请扫描包含的属性文件。有一个常见的ANT构建模式,将版本保存在属性文件中(更容易更新)。
第四,开始下载该项目可用的JAR文件。偶尔版本号真的丢失了,验证特定版本的唯一方法是找到已知的旧版本并进行JAR到JAR比较。
还有其他技术,但这4个几乎涵盖了所有情况。对于一些命名非常糟糕的小众库来说,可能会很具有挑战性。

1
#4 同时很有趣又令人悲伤,就像是刚从《迪尔伯特》漫画中走出来一样。我也曾经历过,深感痛苦。 - Sean Patrick Floyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接