Run Java 7 in a Java 6 environment
Java 6 is dead. Officially so since february 2013. That’s why I use Java 7 for all my open source projects.
At my day job however, we’re still using Java 6. (Because they cannot update Websphere because that would require upgrading the Linux version. Which in turn seems to be a herculean task taking years. Welcome to the corporate world.)
Recently, I wanted to use one of my private projects on my job, but because classes compiled for Java 7 cannot run on Java 6, I was stuck. Too bad.
Then I found this stackoverflow page suggesting that it would be as easy as changing the version byte in the class file in order to make it runnable on Java 6. I tried it and… yes, it worked like a charm. I know this is hackish, but it’s not a critical part of the application and I’m the author of the library, so I can do whatever is needed should there pop up any problem. I was happy again.
Shortly after, I stumbled over this blog post which describes how spring ensures compatibility with different Java versions. They use the great animal sniffer. It’s a tool that checks your code and verifies that it doesn’t use APIs from future JDKs.
Great. I tested it on my library and sure enough, it found some violations. They were all caused by the usage of the Throwable.addSuppressed(Throwable)
method. The java compiler uses this method when it compiles try-with-resources statements. There’s no way to change that and try-with-resources was one of the main reasons for me to use Java 7 language features. Bummer. What should I do?
Well, if I can change the version byte in class files, I can also change more complicated things. Using good old asm, it was surprisingly simple to implement a solution.
class Transform7to6 extends ClassVisitor implements Opcodes {
private Transform7to6(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
if (version > 0x33) {
throw new IllegalStateException(file + " has a version > 7. Cannot be converted.");
}
if (version < 0x33) {
throw new NoConversionNeededException();
}
super.visit(0x32, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
return new Jdk7RemovingVisitor(
super.visitMethod(access, name, desc, signature, exceptions));
}
private static class Jdk7RemovingVisitor extends MethodVisitor implements Opcodes {
public Jdk7RemovingVisitor(MethodVisitor mv) {
super(ASM5, mv);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String desc, boolean itf) {
if (owner.equals("java/lang/Throwable") && name.equals("addSuppressed")) {
super.visitInsn(POP2);
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
}
}
It replaces invokevirtual java/lang/Throwable.addSuppressed(Ljava/lang/Throwable;)V
calls with a simple pop2
. This discards both the receiving Throwable object and the parameter from the stack. It’s the simplest way to just not call the offending method.
Now when an exception occurs, we might lose some information about the suppressed Throwable, but this is way better than getting a strange NoSuchMethodException
.
Embedding the whole thing together with animal sniffer in a nice maven plugin was the next step. It took a lot more time. Are the maven guys making their APIs so difficult to use on purpose?
In the end, I succeeded and the result is available as guru.nidi.maven.plugins:tools-maven-plugin:1.0.16
with the goal backport7to6
. It’s by no means a complete retroweaver but it’s included in the build process of my projects and it’s working for my purposes.