Can I determine the version of a Java library at runtime?
Third party Java library means a Jar file, and the Jar file manifest has properties specifically to specify the version of the library.
Beware: Not all Jar files actually specify the version, even though they should.
Built-in Java way to read that information is to use reflection, but you need to know some class in the library to query. Doesn't really matter which class/interface.
Example
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());
}
}
Output
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
Although there is no universal standard, there is a hack that works for most open source libraries, or anything that is released through a Maven repository through the Maven Release Plugin or compatible mechanisms. Since most other build systems on the JVM are Maven compatible, this should apply to libraries distributed through Gradle or Ivy as well (and possibly others).
The Maven release plugin (and all compatible processes) create a file in the released Jar called META-INF/${groupId}.${artifactId}/pom.properties
, which contains the properties groupId
, artifactId
and version
.
By checking for this file and parsing it, we can detect the versions of a majority of library versions out there. Example code (Java 8 or higher):
/**
* 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; }
}
To test this code, I'll try 3 classes, one from VAVR, one from Guava, and one from the 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);
}
Output, on my machine:
Optional[0.9.2]
Optional[24.1-jre]
Optional.empty