Статьи

Программное определение версии компиляции JDK для Java-класса

Когда необходимо определить, какая версия JDK использовалась для компиляции конкретного файла Java .class , часто используется подход , заключающийся в использовании javap и поиске указанной «основной версии» в выходных данных javap . Я ссылался на этот подход в своем блоге Autoboxing, Unboxing и NoSuchMethodError , но опишу его более подробно здесь, прежде чем перейти к тому, как это сделать программно.

В следующем фрагменте кода демонстрируется запуск javap -verbose класса конфигурации Apache Commons ServletFilterCommunication содержится в commons-configuration-1.10.jar .

javapVerboseMajorVersion49CommonsConfiguration

Я обвел «главную версию» на снимке экрана, показанном выше. Число, указанное после «основной версии:» (в данном случае 49), указывает, что версия JDK, используемая для компиляции этого класса, – J2SE 5 . На странице Википедии для файла классов Java перечислены цифры «основной версии», соответствующие каждой версии 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

Это простой способ определить версию JDK, использованную для компиляции файла .class , но это может оказаться утомительным, если сделать это для множества классов в каталоге или JAR-файлах. Было бы проще, если бы мы могли программно проверить эту основную версию, чтобы она могла быть записана в сценарии. К счастью, Java поддерживает это. Матиас Эрнст (Matthias Ernst ) опубликовал « Фрагмент кода: программный вызов javap », в котором он демонстрирует использование JavapEnvironment из JAR инструментов JDK для программного выполнения функциональности javap , но есть более простой способ определить конкретные байты файла .class которые указывают на версия JDK, используемая для компиляции.

В блоге « Определите версию компилятора Java из информации о форматах основных и второстепенных форматов классов » и в потоке StackOverflow « API Java, чтобы узнать версию JDK, для которой скомпилирован файл класса? ”Продемонстрируйте чтение соответствующих двух байтов из файла Java .class с использованием DataInputStream .

Базовый доступ к версии JDK, используемой для компиляции файла .class

Следующий листинг кода демонстрирует минималистичный подход к доступу к версии JDK-компиляции файла .class .

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

Код создает экземпляр FileInputStream в интересующем (предполагаемом) файле .class и этот FileInputStream используется для создания экземпляра DataInputStream . Первые четыре байта допустимого файла .class содержат цифры, указывающие, что это допустимый скомпилированный класс Java, и пропускаются. Следующие два байта читаются как неподписанные короткие и представляют младшую версию. После этого идут два наиболее важных байта для наших целей. Они также читаются как неподписанные короткие и представляют основную версию. Эта основная версия напрямую связана с конкретными версиями JDK. Эти значимые байты (magic, minor_version и major_version) описаны в Главе 4 («Формат файла класса») Спецификации виртуальной машины Java .

В приведенном выше листинге кода «волшебные» 4 байта просто пропущены для удобства понимания. Однако я предпочитаю проверять эти четыре байта, чтобы убедиться, что они соответствуют ожидаемым для файла .class . Спецификация JVM объясняет, чего следует ожидать от этих первых четырех байтов: «Магический элемент предоставляет магическое число, идентифицирующее формат файла класса; оно имеет значение 0xCAFEBABE ». Следующий листинг кода пересматривает предыдущий листинг кода и добавляет проверку, чтобы убедиться, что данный файл находится в скомпилированном Java-файле .class . Обратите внимание, что проверка специально использует шестнадцатеричное представление CAFEBABE для удобства чтения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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();

После того, как наиболее важные части этого уже рассмотрены, следующий листинг кода предоставляет полный листинг для Java-класса, который я называю ClassVersion.java . Он имеет функцию main(String[]) поэтому его функциональность может быть легко использована из командной строки.

ClassVersion.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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 .

ClassVersionShowingItsOwnCompilationVersion

Как показывает последний снимок экрана консоли PowerShell, версия класса была скомпилирована с JDK 8.

С этим ClassVersion у нас есть возможность использовать Java, чтобы сообщить нам, когда был скомпилирован определенный файл .class . Однако это не намного проще, чем просто использовать javap и искать «основную версию» вручную. Что делает его более мощным и простым в использовании, так это использование его в сценариях. Имея это в виду, теперь я перехожу к сценариям Groovy, которые используют этот класс для определения версий JDK, используемых для компиляции нескольких файлов .class в JAR или каталоге.

Следующий листинг кода представляет собой пример скрипта Groovy, который может использовать класс ClassVersion . Этот скрипт демонстрирует версию JDK, используемую для компиляции всех файлов .class в указанном каталоге и его подкаталогах.

displayCompiledJdkVersionsOfClassFilesInDirectory.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/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"

Пример вывода, генерируемого только что перечисленным скриптом, показан далее.

allClassesInDirectoryShownWithCompiledJdkVersion

Далее показан другой скрипт Groovy, который можно использовать для определения версии JDK, используемой для компиляции файлов .class в любых файлах JAR в указанном каталоге или в одном из его подкаталогов.

displayCompiledJdkVersionsOfClassFilesInJar.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/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, использованного в первом из этого поста, показаны далее. Все .class содержащиеся в JAR, имеют версию JDK, с которой они были скомпилированы для распечатки в стандартный вывод.

allClassesInJarCompiledVersionsScriptOutput

Другие идеи

Только что показанные сценарии демонстрируют некоторые полезности, достигнутые благодаря возможности программного доступа к версии JDK, используемой для компиляции классов Java. Вот некоторые другие идеи для улучшения этих сценариев. В некоторых случаях я использую эти улучшения, но не показываю их здесь, чтобы сохранить лучшую ясность и избежать увеличения продолжительности поста.

  • ClassVersion.java мог быть написан на Groovy.
  • ClassVersion.java была бы более гибкой, если бы она возвращала отдельные фрагменты информации, а не выводила ее на стандартный вывод. Точно так же, даже возврат всей строки, которую он производит, будет более гибким, чем если предположить, что вызывающие абоненты хотят, чтобы вывод был записан в стандартный вывод.
  • Было бы легко объединить вышеупомянутые сценарии, чтобы указать версии JDK, используемые для компиляции отдельных файлов .class к которым непосредственно обращаются в каталогах, а также .class содержащиеся в файлах JAR из того же сценария.
  • Полезным вариантом продемонстрированных сценариев является тот, который возвращает все .class скомпилированные с определенной версией JDK, до определенной версии JDK или после определенной версии JDK.

Вывод

Целью этого поста было продемонстрировать программное определение версии JDK, используемой для компиляции исходного кода Java в .class . В публикации демонстрировалось определение версии JDK, используемой для компиляции, на основе байтов «основной версии» структуры файлов класса JVM, а затем показывалось, как использовать API-интерфейсы Java для чтения и обработки файлов .class и определения версии JDK, используемой для их компиляции. Наконец, несколько примеров сценариев, написанных на Groovy, демонстрируют ценность программного доступа к этой информации.