본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Tree API] 7. Method

목차

    개인 학습 정리

    MethodNode

    public class MethodNode extends MethodVisitor {
        public int access; // 메서드의 접근 제어자. 예: ACC_PUBLIC, ACC_PRIVATE 등
        public String name; // 메서드 이름
        public String desc; // 메서드 서술자(descriptor). 매개변수와 반환 타입 포함
        public String signature; // 메서드 시그니처. 제네릭 정보 포함
        public List<String> exceptions; // 메서드가 던질 수 있는 예외 타입 목록
        public List<ParameterNode> parameters; // 메서드 매개변수 목록
        public List<AnnotationNode> visibleAnnotations; // 런타임에서 보이는 어노테이션 목록
        public List<AnnotationNode> invisibleAnnotations; // 런타임에서 보이지 않는 어노테이션 목록
        public List<TypeAnnotationNode> visibleTypeAnnotations; // 런타임에서 보이는 타입 어노테이션 목록
        public List<TypeAnnotationNode> invisibleTypeAnnotations; // 런타임에서 보이지 않는 타입 어노테이션 목록
        public List<Attribute> attrs; // 메서드에 추가된 비표준 속성 목록
        public Object annotationDefault; // 어노테이션 타입 메서드의 기본값
        public int visibleAnnotableParameterCount; // 런타임에서 보이는 어노테이션을 가진 매개변수의 수
        public List<AnnotationNode>[] visibleParameterAnnotations; // 런타임에서 보이는 매개변수 어노테이션 배열
        public int invisibleAnnotableParameterCount; // 런타임에서 보이지 않는 어노테이션을 가진 매개변수의 수
        public List<AnnotationNode>[] invisibleParameterAnnotations; // 런타임에서 보이지 않는 매개변수 어노테이션 배열
        public InsnList instructions; // 메서드 내부의 바이트코드 명령어 목록
        public List<TryCatchBlockNode> tryCatchBlocks; // 메서드 내의 try-catch 블록 목록
        public int maxStack; // 메서드 스택의 최대 깊이
        public int maxLocals; // 메서드에서 사용되는 로컬 변수의 최대 수
        public List<LocalVariableNode> localVariables; // 메서드 내의 로컬 변수 목록
        public List<LocalVariableAnnotationNode> visibleLocalVariableAnnotations; // 런타임에서 보이는 로컬 변수 어노테이션 목록
        public List<LocalVariableAnnotationNode> invisibleLocalVariableAnnotations; // 런타임에서 보이지 않는 로컬 변수 어노테이션 목록
        private boolean visited; // 방문 여부를 표시하는 플래그
    }

     

    InsnList

    InsnList는 AbstractInsnNode 객체들의 이중 연결 리스트(double linked list)로 구성된 지시문(instruction) 리스트를 나타낸다.
     

    NOTE:
    ClassNode나 MethodNode가 각 구성 요소의 정보를 Node의 단계로 저장하는데 비해, 지시문 리스트는 이중 연결 구조인 이유는 아마도 특정 지시문들이 Jump(분기 점프) 등의 이전 참조를 확인해야 하기 때문일 것이다.
    명령어들은 이전 지시사항이나 값들에 영향을 많이 받아서 이런 구조를 사용했을 것이라 추측한다.

     
    AbstractInsnNode
    이 클래스는 바이트코드 지시문을 나타낸다. 연산, 점프 등 주로 사용하던 명령어 집합의 연장선이다. (Core, Tree 등에도 몇 번 사용했던 구문들..)
     

    ex : 
    InsnList il = new InsnList(); // <- 이것이 지시사항 연결 리스트
    il.add(new VarInsnNode(ALOAD, 0)); //이것이 AbstractInsnNode 구현체

     
    InsnList(연결 리스트)는 각 AbstractInsnNode 객체 자체에 저장된 링크를 통해 구성되며, 이는 명령어 객체와 명령어 리스트를 사용하는 방법에 여러 중요한 영향을 미치는데, 주요 포인트는 다음과 같다.
     
    1. 하나의 AbstractInsnNode 객체는 지시문 리스트(InsnList)에서 한 번만 나타날 수 있다.
    즉, 동일한 객체를 리스트에 추가할 수 없다.

    ex : 
    InsnList il = new InsnList(); // 리스트
    
    AbstractInsnNode insnNode = new VarInsnNode(ALOAD, 0); //AbstractInsnNode 구현체
    
    il.add(insnNode);
    il.add(insnNode); //동일한 객체를 여러 개 넣을 수 없다.

     
    2. AbstractInsnNode 객체는 동시에 여러 InsnList에 소속될 수 없다.
    InsnList.add()를 통해 객체가 삽입되면, 이 지시문은 다른 List에서 사용할 수 없다.
     
    3. 한 리스트의 모든 요소를 다른 리스트에 추가하면 첫 번째 리스트가 비워지게 된다.
    => 이는 AbstractInsnNode를 리스트에 추가할 때, 이미 다른 리스트에 속해 있다면 이전에 넣었던 값은 제거된다.

    ex : 
    InsnList il = new InsnList(); // 리스트 1
    InsnList il2 = new InsnList(); // 리스트 2
    AbstractInsnNode insnNode = new VarInsnNode(ALOAD, 0); //AbstractInsnNode 구현체
    
    il.add(insnNode);
    il2.add(insnNode); //여기에 넣는 순간 il에 들어갔던 insnNode는 제거된다.

     

    FieldNode

    public class FieldNode {
        public int access; // 필드의 접근 제어자. 예: ACC_PUBLIC, ACC_PRIVATE 등
        public String name; // 필드 이름
        public String desc; // 필드 타입의 서술자. 예: "Ljava/lang/String;"는 문자열 타입을 의미
        public String signature; // 필드의 시그니처. 제네릭 타입을 사용할 때 필요
        public Object value; // 필드의 초기값. 정적 필드에만 사용됨
    }

     

    상태 변환(state)

    상태(state)는 객체나 함수가 작업을 수행하는 동안 정보를 저장하는 메커니즘을 말한다.
     
    상태 없는 변환 예시
    AddTimerTransformer 예제에서는 각 메서드의 실행 시간을 측정하기 위해 메서드의 시작 부분에 시간 측정 코드를 추가하고, 메서드의 종료 지점 직전에 시간을 기록하여 총 실행 시간을 계산한다.
     
    각 변환은 독립적으로 수행되며, 메서드의 시작 시간과 종료 시간을 계산하는 데 필요한 정보는 메서드의 변환 과정에서만 임시로 사용되고, 다른 메서드의 변환에는 영향을 주지 않는다.
     

    public class AddTimerTransformer extends ClassTransformer {
        public AddTimerTransformer(ClassTransformer ct) {
            super(ct);
        }
    
        @Override
        public void transform(ClassNode cn) {
    
            //cn method 영역 순회
            for (MethodNode mn : (List<MethodNode>) cn.methods) {
                //생성자(static, 인스턴스)일 경우 스킵
                if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
                    continue;
                }
                InsnList insns = mn.instructions; //insns : mn 메서드에 속한 지시사항들의 List
                if (insns.size() == 0) {
                    continue;
                }
                //j는 지시사항 List를 순회하는 insns의 Iterater
                //리스트 인덱스 순회보다, InsnList는 Iterator 사용이 효과적이라고 함.
                Iterator<AbstractInsnNode> j = insns.iterator();
                while (j.hasNext()) {
                    AbstractInsnNode in = j.next();
                    int op = in.getOpcode(); //operands 지시문 받기
                    if ((op >= IRETURN && op <= RETURN) || op == ATHROW) { //return 구문 전(timer+=..)과 Athrow 구문 전(timer-=) 사용
                        InsnList il = new InsnList();
                        il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
                        il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
                                "currentTimeMillis", "()J"));
                        il.add(new InsnNode(LADD));
                        il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
                        insns.insert(in.getPrevious(), il);
                    }
                }
                InsnList il = new InsnList();
                il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
                il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
                        "currentTimeMillis", "()J"));
                il.add(new InsnNode(LSUB));
                il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
                insns.insert(il);
                mn.maxStack += 4;
            }
    
            //timer 필드 선언
            int acc = ACC_PUBLIC + ACC_STATIC;
            cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
            super.transform(cn);
        }
    }

     

    원본 번역 : 메소드

     
    이 장에서는 ASM 트리 API를 사용하여 메소드를 생성하고 변환하는 방법에 대해 설명한다. 이는 먼저 트리 API만을 소개하고 일부 예시를 들어 설명한 다음, 코어 API와 함께 구성하는 방법을 제시한다. 제네릭과 애너테이션에 대한 트리 API는 다음 장에서 소개된다.
     

    7.1. 인터페이스 및 컴포넌트

    7.1.1. 소개

    메소드를 생성하고 변환하기 위한 ASM 트리 API는 MethodNode 클래스를 기반으로 한다(그림 7.1 참조).
     

    public class MethodNode ...{
        public int access;
        public String name;
        public String desc;
        public String signature;
        public List<String> exceptions;
        public List<AnnotationNode> visibleAnnotations;
        public List<AnnotationNode> invisibleAnnotations;
        public List<Attribute> attrs;
        public Object annotationDefault;
        public List<AnnotationNode>[] visibleParameterAnnotations;
        public List<AnnotationNode>[] invisibleParameterAnnotations;
        public InsnList instructions;
        public List<TryCatchBlockNode> tryCatchBlocks;
        public List<LocalVariableNode> localVariables;
        public int maxStack;
        public int maxLocals;
    }

    ( Figure 7.1.: The MethodNode class (only fields are shown))
     
    이 클래스의 대부분의 필드는 ClassNode의 해당 필드와 유사하다.
    가장 중요한 필드는 instructions 필드부터 시작되는 마지막 필드들이다. 이 필드는 InsnList 객체로 관리되는 명령어 리스트이며, 그 공개 API는 다음과 같다:
     
    명령어 지시자 리스트 객체인 InsnList

    public class InsnList { // public accessors omitted
        int size();
        AbstractInsnNode getFirst();
        AbstractInsnNode getLast();
        AbstractInsnNode get(int index);
        boolean contains(AbstractInsnNode insn);
        int indexOf(AbstractInsnNode insn);
        void accept(MethodVisitor mv);
        ListIterator iterator();
        ListIterator iterator(int index);
        AbstractInsnNode[] toArray();
        void set(AbstractInsnNode location, AbstractInsnNode insn);
        void add(AbstractInsnNode insn);
        void add(InsnList insns);
        void insert(AbstractInsnNode insn);
        void insert(InsnList insns);
        void insert(AbstractInsnNode location, AbstractInsnNode insn);
        void insert(AbstractInsnNode location, InsnList insns);
        void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
        void insertBefore(AbstractInsnNode location, InsnList insns);
        void remove(AbstractInsnNode insn);
        void clear();
    }
    

     
     
     
    InsnList는 AbstractInsnNode 객체 자체에 저장된 링크를 가진 명령어들의 이중 연결 리스트이다. 이 점은 명령어 객체와 명령어 리스트를 사용하는 방식에 많은 영향을 미치므로 매우 중요하다:

    • AbstractInsnNode 객체는 명령어 리스트에서 한 번 이상 나타날 수 없다.
    • AbstractInsnNode 객체는 동시에 여러 명령어 리스트에 속할 수 없다.
    • 결과적으로, AbstractInsnNode를 리스트에 추가하려면 속해 있던 리스트에서 제거해야 한다(있는 경우).
    • 다른 결과로, 한 리스트의 모든 요소를 다른 리스트에 추가하면 첫 번째 리스트가 비워진다.

    AbstractInsnNode 클래스는 바이트코드 명령어를 나타내는 클래스의 상위 클래스이다. 그 공개 API는 다음과 같다:
     
    AbstractInsnNode

    public abstract class AbstractInsnNode {
        public int getOpcode();
        public int getType();
        public AbstractInsnNode getPrevious();
        public AbstractInsnNode getNext();
        public void accept(MethodVisitor cv);
        public AbstractInsnNode clone(Map labels);
    }

     
    하위 클래스는 MethodVisitor 인터페이스의 visitXxx Insn 메소드에 해당하는 Xxx InsnNode 클래스들이며, 모두 동일한 방식으로 구축된다.
    예를 들어, VarInsnNode 클래스는 visitVarInsn 메소드에 해당하며 다음과 같은 구조를 가진다:

    public class VarInsnNode extends AbstractInsnNode {
        public int var;
        public VarInsnNode(int opcode, int var) {
        super(opcode);
        this.var = var;
        }
        ...
    }

     
     
    레이블과 프레임, 그리고 라인 번호는 명령어가 아니지만, LabelNode, FrameNode, LineNumberNode 클래스와 같은 AbstractInsnNode 클래스의 하위 클래스로 표현된다.
     
    이를 통해 코어 API에서와 같이 실제 명령어 바로 앞에 삽입할 수 있다(레이블과 프레임은 해당 명령어 바로 앞에 방문된다). 따라서 점프 명령어의 대상을 찾기 쉽다:
    AbstractInsnNode 클래스가 제공하는 getNext 메소드를 사용하면 대상 레이블 다음에 있는 첫 번째 실제 명령어이다. 또 다른 결과는, 코어 API와 마찬가지로, 레이블이 변경되지 않는 한 명령어를 제거해도 점프 명령어가 깨지지 않는다는 것이다.
     

    NOTE : AbstractInsnNode 클래스에서 getNext 메소드는 현재 노드(명령 또는 레이블, 프레임, 줄 번호 등을 나타내는 노드)의 바로 다음에 오는 노드를 반환하는 메소드이다.
    이 메서드를 사용해서 바로 다음 노드로 이동할 수 있다. 그리고 구체적으로는, jump 등의 타겟을 찾을 때 유용하다.

    getNext메소드를 사용하면, 타겟 레이블 다음에 오는 실제 명령(instruction) 노드를 쉽게 찾을 수 있다.
    즉, 점프 명령이 가리키는 다음 실행할 명령으로 쉽게 이동할 수 있게 해준다.

     

    7.1.2. 메소드 생성(Generating methods)

     
    트리 API로 메소드를 생성하는 것은 MethodNode를 생성하고 그 필드를 초기화하는 것으로 구성된다. 가장 흥미로운 부분은 메소드의 코드 생성이다. 예를 들어, 섹션 3.1.5의 checkAndSetF 메소드는 다음과 같이 생성될 수 있다:
     

    MethodNode mn = new MethodNode(...);
    InsnList il = mn.instructions;
    il.add(new VarInsnNode(ILOAD, 1));
    
    LabelNode label = new LabelNode();
    il.add(new JumpInsnNode(IFLT, label));
    il.add(new VarInsnNode(ALOAD, 0));
    il.add(new VarInsnNode(ILOAD, 1));
    il.add(new FieldInsnNode(PUTFIELD, "pkg/Bean", "f", "I"));
    LabelNode end = new LabelNode();
    il.add(new JumpInsnNode(GOTO, end));
    il.add(label);
    
    il.add(new FrameNode(F_SAME, 0, null, 0, null));
    il.add(new TypeInsnNode(NEW, "java/lang/IllegalArgumentException"));
    il.add(new InsnNode(DUP));
    il.add(new MethodInsnNode(INVOKESPECIAL,
    "java/lang/IllegalArgumentException", "<init>", "()V"));
    il.add(new InsnNode(ATHROW));
    il.add(end);
    il.add(new FrameNode(F_SAME, 0, null, 0, null));
    il.add(new InsnNode(RETURN));
    
    mn.maxStack = 2;
    mn.maxLocals = 2;

     
    클래스와 마찬가지로, 트리 API를 사용하여 메소드를 생성하는 것은 더 많은 시간이 걸리고 더 많은 메모리를 소비한다. 하지만 이는 메소드의 내용을 어떤 순서로든 생성할 수 있게 한다. 특히 명령어는 순차적 순서와 다른 순서로 생성될 수 있으며, 이는 일부 경우에 유용할 수 있다.
     

    public Type compile(InsnList output) {
        InsnList il1 = new InsnList();
        InsnList il2 = new InsnList();
        Type t1 = e1.compile(il1);
        Type t2 = e2.compile(il2);
        Type t = ...; // compute common super type of t1 and t2
        output.addAll(il1); // done in constant time
        output.add(...); // cast instruction from t1 to t
        output.addAll(il2); // done in constant time
        output.add(...); // cast instruction from t2 to t
        output.add(new InsnNode(t.getOpcode(IADD)));
        return t;
    }

     
    예를 들어, 표현식 컴파일러를 고려해보자. 일반적으로 표현식 e1+e2는 e1에 대한 코드를 생성한 다음, e2에 대한 코드를 생성하고, 두 값을 더하는 코드를 생성한다. 그러나 e1과 e2가 동일한 기본 타입이 아닌 경우, e1에 대한 코드 바로 뒤에 캐스트를 삽입해야 하고, e2에 대한 코드 바로 뒤에 또 다른 캐스트를 삽입해야 한다. 그러나 정확히 삽입해야 할 캐스트는 e1과 e2의 타입에 따라 달라진다.
     
    이제 컴파일된 코드를 생성하는 메소드가 표현식의 타입을 반환한다고 가정하면, 코어 API를 사용하는 경우 문제가 발생한다: e1 뒤에 삽입해야 할 캐스트는 e2가 컴파일된 후에만 알 수 있지만, 이는 이전에 방문한 명령어 사이에 명령어를 삽입할 수 없기 때문에 너무 늦다. 트리 API를 사용하면 이 문제가 존재하지 않는다. 예를 들어, 다음과 같은 compile 메소드를 사용할 수 있다:
     

    NOTE : 일반적으로 Tree API는 Core API에 비해 비효율적이나, 특정 경우의 경우 순서를 통해 유용하게 로직을 구성할 수 있다. 이는 Tree API가 Node tree 구조를 통해 순서를 보장받을 수 있기 때문이다(List)

     
     

    7.1.3. 메소드 변환

     
    트리 API로 메소드를 변환하는 것은 MethodNode 객체의 필드를 수정하는 것으로 단순히 구성된다.
    특히 명령어 리스트를 수정할 수 있지만, 일반적인 패턴은 이를 반복하면서 수정하는 것이다.
     
    실제로, 일반적인 ListIterator 계약과 달리, InsnList에서 반환된 ListIterator는 많은 동시(list modifications) 목록 수정을 지원한다. InsnList 메소드를 사용하여 현재 요소를 포함하여 이전의 하나 이상의 요소를 제거하거나, 다음 요소(즉, 현재 요소 바로 다음이 아니라 그 후속 요소) 이후의 하나 이상의 요소를 제거하거나, 현재 요소 이전이나 그 후속 요소 이후에 하나 이상의 요소를 삽입할 수 있다. 이 변경사항은 반복자에 반영되며, 즉 다음 요소 이후에 삽입(또는 제거)된 요소는 반복자에서 보여진다(또는 보여지지 않는다).
     
    특정 명령어 i 다음에 여러 명령어를 삽입해야 할 때 사용되는 또 다른 일반적인 패턴은 이러한 새 명령어를 임시 명령어 리스트에 추가하고, 이 임시 리스트를 한 단계로 메인 리스트에 삽입하는 것이다:

    InsnList il = new InsnList();
    il.add(...);
    ...
    il.add(...);
    mn.instructions.insert(i, il);

     
    명령어를 하나씩 삽입하는 것도 가능하지만, 삽입 지점을 각 삽입 후에 업데이트해야 하므로 더 번거롭다.
     

    7.1.4. 상태 없는 변환과 상태 있는 변환(Stateless and statefull transformations)

     
    트리 API를 사용하여 메소드를 구체적으로 어떻게 변환할 수 있는지 몇 가지 예를 들어 보자.
    코어 API와 트리 API 사이의 차이를 보기 위해, 섹션 3.2.4의 AddTimerAdapter 예제와 섹션 3.2.5의 RemoveGetFieldPutFieldAdapter를 다시 구현해 볼 것이다. 타이머 예제는 다음과 같이 구현될 수 있다:
     

    public class AddTimerTransformer extends ClassTransformer {
        public AddTimerTransformer(ClassTransformer ct) {
            super(ct);
        }
    
        @Override
        public void transform(ClassNode cn) {
            for (MethodNode mn : (List<MethodNode>) cn.methods) {
                if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
                    continue;
                }
                InsnList insns = mn.instructions;
                if (insns.size() == 0) {
                    continue;
                }
                Iterator<AbstractInsnNode> j = insns.iterator();
                while (j.hasNext()) {
                    AbstractInsnNode in = j.next();
                    int op = in.getOpcode();
                    if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
                        InsnList il = new InsnList();
                        il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
                        il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
                                "currentTimeMillis", "()J"));
                        il.add(new InsnNode(LADD));
                        il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
                        insns.insert(in.getPrevious(), il);
                    }
                }
                InsnList il = new InsnList();
                il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
                il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
                        "currentTimeMillis", "()J"));
                il.add(new InsnNode(LSUB));
                il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
                insns.insert(il);
                mn.maxStack += 4;
            }
            int acc = ACC_PUBLIC + ACC_STATIC;
            cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
            super.transform(cn);
        }
    }

     
    여기서는 명령어 리스트에 여러 명령어를 삽입하는 이전 섹션에서 논의된 패턴을 볼 수 있다. 이는 임시 명령어 리스트를 사용한다.
    이 예제는 또한 명령어 리스트를 반복하면서 현재 명령어 앞에 명령어를 삽입할 수 있음을 보여준다.
    이 어댑터를 구현하는 데 필요한 코드 양은 코어 API와 트리 API로 대략 비슷하다.
     
    필드 자체 할당을 제거하는 메소드 어댑터(섹션 3.2.5 참조)는 다음과 같이 구현될 수 있다(이전 장의 ClassTransformer 클래스와 유사한 MethodTransformer를 가정한다):

    public class RemoveGetFieldPutFieldTransformer extends
            MethodTransformer {
        public RemoveGetFieldPutFieldTransformer(MethodTransformer mt) {
            super(mt);
        }
    
        @Override
        public void transform(MethodNode mn) {
            InsnList insns = mn.instructions;
            Iterator<AbstractInsnNode> i = insns.iterator();
            while (i.hasNext()) {
                AbstractInsnNode i1 = i.next();
                if (isALOAD0(i1)) {
                    AbstractInsnNode i2 = getNext(i1);
                    if (i2 != null && isALOAD0(i2)) {
                        AbstractInsnNode i3 = getNext(i2);
                        if (i3 != null && i3.getOpcode() == GETFIELD) {
                            AbstractInsnNode i4 = getNext(i3);
                            if (i4 != null && i4.getOpcode() == PUTFIELD) {
                                if (sameField(i3, i4)) {
                                    while (i.next() != i4) {
                                    }
                                    insns.remove(i1);
                                    insns.remove(i2);
                                    insns.remove(i3);
                                    insns.remove(i4);
                                }
                            }
                        }
                    }
                }
            }
            super.transform(mn);
        }
    
        private static AbstractInsnNode getNext(AbstractInsnNode insn) {
            do {
                insn = insn.getNext();
                if (insn != null && !(insn instanceof LineNumberNode)) {
                    break;
                }
            } while (insn != null);
            return insn;
        }
    
        private static boolean isALOAD0(AbstractInsnNode i) {
            return i.getOpcode() == ALOAD && ((VarInsnNode) i).var == 0;
        }
    
        private static boolean sameField(AbstractInsnNode i,
                                         AbstractInsnNode j) {
            return ((FieldInsnNode) i).name.equals(((FieldInsnNode) j).name);
        }
    }
    

     
    여기서도 명령어 리스트를 반복하면서 제거할 수 있음을 볼 수 있다.
    그러나 while (i.next() != i4) 루프는 현재 요소 다음에 있는 명령어를 제거할 수 없기 때문에, 제거해야 할 명령어 다음에 반복자를 배치하기 위해 필요하다.
    방문자와 트리 기반 구현( visitor and tree based implementations ) 모두 시퀀스 내에 라벨과 프레임을 감지할 수 있으며, 이 경우 제거하지 않는다. 그러나 시퀀스 내의 라인 번호를 무시하는 것은 트리 기반 API(see the getNext method) 를 사용할 때보다 코어 API를 사용할 때 더 많은 코드가 필요하다. 그러나 두 구현 사이의 주요 차이점은 트리 API를 사용하면 상태 기계가 필요 없다는 것이다. 특히, 세 개 이상의 연속적인 ALOAD 0 명령어와 같은 특별한 경우, 쉽게 누락될 수 있는 문제는 더 이상 문제가 되지 않는다.
     
    위 구현을 사용하면, while 루프의 각 단계에서 i2, i3, i4가 이후 반복에서 검사될 것이지만, 이 반복에서도 검사될 수 있다. 실제로 각 명령어를 최대 한 번만 검사하는 더 효율적인 구현을 사용할 수 있다:
     

    public class RemoveGetFieldPutFieldTransformer2 extends
            MethodTransformer {
    ...
    
        @Override
        public void transform(MethodNode mn) {
            InsnList insns = mn.instructions;
            Iterator i = insns.iterator();
            while (i.hasNext()) {
                AbstractInsnNode i1 = (AbstractInsnNode) i.next();
                if (isALOAD0(i1)) {
                    AbstractInsnNode i2 = getNext(i);
                    if (i2 != null && isALOAD0(i2)) {
                        AbstractInsnNode i3 = getNext(i);
                        while (i3 != null && isALOAD0(i3)) {
                            i1 = i2;
                            i2 = i3;
                            i3 = getNext(i);
                        }
                        if (i3 != null && i3.getOpcode() == GETFIELD) {
                            AbstractInsnNode i4 = getNext(i);
                            if (i4 != null && i4.getOpcode() == PUTFIELD) {
                                if (sameField(i3, i4)) {
                                    insns.remove(i1);
                                    insns.remove(i2);
                                    insns.remove(i3);
                                    insns.remove(i4);
                                }
                            }
                        }
                    }
                }
            }
            super.transform(mn);
        }
    
        private static AbstractInsnNode getNext(Iterator i) {
            while (i.hasNext()) {
                AbstractInsnNode in = (AbstractInsnNode) i.next();
                if (!(in instanceof LineNumberNode)) {
                    return in;
                }
            }
            return null;
        }
    ...
    }

     
    이전 구현과의 차이점은 getNext 메소드가 이제 리스트 반복자에 작용한다는 것이다. 시퀀스가 인식되면 반복자는 그 바로 뒤에 있으므로, while (i.next() != i4) 루프는 더 이상 필요하지 않다.
    그러나 여기서 세 개 이상의 연속적인 ALOAD 0 명령어의 특별한 경우가 다시 나타난다(while (i3 != null) 루프 참조).
     

    7.1.5. 전역 변환(Global transformations)

     
    지금까지 본 모든 메소드 변환은 지역 기반이었으며, 즉 i의 변환은 i로부터 고정된 거리에 있는 명령어에만 의존했다. 그러나 명령어 i의 변환이 i로부터 임의의 거리에 있는 명령어에 의존할 수 있는 전역 변환도 있다. 이러한 변환에는 트리 API가 정말 유용하며, 코어 API를 사용하여 구현하는 것은 정말 복잡할 것이다.
     
    한 예는 GOTO 레이블 명령어로 점프하는 것을 레이블로 점프하는 것으로 대체하고, GOTO를 RETURN 명령어로 대체하는 변환인데, 실제로 점프 명령어의 대상은 이 명령어로부터 임의의 거리에 있을 수 있다. 이러한 변환은 다음과 같이 구현될 수 있다:
     

    public class OptimizeJumpTransformer extends MethodTransformer {
        public OptimizeJumpTransformer(MethodTransformer mt) {
            super(mt);
        }
    
        @Override
        public void transform(MethodNode mn) {
            InsnList insns = mn.instructions;
            Iterator<AbstractInsnNode> i = insns.iterator();
            while (i.hasNext()) {
                AbstractInsnNode in = i.next();
                if (in instanceof JumpInsnNode) {
                    LabelNode label = ((JumpInsnNode) in).label;
                    AbstractInsnNode target;
    // while target == goto l, replace label with l
                    while (true) {
                        target = label;
                        while (target != null && target.getOpcode() < 0) {
                            target = target.getNext();
                        }
                        if (target != null && target.getOpcode() == GOTO) {
                            label = ((JumpInsnNode) target).label;
                        } else {
                            break;
                        }
                    }
    // update target
                    ((JumpInsnNode) in).label = label;
    // if possible, replace jump with target instruction
                    if (in.getOpcode() == GOTO && target != null) {
                        int op = target.getOpcode();
                        if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
    // replace ’in’ with clone of ’target’
                            insns.set(in, target.clone(null));
                        }
                    }
                }
            }
            super.transform(mn);
        }
    }

     
    이 코드는 점프 명령어 in이 발견될 때, 그 대상이 label에 저장된다.
    그런 다음 가장 안쪽 while 루프를 사용하여 이 레이블 다음에 오는 명령어를 검색한다(AbstractInsnNode 객체는 실제 명령어를 나타내지 않는 경우(FrameNode나 LabelNode 등)에는 음수 "opcode"를 가진다). 이 명령어가 GOTO인 동안 label은 이 명령어의 대상으로 대체되고, 이전 단계가 반복된다. 마지막으로 in의 대상 레이블이 이 업데이트된 label 값으로 대체되고, in 자체가 GOTO이고 업데이트된 대상이 RETURN 명령어인 경우, in은 이 return 명령어의 복제본으로 대체된다(명령어 객체는 명령어 리스트에 한 번 이상 나타날 수 없다는 것을 기억하라).
     
    이 변환의 효과는 섹션 3.1.5에서 정의된 checkAndSetF 메소드에 다음과 같이 나타난다:
     

     
    이 변환은 점프 명령어(보다 정확하게는 제어 흐름 그래프)를 변경하지만, 메소드의 프레임을 업데이트할 필요가 없다.
    실제로 각 명령어에서 실행 프레임의 상태는 동일하게 유지되며, 새로운 점프 대상이 도입되지 않기 때문에 새로운 프레임을 방문할 필요가 없다.
     
    그러나 프레임이 더 이상 필요하지 않을 수도 있다.
    예를 들어, 위 예제에서는 변환 후 end 레이블이 더 이상 사용되지 않으며, F_SAME 프레임과 그 뒤의 RETURN 명령어도 마찬가지이다. 다행히도 엄격히 필요한 것보다 더 많은 프레임을 방문하는 것이나 메소드에 사용되지 않는 코드(죽은 코드 또는 도달할 수 없는 코드라고 함)를 포함하는 것은 완전히 합법적이다. 따라서 위의 메소드 어댑터는 죽은 코드와 프레임을 제거하도록 개선될 수 있지만, 정확하다.
     

    7.2. 컴포넌트 구성(Components composition)

    지금까지 MethodNode 객체를 생성하고 변환하는 방법만을 살펴보았지만, 클래스의 바이트 배열 표현과의 연결은 아직 보지 못했다. 클래스와 마찬가지로, 이 연결은 코어 API와 트리 API 컴포넌트를 구성함으로써 수행된다. 이 섹션에서 설명한다.

     

    7.2.1. 소개

    그림 7.1에 표시된 필드 외에도 MethodNode 클래스는 MethodVisitor 클래스를 확장하며, MethodVisitor 또는 ClassVisitor를 매개변수로 받는 두 개의 accept 메소드도 제공한다. accept 메소드는 MethodNode 필드 값에 기반한 이벤트를 생성하며, MethodVisitor 메소드는 반대 작업을 수행한다. 즉, 받은 이벤트에 기반하여 MethodNode 필드를 설정한다.

     

    7.2.2. 패턴

     

    클래스와 마찬가지로, 트리 기반 메소드 변환기를 코어 API와 함께 메소드 어댑터처럼 사용할 수 있다. 클래스에 사용될 수 있는 두 가지 패턴은 메소드에도 마찬가지로 유효하며, 정확히 동일한 방식으로 작동한다. 상속을 기반으로 한 패턴은 다음과 같다:

     

    public class MyMethodAdapter extends MethodNode {
        public MyMethodAdapter(int access, String name, String desc,
                               String signature, String[] exceptions, MethodVisitor mv) {
            super(ASM4, access, name, desc, signature, exceptions);
            this.mv = mv;
        }
        @Override public void visitEnd() {
    // put your transformation code here
            accept(mv);
        }
    }

     

    대리( delegation )를 기반으로 한 패턴은 다음과 같다:

    public class MyMethodAdapter extends MethodVisitor {
        MethodVisitor next;
        public MyMethodAdapter(int access, String name, String desc,
                               String signature, String[] exceptions, MethodVisitor mv) {
            super(ASM4,
                    new MethodNode(access, name, desc, signature, exceptions));
            next = mv;
        }
        @Override public void visitEnd() {
            MethodNode mn = (MethodNode) mv;
    // put your transformation code here
            mn.accept(next);
        }
    }

     

    첫 번째 패턴의 변형은 ClassAdapter의 visitMethod 안에서 익명 내부 클래스를 직접 사용하는 것이다:

    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        return new MethodNode(ASM4, access, name, desc, signature, exceptions)
        {
            @Override public void visitEnd() {
    // put your transformation code here
                accept(cv);
            }
        };
    }

     

    이 패턴들은 메소드에 대해서만 트리 API를 사용하고, 클래스에 대해서는 코어 API를 사용할 수 있음을 보여준다. 실제로 이 전략은 자주 사용된다.