 | Level: Intermediate Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.
07 Jun 2005 Are you tired of building and maintaining toString() methods for all your data classes? In this edition of Classworking toolkit, consultant Dennis Sosnoski shows how you can automate the process using J2SE 5.0 annotations and the ASM bytecode manipulation framework. He takes advantage of the new J2SE 5.0 instrumentation API to invoke ASM as classes are loaded into the JVM, providing on-the-fly class modification at run time.
With J2SE 5.0, Sun has added a number of new features to the Java™ platform.
One of the most important new features is support for annotations. Annotations
promise to be useful for associating many types of metadata with Java code and
are already being used extensively to replace custom configuration files in new
and updated JSRs for extensions to the Java platform. In this column, I'll show
how you can use the ASM bytecode manipulation framework in combination with
another new J2SE 5.0 feature -- the instrumentation package -- to transform
classes as they're being loaded into the JVM as directed by annotations.
Annotation basics
Many articles have already been published discussing J2SE 5.0 annotations
(see Resources for some), so I'll just include a brief summary
here. Annotations are a form of metadata for Java code. They're similar in
function to the XDoclet-style metadata that's become increasingly popular for
working with complex framework configurations, but the implementation has much
more in common with C# attributes than with XDoclet.
The Java implementation of this language feature uses an interface-like
structure with some special extensions to the Java language syntax. I find it
clearer to ignore this interface-like structure for most purposes and instead
think of annotations as a hashmap of name-value pairs. Each annotation type
defines a fixed set of names associated with that annotation. Each name may be
given a default value but otherwise need to be defined for each use of the
annotation. Annotations can be specified to apply to a particular type of Java
component (such as a class, field, method, and so on) and can even be applied
to other annotations. (In fact, the way you restrict the components an annotation
can be used for is by using a special predefined annotation on the definition of
the annotation to be restricted.)
 | |
Unlike regular interfaces, annotations must use the keyword @interface in their definitions. Also unlike regular
interfaces, annotations can only define "methods" that take no parameters and
return only simple values (primitive types, String,
Class, enum types,
annotations, and arrays of any of these types). These "methods" are the names
for the values associated with the annotation.
Annotations are used as modifiers on declarations, just like public, final, and other keyword
modifiers defined by the Java language prior to J2SE 5.0. The use of an
annotation is indicated by the @ symbol followed by the annotation name. If
values are to be supplied for the annotation, these are given as name-value
pairs in parentheses following the annotation name.
Listing 1 shows a sample annotation declaration
followed by the definition of a class using that annotation on some methods.
This LogMe annotation is intended to flag methods
that should be included in the logging for an application. I've given the
annotation two values: one representing the level of logging in which this call
is to be included and the other the name to be used for the method call (defaulting
to an empty string, on the assumption that the code that handles this
annotation will substitute the actual method name when no name is supplied). I
then use this annotation on a pair of methods in the StringArray class, for the merge() method just using the default values, and for the
indexOf() method supplying explicit values.
Listing 1. Logging annotation definition and usage
example
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
* Annotation for method to be included in logging.
*/
@Target({ElementType.METHOD})
public @interface LogMe {
int level() default 0;
String name() default "";
}
public class StringArray
{
private final String[] m_list;
public StringArray(String[] list) {
...
}
public StringArray(StringArray base, String[] adds) {
...
}
@LogMe private String[] merge(String[] list1, String[]list2) {
...
}
public String get(int index) {
return m_list[index];
}
@LogMe(level=1, name="lookup") public int indexOf(String value) {
...
}
public int size() {
return m_list.length;
}
}
|
The next section introduces a different (and
I think more interesting) application.
Building a toString()
The Java platform provides a convenient hook for generating a text description of an
object in the form of the toString() method. The
ultimate base class java.lang.Object provides a
default implementation of this method but encourages overriding the default
implementation to supply a more useful description. Many developers make a
habit of providing their own implementations, at least for classes that are
largely data representations. I'll confess up front that I'm not one of them --
I've often found toString() useful, but generally I don't bother overriding the default. To be useful, a toString() implementation needs to be kept up to date as
fields are added and removed from the class, and I find this step too much
trouble to be worthwhile in general.
Combining annotations with class file modification can provide a way out of
this dilemma. The problem I have with maintaining a toString() method is because of the code being separate from the
field declarations in the class, meaning I have another thing I need to
remember to change anytime I add or remove a field. By using annotations on the
field declarations, I can easily indicate which fields I want included in the
toString() method, while leaving the actual
implementation of the method to a classworking tool. That way everything is in
one place (the field declaration), and I get my useful description out of toString() without needing to maintain the code.
Sampling the source
Before launching into implementing an annotation for toString() method construction, I'll give a sample of what
I'd like to accomplish. Listing 2 shows a simple data holder
class with a toString() method included in the source
code:
Listing 2. Data class with the toString() method
public class Address
{
private String m_street;
private String m_city;
private String m_state;
private String m_zip;
public Address() {}
public Address(String street, String city, String state, String zip) {
m_street = street;
m_city = city;
m_state = state;
m_zip = zip;
}
public String getCity() {
return m_city;
}
public void setCity(String city) {
m_city = city;
}
...
public String toString() {
StringBuffer buff = new StringBuffer();
buff.append("Address: street=");
buff.append(m_street);
buff.append(", city=");
buff.append(m_city);
buff.append(", state=");
buff.append(m_state);
buff.append(", zip=");
buff.append(m_zip);
return buff.toString();
}
}
|
For the Listing 2 sample, I've chosen to include all the
fields in the toString() output in the same order
they're declared in the class and to preface each field value with a "name="
text to identify it in the output. For this case, the text is generated directly
from the field name by just stripping off the leading "m_" prefix I use to
identify member fields. In other cases, I may want to only include certain fields
in the output, change the order, change the identifier text used for a value,
or even skip the identifier text completely. The annotation format should be
flexible enough to express all these possibilities.
Defining the annotation
You can define annotations for toString() generation in many different ways. To really make it useful, I'd want to
minimize the number of annotations required, perhaps by using a class
annotation to flag the classes where I want the method generated in combination
with individual field annotations to override the default handling of fields.
That's not too difficult to do, but the implementation code becomes fairly
complex. For this article, I'm going to keep it simple and just use an
annotation on the individual fields to be included in the description of an
instance.
The factors I want to control are which fields are included, whether a field
value has leading text, whether that text is based on the field name,
and the order of fields in the output. Listing 3 gives a basic annotation for this purpose:
Listing 3. Annotation for toString() generation
package com.sosnoski.asm;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
public @interface ToString {
int order() default 0;
String text() default "";
}
|
The Listing 3 annotation just defines a pair of named
values, giving the order and lead text to be used for a field. I've restricted
the use of this annotation to field declarations with the @Target line. I've also defined defaults for each of the
values. These defaults don't apply to the generated annotation information that
goes into the binary class representation (they only apply when annotations are
accessed at run time as a pseudo-interface, which I won't be doing), so I don't
actually care what values are used here. By defining defaults, I'm just making
the values optional so that I don't have to specify them each time I use the
annotation.
One factor to keep in mind when working with annotations is that the named
values must always be compile-time constants and can never be null. This rule applies to both the default values, if given,
and to values set by the user. I gather this decision was made on the basis of
consistency with the earlier Java language definition, though I find it strange
that a specification that makes such major modifications to the Java language
would limit itself to consistency in this one area.
Implementing the generation
Now that I have laid the groundwork, it's time to look at implementing the
classworking transformation that will add toString()
methods to annotated classes as they're being loaded. This implementation involves three
separate pieces of code: intercepting the classloading, accessing the annotation
information, and the actual transformation.
Intercepting with instrumentation
J2SE 5.0 adds many features to the Java platform.
I'm not personally convinced that all these additions are really
improvements. However, two little-noticed new features that are truly
useful for classworking are the java.lang.instrument package and
JVM interface, which let you (among other things) specify class
transformation agents to be used when executing a program.
To use a transformation agent, you need to specify the agent class when you
start the JVM. When using the java command to launch the JVM,
you can specify agents using command line parameters of the form -javaagent:jarpath[=options], where "jarpath" is the path
to the JAR file containing the agent class, and "options" is a parameter
string for the agent. The agent JAR file uses a special manifest
attribute to specify the actual agent class, which must define a method public static void premain(String options, Instrumentation
inst). This agent premain() method will be
called before the application's main() method and is
able to register an actual transformer with the passed-in java.lang.instrument.Instrumentation class instance.
The transformer class must implement the java.lang.instrument.ClassFileTransformer interface, which
defines a single transform() method. When a
transformer instance is registered with the Instrumentation class instance, that transformer instance
will be called for each class being created in the JVM. The transformer gets
access to the binary class representation and can modify the class
representation before it is loaded by the JVM.
Listing 4 gives the agent and transformer class (both the
same class in this case, though they don't need to be) implementation for
processing the annotations. The transform()
implementation uses ASM to scan the supplied binary class representation and
look for the appropriate annotations, collecting information about the
annotated fields of the class. If annotated fields are found, the class is
modified to include a generated toString() method and
the modified binary representation is returned. Otherwise the transform() method just returns null to indicate that no modifications are necessary.
Listing 4. Agent and transformer class
package com.sosnoski.asm;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class ToStringAgent implements ClassFileTransformer
{
// transformer interface implementation
public byte[] transform(ClassLoader loader, String cname, Class class,
ProtectionDomain domain, byte[] bytes)
throws IllegalClassFormatException {
System.out.println("Processing class " + cname);
try {
// 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 " + cname);
ClassWriter writer = new ClassWriter(false);
ToStringGenerator gen = new ToStringGenerator(writer,
cname.replace('.', '/'), fields);
creader.accept(gen, false);
return writer.toByteArray();
}
} catch (IllegalStateException e) {
throw new IllegalClassFormatException("Error: " + e.getMessage() +
" on class " + cname);
}
return null;
}
// Required method for instrumentation agent.
public static void premain(String arglist, Instrumentation inst) {
inst.addTransformer(new ToStringAgent());
}
}
|
The instrumentation features of J2SE 5.0 go beyond what I've shown here,
including the ability to access all the classes that have been loaded into the
JVM and even to redefine existing classes (if supported by the JVM). For this
column, I'll skip these other features and just move on to the ASM code used to
process the annotations and modify a class.
Accumulating the metadata
ASM 2.0 makes processing annotations easy. As you learned last
month, ASM uses a visitor approach for reporting all components of class data. J2SE 5.0 annotations are reported using the org.objectweb.asm.AnnotationVisitor interface. This
interface defines several methods, of which I'm only going to use two: visitAnnotation() is the method called when processing an
annotation, and visit() is the method called when
processing a particular name-value pair for an annotation. I also need the
actual field information, which is reported using the visitField() method of the basic
org.objectweb.asm.ClassVisitor interface.
Implementing all the methods of the two interfaces of interest would be
tedious, but fortunately ASM provides a convenient org.objectweb.asm.commons.EmptyVisitor class as a base
for writing your own visitors. EmptyVisitor just
provides an empty implementation of all the various types of visitors, allowing
you to subclass and override only the visitor methods of interest. Listing 5 gives my implementation of the FieldCollector class for processing ToString annotations, extending the EmptyVisitor class. It also includes the FieldInfo class used to hold collected field
information.
Listing 5. Annotation processing class
package com.sosnoski.asm;
import java.util.ArrayList;
import java.util.Arrays;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.EmptyVisitor;
/**
* Visitor implementation to collect field annotation information from class.
*/
public class FieldCollector extends EmptyVisitor
{
private boolean m_isIncluded;
private int m_fieldAccess;
private String m_fieldName;
private Type m_fieldType;
private int m_fieldOrder;
private String m_fieldText;
private ArrayList m_fields = new ArrayList();
// finish field handling, once we're past it
private void finishField() {
if (m_isIncluded) {
m_fields.add(new FieldInfo(m_fieldName, m_fieldType,
m_fieldOrder, m_fieldText));
}
m_isIncluded = false;
}
// return array of included field information
public FieldInfo[] getFields() {
finishField();
FieldInfo[] infos =
(FieldInfo[])m_fields.toArray(new FieldInfo[m_fields.size()]);
Arrays.sort(infos);
return infos;
}
// process field found in class
public FieldVisitor visitField(int access, String name, String desc,
String sig, Object init) {
// finish processing of last field
finishField();
// save information for this field
m_fieldAccess = access;
m_fieldName = name;
m_fieldType = Type.getReturnType(desc);
m_fieldOrder = Integer.MAX_VALUE;
// default text is empty if non-String object, otherwise from field name
if (m_fieldType.getSort() == Type.OBJECT &&
!m_fieldType.getClassName().equals("java.lang.String")) {
m_fieldText = "";
} else {
String text = name;
if (text.startsWith("m_") && text.length() > 2) {
text = Character.toLowerCase(text.charAt(2)) +
text.substring(3);
}
m_fieldText = text;
}
return super.visitField(access, name, desc, sig, init);
}
// process annotation found in class
public AnnotationVisitor visitAnnotation(String sig, boolean visible) {
// flag field to be included in representation
if (sig.equals("Lcom/sosnoski/asm/ToString;")) {
if ((m_fieldAccess & Opcodes.ACC_STATIC) == 0) {
m_isIncluded = true;
} else {
throw new IllegalStateException("ToString " +
"annotation is not supported for static field +" +
" m_fieldName");
}
}
return super.visitAnnotation(sig, visible);
}
// process annotation name-value pair found in class
public void visit(String name, Object value) {
// ignore anything except the pair defined for toString() use
if ("order".equals(name)) {
m_fieldOrder = ((Integer)value).intValue();
} else if ("text".equals(name)) {
m_fieldText = value.toString();
}
}
}
package com.sosnoski.asm;
import org.objectweb.asm.Type;
/**
* Information for field value to be included in string representation.
*/
public class FieldInfo implements Comparable
{
private final String m_field;
private final Type m_type;
private final int m_order;
private final String m_text;
public FieldInfo(String field, Type type, int order,
String text) {
m_field = field;
m_type = type;
m_order = order;
m_text = text;
}
public String getField() {
return m_field;
}
public Type getType() {
return m_type;
}
public int getOrder() {
return m_order;
}
public String getText() {
return m_text;
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Object comp) {
if (comp instanceof FieldInfo) {
return m_order - ((FieldInfo)comp).m_order;
} else {
throw new IllegalArgumentException("Wrong type for comparison");
}
}
}
|
The Listing 5 code saves the field information at the
time a field is visited, because this information will be needed later if the field has an
annotation present. When an annotation is visited, the code checks whether it is a
ToString annotation, and if so sets a flag that the
current field should be included in the list to be used for generating the
toString() method. When an annotation name-value pair
is visited, the code checks for the two names defined by the ToString annotation and saves the value for each name when
found. The real default values for these names (as opposed to the defaults used
in the annotation definition) are set up in the field visitor method, so any
value specified by the user will overwrite these defaults.
ASM visits the field first, followed by the annotation and annotation values.
There's no particular method that gets called when you're done processing the
annotations for a field, so I just have a finishField() method that I call when processing a new field
and when the completed list of fields is requested. The getFields() method provides this completed list of fields
to the caller, ordered as determined by the annotation values.
Transforming the class
Listing 6 shows the final portion of the implementation code, which actually adds the
toString() method to a class. This code is similar to the code in last
month's column for constructing a class
using ASM, but it needs to be structured differently
to modify an existing class. Here the visitor approach used by ASM
adds a little complexity -- to modify an existing class, you need to visit all
the current class content and pass it through to a class writer, filtering out
portions you want to remove and adding your new content directly to the writer.
org.objectweb.asm.ClassAdapter is a convenient base class for this purpose. It implements the pass-through handling
for a supplied class writer instance, letting you override only the methods
where you need special handling.
Listing 6. Adding the toString() method
package com.sosnoski.asm;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* Visitor to add <code>toString</code> method to a class.
*/
public class ToStringGenerator extends ClassAdapter
{
private final ClassWriter m_writer;
private final String m_internalName;
private final FieldInfo[] m_fields;
public ToStringGenerator(ClassWriter cw, String iname, FieldInfo[] props) {
super(cw);
m_writer = cw;
m_internalName = iname;
m_fields = props;
}
// called at end of class
public void visitEnd() {
// set up to build the toString() method
MethodVisitor mv = m_writer.visitMethod(Opcodes.ACC_PUBLIC,
"toString", "()Ljava/lang/String;", null, null);
mv.visitCode();
// create and initialize StringBuffer instance
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuffer");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuffer",
"<init>", "()V");
// start text with class name
String name = m_internalName;
int split = name.lastIndexOf('/');
if (split >= 0) {
name = name.substring(split+1);
}
mv.visitLdcInsn(name + ":");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
"append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
// loop through all field values to be included
boolean newline = false;
for (int i = 0; i < m_fields.length; i++) {
// check type of field (objects other than Strings need conversion)
FieldInfo prop = m_fields[i];
Type type = prop.getType();
boolean isobj = type.getSort() == Type.OBJECT &&
!type.getClassName().equals("java.lang.String");
// format lead text, with newline for object or after object
String lead = (isobj || newline) ? "\n " : " ";
if (prop.getText().length() > 0) {
lead += prop.getText() + "=";
}
mv.visitLdcInsn(lead);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuffer", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuffer;");
// load the actual field value and append
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, m_internalName,
prop.getField(), type.getDescriptor());
if (isobj) {
// convert objects by calling toString() method
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
type.getInternalName(), "toString",
"()Ljava/lang/String;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuffer", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuffer;");
} else {
// append other types directly to StringBuffer
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuffer", "append", "(" +
type.getDescriptor() + ")Ljava/lang/StringBuffer;");
}
newline = isobj;
}
// finish the method by returning accumulated text
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
"toString", "()Ljava/lang/String;");
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(3, 1);
mv.visitEnd();
super.visitEnd();
}
}
|
In Listing 6, the only method I need to override is
the visitEnd() method. This method gets called after
all the existing class information has been visited, so it's a convenient place
to implement the addition of new content. I've used the visitEnd() method to add the toString() method to the class being processed. In the code
generation, I've added a few features for nicely formatting the toString() output, but the basic principle is simple --
just loop through the supplied array of fields, generating the code to append
first the lead text and then the actual field value to a StringBuffer instance.
Because the current code will only work with J2SE 5.0 (due to the use of the
instrumentation methods to intercept classloading), I could have used the new
StringBuilder class as a more efficient equivalent to
StringBuffer. I chose to go with the older
alternative because of some follow-up work I'm going to do with this code next
column, but it's worth keeping StringBuilder in mind
for your own J2SE 5.0-specific code.
ToString in action
Listing 7 shows some test classes for the ToString annotation. I've used a mixture of different
styles for the actual annotations, specifying name-value pairs in some cases
and just using the annotation by itself in other cases. The Run class just creates an instance of the Customer class with some sample data and prints the results
of a toString() method call.
Listing 7. Test classes for ToString
package com.sosnoski.dwct;
import com.sosnoski.asm.ToString;
public class Customer
{
@ToString(order=1, text="#") private long m_number;
@ToString() private String m_homePhone;
@ToString() private String m_dayPhone;
@ToString(order=2) private Name m_name;
@ToString(order=3) private Address m_address;
public Customer() {}
public Customer(long number, Name name, Address address, String homeph,
String dayph) {
m_number = number;
m_name = name;
m_address = address;
m_homePhone = homeph;
m_dayPhone = dayph;
}
...
}
...
public class Address
{
@ToString private String m_street;
@ToString private String m_city;
@ToString private String m_state;
@ToString private String m_zip;
public Address() {}
public Address(String street, String city, String state, String zip) {
m_street = street;
m_city = city;
m_state = state;
m_zip = zip;
}
public String getCity() {
return m_city;
}
public void setCity(String city) {
m_city = city;
}
...
}
...
public class Name
{
@ToString(order=1, text="") private String m_first;
@ToString(order=2, text="") private String m_middle;
@ToString(order=3, text="") private String m_last;
public Name() {}
public Name(String first, String middle, String last) {
m_first = first;
m_middle = middle;
m_last = last;
}
public String getFirst() {
return m_first;
}
public void setFirst(String first) {
m_first = first;
}
...
}
...
public class Run
{
public static void main(String[] args) {
Name name = new Name("Dennis", "Michael", "Sosnoski");
Address address = new Address("1234 5th St.", "Redmond", "WA", "98052");
Customer customer = new Customer(12345, name, address,
"425 555-1212", "425 555-1213");
System.out.println(customer);
}
}
|
Finally, Listing 8 shows the console output from a test
run (with the first line wrapped to fit):
Listing 8. Console output from test run (first line wrapped)
[dennis@notebook code]$ java -cp lib/asm-2.0.RC1.jar:lib/asm-commons-2.0.RC1.jar
:lib/tostring-agent.jar:classes -javaagent:lib/tostring-agent.jar
com.sosnoski.dwct.Run
Processing class sun/misc/URLClassPath$FileLoader$1
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
|
Conclusions
I've demonstrated how you can use ASM in combination with J2SE 5.0
annotations to perform automatic run time classfile modification. The ToString annotation I used as an example is interesting
and (at least for me) somewhat useful. When
used by itself, it doesn't much
hinder the readability of the code. But if annotations are used for many
different purposes (as will certainly be the case in the future, given how many
Java extensions are being written or rewritten to make use of them), it seems
likely that they will start to become intrusive in the code.
I'll return to this point in a later column when I look at the trade-offs
between annotations and external configuration files. My personal view is that
both have their uses, and although annotations were developed largely as an
easier alternative to configuration files, separate configuration
files are still appropriate in some cases. Just for the record, I think the
ToString annotation is an example of an
appropriate use!
One limitation of working with J2SE 5.0 extensions is that the JDK 1.5
compiler output can only be used with the JDK 1.5 JVM. For the next
Classworking toolkit column, I'm going to look into a tool that gets around
this limitation and show how the ToString
implementation can be modified to work on older JVMs.
Download | Description | Name | Size | Download method |
|---|
| Sample code | j-cwt06075code.zip | 175 KB | HTTP |
|---|
Resources - Click the Code icon at the top or bottom of this article to download the code discussed in this article.
- Get the full details on the fast and flexible ASM
Java bytecode manipulation framework.
- If you're looking for a quick summary of working with annotations in J2SE 5.0, check out Brett McLaughlin's two-part series on the subject (developerWorks, September 2004). Part 1 discusses how to add metadata to your code and Part 2 describes creating custom annotations.
- Learn how to both read and write J2SE 5.0 annotations using ASM in "Create and Read J2SE 5.0 Annotations with the ASM Bytecode Toolkit" by Eugene Kuleshov.
- Interested in how J2SE 5.0 differs from older versions of the Java platform? Check out the developerWorks Taming Tiger series by John Zukowski for an in-depth look at all the changes.
- Find out all about J2SE annotations at JSR-175 -
A Metadata Facility for the Java Programming Language.
- Check out the other columns 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.
- For an excellent reference to the JVM architecture and instruction set, see Inside the Java Virtual Machine, by Bill Venners (Artima Software, Inc., 2004). You can view some sample chapters online to get a look at it before you purchase.
- You can purchase
or view the official Java Virtual Machine Specification online for the definitive word on
all aspects of JVM operation.
- Java
programming dynamics series, also by Dennis Sosnoski, 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.
- Browse for books on these and other technical topics.
About the author  | 
|  | Dennis Sosnoski is the founder and lead consultant of Seattle-area Java consulting company Sosnoski Software Solutions, Inc.,
specialists in J2EE, XML, and Web services support. His professional software development
experience spans over 30 years, with the last several years focused on server-side Java technologies. Dennis is a frequent speaker on XML and Java technologies at
conferences nationwide, and chairs the Seattle Java-XML SIG. Contact Dennis at dms@sosnoski.com. |
Rate this page
|  |