3

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?

7
  • BTW - I am using Netbeans. Commented Mar 28, 2014 at 10:27
  • Check the dependencies. Likely some of them are for a newer Java already. Commented Mar 29, 2014 at 16:38
  • @user3159253 - Yes but which ones? I have checked all with Java Version Check and all are fine. It must be dynamically connecting to an external lib somewhere but I don't know where. Commented Mar 29, 2014 at 16:56
  • I've prepared a solution to check versions for every JARs in the classpath and then realized that the problem may be caused by a custom classloader in one of the JARs. Can you tell what is in the dependencies list? Commented Mar 31, 2014 at 12:42
  • @user3159253 - Sorry but I have already checked all direct dependencies and there are no versioning issues. The problem is with some indirect dependency - perhaps a jdbc driver or a connection pool or a Lucene search engine or something like that. Custom classloaders are also a possibility. I think the solution should do something at run-time. Commented Mar 31, 2014 at 13:39

2 Answers 2

4

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
  • That is very nice! I will use that next time I hit this problem. Thank you for your time. I do wish Java would identify the class with the error in the exception though. Commented Apr 30, 2014 at 14:55
  • Well, it is thrown by lowlevel native code. Constructing a message String can be rather awkward there. But I tested to provoke such an Error with current Java 7 and it did print the class name and even the version number it found. But I’m afraid that improvement will not make it into a Java 5 update.
    – Holger
    Commented Apr 30, 2014 at 15:04
0

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 "$@"

Not the answer you're looking for? Browse other questions tagged or ask your own question.