Tool to read and display Java .class versions

Do any of you know of a tool that will search for .class files and then display their compiled versions?

I know you can look at them individually in a hex editor but I have a lot of class files to look over (something in my giant application is compiling to Java6 for some reason).

65374 次浏览

It is easy enough to read the class file signature and get these values without a 3rd party API. All you need to do is read the first 8 bytes.

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;

For class file version 51.0 (Java 7), the opening bytes are:

CA FE BA BE 00 00 00 33

...where 0xCAFEBABE are the magic bytes, 0x0000 is the minor version and 0x0033 is the major version.

import java.io.*;


public class Demo {
public static void main(String[] args) throws IOException {
ClassLoader loader = Demo.class.getClassLoader();
try (InputStream in = loader.getResourceAsStream("Demo.class");
DataInputStream data = new DataInputStream(in)) {
if (0xCAFEBABE != data.readInt()) {
throw new IOException("invalid header");
}
int minor = data.readUnsignedShort();
int major = data.readUnsignedShort();
System.out.println(major + "." + minor);
}
}
}

Walking directories (File) and archives (JarFile) looking for class files is trivial.

Oracle's Joe Darcy's blog lists the class version to JDK version mappings up to Java 7:

Target   Major.minor Hex
1.1      45.3        0x2D
1.2      46.0        0x2E
1.3      47.0        0x2F
1.4      48.0        0x30
5 (1.5)  49.0        0x31
6 (1.6)  50.0        0x32
7 (1.7)  51.0        0x33
8 (1.8)  52.0        0x34
9        53.0        0x35

Use the javap tool that comes with the JDK. The -verbose option will print the version number of the class file.

> javap -verbose MyClass
Compiled from "MyClass.java"
public class MyClass
SourceFile: "MyClass.java"
minor version: 0
major version: 46
...

To only show the version:

WINDOWS> javap -verbose MyClass | find "version"
LINUX  > javap -verbose MyClass | grep version

If you are on a unix system you could just do a

find /target-folder -name \*.class | xargs file | grep "version 50\.0"

(my version of file says "compiled Java class data, version 50.0" for java6 classes).

On Unix-like

file /path/to/Thing.class

Will give the file type and version as well. Here is what the output looks like:

compiled Java class data, version 49.0

Yet another java version check

od -t d -j 7 -N 1 ApplicationContextProvider.class | head -1 | awk '{print "Java", $2 - 44}'

In eclipse if you don't have sources attached. Mind the first line after the attach source button.

// Compiled from CDestinoLog.java (version 1.5 : 49.0, super bit)

enter image description here

Maybe this helps somebody, too. Looks there is more easy way to get JAVA version used to compile/build .class. This way is useful to application/class self check on JAVA version.

I have gone through JDK library and found this useful constant: com.sun.deploy.config.BuiltInProperties.CURRENT_VERSION. I do not know since when it is in JAVA JDK.

Trying this piece of code for several version constants I get result below:

src:

System.out.println("JAVA DEV       ver.: " + com.sun.deploy.config.BuiltInProperties.CURRENT_VERSION);
System.out.println("JAVA RUN     v. X.Y: " + System.getProperty("java.specification.version") );
System.out.println("JAVA RUN v. W.X.Y.Z: " + com.sun.deploy.config.Config.getJavaVersion() ); //_javaVersionProperty
System.out.println("JAVA RUN  full ver.: " + System.getProperty("java.runtime.version")  + " (may return unknown)" );
System.out.println("JAVA RUN       type: " + com.sun.deploy.config.Config.getJavaRuntimeNameProperty() );

output:

JAVA DEV       ver.: 1.8.0_77
JAVA RUN     v. X.Y: 1.8
JAVA RUN v. W.X.Y.Z: 1.8.0_91
JAVA RUN  full ver.: 1.8.0_91-b14 (may return unknown)
JAVA RUN       type: Java(TM) SE Runtime Environment

In class bytecode there is really stored constant - see red marked part of Main.call - constant stored in .class bytecode

Constant is in class used for checking if JAVA version is out of date (see How Java checks that is out of date)...

A java-based solution using version magic numbers. Below it is used by the program itself to detect its bytecode version.

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
public class Main {
public static void main(String[] args) throws DecoderException, IOException {
Class clazz = Main.class;
Map<String,String> versionMapping = new HashMap();
versionMapping.put("002D","1.1");
versionMapping.put("002E","1.2");
versionMapping.put("002F","1.3");
versionMapping.put("0030","1.4");
versionMapping.put("0031","5.0");
versionMapping.put("0032","6.0");
versionMapping.put("0033","7");
versionMapping.put("0034","8");
versionMapping.put("0035","9");
versionMapping.put("0036","10");
versionMapping.put("0037","11");
versionMapping.put("0038","12");
versionMapping.put("0039","13");
versionMapping.put("003A","14");


InputStream stream = clazz.getClassLoader()
.getResourceAsStream(clazz.getName().replace(".", "/") + ".class");
byte[] classBytes = IOUtils.toByteArray(stream);
String versionInHexString =
Hex.encodeHexString(new byte[]{classBytes[6],classBytes[7]});
System.out.println("bytecode version: "+versionMapping.get(versionInHexString));
}
}

The simplest way is to scan a class file using many of the answers here which read the class file magic bytes.

However some code is packaged in jars or other archive formats like WAR and EAR, some of which contain other archives or class files, plus you now have multi-release JAR files - see JEP-238 which use different JDK compilers per JAR.

This program scans classes from a list of files + folders and prints summary of java class file versions for each component including each JAR within WAR/EARs:

public static void main(String[] args) throws IOException {
var files = Arrays.stream(args).map(Path::of).collect(Collectors.toList());
ShowClassVersions v = new ShowClassVersions();
for (var f : files) {
v.scan(f);
}
v.print();
}

Example output from a scan:

Version: 49.0 ~ JDK-5
C:\jars\junit-platform-console-standalone-1.7.1.jar
Version: 50.0 ~ JDK-6
C:\jars\junit-platform-console-standalone-1.7.1.jar
Version: 52.0 ~ JDK-8
C:\java\apache-tomcat-10.0.12\lib\catalina.jar
C:\jars\junit-platform-console-standalone-1.7.1.jar
Version: 53.0 ~ JDK-9
C:\java\apache-tomcat-10.0.12\lib\catalina.jar
C:\jars\junit-platform-console-standalone-1.7.1.jar

The scanner:

public class ShowClassVersions {
private TreeMap<String, ArrayList<String>> vers = new TreeMap<>();
private static final byte[] CLASS_MAGIC = new byte[] { (byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe };
private final byte[] bytes = new byte[8];


private String versionOfClass(InputStream in) throws IOException  {
int c = in.readNBytes(bytes, 0, bytes.length);
if (c == bytes.length && Arrays.mismatch(bytes, CLASS_MAGIC) == CLASS_MAGIC.length) {
int minorVersion = (bytes[4] << 8) + (bytes[4] << 0);
int majorVersion = (bytes[6] << 8) + (bytes[7] << 0);
return ""+ majorVersion + "." + minorVersion;
}
return "Unknown";
}


private Matcher classes = Pattern.compile("\\.(class|ear|war|jar)$").matcher("");


// This code scans any path (dir or file):
public void scan(Path f) throws IOException {
try (var stream = Files.find(f, Integer.MAX_VALUE,
(p, a) -> a.isRegularFile() && classes.reset(p.toString()).find())) {
stream.forEach(this::scanFile);
}
}


private void scanFile(Path f) {
String fn = f.getFileName().toString();
try {
if (fn.endsWith(".ear") || fn.endsWith(".war") || fn.endsWith(".jar"))
scanArchive(f);
else if (fn.endsWith(".class"))
store(f.toAbsolutePath().toString(), versionOfClass(f));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}


private void scanArchive(Path p) throws IOException {
try (InputStream in = Files.newInputStream(p)) {
scanArchive(p.toAbsolutePath().toString(), Files.newInputStream(p));
}
}


private void scanArchive(String desc, InputStream in) throws IOException {
HashSet<String> versions = new HashSet<>();
ZipInputStream zip = new ZipInputStream(in);
for (ZipEntry entry = null; (entry = zip.getNextEntry()) != null; ) {
String name = entry.getName();
// There could be different compiler versions per class in one jar
if (name.endsWith(".class")) {
versions.add(versionOfClass(zip));
} else if (name.endsWith(".jar") || name.endsWith(".war")) {
scanArchive(desc + " => " + name, zip);
}
}
if (versions.size() > 1)
System.out.println("Warn: "+desc+" contains multiple versions: "+versions);


for (String version : versions)
store(desc, version);
}


private String versionOfClass(Path p) throws IOException {
try (InputStream in = Files.newInputStream(p)) {
return versionOfClass(in);
}
}


private void store(String path, String jdkVer) {
vers.computeIfAbsent(jdkVer, k -> new ArrayList<>()).add(path);
}


// Could add a mapping table for JDK names, this guesses based on (JDK17 = 61.0)
public void print() {
for (var ver : vers.keySet()) {
System.out.println("Version: " + ver + " ~ " +jdkOf(ver));
for (var p : vers.get(ver)) {
System.out.println("   " + p);
}
}
}


private static String jdkOf(String ver)  {
try {
return "JDK-"+((int)Float.parseFloat(ver)-44);
}
catch(NumberFormatException nfe)
{
return "JDK-??";
}
}
}

Read the 8th byte to decimal:

Unix-like: hexdump -s 7 -n 1 -e '"%d"' Main.class

Windows: busybox.exe hexdump -s 7 -n 1 -e '"%d"' Main.class

Output example:

55

Explain:

  • -s 7 Offset 7
  • -n 1 Limit 1
  • -e '"%d"' Print as decimal

Version map:

JDK 1.1 = 45 (0x2D hex)
JDK 1.2 = 46 (0x2E hex)
JDK 1.3 = 47 (0x2F hex)
JDK 1.4 = 48 (0x30 hex)
Java SE 5.0 = 49 (0x31 hex)
Java SE 6.0 = 50 (0x32 hex)
Java SE 7 = 51 (0x33 hex)
Java SE 8 = 52 (0x34 hex)
Java SE 9 = 53 (0x35 hex)
Java SE 10 = 54 (0x36 hex)
Java SE 11 = 55 (0x37 hex)
Java SE 12 = 56 (0x38 hex)
Java SE 13 = 57 (0x39 hex)
Java SE 14 = 58 (0x3A hex)
Java SE 15 = 59 (0x3B hex)
Java SE 16 = 60 (0x3C hex)
Java SE 17 = 61 (0x3D hex)