본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Core API] 4. Metadata

목차

    개인 내용 정리

    시그니처와 SignatureVisitor

     

    시그니처 : 클래스, 인터페이스, 필드, 메소드 등을 JVM이나 컴파일러가 인식할 수 있도록 정의한 기술적인 정보.

     
    SignatureVisitor는 ASM 라이브러리에서 제공하는 인터페이스로, 바이트코드의 시그니처를 방문하여 읽거나 수정할 때 사용된다.
    이 인터페이스를 사용하면 제네릭 타입, 클래스 시그니처, 메소드 시그니처 등을 분석하고 조작할 수 있다.
    여러 메서드를 통해 세부적인 시그니처 요소들을 방문하고 수정할 수 있으며, 이는 타입 파라미터, 반환 타입, 예외 타입 등 다양한 요소들을 처리할 수 있게 해준다.
     

    타입 시그니처(Type Signature)

    예를 들어, List<E> 클래스에서 E라고 할 수 있을 것이다. 특정 input의 Type을 지정하는 값이다.
    제네릭을 사용할 때, 타입 시그니처는 특정 타입 매개변수를 사용하는 클래스, 인터페이스, 필드, 메소드를 정의할 때 사용된다.
     

    타입 시그니처는 이러한 타입 매개변수와 그들이 사용되는 방식을 설명한다. 문법 규칙은 대체로 다음과 같다:

    • 클래스 타입 매개변수: <T:Ljava/lang/Object;>
    • 메소드 타입 매개변수: <T:Ljava/lang/Comparable<TT;>;>(TT;)V

    여기서 <T:Ljava/lang/Object;>는 T가 Object의 하위 타입임을 나타낸다. TT;는 특정 타입 T를 참조한다.

    4. 메타데이터(Metadata)

     
    이 장에서는 주요 API를 사용하여 컴파일된 자바 클래스의 메타데이터, 예를 들어 어노테이션을 생성하고 변환하는 방법을 설명한다. 각 섹션은 하나의 메타데이터 유형을 소개로 시작하고, 그에 해당하는 ASM 인터페이스, 컴포넌트 및 도구를 소개하여 이러한 메타데이터를 생성하고 변환하는 방법과 일부 예제를 제시할 것이다.
     

    4.1. 제네릭(Generic)

    List<E>와 같은 제네릭 클래스 및 이를 사용하는 클래스는 선언하거나 사용하는 제네릭 타입에 대한 정보를 포함한다.
    이 정보는 런타임에 바이트코드 명령어에 의해 사용되지 않지만, 리플렉션 API를 통해 접근할 수 있다. 또한 별도 컴파일에 의해 컴파일러에 의해 사용된다.
     

    4.1.1. 구조(Structure)

    제네릭 타입에 대한 정보는 후방 호환성 이유로 인해 타입 또는 메소드 디스크립터(자바 5에서 제네릭이 도입되기 훨씬 이전에 정의됨)에 저장되지 않고, 타입, 메소드 및 클래스 시그니처라고 하는 유사 구조에 저장된다.
    For backward compatibility reasons the information about generic types is not stored in type or method descriptors (which were defined long before the introduction of generics in Java 5)
     
    이러한 시그니처는 제네릭 타입이 관련될 때 클래스, 필드 및 메소드 선언에 디스크립터와 함께 저장된다.
    (제네릭 타입은 메소드의 바이트코드에 영향을 주지 않으며, 컴파일러는 정적 타입 검사를 수행하기 위해 이를 사용하지만, 필요한 경우 타입 캐스트를 다시 도입하여 메소드를 컴파일한다).
     
    타입 및 메소드 디스크립터와 달리, 제네릭 타입의 재귀적 특성 때문에(예를 들어 List<List<E>>) 타입 시그니처의 문법은 상당히 복잡하다.
     
    다음 규칙에 의해 주어진다(이 규칙의 완전한 설명은 자바 가상 머신 사양을 참조):
     

     
     
    1. 첫 번째 규칙은 타입 시그니처가 원시 타입 디스크립터(premitive type discripter) 또는 필드 타입 시그니처(field type signature)중 하나라는 것을 말한다.
     
    2. 두 번째 규칙은 필드 타입 시그니처( field type signature )를 클래스 타입 시그니처, 배열 타입 시그니처 또는 타입 변수로 정의(define class type signature, an array type signature, or a type variable)한다.
     
    3. 세 번째 규칙은 클래스 타입 시그니처를 정의한다: 메인 클래스 이름이나 내부 클래스 이름(점으로 구분됨) 뒤에 꺾쇠괄호 안에 가능한 타입 인수를 가진 클래스 타입 디스크립터이다.
     
    4. 나머지 규칙은 타입 인수 및 타입 변수를 정의한다. 타입 인수는 자체 타입 인수를 가진 완전한 필드 타입 시그니처일 수 있으므로 타입 시그니처는 매우 복잡할 수 있다(그림 4.1 참조).
     

    Figure 4.1.: Sample type signatures

     
    메소드 시그니처는 타입 시그니처가 타입 디스크립터를 확장하는 것처럼 메소드 디스크립터를 확장한다.
    메소드 시그니처는 메소드 매개변수의 타입 시그니처와 반환 타입의 시그니처를 설명한다. 메소드 디스크립터와 달리, 메소드에 의해 던져진 예외의 시그니처를 포함하며, ^로 시작하고, 선택적인 형식 타입 매개변수를 꺾쇠괄호 사이에 포함할 수도 있다:
     

     
    예를 들어, 타입 변수 T에 의해 매개변수화된 다음 제네릭 정적 메소드의 메소드 시그니처는 다음과 같은 메소드 시그니처이다:
     

    static <T> Class<? extends T> m (int n)

     
    is the following method signature:
     

    <T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;

     
    마지막으로 클래스 시그니처는 클래스 타입 시그니처와 혼동되어서는 안 되며, 슈퍼 클래스의 타입 시그니처, 구현된 인터페이스의 타입 시그니처 및 선택적 형식 타입 매개변수를 따른다:

    ClassSignature: TypeParams? ClassTypeSignature ClassTypeSignature*

     
    예를 들어, C<E> extends List<E>로 선언된 클래스의 클래스 시그니처는 <E:Ljava/lang/Object;>Ljava/util/List<TE;>;이다.
     

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

    디스크립터와 마찬가지로, 동일한 효율성 이유로(섹션 2.3.1 참조), ASM API는 컴파일된 클래스에 저장된 시그니처를 그대로 노출한다.
    (시그니처의 주요 발생은 ClassVisitor 클래스의 visit, visitField 및 visitMethod 메소드에서 각각 선택적 클래스, 타입 또는 메소드 시그니처 인수로 있다).
     
    다행히도, org.objectweb.asm.signature 패키지에 기반한 시그니처를 생성하고 변환하기 위한 일부 도구도 제공한다. SignatureVisitor 추상 클래스를 기반으로 한다(그림 4.2 참조).
     

    Figure 4.2.: The SignatureVisitor class

     
    이 추상 클래스는 타입 시그니처, 메소드 시그니처 및 클래스 시그니처를 방문하는 데 사용된다.
    타입 시그니처를 방문하는 데 사용되는 메소드는 굵은 글씨로 되어 있으며, 이전 문법 규칙을 반영하는 다음 순서로 호출되어야 한다(두 메소드가 SignatureVisitor를 반환하는데, 이는 타입 시그니처의 재귀적 정의 때문이다):
     

     
    메소드 시그니처를 방문하는 데 사용되는 메소드는 다음과 같다:
     

     
    마지막으로 클래스 시그니처를 방문하는 데 사용되는 메소드는:
     

     
    이러한 메소드 대부분은 SignatureVisitor를 반환하는데, 이는 타입 시그니처를 방문하기 위한 것이다.
    ClassVisitor에 의해 반환된 MethodVisitors와 달리, SignatureVisitor에 의해 반환된 SignatureVisitors는 null이 아니어야 하며, 순차적으로 사용되어야 한다사실 중첩된 시그니처가 완전히 방문되기 전에 부모 방문자의 어떤 메소드도 호출되어서는 안 된다.
     
    - 클래스와 마찬가지로, ASM API는 이 API를 기반으로 한 두 가지 컴포넌트를 제공한다:
    SignatureReader 컴포넌트는 시그니처를 파싱하고 주어진 시그니처 방문자에 적절한 방문 메소드를 호출하며, SignatureWriter 컴포넌트는 받은 메소드 호출을 기반으로 시그니처를 구축한다.
    - 이 두 클래스는 클래스 및 메소드와 같은 원칙을 사용하여 시그니처를 생성하고 변환하는 데 사용될 수 있다.
    예를 들어, 일부 시그니처에서 클래스 이름을 변경하고 싶다고 가정해 보자. 이는 visitClassType 및 visitInnerClassType 메소드를 제외한 모든 메소드 호출을 변경하지 않고 전달하는 다음 시그니처 어댑터로 수행될 수 있다(여기서 sv 메소드는 항상 this를 반환한다고 가정하는데, SignatureWriter의 경우 그렇다):
     

    public class RenameSignatureAdapter extends SignatureVisitor {
        private SignatureVisitor sv;
        private Map<String, String> renaming;
        private String oldName;
        public RenameSignatureAdapter(SignatureVisitor sv,
                                      Map<String, String> renaming) {
            super(ASM4);
            this.sv = sv;
            this.renaming = renaming;
        }
        public void visitFormalTypeParameter(String name) {
            sv.visitFormalTypeParameter(name);
        }
        public SignatureVisitor visitClassBound() {
            sv.visitClassBound();
            return this;
        }
        public SignatureVisitor visitInterfaceBound() {
            sv.visitInterfaceBound();
            return this;
        }
    ...
        public void visitClassType(String name) {
            oldName = name;
            String newName = renaming.get(oldName);
            sv.visitClassType(newName == null ? name : newName);
        }
        public void visitInnerClassType(String name) {
            oldName = oldName + "." + name;
            String newName = renaming.get(oldName);
            sv.visitInnerClassType(newName == null ? name : newName);
        }
        public void visitTypeArgument() {
            sv.visitTypeArgument();
        }
        public SignatureVisitor visitTypeArgument(char wildcard) {
            sv.visitTypeArgument(wildcard);
            return this;
        }
        public void visitEnd() {
            sv.visitEnd();
        }
    }

     
    다음 코드의 결과는 "LA<TK;TV;>.B<TK;>;"이다:

     

    4.1.3. 도구

    섹션 2.3에서 소개된 TraceClassVisitor 및 ASMifier 클래스는 클래스 파일에 포함된 시그니처를 내부 형식으로 출력한다. 일부 제네릭 타입에 해당하는 시그니처를 찾기 위해 이러한 명령줄 도구를 사용할 수 있다: 일부 제네릭 타입을 가진 자바 클래스를 작성하고, 컴파일한 다음, 이 도구를 사용하여 해당 시그니처를 찾는다.
     

    4.2. 어노테이션

     
    클래스, 필드, 메소드 및 메소드 매개변수 애너테이션, 예를 들어 @Deprecated 또는 @Override는(그들의 보존 정책이 RetentionPolicy.SOURCE가 아닌 경우) 컴파일된 클래스에 저장된다.
    이 정보는 런타임에 바이트코드 명령어에 의해 사용되지 않지만, 보존 정책이 RetentionPolicy.RUNTIME인 경우 리플렉션 API를 통해 접근할 수 있다. 또한 컴파일러에 의해 사용될 수도 있다.
     
     

    4.2.1. 구조

    소스 코드의 애너테이션은 @Deprecated, @Retention(RetentionPolicy.CLASS) 또는 @Task(desc="refactor", id=1)과 같은 다양한 형태를 가질 수 있다. 그러나 내부적으로 모든 애너테이션은 동일한 형태를 가지며, 애너테이션 타입과 이름-값 쌍 세트에 의해 지정된다. 여기서 값은 다음으로 제한된다:

    • 기본형, String 또는 Class 값,
    • 열거형 값,
    • 애너테이션 값,
    • 위 값들의 배열.

    ( primitive, String or Class values, • enum values, • annotation values, • arrays of the above values.)
    애너테이션은 다른 애너테이션 또는 심지어 애너테이션 배열을 포함할 수 있다.
    따라서 애너테이션은 상당히 복잡할 수 있다.
     

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

    애너테이션을 생성하고 변환하기 위한 ASM API는 AnnotationVisitor 추상 클래스를 기반으로 한다(그림 4.3 참조).
     

    Figure 4.3.: The AnnotationVisitor class

     
    이 클래스의 메소드는 애너테이션의 이름-값 쌍을 방문하는 데 사용된다(애너테이션 타입은 이 타입을 반환하는 메소드, 즉 visitAnnotation 메소드에서 방문된다).
    첫 번째 메소드는 기본형, String 및 Class 값(후자는 Type 객체로 표현됨)에 사용되며, 나머지는 열거형, 애너테이션 및 배열 값에 사용된다. visitEnd을 제외하고는 어떤 순서로든 호출될 수 있다:
     

     
    두 메소드가 AnnotationVisitor를 반환한다.
    ( Note that two methods return an AnnotationVisitor: ) 이는 애너테이션이 다른 애너테이션을 포함할 수 있기 때문이다. ClassVisitor에 의해 반환된 MethodVisitors의 경우와 다르게 두 메소드에 의해 반환된 AnnotationVisitors는 순차적으로 사용되어야 한다: 중첩된 애너테이션이 완전히 방문되기 전에 부모 방문자의 어떤 메소드도 호출되어서는 안 된다.
     
    또한 visitArray 메소드도 배열의 요소를 방문하기 위해 AnnotationVisitor를 반환한다. 그러나 배열의 요소는 이름이 없기 때문에 visitArray에 의해 반환된 방문자의 메소드에 의해 이름 인수는 무시되며 null로 설정될 수 있다. 애너테이션 추가, 제거 및 탐지 필드 및 메소드와 마찬가지로, visitAnnotation 메소드에서 null을 반환함으로써 애너테이션을 제거할 수 있다:
     
    애너테이션 추가, 제거 및 탐지( Adding, removing and detecting annotations)
     
    1. 제거
    필드 및 메소드와 마찬가지로, visitAnnotation 메소드에서 null을 반환함으로써 애너테이션을 제거할 수 있다:

    public class RemoveAnnotationAdapter extends ClassVisitor {
        private String annDesc;
        public RemoveAnnotationAdapter(ClassVisitor cv, String annDesc) {
            super(ASM4, cv);
            this.annDesc = annDesc;
        }
        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean vis) {
            if (desc.equals(annDesc)) {
                return null;
            }
            return cv.visitAnnotation(desc, vis);
        }
    }

     
     
    2. 추가
    클래스 애너테이션을 추가하는 것은 ClassVisitor 클래스의 메소드가 호출되어야 하는 제약 때문에 더 어렵다. 실제로 visitAnnotation 다음에 올 수 있는 모든 메소드를 오버라이드하여 모든 애너테이션이 방문되었는지 탐지해야 한다(메소드 애너테이션은 visitCode 메소드 덕분에 추가하기 더 쉽다):
     

    public class AddAnnotationAdapter extends ClassVisitor {
        private String annotationDesc;
        private boolean isAnnotationPresent;
    
        public AddAnnotationAdapter(ClassVisitor cv, String annotationDesc) {
            super(ASM4, cv);
            this.annotationDesc = annotationDesc;
        }
    
        @Override
        public void visit(int version, int access, String name,
                          String signature, String superName, String[] interfaces) {
            int v = (version & 0xFF) < V1_5 ? V1_5 : version;
            cv.visit(v, access, name, signature, superName, interfaces);
        }
    
        @Override
        public AnnotationVisitor visitAnnotation(String desc,
                                                 boolean visible) {
            if (visible && desc.equals(annotationDesc)) {
                isAnnotationPresent = true;
            }
            return cv.visitAnnotation(desc, visible);
        }
    
        @Override
        public void visitInnerClass(String name, String outerName,
                                    String innerName, int access) {
            addAnnotation();
            cv.visitInnerClass(name, outerName, innerName, access);
        }
    
        @Override
        public FieldVisitor visitField(int access, String name, String desc,
                                       String signature, Object value) {
            addAnnotation();
            return cv.visitField(access, name, desc, signature, value);
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name,
                                         String desc, String signature, String[] exceptions) {
            addAnnotation();
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
    
        @Override
        public void visitEnd() {
            addAnnotation();
            cv.visitEnd();
        }
    
        private void addAnnotation() {
            if (!isAnnotationPresent) {
                AnnotationVisitor av = cv.visitAnnotation(annotationDesc, true);
                if (av != null) {
                    av.visitEnd();
                }
                isAnnotationPresent = true;
            }
        }
    }

     
    이 어댑터는 클래스 버전이 1.5보다 작은 경우 1.5로 업그레이드한다. 이는 JVM이 버전이 1.5보다 작은 클래스의 애너테이션을 무시하기 때문에 필요하다.
     
    클래스 및 메소드 어댑터에서 애너테이션을 사용하는 마지막이자 아마도 가장 흔한 사용 사례는 애너테이션을 사용하여 변환을 매개변수화하는 것이다.
    예를 들어, @Persistent 애너테이션이 있는 필드에 대해서만 필드 접근을 변환하거나, @Log 애너테이션이 있는 메소드에만 로깅 코드를 추가하는 등의 작업을 수행할 수 있다.
     
    이러한 모든 사용 사례는 애너테이션이 먼저 방문되어야 하기 때문에 쉽게 구현될 수 있다: 클래스 애너테이션은 필드 및 메소드보다 먼저 방문되어야 하며, 메소드 및 매개변수 애너테이션은 코드보다 먼저 방문되어야 한다. 따라서 원하는 애너테이션이 감지되면 플래그를 설정하고 나중에 변환에서 이를 사용하기만 하면 된다(위 예제에서 isAnnotationPresent 플래그와 함께 수행된 것처럼).
     

    4.2.3. 도구

    섹션 2.3에서 소개된 TraceClassVisitor, CheckClassAdapter 및 ASMifier 클래스는 애너테이션도 지원한다(메소드에 대해서처럼, 개별 애너테이션 대신 클래스 수준에서 작업하기 위해 TraceAnnotationVisitor 또는 CheckAnnotationAdapter를 사용할 수도 있다). 특정 애너테이션을 생성하는 방법을 보기 위해 사용될 수 있다. 예를 들어 사용하는 경우:
     

    사소한 리팩토링 후 다음과 같이 읽는 코드를 출력한다:
     

    package asm.java.lang;
    import org.objectweb.asm.*;
    public class DeprecatedDump implements Opcodes {
        public static byte[] dump() throws Exception {
            ClassWriter cw = new ClassWriter(0);
            AnnotationVisitor av;
            cw.visit(V1_5, ACC_PUBLIC + ACC_ANNOTATION + ACC_ABSTRACT
                            + ACC_INTERFACE, "java/lang/Deprecated", null,
                    "java/lang/Object",
                    new String[] { "java/lang/annotation/Annotation" });
            {
                av = cw.visitAnnotation("Ljava/lang/annotation/Documented;",
                        true);
                av.visitEnd();
            }
            {
                av = cw.visitAnnotation("Ljava/lang/annotation/Retention;", true);
                av.visitEnum("value", "Ljava/lang/annotation/RetentionPolicy;",
                        "RUNTIME");
                av.visitEnd();
            }
            cw.visitEnd();
            return cw.toByteArray();
        }
    }

     
    이 코드는 ACC_ANNOTATION 플래그를 사용하여 애너테이션 클래스를 생성하는 방법과, 값 없이 및 열거형 값이 있는 두 클래스 애너테이션을 생성하는 방법을 보여준다.
    메소드 및 매개변수 애너테이션은 MethodVisitor 클래스에 정의된 visitAnnotation 및 visitParameterAnnotation 메소드를 사용하여 유사한 방식으로 생성될 수 있다.
     

    4.3. 디버그

    javac -g로 컴파일된 클래스에는 소스 파일 이름, 소스 라인 번호와 바이트코드 명령어 간의 매핑, 소스 코드의 지역 변수 이름과 바이트코드의 지역 변수 슬롯 간의 매핑이 포함된다. 이 선택적 정보는 디버거와 예외 스택 추적에서 사용 가능할 때 사용된다.
     

    4.3.1. 구조

    클래스의 소스 파일 이름은 전용 클래스 파일 구조 섹션에 저장된다(그림 2.1 참조).
    소스 라인 번호와 바이트코드 명령어 간의 매핑은 메소드의 컴파일된 코드 섹션에 (라인 번호, 라벨) 쌍 목록으로 저장된다. 예를 들어, l1, l2, l3가 이 순서로 나타나는 세 라벨이라면, 다음 쌍은:
     

     
    l1과 l2 사이의 명령어가 n1 라인에서, l2와 l3 사이의 명령어가 n2 라인에서, l3 이후의 명령어가 n3 라인에서 왔음을 의미한다. 주어진 라인 번호가 여러 쌍에 나타날 수 있다. 이는 한 소스 라인에 나타나는 표현식에 해당하는 명령어가 바이트코드에서 연속적이지 않을 수 있기 때문이다.
    예를 들어, (init; cond; incr) statement;는 일반적으로 다음 순서로 컴파일된다: init statement incr cond.
     
    소스 코드의 지역 변수 이름과 바이트코드의 지역 변수 슬롯 간의 매핑은 (이름, 타입 디스크립터, 타입 시그니처, 시작, 끝, 인덱스) 튜플 목록으로 메소드의 컴파일된 코드 섹션에 저장된다. 이러한 튜플은 시작과 끝 라벨 사이에서, 슬롯 인덱스에 있는 지역 변수가 소스 코드의 이름과 타입에 해당한다는 것을 의미한다. 컴파일러는 다른 스코프를 가진 서로 다른 소스 지역 변수를 저장하기 위해 동일한 지역 변수 슬롯을 사용할 수 있다. 반대로 하나의 소스 지역 변수가 비연속적 스코프를 가진 지역 변수 슬롯으로 컴파일될 수 있다. 예를 들어, 다음과 같은 상황이 가능하다:
     

     
    상응하는 튜플들은 다음과 같다.

     

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

    디버그 정보는 ClassVisitor 및 MethodVisitor 클래스의 세 메소드를 사용하여 방문된다:

    • 소스 파일 이름은 ClassVisitor 클래스의 visitSource 메소드를 사용하여 방문된다;
    • 소스 라인 번호와 바이트코드 명령어 간의 매핑은 MethodVisitor 클래스의 visitLineNumber 메소드를 사용하여 한 쌍씩 방문된다;
    • 소스 코드의 지역 변수 이름과 바이트코드의 지역 변수 슬롯 간의 매핑은 MethodVisitor 클래스의 visitLocalVariable 메소드를 사용하여 한 튜플씩 방문된다. visitLineNumber 메소드는 인수로 전달된 라벨이 방문된 후에 호출되어야 한다. 실제로는 이 라벨 직후에 호출되어 메소드 방문자에서 현재 명령어의 소스 라인을 아주 쉽게 알 수 있다:
    public class MyAdapter extends MethodVisitor {
        int currentLine;
        public MyAdapter(MethodVisitor mv) {
            super(ASM4, mv);
        }
        @Override
        public void visitLineNumber(int line, Label start) {
            mv.visitLineNumber(line, start);
            currentLine = line;
        }
    ...
    }
    

     
    마찬가지로 visitLocalVariable 메소드는 인수로 전달된 라벨이 방문된 후에 호출되어야 한다. 여기에는 이전 섹션에서 제시된 쌍과 튜플에 해당하는 예제 메소드 호출이 있다:

    디버그 정보 무시( Ignoring debug information)
     
    라인 번호와 지역 변수 이름을 방문하기 위해 ClassReader 클래스는 점프 명령어에 의해 필요하지 않지만 디버그 정보를 나타내기 위해서만 필요한 "인공" Label 객체를 도입해야 할 수 있다. 이는 3.2.5절에서 설명한 상황과 같이, 명령어 시퀀스 중간에 있는 Label이 점프 대상으로 간주되어 이 시퀀스가 제거되는 것을 방지하는 등의 거짓 긍정을 도입할 수 있다.
    이러한 거짓 긍정을 방지하기 위해 ClassReader.accept 메소드에서 SKIP_DEBUG 옵션을 사용할 수 있다. 이 옵션을 사용하면 클래스 리더는 디버그 정보를 방문하지 않고 이를 위한 인공 라벨을 생성하지 않는다. 물론 디버그 정보가 클래스에서 제거되므로 이 옵션은 애플리케이션에 문제가 되지 않는 경우에만 사용할 수 있다. 참고: ClassReader 클래스는 클래스 구조만 필요한 경우에 유용할 수 있는 컴파일된 코드 방문을 건너뛰기 위한 SKIP_CODE, 스택 맵 프레임을 건너뛰기 위한 SKIP_FRAMES, 이 프레임을 압축 해제하기 위한 EXPAND_FRAMES와 같은 다른 옵션을 제공한다.
     

    4.3.3. 도구

    제네릭 타입 및 애너테이션과 마찬가지로, TraceClassVisitor, CheckClassAdapter 및 ASMifier 클래스를 사용하여 디버그 정보로 작업하는 방법을 알아낼 수 있다.
     

    Reference

     
    https://asm.ow2.io/asm4-guide.pdf
     
    ASM USER GUIDE
     
    Copyright c 2007, 2011 Eric Bruneton All rights reserved. Redistribution and use in source (LYX format) and compiled forms (LATEX, PDF, PostScript, HTML, RTF, etc), with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code (LYX format) must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in compiled form (converted to LATEX, PDF, PostScript, HTML, RTF, and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this documentation without specific prior written permission.
     
    THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.