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.

The following code snippet demonstrates running javap -verbose against the Apache Commons Configuration class ServletFilterCommunication contained in commons-configuration-1.10.jar.

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 VersionJDK Version
52Java SE 8
51Java SE 7
50Java SE 6
49J2SE 5
48JDK 1.4
47JDK 1.3
46JDK 1.2
45JDK 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 blog post "Identify Java Compiler version from Class Format Major/Minor version information" and the StackOverflow thread "Java API to find out the JDK version a class file is compiled for?" demonstrate reading the relevant two bytes from the Java .class file using DataInputStream.

Basic Access to JDK Version Used to Compile .class File

The next code listing demonstrates the minimalistic approach to access a .class file's JDK compilation version.

final DataInputStream input = new DataInputStream(new FileInputStream(pathFileName));
input.skipBytes(4);
final int minorVersion = input.readUnsignedShort();
final int majorVersion = input.readUnsignedShort();

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.

ClassVersion.java
import static java.lang.System.out;

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Prints out the JDK version used to compile .class files. 
 */
public class ClassVersion
{
   private static final Map<Integer, String> majorVersionToJdkVersion;

   static
   {
      final Map<Integer, String> tempMajorVersionToJdkVersion = new HashMap<>();
      tempMajorVersionToJdkVersion.put(45, "JDK 1.1");
      tempMajorVersionToJdkVersion.put(46, "JDK 1.2");
      tempMajorVersionToJdkVersion.put(47, "JDK 1.3");
      tempMajorVersionToJdkVersion.put(48, "JDK 1.4");
      tempMajorVersionToJdkVersion.put(49, "J2SE 5");
      tempMajorVersionToJdkVersion.put(50, "Java SE 6");
      tempMajorVersionToJdkVersion.put(51, "Java SE 7");
      tempMajorVersionToJdkVersion.put(52, "Java SE 8");
      majorVersionToJdkVersion = Collections.unmodifiableMap(tempMajorVersionToJdkVersion);
   }

   /**
    * Print (to standard output) the major and minor versions of JDK that the
    * provided .class file was compiled with.
    *
    * @param pathFileName Name of (presumably) .class file from which the major
    * and minor versions of the JDK used to compile that class are to be
    * extracted and printed to standard output.
    */
   public static void printCompiledMajorMinorVersions(final String pathFileName)
   {
      try
      {
         final DataInputStream input = new DataInputStream(new FileInputStream(pathFileName));
         printCompiledMajorMinorVersions(input, pathFileName);
      }
      catch (FileNotFoundException fnfEx)
      {
         out.println("ERROR: Unable to find file " + pathFileName);
      }
   }

   /**
    * Print (to standard output) the major and minor versions of JDK that the
    * provided .class file was compiled with.
    *
    * @param input DataInputStream instance assumed to represent a .class file
    *    from which the major and minor versions of the JDK used to compile
    *    that class are to be extracted and printed to standard output.
    * @param dataSourceName Name of source of data from which the provided
    *    DataInputStream came.
    */
   public static void printCompiledMajorMinorVersions(
      final DataInputStream input, final String dataSourceName)
   {  
      try
      {
         // 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(
               dataSourceName + " is NOT a Java .class file.");
         }
         final int minorVersion = input.readUnsignedShort();
         final int majorVersion = input.readUnsignedShort();
         out.println(
              dataSourceName + " was compiled with "
            + convertMajorVersionToJdkVersion(majorVersion)
            + " (" + majorVersion + "/" + minorVersion + ")");
      }
      catch (IOException exception)
      {
         out.println(
              "ERROR: Unable to process file " + dataSourceName
            + " to determine JDK compiled version - " + exception);
      }
   }

   /**
    * Accepts a "major version" and provides the associated name of the JDK
    * version corresponding to that "major version" if one exists.
    *
    * @param majorVersion Two-digit major version used in .class file.
    * @return Name of JDK version associated with provided "major version."
    */
   public static String convertMajorVersionToJdkVersion(final int majorVersion)
   {
      return  majorVersionToJdkVersion.get(majorVersion) != null
            ? majorVersionToJdkVersion.get(majorVersion)
            : "Unknown JDK version for 'major version' of " + majorVersion;
   }

   public static void main(final String[] arguments)
   {
      if (arguments.length < 1)
      {
         out.println("USAGE: java ClassVersion <nameOfClassFile.class>");
         System.exit(-1);
      }
      printCompiledMajorMinorVersions(arguments[0]);
   }
}

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.

displayCompiledJdkVersionsOfClassFilesInDirectory.groovy
#!/usr/bin/env groovy

// displayCompiledJdkVersionsOfClassFilesInDirectory.groovy
//
// Displays the version of JDK used to compile Java .class files in a provided
// directory and in its subdirectories.
//

if (args.length < 1)
{
   println "USAGE: displayCompiledJdkVersionsOfClassFilesInDirectory.groovy <directory_name>"
   System.exit(-1)
}

File directory = new File(args[0])
String directoryName = directory.canonicalPath
if (!directory.isDirectory())
{
   println "ERROR: ${directoryName} is not a directory."
   System.exit(-2)
}

print "\nJDK USED FOR .class COMPILATION IN DIRECTORIES UNDER "
println "${directoryName}\n"
directory.eachFileRecurse
{ file ->
   String fileName = file.canonicalPath
   if (fileName.endsWith(".class"))
   {
      ClassVersion.printCompiledMajorMinorVersions(fileName)
   }
}
println "\n"

An example of the output generated by the just-listed script is shown next.

Another Groovy script is shown next and can be used to identify the JDK version used to compile .class files in any JAR files in the specified directory or one of its subdirectories.

displayCompiledJdkVersionsOfClassFilesInJar.groovy
#!/usr/bin/env groovy

// displayCompiledJdkVersionsOfClassFilesInJar.groovy
//
// Displays the version of JDK used to compile Java .class files in JARs in the
// specified directory or its subdirectories.
//

if (args.length < 1)
{
   println "USAGE: displayCompiledJdkVersionsOfClassFilesInJar.groovy <jar_name>"
   System.exit(-1)
}

import java.util.zip.ZipFile
import java.util.zip.ZipException

String rootDir = args ? args[0] : "."
File directory = new File(rootDir)
directory.eachFileRecurse
{ file->
   if (file.isFile() && file.name.endsWith("jar"))
   {
      try
      {
         zip = new ZipFile(file)
         entries = zip.entries()
         entries.each
         { entry->
            if (entry.name.endsWith(".class"))
            {
               println "${file}"
               print "\t"
               ClassVersion.printCompiledMajorMinorVersions(new DataInputStream(zip.getInputStream(entry)), entry.name)
            }
         }
      }
      catch (ZipException zipEx)
      {
         println "Unable to open file ${file.name}"
      }
   }
}
println "\n"

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.

No comments: