Dustin's Pages

Friday, January 30, 2015

Programmatically Determining Java Class's JDK Compilation Version

When it is necessary to determine which JDK version was used to compile a particular Java .class file, an approach that is often used is to use javap and to look for the listed "major version" in the javap output. I referenced this approach in my blog post Autoboxing, Unboxing, and NoSuchMethodError, but describe it in more detail here before moving onto how to accomplish this programatically.

I circled the "major version" in the screen snapshot shown above. The number listed after "major version:" (49 in this case) indicates that the version of JDK used to compile this class is J2SE 5. The Wikipedia page for Java Class File lists the "major version" numerals corresponding to each JDK version:

Major Version

JDK Version

52

Java SE 8

51

Java SE 7

50

Java SE 6

49

J2SE 5

48

JDK 1.4

47

JDK 1.3

46

JDK 1.2

45

JDK 1.1

This is an easy way to determine the version of the JDK used to compile the .class file, but it can become tedious to do this on numerous classes in a directory or JAR files. It would be easier if we could programmatically check this major version so that it could be scripted. Fortunately, Java does support this. Matthias Ernst has posted "Code snippet: calling javap programmatically," in which he demonstrates use of JavapEnvironment from the JDK tools JAR to programatically perform javap functionality, but there is an easier way to identify the specific bytes of the .class file that indicate the version of JDK used for compilation.

The code instantiates a FileInputStream on the (presumed) .class file of interest and that FileInputStream is used to instantiate a DataInputStream. The first four bytes of a valid .class file contain numerals indicating it is a valid Java compiled class and are skipped. The next two bytes are read as an unsigned short and represent the minor version. After that comes the most important two bytes for our purposes. They are also read in as an unsigned short and represent the major version. This major version directly correlates with specific versions of the JDK. These significant bytes (magic, minor_version, and major_version) are described in Chapter 4 ("The class File Format") of The Java Virtual Machine Specification.

In the code listing above, the "magic" 4 bytes are simply skipped for convenience in understanding. However, I prefer to check those four bytes to ensure that they are what are expected for a .class file. The JVM Specification explains what should be expected for these first four bytes, "The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE." The next code listing revises the previous code listing and adds a check to ensure that the file in question in a Java compiled .class file. Note that the check specifically uses the hexadecimal representation CAFEBABE for readability.

final DataInputStream input = new DataInputStream(new FileInputStream(pathFileName));
// The first 4 bytes of a .class file are 0xCAFEBABE and are "used to
// identify file as conforming to the class file format."
// Use those to ensure the file being processed is a Java .class file.
final String firstFourBytes =
Integer.toHexString(input.readUnsignedShort())
+ Integer.toHexString(input.readUnsignedShort());
if (!firstFourBytes.equalsIgnoreCase("cafebabe"))
{
throw new IllegalArgumentException(
pathFileName + " is NOT a Java .class file.");
}
final int minorVersion = input.readUnsignedShort();
final int majorVersion = input.readUnsignedShort();

With the most important pieces of it already examined, the next code listing provides the full listing for a Java class I call ClassVersion.java. It has a main(String[]) function so that its functionality can be easily used from the command line.

The next screen snapshot demonstrates running this class against its own .class file.

As the last screen snapshot of the PowerShell console indicates, the version of the class was compiled with JDK 8.

With this ClassVersion in place, we have the ability to use Java to tell us when a particular .class file was compiled. However, this is not much easier than simply using javap and looking for the "major version" manually. What makes this more powerful and easier to use is to employ it in scripts. With that in mind, I now turn focus to Groovy scripts that take advantage of this class to identify JDK versions used to compile multiple .class files in a JAR or directory.

The next code listing is an example of a Groovy script that can make using of the ClassVersion class. This script demonstrates the version of JDK used to compile all .class files in a specified directory and its subdirectories.

The early portions of the output from running this script against the JAR used at the first of this post is shown next. All .class files contained in the JAR have the version of JDK they were compiled against printed to standard output.

Other Ideas

The scripts just shown demonstrate some of the utility achieved from being able to programatically access the version of JDK used to compile Java classes. Here are some other ideas for enhancements to these scripts. In some cases, I use these enhancements, but did not shown them here to retain better clarity and to avoid making the post even longer.

ClassVersion.java could have been written in Groovy.

ClassVersion.java's functionality would be more flexible if it returned individual pieces of information rather than printing it to standard output. Similarly, even returning of the entire Strings it produces would be more flexible than assuming callers want output written to standard output.

It would be easy to consolidate the above scripts to indicate JDK versions used to compile individual .class files directly accessed in directories as well as .class files contained in JAR files from the same script.

A useful variation of the demonstrated scripts is one that returns all .class files compiled with a particular version of JDK, before a particular version of the JDK, or after a particular version of the JDK.

Conclusion

The objective of this post has been to demonstrate programmatically determining the version of JDK used to compile Java source code into .class files. The post demonstrated determining version of JDK used for compilation based on the "major version" bytes of the JVM class file structure and then showed how to use Java APIs to read and process .class files and identify the version of JDK used to compile them. Finally, a couple of example scripts written in Groovy demonstrate the value of programatic access to this information.