 | Level: Intermediate Dennis Sosnoski (dms@sosnoski.com), Consultant, Sosnoski Software Solutions, Inc.
06 Jul 2005 Many of the J2SE 5.0 language features would be just as useful for older JVMs, but the compiler that implements these features generates code that requires JDK 5.0 or later. Fortunately, there's an open source project that bridges the gap between J2SE 5.0 and older JVMs -- Retroweaver. Retroweaver converts your class files to eliminate the JDK 5.0 dependency while adding its own library of support functions to make most 5.0 features fully usable in older JVMs. If you like J2SE 5.0 language features but can't make the jump to using JDK 5.0 at run time, Retroweaver may be just what you need.
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.
Retroweaving J2SE 5.0
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.
What it does
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.
Retroweaver in action
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.
Annotations -- on JDK 1.4
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.
Taking back ToStringAgent
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.
Conclusions
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.
Download | Description | Name | Size | Download method |
|---|
| Sample code | j-cwt07065code.zip | 1.44 MB | HTTP |
|---|
Resources - Click the Code icon at the top or bottom of this article to download the source code discussed in this article.
- 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.
About the author  | 
|  | 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. |
Rate this page
|  |