Статьи

Управление файлами классов Java с помощью ASM 4 — часть вторая: Tree API

Что такое API дерева ASM: API дерева ASM является частью ASM, которая позволяет создавать / изменять класс в памяти. Класс рассматривается как дерево информации. Как и весь класс, это экземпляр ClassNode, который содержит список объектов FieldNode, список объектов MethodNode и т. Д. В этой статье предполагается, что читатель уже прочитал первую часть здесь.

Простой класс через дерево API: давайте используем дерево API для создания нашего первого класса. Опять же, я собираюсь перейти прямо к примеру кода, потому что нет ничего лучше, чем пример кода. Сгенерированный класс имеет метод main, который печатает «Hello World!».

TreeAPIDemo.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.geekyarticles.asm;
 
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
 
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
 
 
public class TreeAPIDemo {
    public static void main(String [] args) throws Exception{
         
        ClassNode classNode=new ClassNode(4);//4 is just the API version number
         
        //These properties of the classNode must be set
        classNode.version=Opcodes.V1_6;//The generated class will only run on JRE 1.6 or above
        classNode.access=Opcodes.ACC_PUBLIC;
        classNode.signature="Lcom/geekyarticles/asm/Generated;";
        classNode.name="com/geekyarticles/asm/Generated";
        classNode.superName="java/lang/Object";
         
        //Create a method
        MethodNode mainMethod=new MethodNode(4,Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC,"main", "([Ljava/lang/String;)V",null, null);
         
        mainMethod.instructions.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
        mainMethod.instructions.add(new LdcInsnNode("Hello World!"));
        mainMethod.instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));
        mainMethod.instructions.add(new InsnNode(Opcodes.RETURN));
 
        //Add the method to the classNode
        classNode.methods.add(mainMethod);
         
        //Write the class
        ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES);
        classNode.accept(cw);
         
        //Dump the class in a file
        File outDir=new File("out/com/geekyarticles/asm");
        outDir.mkdirs();
        DataOutputStream dout=new DataOutputStream(new FileOutputStream(new File(outDir,"Generated.class")));
        dout.write(cw.toByteArray());
        dout.flush();
        dout.close();
         
    }
}

Как видите, код очень прост. Основное преимущество по сравнению с BCEL заключается в том, что в отличие от BCEL, ASM не требует явного добавления каждой константы в пул констант. Вместо этого ASM заботится о самом постоянном пуле.

Чтение файла класса: ClassNode — это ClassVisitor. Таким образом, чтение класса для использования в дереве API так же просто, как создание объекта ClassReader и использование его для чтения файла класса, а также передача объекта ClassNode в его методе accept в качестве параметра. Как только это сделано, переданный ClassNode полностью инициализируется со всей информацией, представленной в классе. В следующем примере мы распечатаем все методы в классе.

TreeAPIClassReaderDemo.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.geekyarticles.asm;
 
import java.io.FileInputStream;
import java.io.InputStream;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
 
public class TreeAPIClassReaderDemo {
 
    public static void main(String[] args) throws Exception{
        InputStream in=new FileInputStream("out/com/geekyarticles/asm/Generated.class");
         
        ClassReader cr=new ClassReader(in);
        ClassNode classNode=new ClassNode();
         
        //ClassNode is a ClassVisitor
        cr.accept(classNode, 0);
         
        //Let's move through all the methods
         
        for(MethodNode methodNodeclassNode.methods){
            System.out.println(methodNode.name+"  "+methodNode.desc);
        }
 
    }
 
}

Изменение файла класса. Изменение файла класса представляет собой комбинацию двух вышеописанных процедур. Сначала мы читаем класс обычным способом, вносим необходимые изменения в данные, а затем записываем их обратно в файл. Следующая программа реализует автоматическое внедрение некоторого регистрационного кода. В настоящее время наш класс Logger печатает только на стандартный вывод. Каждый метод, аннотированный @Loggable, будет зарегистрирован, когда они начнутся и когда возврат. В этом мы не регистрируем бросок-исключение. Однако это также может быть реализовано таким же образом, проверяя код операции ATHROW.

LoggingInsertion.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.geekyarticles.asm;
 
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Iterator;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
 
public class LoggingInsertion {
 
    public static void main(String[] args) throws Exception{
        InputStream in=LoggingInsertion.class.getResourceAsStream("/com/geekyarticles/asm/LoggingTest.class");
 
        ClassReader cr=new ClassReader(in);
        ClassNode classNode=new ClassNode();
        cr.accept(classNode, 0);
 
        //Let's move through all the methods
 
        for(MethodNode methodNodeclassNode.methods){
            System.out.println(methodNode.name+"  "+methodNode.desc);
            boolean hasAnnotation=false;
            if(methodNode.visibleAnnotations!=null){
                for(AnnotationNode annotationNodemethodNode.visibleAnnotations){
                    if(annotationNode.desc.equals("Lcom/geekyarticles/asm/Loggable;")){
                        hasAnnotation=true;
                        break;
                    }
                }
            }
            if(hasAnnotation){
                //Lets insert the begin logger
                InsnList beginList=new InsnList();
                beginList.add(new LdcInsnNode(methodNode.name));
                beginList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/geekyarticles/asm/Logger", "logMethodStart", "(Ljava/lang/String;)V"));
                 
                Iterator<AbstractInsnNode> insnNodes=methodNode.instructions.iterator();
                while(insnNodes.hasNext()){
                    System.out.println(insnNodes.next().getOpcode());
                }
                 
                methodNode.instructions.insert(beginList);
                System.out.println(methodNode.instructions);
                 
                //A method can have multiple places for return
                //All of them must be handled.
                insnNodes=methodNode.instructions.iterator();
                while(insnNodes.hasNext()){
                    AbstractInsnNode insn=insnNodes.next();
                    System.out.println(insn.getOpcode());
                     
                    if(insn.getOpcode()==Opcodes.IRETURN
                            ||insn.getOpcode()==Opcodes.RETURN
                            ||insn.getOpcode()==Opcodes.ARETURN
                            ||insn.getOpcode()==Opcodes.LRETURN
                            ||insn.getOpcode()==Opcodes.DRETURN){
                        InsnList endList=new InsnList();
                        endList.add(new LdcInsnNode(methodNode.name));
                        endList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/geekyarticles/asm/Logger", "logMethodReturn", "(Ljava/lang/String;)V"));
                        methodNode.instructions.insertBefore(insn, endList);
                    }
                     
                }
                 
            }
        }
 
        //We are done now. so dump the class
        ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES);
        classNode.accept(cw);
 
 
        File outDir=new File("out/com/geekyarticles/asm");
        outDir.mkdirs();
        DataOutputStream dout=new DataOutputStream(new FileOutputStream(new File(outDir,"LoggingTest.class")));
        dout.write(cw.toByteArray());
        dout.flush();
        dout.close();
 
    }
 
}

LoggingTest.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package com.geekyarticles.asm;
 
public class LoggingTest {
    public static void run1(){
        System.out.println("run 1");
    }
    @Loggable
    public static void run2(){
        System.out.println("run 2");
    }
     
    @Loggable
    public static void main(String [] args){
        run1();
        run2();
    }
}

Logger.java

01
02
03
04
05
06
07
08
09
10
11
package com.geekyarticles.asm;
 
public class Logger {
    public static void logMethodStart(String methodName){
        System.out.println("Starting method: "+methodName);
    }
     
    public static void logMethodReturn(String methodName){
        System.out.println("Ending method: "+methodName);
    }
}

Loggable.java

01
02
03
04
05
06
07
08
09
10
11
12
package com.geekyarticles.asm;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
 
}

Если вы запустите эту программу, сгенерированный файл будет зависеть от класса Logger. Вручную скопируйте класс Logger в правильный пакет в каталоге out. Если вы запустите сгенерированный класс (который является модифицированной версией класса LoggingTest), вывод будет следующий.

1
2
3
4
5
6
7
bash-4.1$ java  com.geekyarticles.asm.LoggingTest
Starting method: main
run 1
Starting method: run2
run 2
Ending method: run2
Ending method: main

Обратите внимание, что в отличие от обычных списков, объект InsnList может быть изменен во время итерации по нему. Любые изменения незамедлительно отражаются. Таким образом, если после текущей позиции будут вставлены некоторые инструкции, они также будут повторяться.

Ссылка: Управление файлами классов Java с помощью ASM 4 — Часть вторая: Tree API от нашего партнера JCG   Дебашиш Рэй Чаудхури в блоге Geeky Articles .