Bad version number in .class file - how to find wh

2019-05-22 12:22发布

问题:

I believe I've seen all of the other posts on this issue. I think this is a different question because I am looking for a way to determine which class is causing the problem.

I build my jar using Maven.

If I ask it to build for Java 5 and I run it under Java 6 it works fine.

If I ask it to build for Java 6 and I run it under Java 6 it works fine.

If I ask it to build for Java 5 and I run it under Java 5 it fails:

java.lang.RuntimeException: public static void ....main(java.lang.String[]) failed for arguments (String[]{...})
Caused by: java.lang.reflect.InvocationTargetException: 
...
Caused by: java.lang.UnsupportedClassVersionError: Bad version number in .class file
at java.lang.ClassLoader.defineClass1(Native Method)
    ...

I have inspected the jar using Java Version Check and it reports all classes in the jar as built for Java 5.

I can only conclude that it is hacking its own classpath and linking up with some other library/jar on my machine that is not Java 5. This is quite possible but I do not have access to the source of that process and as far as I am aware I cannot single-step into the class loader to find out which class it is loading.

Are there any techniques I can use that will help me work out which class is causing the exception?

回答1:

One way to catch almost all class definitions is to use a Java Agent using the Instrumentation API. This API allows to transform all byte codes before they are used (with the exception of some of the core classes) but, of course, you can use it to just extract the version number and generate a report without changing the byte code.

One nice property of the class file transformation is that the JVM will call the transformers before rejecting malformed/ unsupported byte code to give them a chance to transform the code to something usable. So we will see the responsible byte code before the UnsupportedClassVersionError is thrown.

Here is the code of the Java Agent which does the job:

package versioncheck;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class CheckAgent implements ClassFileTransformer
{
  public static void premain(String agentArgs, Instrumentation inst)
  {
    inst.addTransformer(new CheckAgent(), inst.isRetransformClassesSupported());
  }
  public static void agentmain(String agentArgs, Instrumentation inst)
  {
    inst.addTransformer(new CheckAgent(), inst.isRetransformClassesSupported());
  }
  private int supported;
  CheckAgent()
  {
    supported=(int)Double.parseDouble(System.getProperty("java.class.version"));
  }
  public byte[] transform(ClassLoader loader, String className,
      Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
      byte[] classfileBuffer) throws IllegalClassFormatException
  {
    int version=(classfileBuffer[6]&0xff)<<8 | (classfileBuffer[7]&0xff);
    if(version>supported)
      System.out.println(className+" v"+version+" from "
      +protectionDomain.getCodeSource());
    return null;
  }
}

Then you need to put this class in a jar together with a manifest containing the following lines:

Premain-Class: versioncheck.CheckAgent
Agent-Class: versioncheck.CheckAgent
Can-Redefine-Classes: true

And the last thing to do is to add the following parameter to command line which starts your Java application: -javaagent:pathtothe.jar

See also http://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html

As a side note, such a Java Agent could also fix the problem by transforming (down-porting) the byte code to a lower version. But that’s far beyond the original question…



回答2:

Well to check what JVM versions JARs in your classpath have, I would do something like the recipe below. But as I already mentioned in the last comment, this solution doesn't cover cases when a JAR have a custom classloader which modifies classpath and does other evil things. Nevertheless you may try to use this solution and see if it helps. If no, a more elaborate solution is required...

  1. retrieve a full classpath for your application. Usually I perform this step with Maven AppAssembler but certainly there're other ways. AppAssembler is handy because it builds a shell script with the classpath specified explicitly, so you may simply copy-paste it.

  2. Run the following bash script with the classpath as the argument. The script uses file, unzip and bc internally, so you should have these tools installed.

The script:

#!/bin/sh -e

# force numeric to use decimal point
LC_NUMERIC=C
export LC_NUMERIC

tempdir=

cleanup() {
    trap - EXIT
    if test -n "$tempdir"; then
        rm -rf "$tempdir" ||:
    fi
    exit "$@"
}

debug() {
    test -z "$DEBUG" || echo "$@" >&2 ||:
}

get_jar_version() {
    local ver maxVersion compare
    maxVersion=0.0
    for ver in $(find "$1" -type f -name '*.class' -exec file {} \; | grep 'Java class data, version' | sed -e 's;.*Java class data, version ;;'); do
        debug "Version = '$ver'"
        compare=$(echo "$ver > $maxVersion" | bc)
        if [ "x$compare" = "x1" ]; then
            maxVersion="$ver"
        fi
    done
    debug "maxVersion=$maxVersion"
    echo -n "$maxVersion"
}

process_classpath_jars() {
    trap cleanup EXIT
    local cp jar oldIFS maxVersion
    oldIFS="$IFS"
    for cp in "$@"; do
        IFS=':'
        for jar in $cp; do
            if [ -z "$jar" -o "${jar%.jar}" = "$jar" ]; then
                continue
            fi
            debug "processing JAR $jar"
            tempdir=$(mktemp -d jar.XXXXXXX)
            unzip -qq "$jar" -d "$tempdir"
            IFS="$oldIFS"
            maxVersion=$(get_jar_version "$tempdir")
            rm -rf "$tempdir" ||:
            tempdir=
            IFS=':'
            echo "$jar is compiled for $maxVersion JVM"
        done
        IFS="$oldIFS"
    done
}

process_classpath_jars "$@"


标签: java class