![]() |
|
||||||||
| Consortium Activities Projects Forge Events | |||||||||
ASM |
by Eugene Kuleshov | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? | visitAnnotationDefault |
Visits the default value for annotation interface method |
| * | visitAnnotation |
Visits a method annotation |
| * | visitParameterAnnotation |
Visits a method parameter annotation |
| * | visitAttribute |
Visits a non-standard method attribute |
| ? | visitCode |
Starts the visit of the method's code for non-abstract and non-native methods |
| * | visitInsn |
Visits a zero operand instruction: NOP,
ACONST_NULL, ICONST_M1, ICONST_0,
ICONST_1, ICONST_2, ICONST_3,
ICONST_4, ICONST_5, LCONST_0,
LCONST_1, FCONST_0, FCONST_1,
FCONST_2, DCONST_0, DCONST_1,
IALOAD, LALOAD, FALOAD,
DALOAD, AALOAD, BALOAD,
CALOAD, SALOAD, IASTORE,
LASTORE, FASTORE, DASTORE,
AASTORE, BASTORE, CASTORE,
SASTORE, POP, POP2,
DUP, DUP_X1, DUP_X2,
DUP2, DUP2_X1, DUP2_X2,
SWAP, IADD, LADD,
FADD, DADD, ISUB,
LSUB, FSUB, DSUB,
IMUL, LMUL, FMUL,
DMUL, IDIV, LDIV,
FDIV, DDIV, IREM,
LREM, FREM, DREM,
INEG, LNEG, FNEG,
DNEG, ISHL, LSHL,
ISHR, LSHR, IUSHR,
LUSHR, IAND, LAND,
IOR, LOR, IXOR, LXOR,
I2L, I2F, I2D, L2I,
L2F, L2D, F2I, F2L,
F2D, D2I, D2L, D2F,
I2B, I2C, I2S, LCMP,
FCMPL, FCMPG, DCMPL,
DCMPG, IRETURN, LRETURN,
FRETURN, DRETURN, ARETURN,
RETURN, ARRAYLENGTH, ATHROW,
MONITORENTER, or MONITOREXIT. |
visitFieldInsn |
Visits a field instruction: GETSTATIC,
PUTSTATIC, GETFIELD or
PUTFIELD. | |
visitIntInsn |
Visits an instruction with a single int operand: BIPUSH,
SIPUSH, or NEWARRAY. | |
visitJumpInsn |
Visits a jump instruction: IFEQ, IFNE,
IFLT, IFGE, IFGT,
IFLE, IF_ICMPEQ, IF_ICMPNE,
IF_ICMPLT, IF_ICMPGE, IF_ICMPGT,
IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE,
GOTO, JSR, IFNULL, or
IFNONNULL. | |
visitTypeInsn |
Visits a type instruction: NEW, ANEWARRAY,
CHECKCAST, or INSTANCEOF. | |
visitVarInsn |
Visits a local variable instruction: ILOAD,
LLOAD, FLOAD, DLOAD,
ALOAD, ISTORE, LSTORE,
FSTORE, DSTORE, ASTORE, or
RET. | |
visitMethodInsn |
Visits a method instruction: INVOKEVIRTUAL,
INVOKESPECIAL, INVOKESTATIC, or
INVOKEINTERFACE. | |
visitIincInsn |
Visits an IINC instruction. | |
visitLdcInsn |
Visits an LDC instruction. | |
visitMultiANewArrayInsn |
Visits a MULTIANEWARRAY instruction. | |
visitLookupSwitchInsn |
Visits a LOOKUPSWITCH instruction. | |
visitTableSwitchInsn |
Visits a TABLESWITCH instruction. | |
visitLabel |
Visits a label. | |
visitLocalVariable |
Visits a local variable declaration. | |
visitLineNumber |
Visits a line number declaration. | |
visitTryCatchBlock |
Visits a try-catch block. | |
visitMaxs |
Visits the maximum stack size and the maximum number of local variables of the method. | |
visitEnd |
Visits the end of the method. | |
Note that the visitEnd method must always be
called at the end of method processing. ClassReader does that for
you, but it should be taken care of in a custom bytecode producer; e.g., when a
class is generated from scratch or when new methods are introduced.
Also note that if a method actually has some bytecode (i.e., if it is not
abstract and not a native method), then visitCode must be called
before the first visit...Insn call, and the visitMaxs
method must be called after last visit...Insn call.
Each of the visitIincInsn, visitLdcInsn,
visitMultiANewArrayInsn, visitLookupSwitchInsn, and
visitTableSwitchInsn methods uniquely represent one bytecode
instruction. The rest of the visit...Insn methods--namely,
visitInsn, visitFieldInsn, visitIntInsn,
visitJumpInsn, visitTypeInsn,
visitVarInsn, and visitMethodInsn--represent more then
one bytecode instruction, with their opcodes passed in a first method parameter.
All constants for those opcodes are defined in the Opcodes
interface. This approach is very performant for bytecode parsing and formatting.
Unfortunately, this could be a challenge to the developer who is trying to
generate code, because ClassWriter does not verify these
constraints. However, there is a CheckClassAdapter that could be
used during development to test generated code.
Another challenge with any kind of bytecode generation or transformation is
that offsets within method code can change and should be adjusted when
additional instructions are inserted or removed from the method code. This is
applicable to parameters of all jump opcodes (if,
goto, jsr, and switch), as well as to
try-catch blocks, line number and local variable declarations, and to some of
the special attributes (e.g., StackMap, used by CLDC). However, ASM
hides this complexity from the developer. In order to specify positions in the
method bytecode and not have to use absolute offsets, a unique instance of the
Label class should be passed to the visitLabel method.
Other MethodVisitor methods such as visitJumpInsn,
visitLookupSwitchInsn, visitTableSwitchInsn,
visitTryCatchBlock, visitLocalVariable, and
visitLineNumber can use these Label instances even
before the visitLabel call, as long as the instance will be called
later in a method.
The above may sound complicated, and at first glance requires deep knowledge
of the bytecode instructions. However, using ASMifierClassVisitor
on compiled classes allows you to see how any given bytecode could be generated
with ASM. Moreover, applying ASMifier on two compiled classes (an
original one and one after applying the required transformation) and then
running diff on the output gives a good hint as to what ASM calls
should be used in the transformer. This process is explained in more detail in
several articles (see the Resources
section below). There is even a plugin for the Eclipse
IDE, shown in Figure 4, that provides a great support for generating ASM
code and comparing ASMifier output right from Java sources, and
also includes a contextual bytecode reference.

Figure 4. Eclipse ASM plugin (Click on the
picture to see a full-size image)
There are already a few articles that explain how to generate bytecode with ASM (see the Resources section for some links). For a change, let's see how ASM can be used to analyze existing classes. One interesting application is to capture information about external classes or packages used by any given module or .jar file. For simplicity, this example will only capture outgoing dependencies and won't keep track of the dependency types (e.g., superclass, method parameters, local variable types, etc.).
Notice that for analysis purposes, we don't need to create new instances of child visitors for annotations, fields, and methods. All of these visitors, including class and signature visitor, could be implemented in a single class:
public class DependencyVisitor implements
AnnotationVisitor, SignatureVisitor,
ClassVisitor, FieldVisitor, MethodVisitor {
...
For this example, we will track dependencies between packages, so individual classes should be aggregated by the package name:
private String getGroupKey(String name) {
int n = name.lastIndexOf('/');
if(n>-1) name = name.substring(0, n);
packages.add(name);
return name;
}
In order to collect dependencies, visitor interfaces such as
ClassVisitor, AnnotationVisitor,
FieldVisitor, and MethodVisitor should selectively
aggregate parameters of their methods. There are several common cases:
First of all, there are class names in internal
form (super class, interfaces, exceptions, field and method owners); e.g.,
java/lang/String:
private void addName(String name) {
if(name==null) return;
String p = getGroupKey(name);
if(current.containsKey(p)) {
current.put(p, current.get(p)+1);
} else {
current.put(p, 1);
}
}
In this case, current is the current group of dependencies
(e.g., package).
Another case is type descriptors (annotations, enum and field types,
parameters of the newarray instruction, etc.); e.g.,
Ljava/lang/String;, J, and [[[I. These
can be parsed with Type.getType( desc) to obtain the class name in
internal form:
private void addDesc(String desc) {
addType(Type.getType(desc));
}
private void addType(Type t) {
switch(t.getSort()) {
case Type.ARRAY:
addType(t.getElementType());
break;
case Type.OBJECT:
addName(t.getClassName().replace('.','/'));
break;
}
}
Method descriptors used in method declarations and in invoke instructions
describe parameter types and return a type; e.g.,
([java/lang/String;II)V. The helper methods
Type.getReturnType(methodDescriptor) and
Type.getArgumentTypes(methodDescriptor) can parse such descriptors
and extract parameter and return types.
private void addMethodDesc(String desc) {
addType(Type.getReturnType(desc));
Type[] types = Type.getArgumentTypes(desc);
for(int i = 0; i < types.length; i++) {
addType(types[ i]);
}
}
The special case is the signature parameter used in many "visit"
methods to specify Java 5 generics info. If it is present (i.e., non-null), this
parameter overrides the descriptor parameter and contains an encoded form of the
generics information. SignatureReader class could be used to parse
this value. So we can implement a SignatureVisitor, which will be
called for each signature artifact.
private void addSignature(String sign) {
if(sign!=null) {
new SignatureReader(sign).accept(this);
}
}
private void addTypeSignature(String sign) {
if(sign!=null) {
new SignatureReader(sign).acceptType(this);
}
}
Methods implementing the ClassVisitor interface, such as
visit(), visitField(), visitMethod(), and
visitAnnotation(), can collect information about dependencies on
superclasses and interfaces, types used by fields, method parameters, return
values, and exceptions, as well as types of the annotations. For example:
public void visit(int version, int access,
String name, String signature,
String superName, String[] interfaces) {
String p = getGroupKey(name);
current = groups.get(p);
if(current==null) {
current = new HashMap<String,Integer>();
groups.put(p, current);
}
if(signature==null) {
addName(superName);
addNames(interfaces);
} else {
addSignature(signature);
}
}
public FieldVisitor visitField(int access,
String name, String desc,
String signature, Object value) {
if(signature==null) {
addDesc(desc);
} else {
addTypeSignature(signature);
}
if(value instanceof Type) {
addType((Type) value);
}
return this;
}
public MethodVisitor visitMethod(int access,
String name, String desc,
String signature, String[] exceptions) {
if(signature==null) {
addMethodDesc(desc);
} else {
addSignature(signature);
}
addNames(exceptions);
return this;
}
public AnnotationVisitor visitAnnotation(
String desc, boolean visible) {
addDesc(desc);
return this;
}
Methods implementing the MethodVisitor interface can collect
dependencies on types of the parameter annotations and types used in bytecode
instructions that can use object references:
public AnnotationVisitor
visitParameterAnnotation(int parameter,
String desc, boolean visible) {
addDesc(desc);
return this;
}
/**
* Visits a type instruction
* NEW, ANEWARRAY, CHECKCAST or INSTANCEOF.
*/
public void visitTypeInsn(int opcode,
String desc) {
if(desc.charAt(0)=='[') {
addDesc(desc);
} else {
addName(desc);
}
}
/**
* Visits a field instruction
* GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD.
*/
public void visitFieldInsn(int opcode,
String owner, String name, String desc) {
addName(owner);
addDesc(desc);
}
/**
* Visits a method instruction INVOKEVIRTUAL,
* INVOKESPECIAL, INVOKESTATIC or
* INVOKEINTERFACE.
*/
public void visitMethodInsn(int opcode,
String owner, String name, String desc) {
addName(owner);
addMethodDesc(desc);
}
/**
* Visits a LDC instruction.
*/
public void visitLdcInsn(Object cst) {
if(cst instanceof Type) {
addType((Type) cst);
}
}
/**
* Visits a MULTIANEWARRAY instruction.
*/
public void visitMultiANewArrayInsn(
String desc, int dims) {
addDesc(desc);
}
/**
* Visits a try catch block.
*/
public void visitTryCatchBlock(Label start,
Label end, Label handler, String type) {
addName(type);
}
Now we can use DependencyVisitor to collect dependencies from
the entire .jar file. For example:
DependencyVisitor v = new DependencyVisitor();
ZipFile f = new ZipFile(jarName);
Enumeration<? extends ZipEntry> en = f.entries();
while(en.hasMoreElements()) {
ZipEntry e = en.nextElement();
String name = e.getName();
if(name.endsWith(".class")) {
ClassReader cr =
new ClassReader(f.getInputStream(e));
cr.accept(v, false);
}
}
The collected information can be represented in many different ways. One can build dependency trees and calculate some metrics, or create some visualizations. For example, Figure 5 shows how ant.1.6.5.jar looks in a visualization I built on top of the collected information using some simple Java2D code. The following diagram shows packages from the input .jar on a horizontal axis and external dependencies on a vertical axis. The darker a box's color is, the more times the package is referenced.

Figure 5. Dependencies in ant.1.6.5.jar, as discovered
with ASM
The complete code of this tool will be included into the next ASM release. It can be also obtained from ASM CVS.
You can skip this section if you haven't used ASM 1.x.
The major structural change in ASM 2.0 is that all J2SE 5.0 features are
built into the ASM visitor/filter event flow. So the new API allows you to deal
with generics and annotations in a much more lightweight and semantically
natural way. Instead of explicitly creating annotation attribute instances, we
have generics and annotation data within the event flow. For example, in ASM
1.x, the ClassVisitor interface used the following method:
CodeVisitor visitMethod(int access, String name,
String desc, String[] exceptions,
Attribute attrs);
This has been split into several methods in ASM 2.0:
MethodVisitor visitMethod(int access,
String name, String desc, String signature,
String[] exceptions)
AnnotationVisitor visitAnnotation(String desc,
boolean visible)
void visitAttribute(Attribute attr)
In the 1.x API, in order to define generics info, you'd have to create
specific instances of the SignatureAttribute, and to define
annotations, you'd need instances of the
RuntimeInvisibleAnnotations,
RuntimeInvisibleParameterAnnotations,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations, and
AnnotationDefault. Then you'd put these instances into the
attrs parameter of the appropriate visit method.
In ASM 2.0, a new signature parameter has been added to
represent generics info. The new AnnotationVisitor interface is
used to handle all annotations. There is no need to create an attrs
collection, and annotation data is more strictly typed. However, when migrating
existing code, especially when "adapter" classes have been used; it is necessary
to be careful and make sure that all methods overwritten from the adapter are
updated to new signatures, because the compiler will raise no warnings.
There are several other changes introduced in ASM 2.0.
FieldVisitor and
AnnotationVisitor.
CodeVisitor into MethodVisitor.
visitCode() method added to the
MethodVisitor to easily detect first instruction.
Constants interface renamed into Opcodes.
attrs package are incorporated
into ASM's event model.
TreeClassAdapter and TreeCodeAdapter are
incorporated into the ClassNode and MethodNode.
LabelNode class to make elements of
instructions collection common type of
AbstractInsnNode. In general, it would be a good idea to run tool like JDiff and review the differences between the ASM 1.x and 2.0 APIs.
ASM 2.0 hides many bytecode complexities from the developer and allows one to efficiently work with Java features on a bytecode level. The framework allows you not only to transform and generate bytecode, but also to pull out significant details about existing classes. The API is being constantly improved--version 2.0 incorporates the generics and annotations introduced in J2SE 5.0. Since then, support for the new features introduced in Mustang (see "Java SE 6 Snapshot Releases") have been added to the ASM framework.