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?
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…
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...
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.
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 "$@"