Skip to main content

Classworking toolkit: Use J2SE 5.0 features on older JVMs

Can't move to JDK 5.0? Learn how an open source tool lets you use these features on older JVMs

Dennis Sosnoski (dms@sosnoski.com), Consultant, Sosnoski Software Solutions, Inc.
Author photo
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.

Summary:  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.

View more content in this series

Date:  06 Jul 2005
Level:  Intermediate
Activity:  2289 views

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.

Ask the expert: Dennis Sosnoski on JVM and bytecode issues

For comments or questions about the material covered in this article series, as well as anything else that pertains to Java bytecode, the Java binary class format, or general JVM issues, visit the JVM and Bytecode discussion forum, moderated by Dennis Sosnoski.

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

DescriptionNameSizeDownload method
Sample codej-cwt07065code.zip1.44 MB HTTP

Information about download methods


Resources

About the author

Author photo

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.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=88105
ArticleTitle=Classworking toolkit: Use J2SE 5.0 features on older JVMs
publish-date=07062005
author1-email=dms@sosnoski.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers