J2SE 5.0 brought immense changes to the Java™ language, to the point where
even seasoned Java developers require in-depth training before they're up to
speed on working with 5.0 features. Unfortunately, the JDK 5.0 compiler that
implements these language features will only support them when generating code
that's versioned specifically for JDK 5.0 or higher. If you try to run the
generated code on an earlier JVM, you'll get a java.lang.UnsupportedClassVersionError error.
Even though the generated classes specify JDK 5.0 and higher JVMs, that's not the end of the story. Developers have observed that some of the new features actually generate code that's fully compatible with older JVMs, while other features can be made compatible with some minor extensions to the standard libraries. One developer in particular, Toby Reyelts, decided to do something to get around the limitations of the JDK 5.0 compiler. The result is the open source Retroweaver project (see Resources). Retroweaver uses classworking techniques to modify the binary class representations generated by the JDK 5.0 compiler so the classes can be used with earlier JVMs.
For this article, I'm going to show the basics of working with Retroweaver. Retroweaver is actually so easy to use that it won't take much space to cover it, so I'm also going to modify the annotations+ASM run time code generation approach I covered last month to work with pre-5.0 JDKs, using Retroweaver to sidestep the JDK 5.0 compiler restrictions.
Retroweaver consists of two logical components: a bytecode enhancer and a runtime library. The bytecode enhancer uses classworking techniques to modify the class files generated by the JDK 5.0 compiler, making these classes usable with older JVMs. As part of class file modification, Retroweaver may need to replace references to standard classes added in J2SE 5.0. The actual replacement classes are included in the runtime library so that they're available when you execute the modified code.
In terms of the standard development cycle, the bytecode enhancer needs to be run after your Java code has been compiled and before the class files are packaged up for deployment. This change can be a problem when you're using an IDE -- "Integrating" a class transformation tool into a "Development Environment" can be painful, because IDEs generally assume they own the class files. One way to limit the pain is to just use JDK 5.0 for most of your testing within the IDE. That way you only need to transform the class files when you're about to package the files for deployment or when you want to test with the actual deployment JVM. If you use an Ant-style build procedure, there's no problem; you just add the Retroweaver bytecode enhancer as a step after the compile.
Retroweaver has one minor limitation: Even though Retroweaver lets you use J2SE 5.0 language features in code that runs on older JVMs, it does not support all the additions to the standard Java classes that were also included in J2SE 5.0. If your code uses any of the classes or methods added in J2SE 5.0, you'll get an error when you try to load the code in older JVMs, even after Retroweaver processing. Avoiding the J2SE 5.0 additions to the standard libraries shouldn't be a major problem, but it can potentially catch you by surprise if you use autosuggestion popups in your IDE and accidentally pick a method or class that was only added in J2SE 5.0.
J2SE 5.0 makes changes in both the JVM and the actual Java language, but the JVM changes are fairly minor. There's a new character usable in identifiers within the bytecode ("+"), a modification to a pair of instructions to work with class references, and a different approach to synthetic components. Retroweaver handles these JVM changes in the bytecode enhancement step by just reversing them, substituting the approach that was used for the same purpose prior to J2SE 5.0 (in the case of the + character in identifiers, by replacing it with $).
The language changes included in J2SE 5.0 are a little more complex. Some of
the most interesting changes, such as the enhanced for
loop, are basically just syntax changes that provide a shortcut for expressing
a programming operation. Likewise with the generics changes -- the generic type
information is used by the compiler to enforce compile-time safe usage, but the
generated bytecode still uses casts everywhere. But most of the changes make use
of added classes or methods in the core Java APIs, so you can't just take the
bytecode generated for JDK 5.0 and run it directly on earlier JVMs. Retroweaver
provides its own equivalents for the new classes required to support the J2SE
5.0 language changes, and it substitutes references to its own classes for the
references to the standard classes as part of the bytecode enhancement step.
The Retroweaver bytecode enhancements can't provide full support for all the J2SE 5.0 language changes. For instance, there's no run-time support for working with annotations, because the run-time support involves changes to the basic JVM classloader implementation. But generally, the missing support only involves minor features that will not affect normal users.
Using Retroweaver is almost ridiculously easy. You can either use a simple GUI interface or a console application to run the bytecode enhancements on your application class files. Either way, you just point Retroweaver at the root directory of the class file tree to be converted. At run time, if you're using any of the features that require run-time support (such as enums), you then need to include the Retroweaver runtime jar in your classpath .
Listing 1 gives a simple example program that makes use of a few J2SE 5.0
features. com.sosnoski.dwct.Primitive is an enum class for the Java language primitive types. The main() method uses an enhanced for loop to iterate through
the different primitives and a simple switch
statement on the current instance to set the size value of each primitive.
Listing 1. Simple J2SE 5.0 enum example
package com.sosnoski.dwct;
public enum Primitive
{
BOOLEAN, BYTE, CHARACTER, DOUBLE, FLOAT, INT, LONG, SHORT;
public static void main(String[] args) {
for (Primitive p : Primitive.values()) {
int size = -1;
switch (p) {
case BOOLEAN:
case BYTE:
size = 1;
break;
case CHARACTER:
case SHORT:
size = 2;
break;
case FLOAT:
case INT:
size = 4;
break;
case DOUBLE:
case LONG:
size = 8;
break;
}
System.out.println(p + " is size " + size);
}
}
}
|
Compiling and running the Listing 1 code using JDK 5.0 gives the output shown
in Listing 2. Neither compiling nor running the Listing 1 code works under
earlier JDKs though; compiling fails because of the J2SE 5.0-specific
features, and running fails with a java.lang.UnsupportedClassVersionError
exception.
Listing 2. enum example output
[dennis@notebook code]$ java -cp classes com.sosnoski.dwct.Primitive BOOLEAN is size 1 BYTE is size 1 CHARACTER is size 2 DOUBLE is size 8 FLOAT is size 4 INT is size 4 LONG is size 8 SHORT is size 2 |
Listing 3 shows running Retroweaver on the Primitive
class -- which actually compiles to a pair of class files, one for the enum class
and another to support using the enum in a switch
statement. (Note that the listing is wrapped to fit the page width.)
Listing 3. enum example output
[dennis@notebook code]$ java -cp retro/release/retroweaver.jar:retro/lib/bcel-5.1.jar:retro/lib/ jace.jar:retro/lib/Regex.jar com.rc.retroweaver.Weaver -source classes [RetroWeaver] Weaving /home/dennis/writing/articles/devworks/series/may05/code/ classes/com/sosnoski/dwct/Primitive$1.class [RetroWeaver] Weaving /home/dennis/writing/articles/devworks/series/may05/code/ classes/com/sosnoski/dwct/Primitive.class |
After running Retroweaver, the classes are usable on both JDK 5.0 and JDK 1.4 JVMs. When the modified classes are run using a 1.4 JVM, the output is the same as in Listing 2. Retroweaver provides command line options to specify older 1.3 and 1.2 JVMs in place of the default 1.4 target, but the version of the runtime jar I downloaded required 1.4 and I didn't try rebuilding it to check the support for earlier JVMs.
Now that you've seen how Retroweaver lets you use J2SE 5.0 features in your
source code while running on earlier JVMs, I'll return to the code from last
month. In case you haven't read that column (shame on you), I'll summarize: I
showed how you can use ASM 2.0 to implement run-time class transformations based
on annotations, with the specific example of an annotation used to specify which
fields should be included in a toString() method.
The code from last month only worked with JDK 5.0 or later. For this month,
I'm going to modify the code to work on earlier JVMs. Used in combination with
Retroweaver, the benefits of automatic toString() generation will be extended to the multitudes of Java developers
stuck with pre-J2SE 5.0 run times.
The com.sosnoski.asm.ToStringAgent class I used to
implement the toString() method generation with
JDK 5.0 has one small problem for older JVMs: It uses the instrumentation API,
added with J2SE 5.0, to intercept class loading and modify classes at run time.
Intercepting class loading in earlier JVMs is less flexible, but not
impossible -- you just need to replace the classloader used for the application
program with your own version. With all the application classes loaded through
your custom classloader, you're then free to modify the class representations
before they're actually supplied to the JVM.
I used this technique of substituting a custom classloader to modify classes at run time in an earlier article (see Resources). I'm not going to duplicate the background material here, but it's in the article if you're interested.
Updating the code from last month to use the custom classloader approach
is easy. Listing 4 shows the class with all the modifications. This class replaces the
com.sosnoski.asm.ToStringAgent class used in last month's
column. The other classes used in that column remain the same.
Listing 4. ToStringLoader code
package com.sosnoski.asm;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class ToStringLoader extends URLClassLoader
{
private ToStringLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
// override of ClassLoader method
protected Class findClass(String name) throws ClassNotFoundException {
String resname = name.replace('.', '/') + ".class";
InputStream is = getResourceAsStream(resname);
if (is == null) {
System.err.println("Unable to load class " + name +
" for annotation checking");
return super.findClass(name);
} else {
System.out.println("Processing class " + name);
try {
// read the entire content into byte array
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int length;
while ((length = is.read(buff)) >= 0) {
bos.write(buff, 0, length);
}
byte[] bytes = bos.toByteArray();
// scan class binary format to find fields for toString() method
ClassReader creader = new ClassReader(bytes);
FieldCollector visitor = new FieldCollector();
creader.accept(visitor, true);
FieldInfo[] fields = visitor.getFields();
if (fields.length > 0) {
// annotated fields present, generate the toString() method
System.out.println("Modifying " + name);
ClassWriter writer = new ClassWriter(false);
ToStringGenerator gen = new ToStringGenerator(writer,
name.replace('.', '/'), fields);
creader.accept(gen, false);
bytes = writer.toByteArray();
}
// return the (possibly modified) class
return defineClass(bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Error reading class " + name);
}
}
}
public static void main(String[] args) {
if (args.length >= 1) {
try {
// get paths to be used for loading
ClassLoader base = ClassLoader.getSystemClassLoader();
URL[] urls;
if (base instanceof URLClassLoader) {
urls = ((URLClassLoader)base).getURLs();
} else {
urls = new URL[] { new File(".").toURI().toURL() };
}
// load the target class using custom class loader
ToStringLoader loader =
new ToStringLoader(urls, base.getParent());
Class clas = loader.loadClass(args[0]);
// invoke the "main" method of the application class
Class[] ptypes = new Class[] { args.getClass() };
Method main = clas.getDeclaredMethod("main", ptypes);
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
Thread.currentThread().setContextClassLoader(loader);
main.invoke(null, new Object[] { pargs });
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("Usage: com.sosnoski.asm.ToStringLoader " +
"report-class main-class args...");
}
}
}
|
To make use of my Listing 4 code, I still need to compile the
annotations-related code using JDK 5.0, then run Retroweaver on the resulting
set of classes. I also need to include the retroweaver.jar run-time code
in my classpath (because Retroweaver uses its own classes for converted
annotations). Listing 5 shows the output from running the same test code as
last month, but this time using Retroweaver and the ToStringLoader class from Listing 4 with the command line wrapped to fit the
page width).
Listing 5. ToString annotations on JDK 1.4
[dennis@notebook code]$ java -cp classes:retro/release/retroweaver-rt.jar:lib/ asm-2.0.RC1.jar:lib/asm-commons-2.0.RC1.jar com.sosnoski.asm.ToStringLoader com.sosnoski.dwct.Run Processing class com.sosnoski.dwct.Run Processing class com.sosnoski.dwct.Name Modifying com.sosnoski.dwct.Name Processing class com.sosnoski.dwct.Address Modifying com.sosnoski.dwct.Address Processing class com.sosnoski.dwct.Customer Modifying com.sosnoski.dwct.Customer Customer: #=12345 Name: Dennis Michael Sosnoski Address: street=1234 5th St. city=Redmond state=WA zip=98052 homePhone=425 555-1212 dayPhone=425 555-1213 |
The part at the end of Listing 5 that shows the output from the generated
toString() methods is identical to the results from
the JDK 5.0 version of the code in last month. Even the list of classes
being processed is nearly the same, despite the different technique used for
intercepting the classloading. The custom classloader approach used for JDK 1.4
doesn't provide the full flexibility of the JDK 5.0 instrumentation API, but it
works with all recent JVMs and allows you to modify any of your application
classes.
In this column, I've shown how you can use Retroweaver to make J2SE 5.0 Java
code runnable on older JVMs. If you love the new J2SE 5.0 language features and
can't wait to make use of them in your applications, Retroweaver offers the
perfect solution: You can begin using the language features immediately for
development, without affecting your production platform. As an example of
Retroweaver in action, I also backported my annotation-based ToString generator from last month to run on earlier
JVMs.
For next month's column, I'm going back to an issue I mentioned briefly in an earlier column: the trade-offs between annotations and external configuration files. After having been configuration file-crazy for many years, the entire set of Java extensions looks to be converting en masse to using annotations instead. But are annotations always the best way to provide configuration-type information? I've got my doubts, and I'll provide some examples along with my personal best practices guidelines next month.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample code | j-cwt07065code.zip | 1.44 MB | HTTP |
Information about download methods
- Want to get started using J2SE 5.0 language features on older JVMs? Go
straight to the (open) source for the Retroweaver project.
- Get the full details on the fast and flexible ASM
Java bytecode manipulation framework.
- Interested in how J2SE 5.0 differs from older versions of the Java platform? Check out the Taming Tiger series by John Zukowski for a look at all the changes.
- Find out all about J2SE annotations at JSR-175:
A Metadata Facility for the Java Programming Language.
- For an in-depth discussion of class transformation at run time using a custom classloader, see the
author's article, "Java
programming dynamics, Part 5: Transforming classes on-the-fly" (developerWorks, February 2004).
- Don't miss the other articles in the Classworking toolkit series by Dennis Sosnoski.
- Learn more about the Java bytecode design in "Java bytecode: Understanding
bytecode makes you a better programmer" (developerWorks, July 2001) by Peter Haggar.
- Collect the whole Java
programming dynamics series by author Dennis Sosnoski as he takes you on a tour
of the Java class structure, reflection, and classworking.
- The open source Jikes Project
provides a very fast and highly compliant compiler for the Java programming
language. Use it to generate your bytecode the old fashioned way -- from Java
source code.
- To learn more about Java technology, visit the
developerWorks Java zone. You'll find technical documentation, how-to articles,
education, downloads, product information, and more.
- Visit the New to Java technology site for the latest resources to help you get started with Java programming.
- Get involved in the developerWorks community by participating in
developerWorks blogs.

Dennis Sosnoski is the founder and lead consultant of Java technology consulting company Sosnoski Software Solutions, Inc., specialists in XML and Web services training and consulting. His professional software development experience spans over 30 years, with the last several years focused on server-side XML and Java technologies. Dennis is the lead developer of the open source JiBX XML Data Binding framework built around Java classworking technology and the associated JibxSoap Web services framework, as well as a committer on the Apache Axis2 Web services framework. He was also one of the expert group members for the JAX-WS 2.0 specification.





