以编程方式确定Java类的JDK编译版本

当需要确定使用哪个JDK版本来编译特定的Java .class文件时, 通常使用方法是使用javap并在javap输出中查找列出的“主要版本”。 我在博客文章Autoboxing,Unboxing和NoSuchMethodError中引用了这种方法,但是在继续以编程方式实现此方法之前,请先在此进行详细说明。

以下代码段演示了如何对commons-configuration-1.10.jar包含的Apache Commons Configuration类ServletFilterCommunication运行javap -verbose



以编程方式确定Java类的JDK编译版本

我在上面显示的屏幕快照中圈了“主要版本”。 在“主要版本:”之后列出的数字(在本例中为49)表示用于编译此类的JDK版本是J2SE 5 Java类文件Wikipedia页面列出了与每个JDK版本相对应的“主要版本”数字:

主要版本 JDK版本
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

这是确定用于编译.class文件的JDK版本的简便方法,但是对目录或JAR文件中的多个类执行此操作可能会变得很乏味。 如果我们可以通过编程方式检查此主要版本,以便可以编写脚本,则会更容易。 幸运的是,Java确实支持这一点。 Matthias Ernst发布了“ 代码段:以编程方式调用javap ”,其中他演示了如何使用JDK工具JAR中的JavapEnvironment以编程方式执行javap功能,但是有一种更简便的方法来标识.class文件中指示字节的特定字节。用于编译的JDK版本。

博客文章“ 从类格式主要/次要版本信息中识别Java编译器版本 ”和StackOverflow线程“ Java API来找出要为其编译类文件的JDK版本? ”演示了如何使用DataInputStreamJava .class文件中读取相关的两个字节。

对用于编译.class文件的JDK版本的基本访问

下一个代码清单演示了访问.class文件的JDK编译版本的简约方法。

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

该代码在感兴趣的(假定的) .class文件上实例化FileInputStream ,并且该FileInputStream用于实例化DataInputStream 有效.class文件的前四个字节包含数字,指示该数字是有效的Java编译类,因此被跳过。 接下来的两个字节被读取为无符号的short,代表次要版本。 在那之后是最重要的两个字节。 它们也以未签名的缩写形式读入,代表主要版本。 这个主要版本与JDK的特定版本直接相关。 Java虚拟机规范的 第4章 (“类文件格式”)中描述了这些有效字节(magic,minor_version和major_version)。

在上面的代码清单中,为方便理解,仅跳过了“魔术” 4个字节。 但是,我更喜欢检查这四个字节,以确保它们是.class文件所期望的。 JVM规范解释了这前四个字节应该是什么,“魔术项目提供了标识类文件格式的魔术数字; 它的值为0xCAFEBABE。” 下一个代码清单将修改前面的代码清单,并添加检查以确保所讨论的文件在Java编译的.class文件中。 请注意,该检查专门使用十六进制表示形式CAFEBABE以提高可读性。

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();

在检查了其中最重要的部分之后,下面的代码清单提供了我称为ClassVersion.java的Java类的完整清单。 它具有main(String[])函数,因此可以从命令行轻松使用其功能。

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]);
   }
}

下一个屏幕快照演示了如何针对自己的.class文件运行此类。

以编程方式确定Java类的JDK编译版本

如PowerShell控制台的最后一个屏幕快照所示,该类的版本是使用JDK 8编译的。

有了这个ClassVersion ,我们就可以使用Java告诉我们何时编译特定的.class文件。 但是,这比简单地使用javap并手动寻找“主要版本”要容易得多。 使它更强大和更易于使用的是在脚本中使用它。 考虑到这一点,我现在将重点放在利用此类的Groovy脚本中,以识别用于编译JAR或目录中的多个.class文件的JDK版本。

下一个代码清单是可以使用ClassVersion类的Groovy脚本的示例。 该脚本演示了用于编译指定目录及其子目录中的所有.class文件的JDK版本。

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"

接下来显示的是刚刚列出的脚本生成的输出示例。

以编程方式确定Java类的JDK编译版本

接下来显示另一个Groovy脚本,该脚本可用于标识用于编译指定目录或其子目录之一中的任何JAR文件中的.class文件的JDK版本。

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"

接下来显示针对本文前面部分使用的JAR运行此脚本的输出的早期部分。 JAR中包含的所有.class文件都具有JDK的版本,它们针对打印到标准输出进行编译。

以编程方式确定Java类的JDK编译版本

其他想法

刚刚显示的脚本演示了一些实用程序,这些实用程序是通过能够以编程方式访问用于编译Java类的JDK版本而实现的。 这里是增强这些脚本的其他一些想法。 在某些情况下,我使用了这些增强功能,但此处未显示它们以保持更好的清晰度并避免使发布时间更长。

  • ClassVersion.java可能是用Groovy编写的。
  • ClassVersion.java返回单个信息而不是将其打印到标准输出,则其功能将更加灵活。 同样,即使返回返回的字符串,也比假设调用者希望将输出写入标准输出更为灵活。
  • 这将是容易巩固上述脚本指示用于编译个体JDK版本.class以及在目录中直接访问文件.class包含在JAR文件的文件从相同的脚本。
  • 演示脚本的一种有用的变体是返回在特定版本的JDK之前或之后,使用特定版本的JDK编译的所有.class文件的脚本。

结论

这篇文章的目的是演示以编程方式确定用于将Java源代码编译为.class文件的JDK版本。 文章演示了基于JVM类文件结构的“主要版本”字节确定用于编译的JDK版本,然后演示了如何使用Java API读取和处理.class文件,以及识别用于编译它们的JDK版本。 最后,用Groovy编写的几个示例脚本演示了以编程方式访问此信息的价值。

翻译自: https://www.javacodegeeks.com/2015/02/programmatically-determining-java-classs-jdk-compilation-version.html