semtax의 개발 일지

ASM Library Guideline 챕터 4 정리 본문

개발/Java

ASM Library Guideline 챕터 4 정리

semtax 2020. 1. 10. 18:44
반응형

1. 자바 제네릭(Generic) & 타입구조 소개

사실 자바 제네릭은, 초창기 자바에는 존재하지 않는 문법이었다. 그래서 타입이나 메서드 정보와 문법적으로 흡사함에도 불구하고, 해당영역이 아닌, 별도의 다른영역에 저장된다. 또한 제네릭같은경우 실제 실행되는 바이트코드에는 영향을 미치지 않기 때문에 별도의 영역에 저장을 해도 따로 문제가 생기지는 않는다.

자바 스펙문서상에서 제네릭의 문법은 아래와 같은 EBNF(Extended Bacus-Naur Form)으로 정의되어있다. 문법의 정의가 꽤나 복잡하다는것을 알 수 있다.

TypeSignature: Z | C | B | S | I | F | J | D | FieldTypeSignature

FieldTypeSignature : ClassTypeSignature | [ TypeSignature | TypeVar

ClassTypeSignature: L Id ( / Id)* TypeArgs? ( . Id TypeArgs? )* ;

TypeArgs: < TypeArg+ >

TypeArg: * | (+ | -)? FieldTypeSignature

TypeVar: T Id ;

위의 문법을 적용한 예시는 아래 표와 같다.

Java type and corresponding type signature
List => Ljava/util/List<TE;>;
List<?> => Ljava/util/List<*>;
List<? extends Number> => Ljava/util/List<+Ljava/lang/number;>;
List<? super Integer> => Ljava/util/List<-Ljava/lang/number;>;
List<List[]> => Ljava/util/List<[Ljava/util/List<Ljava/lang/String;>;>;
HashMap<K,V>.HashIterator => Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;

자바 스펙문서상에서 제네릭 메서드의 문법역시 아래와 같은 EBNF(Extended Bacus-Naur Form)으로 정의되어있다. 역시나 문법의 정의가 꽤나 복잡하다는것을 알 수 있다.

MethodTypeSignature: TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception*

Exception: ^ClassTypeSignature | ^TypeVar

TypeParams: < TypeParam+ >

TypeParam: Id : FieldTypeSignature? ( : FieldTypeSignature ) *

위의 문법을 적용한 예시는 아래 표와 같다.

Java type and corresponding type signature
static Class<? extends T> m (int n) => <T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;
C extends List => <E:Ljava/lang/Object;>Ljava/util/List<TE;>;

2. ASM을 이용한 제네릭(Generic) & 자바 타입구조 변경

ASM에서는 이러한 제네릭과 같은 Signature 정보들을 변경하기 위해 SignatureVisitor라는 클래스를 제공하고 있다. 따라서 이 클래스를 사용해서 제네릭과 같은 Signature 정보들을 변경 할 수 있다.

주의할 점은, 이러한 Signature정보(제네릭, 타입)들을 변경할때, SignatureAdapter의 메서드에서 null을 절대로 반환하면 안된다는것이다.

아래 코드는 제네릭의 타입정보를 변경하는 예제이다.

package BCITest;

import org.objectweb.asm.signature.SignatureVisitor;

import java.util.Map;

import static org.objectweb.asm.Opcodes.ASM6;

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(ASM6);
        this.sv = sv;
        this.renaming = renaming;
    }

    @Override
    public void visitFormalTypeParameter(String name) {
        super.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();
    }

    @Override
    public void visitTypeVariable(String name){
        sv.visitTypeVariable(name);
    }

    @Override
    public SignatureVisitor visitTypeArgument(char wildcard) {
        sv.visitTypeArgument(wildcard);
        return this;
    }

    @Override
    public void visitEnd() {
        sv.visitEnd();
    }
}

아래코드는, 위에서 정의한 SignatureAdapter를 사용하는 예제이다.

String s = "Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;"; 
Map<String, String> renaming = new HashMap<String, String>(); renaming.put("java/util/HashMap", "A"); 
renaming.put("java/util/HashMap.HashIterator", "B"); 
SignatureWriter sw = new SignatureWriter();
SignatureVisitor sa = new RenameSignatureAdapter(sw, renaming); 
SignatureReader sr = new SignatureReader(s);
sr.acceptType(sa);
sw.toString();

3. 자바 어노테이션(Annotation) 변경

자바 어노테이션은 자바의 Reflection API를 이용해서 동적으로 클래스의 정보에 접근해서 해당 클래스의 선언을 바꾸는등의 동작을 수행할 수 있는 문법이다.

문법의 생김새는 "@(key="value")" 와 같이 생겼으며 값으로 올 수 있는 것들은 아래와 같다.

  • primitive 타입
  • enum 상수 타입
  • 어노테이션 타입
  • 위 3개에 해당하는 배열 타입

ASM API에서는 어노테이션 정보의 변경을 위해 AnnotationVisitor라는 클래스를 제공하고 있다.

아래 예제는 어노테이션을 제거하는 예제이다

import static org.objectweb.asm.Opcodes.ASM6;

public class RemoveAnnotationAdapter extends ClassVisitor {

    private String annDesc;

    RemoveAnnotationAdapter(ClassVisitor cv, String annDesc){
        super(ASM6, cv);
        this.annDesc = annDesc;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if(desc.equals(annDesc)){
            return null;
        }
        return cv.visitAnnotation(desc, visible);
    }
}

사용방법은 아래와 같다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;

public class AnnotationTransformTestExample {

    public static PrintWriter printWriter = new PrintWriter(System.out);

    public static void addAnnotation() throws IOException {
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr, 0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
        ClassVisitor cv = new AddAnnotationAdapter(tcv, "Ljava/lang/annotation/Documented;");
        cr.accept(cv, 0);
    }

    public static void main(String[] args) throws IOException {
        addAnnotation();
    }
}

두번째 예제는 어노테이션을 추가하는 예제이다. 코드는 아래와 같다.

package BCITest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.V1_8;

public class AddAnnotationAdapter extends ClassVisitor {
    private String annotationDesc;
    private boolean isAnnotationPresent;

    public AddAnnotationAdapter(ClassVisitor cv, String annotationDesc) {
        super(ASM6, 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_8 ? V1_8 : 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();
        super.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;
        }
    }
}

사용방법은 아래와 같다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;

public class AnnotationTransformTestExample {

    public static PrintWriter printWriter = new PrintWriter(System.out);

    public static void addAnnotation() throws IOException {
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr, 0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
        ClassVisitor cv = new AddAnnotationAdapter(tcv, "Ljava/lang/annotation/Documented;");
        cr.accept(cv, 0);
    }

    public static void main(String[] args) throws IOException {
        addAnnotation();
    }
}

4. 디버그 정보

자바와 같은 언어에서 디버그모드로 컴파일을 수행하는 경우 특정 레이블이 몇번째 라인에 위치해 있는지와 같은 정보를 얻어올 수 있다. 해당 정보는 "(라인 넘버, 레이블)" 형태로 저장이된다. 또한, 메서드 내에 선언되어있는 지역변수의 심볼테이블과 같은 정보도 얻어 올 수 있다. 해당정보는 "(변수명, 타입 명세, 타입 시그네처, 시작라인, 끝라인, 인덱스)" 와 같은 형태로 저장이되며, 이는 시작라인과 끝라인 사이에 해당하는 index의 지역변수에 대한 정보를 의미한다.

ASM에서는 디버그 정보를 ClassVisitor의 visitLineNumber나 MethodVisitor의 visitLocalVariable 함수에서 얻어올 수 있다.

사용방식은 대략 아래 코드와 같다.

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;
      } 
          ...
}

또한 ASM에서는 ClassReader에서 SKIP_CODE나 SKIP_FRAME, SKIP_DEBUG와 같은 옵션을 이용해서 해당 정보를 유연하게 스킵이 가능하다.

반응형
Comments