Is it possible to determine the version of a third party Java library at Runtime?
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 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 unthrow(final Callable 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