ASM

ASM User Guide 번역 2

yimoc 2018. 9. 10. 17:30

2018/09/10 - [ASM] - ASM User Guide 번역 1



Part I. 


Core API



2. Classes


이 장에서는 코어 ASM API를 사용하여 컴파일 된 Java 클래스를 생성하고 변환하는 방법을 설명합니다. 그것은 컴파일 된 클래스의 표현부터 시작하여 많은 예제를 사용하여 해당하는 ASM 인터페이스, 컴포넌트 및 툴을 생성 및 변환하는 도구를 제공합니다. 메소드, 주석 및 제네릭의 내용은 다음 장에서 설명합니다.


2.1. Structure


2.1.1. Overview


컴파일 된 클래스의 전체 구조는 아주 간단합니다. 실제로 컴파일 된 클래스는 기본적으로 컴파일 된 응용 프로그램과 달리 구조 정보와 소스 코드의 거의 모든 심볼을 유지합니다. 실제로 컴파일 된 클래스에는 다음이 포함됩니다.


• modifier (예 : public 또는 private), name, super class, interface 및 클래스의 annotation을 설명하는 section


• 클래스에서 선언 된 field 당 하나의 section. 각 섹션은 필드의 modifier , name , type 및 annotation을 설명합니다.


• 클래스에서 선언 된 메서드 및 생성자 당 하나의 section. 각 섹션은 modifier, name, return 및 parameter type 및 메소드의 annotation을 설명합니다. 또한 컴파일 된 코드를 포함하고 있으며 Java 바이트 코드 명령어의 시퀀스 형태로 구현됩니다.


그러나 소스 클래스와 컴파일 된 클래스에는 몇 가지 차이점이 있습니다.


• 컴파일 된 클래스는 하나의 클래스만 설명하지만 소스 파일에는 여러 클래스가 포함될 수 있습니다. 예를 들어, 내부 클래스가 하나 인 클래스를 설명하는 소스 파일은 메인 클래스 용과 내부 클래스 용의 두 클래스 파일로 컴파일됩니다. 그러나 메인 클래스 파일에는 내부 클래스에 대한 참조가 포함되어 있습니다. 클래스 및 메서드 내부에 정의 된 내부 클래스에는 메서드를 둘러싼 참조가 포함되어 있습니다.


• 컴파일 된 클래스에는 물론 주석이 포함되지 않습니다. 하지만 , 클래스, 필드, 메서드 및 코드 속성(attribute)를 포함합니다. 이것은 이러한 요소와 연관시키는 데 사용할 수있습니다. Java 5에서 주석을 도입 한 이후로 동일한 목적으로 사용될 수있는 속성은 거의 쓸모 없게 되었습니다.


• 컴파일 된 클래스에는 패키지 및 가져 오기 섹션이 없으므로 모든 Type의 이름은 fully Qualified합니다. 


매우 중요한 또 다른 구조적 차이점은 컴파일 된 클래스가 상수 풀(Constant pool) 섹션을 포함한다는 것입니다. 이 풀은 클래스에 나타나는 모든 숫자, 문자열 및 type 상수를 포함하는 배열입니다. 이 상수는 상수 풀 섹션에서 한 번만 정의되며 클래스 파일의 다른 모든 섹션에서 해당 인덱스로 참조됩니다. ASM은 상수 풀과 관련된 모든 세부 사항을 숨기므로 바라기를 귀찮게 생각하지 않아도됩니다. 그림 2.1은 컴파일 된 클래스의 전체 구조를 요약 한 것입니다. 정확한 구조는 Java Virtual Machine Specification, 섹션 4에 설명되어 있습니다.



 Modifiers, name, super class, interfaces 

 Constant pool: numeric, string and type constants

 Source file name (optional)

 Enclosing class reference

 Annotation*

 Attribute*

 Inner class*

 Name

 Field*

 Modifiers, name, type

 Annotation*

 Attribute*

 Method*

 Modifiers, name, return and parameter types

 Annotation*

 Attribute*

 Compiled code

Figure 2.1.: Overall structure of a compiled class (* means zero or more)



또 다른 중요한 차이점은 Java 유형은 컴파일 된 클래스와 소스 클래스에서 다르게 표현된다는 것입니다. 다음 섹션에서는 컴파일 된 클래스의 표현에 대해 설명합니다.


2.1.2. Internal names


많은 경우, Type은 클래스 또는 인터페이스 Type으로 제한됩니다. 예를 들어, 클래스의 수퍼 클래스, 클래스에 의해 구현 된 인터페이스 또는 메소드에 의해 던져지는 예외는 기본 유형이나 배열 유형 일 수 없으며 반드시 클래스 또는 인터페이스 Type입니다. 이러한 Type은 내부 이름이있는 컴파일 된 클래스로 표시됩니다. 클래스의 내부 이름은이 클래스의 fully qualified된 이름이며 점(.)이 슬래시(/)로 대체됩니다. 예를 들어 String의 내부 이름은 java / lang / String입니다.


2.1.3. Type descriptors


내부 이름은 클래스 또는 인터페이스 Type으로 제한되는 Type에만 사용됩니다. 필드 Type과 같은 다른 모든 상황에서는 Java Type은 type descriptor(see Figure 2.2)가있는 컴파일 된 클래스로 표시됩니다.


  Java type

 Type descriptor

 boolean

 Z 

 char 

 C 

 byte 

 B 

 short 

 S 

 int 

 I 

 float 

 F 

 long 

 J 

 double 

 D 

 Object 

 Ljava/lang/Object; 

 int[] 

 [I 

 Object[][]

 [[Ljava/lang/Object; 


Figure 2.2.: Type descriptors of some Java types


primitive type의 descriptor는 Single character로 표기됩니다: boolean 형의 경우는 Z, char의 경우는 C, byte 형의 경우는 B, int의 경우는 I, float의 경우는 F, long의 경우는 J, double의 경우는 D입니다. 클래스 타입의 기술자는이 클래스의 내부 이름이며 L이 앞에오고 세미콜론이 뒤 따른다. 예를 들어 String의 유형 설명자는 Ljava / lang / String입니다. 마지막으로 배열 타입의 기술자는 대괄호 뒤에 배열 원소 타입의 기술자가옵니다.


2.1.4. Method descriptors


Method descriptors는 Type descriptor의 List입니다. 하나의 Type Descriptor는 메소드의 parameter type과 return type을 단일 문자로 설명되어있습니다. Method descriptor는 왼쪽 괄호로 시작하여 각 형식 매개 변수의 type descriptor, 이어서 오른쪽 괄호, 그 다음에 반환 유형의 유형 설명자, 혹은 void를 반환하면 V로 시작합니다 (메소드 설명자에는 메소드의 이름 또는 인수 이름을 포함하지 않는다)


 Method declaration in source file 

 Method descriptor 

 void m(int i, float f) 

 (IF)V 

 int m(Object o)

 (Ljava/lang/Object;)I 

 int[] m(int i, String s)

 (ILjava/lang/String;)[I 

 Object m(int[] i)

 ([I)Ljava/lang/Object; 

Figure 2.3.: Sample method descriptors


타입 디스크립터가 어떻게 작동하는지 알게되면, 디스크립터를 이해하는 것이 쉽습니다. 예를 들어 (I)는 int 형의 하나의 인수를 취하고 int를 리턴하는 메소드를 설명합니다. Figure  2.3은 몇 가지 메소드 설명자 예제를 제공합니다.


2.2. Interfaces and components


2.2.1. Presentation

.

컴파일 된 클래스를 생성하고 변환하는 ASM API는 ClassVisitor 추상 클래스를 기반으로합니다 (Figure 2.4 참조). 이 클래스의 각 메소드는 같은 이름의 클래스 파일 구조 섹션에 해당합니다.(그림 2.1 참조). > visitSource() 가 Source 구조에 해당한다.

단순 섹션은 인수가 내용을 설명하고 void를 반환하는 단일 메소드 호출로 찾습니다.>visit으로 '방문하다'보단 '찾다'란 의미가 강하다. 다만 함수 이름이 visit임으로 이렇게 사용하는듯하다. 콘텐츠가 임의의 길이와 복잡성을 가질 수있는 섹션은 보조 visitor 클래스를 반환하는 초기 메서드 호출을 통해 찾습니다.  visitAnnotation, visitField 및 visitMethod 메서드의 경우는 각각 AnnotationVisitor, FieldVisitor 및 MethodVisitor를 반환한다.


이 동일한 원칙이 보조 클래스에 대해 재귀적으로 사용됩니다. 예를 들어 FieldVisitor 추상 클래스 (그림 2.5 참조)의 각 메소드는 동일한 이름의 클래스 파일 하위 구조에 해당하며 visitAnnotation은 ClassVisitor 에서처럼 보조 AnnotationVisitor를 반환합니다.


public abstract class ClassVisitor

public ClassVisitor(int api); 

public ClassVisitor(int api, ClassVisitor cv); 

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces); 

public void visitSource(String source, String debug); 

public void visitOuterClass(String owner, String name, String desc); 

AnnotationVisitor visitAnnotation(String desc, boolean visible); 

public void visitAttribute(Attribute attr); 

public void visitInnerClass(String name, String outerName, String innerName, int access); 

public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);

void visitEnd(); 


Figure 2.4.: The ClassVisitor class


public abstract class FieldVisitor {

public FieldVisitor(int api);

public FieldVisitor(int api, FieldVisitor fv);

public AnnotationVisitor visitAnnotation(String desc, boolean visible);

public void visitAttribute(Attribute attr);

public void visitEnd();

}

Figure 2.5.: The FieldVisitor class




이러한 보조 방문자의 생성 및 사용법은 다음 장에서 설명합니다. 실제로 이 장은 ClassVisitor 클래스만으로 해결할 수있는 간단한 문제로 제한됩니다.


ClassVisitor 클래스의 메서드는이 클래스의 JavaDoc에 지정된, 다음과 같은 순서로 호출해야합니다 :


visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd


즉, visit를 먼저 호출 한 다음,  visitSource를 한 번만 호출 한 다음,  visitAuterClass를 한 번만 호출하고 , 그 다음에 임의의 순서로 visitAnnotation 및 visitAttribute를 호출하고, 다음 visitInnerClass, visitField 및 visitMethod를 임의의 순서로 호출 할 수 있고 visitEnd를 한 번 호출하여 종료되었습니다.


ASM은 ClassVisitor API를 기반으로 클래스를 생성하고 변환하는 세 가지 핵심 컴포넌트를 제공합니다. 


• ClassReader 클래스는 바이트 배열된 컴파일 된 클래스를 파싱하고 인수로 전달 된 ClassVisitor 인스턴스의 해당 visitXxx 메서드를 accept 메서드로 호출합니다. 이벤트 프로듀서로 볼 수 있습니다.


• ClassWriter 클래스는 컴파일 된 클래스를 직접 바이너리 형식으로 빌드하는 ClassVisitor 추상 클래스의 하위 클래스입니다. toByteArray 메소드로 검색 할 수있는 컴파일 된 클래스를 포함하는 바이트 배열을 출력으로 생성합니다. 이벤트 소비자로 볼 수 있습니다.


• ClassVisitor 클래스는 수신 한 모든 메서드 호출을 다른 ClassVisitor 인스턴스에 위임합니다. 이벤트 필터로 볼 수 있습니다.


다음 섹션에서는 이러한 구성 요소로 어떻게 클래스를 생성하고 변환하는지 구체적인 예를 제시합니다.




2.2.2. Parsing classes


기존 클래스를 구문 분석하는 데 필요한 유일한 구성 요소는 ClassReader 구성 요소입니다. 이것을 설명하기위한 예를 들어 봅시다. javap 도구와 비슷한 방법으로 클래스의 내용을 인쇄하고 싶다고 가정 해 보겠습니다. 첫 번째 단계는 방문하는 클래스에 대한 정보를 인쇄하는 ClassVisitor 클래스의 하위 클래스를 작성하는 것입니다.

다음은 지나치게 단순화 된 구현입니다.다.


public class ClassPrinter extends ClassVisitor {

public ClassPrinter() {

super(ASM4);

}

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

System.out.println(name + " extends " + superName + " {");

}

public void visitSource(String source, String debug) { } 

public void visitOuterClass(String owner, String name, String desc) { }

public AnnotationVisitor visitAnnotation(String desc, boolean visible) {

return null;

}

public void visitAttribute(Attribute attr) { }

public void visitInnerClass(String name, String outerName, String innerName, int access) { }

public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {

System.out.println(" " + desc + " " + name);

return null;

}

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

System.out.println(" " + name + desc); return null;

}

public void visitEnd() {

System.out.println("}");

}

}


두 번째 단계는 이 ClassPrinter를 ClassReader 구성 요소와 결합하여 ClassReader가 생성 한 이벤트가 ClassPrinter에서 사용되도록합니다.


ClassPrinter cp = new ClassPrinter();

ClassReader cr = new ClassReader("java.lang.Runnable");

cr.accept(cp, 0);


두 번째 행은 ClassReader를 만들어 Runnable 클래스를 구문 분석합니다. 마지막 줄에서 호출 된 accept 메소드는 Runnable 클래스 바이트 코드를 파싱하고 cp에서 해당 ClassVisitor 메소드를 호출합니다. 결과는 다음과 같습니다.


java/lang/Runnable extends java/lang/Object {

run()V

}


ClassReader 인스턴스를 구성하는 방법은 여러 가지가 있습니다. 읽어야하는 클래스는 이름, 위와 같이 또는 값, 바이트 배열 또는 InputStream으로 지정할 수 있습니다. 클래스의 내용을 읽어 들이기위한 입력 스트림은, 다음과 같이 ClassLoader의 getResourceAsStream 메소드를 사용해 취득 할 수 있습니다.


cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");




2.2.3. Generating classes


클래스를 생성하는 데 필요한 유일한 구성 요소는 ClassWriter 구성 요소입니다. 이것을 설명하기위한 예를 들어 봅시다. 다음 인터페이스를 고려하십시오.


package pkg;

public interface Comparable extends Mesurable {

int LESS = -1;

int EQUAL = 0;

int GREATER = 1;

int compareTo(Object o);


ClassVisitor에 대한 6 가지 메소드 호출로 생성 될 수 있습니다.


ClassWriter cw = new ClassWriter(0);

cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,

"pkg/Comparable", null, "java/lang/Object", new String[] { "pkg/Mesurable" });

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",

null, new Integer(-1)).visitEnd();

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",

null, new Integer(0)).visitEnd();

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",

null, new Integer(1)).visitEnd();

cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I",

null, null).visitEnd();

cw.visitEnd();

byte[] b = cw.toByteArray();



첫 번째 줄은 클래스의 바이트 배열 표현을 실제로 작성하는 ClassWriter 인스턴스를 만듭니다 (생성자 인수는 다음 장에서 설명합니다).


visit 메소드를 호출하여 클래스 헤더를 정의합니다. V1_5 인수는 다른 모든 ASM 상수와 마찬가지로 ASM Opcode 인터페이스에 정의 된 상수입니다. 클래스 버전 인 Java 1.5를 지정합니다. ACC_XXX 상수는 Java 제어자(modifier)에 해당하는 플래그입니다. 여기에서는 클래스가 인터페이스이고 public과 abstract(인스턴스화 될 수 없기 때문에)임을 지정합니다. 다음 인수는 클래스 이름을 내부 형식으로 지정합니다 (2.1.2 절 참조). 컴파일 된 클래스에는 패키지 또는 import 섹션이 없으므로 모든 클래스 이름을 fully qualified해야합니다. 다음 인수는 제네릭에 해당합니다 (4.1 절 참조). 이 경우에는 인터페이스가 type변수에 의해 매개 변수화되지 않았기 때문에 null입니다. 다섯 번째 인수는 내부 형식 (객체 클래스에서 암시 적으로 상속 된 인터페이스 클래스)의 수퍼 클래스입니다. 마지막 인수는 내부 이름으로 기술한 extends된 인터페이스들의 배열입니다.


다음 세 번의 visitField 호출은 비슷하며 세 인터페이스 필드를 정의하는 데 사용됩니다. 첫 번째 인수는 Java 제어자에 해당하는 플래그 집합입니다. 여기서 필드는 public, final 및 static으로 지정합니다. 두 번째 인수는 소스 코드에 나타나는대로 필드의 이름입니다. 세 번째 인수는 type descriptor 양식의 필드 유형입니다. 여기서 필드는 int 필드이며, 그 기술자는 I입니다. 네 번째 인수는 제네릭에 해당합니다. 이 경우 필드 유형이 제네릭을 사용하지 않기 때문에 null입니다. 마지막 인수는 필드의 상수 값입니다.이 인수는 진정한 상수 필드, 즉 final static 필드에만 사용해야합니다. 다른 필드의 경우, 널 (NULL)이어야합니다. 여기에 annotation이 없으므로 반환 된 FieldVisitor 후에 visitEnd 메서드를 호출합니다. 즉, 해당 visitAnnotation 또는 visitAttribute 메서드를 호출하지 않습니다.


마지막으로 visitEnd에 대한 마지막 호출은 클래스가 완료되었음을 cw에 알리고 toByteArray에 대한 호출을 사용하여이를 바이트 배열로 검색하는 데 사용됩니다


Using generated classes


이전 바이트 배열은 나중에 사용할 수 있도록 Comparable.class 파일에 저장할 수 있습니다. 다른 방법으로는 ClassLoader를 사용하여 동적으로 로드 할 수 있습니다. 한 가지 방법은 public인 defineClass 메소드를 가진 ClassLoader 서브 클래스를 정의하는 것입니다.


class MyClassLoader extends ClassLoader {

public Class defineClass(String name, byte[] b) {

return defineClass(name, b, 0, b.length); 

}

}


그런 다음 생성 된 클래스는 다음을 사용하여 직접로드 할 수 있습니다.


Class c = myClassLoader.defineClass("pkg.Comparable", b); 


또 다른 방법은 요청 된 클래스를 생성하기 위해 findClass 메서드가 재정의 된 ClassLoader 하위 클래스를 정의하는 것입니다.


class StubClassLoader extends ClassLoader {

@Override

protected Class findClass(String name) throws ClassNotFoundException {

if (name.endsWith("_Stub")) {

ClassWriter cw = new ClassWriter(0);

...

byte[] b = cw.toByteArray();

return defineClass(name, b, 0, b.length);

}

return super.findClass(name); 

}


실제로 생성 된 클래스를 사용하는 방법은 컨텍스트에 따라 다르며 ASM API의 범위를 벗어납니다. 컴파일러를 작성하는 경우 클래스 생성 프로세스는 컴파일 할 프로그램을 나타내는 추상 구문 트리로 구동되며 생성 된 클래스는 디스크에 저장됩니다. 동적 프록시 클래스 생성기 또는 aspect weaver를 작성하는 경우, 어떤 방법 으로든 ClassLoader를 사용할 것입니다.


2.2.4. Transforming classes


지금까지 ClassReader 및 ClassWriter 구성 요소는 단독으로 사용되었습니다. 이벤트는 "손으로"제작되어 ClassWriter에 의해 직접 소비되거나 대칭 적으로 ClassReader에 의해 생성되어 "손으로"즉 사용자 정의 ClassVisitor 구현에 의해 소비되었습니다. 이러한 구성 요소를 함께 사용하면 모든 것이 정말 흥미로워지기 시작합니다. 첫 번째 단계는 ClassReader가 생성 한 이벤트를 ClassWriter로 전달하는 것입니다. 결과적으로, 클래스 리더에 의해 구문 분석 된 클래스는 클래스 작성자에 의해 재구성됩니다.


byte[] b1 = ...;

ClassWriter cw = new ClassWriter(0);

ClassReader cr = new ClassReader(b1);

cr.accept(cw, 0);

byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1 


이것은 그 자체로는별로 흥미롭지 않습니다 (바이트 배열을 복사하는 더 쉬운 방법이 있습니다!). 그러나 기다려주십시오. 다음 단계는 클래스 판독기와 클래스 작성기 사이에 ClassVisitor를 도입하는 것입니다.


byte[] b1 = ...;

ClassWriter cw = new ClassWriter(0);

// cv forwards all events to cw\

ClassVisitor cv = new ClassVisitor(ASM4, cw) { };

ClassReader cr = new ClassReader(b1);

cr.accept(cv, 0);

byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1


위의 코드에 해당하는 아키텍처는 Figure 2.6에 나와 있으며 구성 요소는 사각형으로 표시되고, 이벤트는 화살표로 표시된다.((시퀀스 다이어그램 에서처럼 수직 타임 라인이 있음).


Figure 2.6 : A transformation chain



그러나 ClassVisitor 이벤트 필터가 아무 것도 필터링하지 않기 때문에 결과는 변경되지 않습니다. 그러나 이제는 클래스를 변형 할 수 있도록 일부 메서드를 재정 의하여 이벤트를 필터링하는 것으로 충분합니다. 예를 들어 다음 ClassVisitor 하위 클래스를 생각해 볼 수 있습니다.


public class ChangeVersionAdapter extends ClassVisitor {

public ChangeVersionAdapter(ClassVisitor cv) {

super(ASM4, cv);

}


@Override

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

cv.visit(V1_5, access, name, signature, superName, interfaces); 

}


이 클래스는 ClassVisitor 클래스의 한 메서드 만 재정의합니다. 결과적으로 모든 호출은 변경된 클래스 버전 번호로 전달되어진다.

visit 메소드에 대한 호출을 제외하고 생성자에 class visitor CV로 변경되지 않고 전달됩니다.


해당 시퀀스 다이어그램이 Figure 2.7에 나와 있습니다.


Figure 2.7 : Sequence diagram for the ChangeVersionAdaptor


visit 메소드의 다른 인수를 수정하면 클래스 버전을 변경하는 것 이외의 다른 변환을 구현할 수 있습니다. 예를 들어, 구현 된 인터페이스 목록에 인터페이스를 추가 할 수 있습니다. 클래스의 이름을 변경하는 것도 가능하지만, 이는 visit 메소드에서 name 인수를 변경하는 것 이상을 필요로합니다. 실제로 클래스의 이름은 컴파일 된 클래스의 여러 다른 위치에 나타날 수 있으며 이러한 모든 변경은 클래스의 실제 이름을 바꾸기 위해 변경되어야 합니다.


Optimization


이전 변환은 원래 클래스에서 4 바이트 만 변경합니다. 그러나 위의 코드에서 b1은 완전히 파싱되고 해당 이벤트는 처음부터 b2를 구성하는 데 사용되며 이는 그리 효율적이지 않습니다. 이러한 부분을 파싱하지 않고 해당 이벤트를 생성하지 않고 직접 b2로 변환되지 않은 b1 부분을 복사하는 것이 훨씬 더 효율적입니다. ASM은 메소드 최적화를 자동으로 수행합니다.


• ClassReader Component에서 MethodVisitor를 감지하면 ,이 메서드의 내용은 변형되지 않으며 응용 프로그램에서도 실제로 볼 수 없습니다.  #MethodVisitor는 ClassVisitor의 accept 메서드에 대한 인수로 전달 된 ClassWriter에 >해석 애모모함


• 이 경우 ClassReader Component는 이 메서드의 내용을 parse하지 않고, 해당 이벤트를 생성하지 않고, 그냥 바이트 배열 표현을 ClassWriter에 복사합니다.


이 최적화는 ClassReader 및 ClassWriter 구성 요소에 의해 수행됩니다.

그들이 서로에 대한 참조를 가지고 있다면, 다음과 같이 설정할 수 있습니다 :


byte[] b1 = ... 

ClassReader cr = new ClassReader(b1);

ClassWriter cw = new ClassWriter(cr, 0);

ChangeVersionAdapter ca = new ChangeVersionAdapter(cw);

cr.accept(ca, 0);

byte[] b2 = cw.toByteArray();


이 최적화 덕분에 위의 코드는 이전 버전보다 2 배 빠릅니다. 왜냐하면 ChangeVersionAdapter가 어떤 방법도 변환하지 않기 때문입니다.

일부 또는 모든 메소드를 변환하는 공통 클래스 변환의 경우 속도향상은 작지만 여전히 눈에 띕니다. 그것은 실제로 10-20 %의 순서입니다. 불행하게도이 최적화는 원래 클래스에 정의 된 모든 상수를 변환 된 상수로 복사해야합니다. 이는 필드, 메소드 또는 지침을 추가하는 변환에 대해서는 문제가되지 않지만 많은 클래스 요소를 제거하거나 이름을 바꾼 변환에 대해서는 최적화되지 않은 경우와 비교하여 더 큰 클래스 파일로 이어집니다. 따라서 "additive"변환에만이 최적화를 사용하는 것이 좋습니다.


Using transformed classes


변환 된 클래스 b2는 이전 섹션에서 설명한대로 디스크에 저장하거나 ClassLoader로로드 할 수 있습니다. 다만, ClassLoader 내부에서 행해지는 클래스 변환은,이 클래스 로더에 의해로드 된 클래스만을 변환 할 수 있습니다. 모든 클래스를 변환하려면 java.lang.instrument 패키지에 정의 된대로 ClassFileTransformer 내에 변환을 넣어야합니다.

(자세한 내용은이 패키지의 문서를 참조하십시오) :


public static void premain(String agentArgs, Instrumentation inst) {

inst.addTransformer(new ClassFileTransformer() { 

public byte[] transform(ClassLoader l, String name, Class c,

ProtectionDomain d, byte[] b)

throws IllegalClassFormatException { 

ClassReader cr = new ClassReader(b); 

ClassWriter cw = new ClassWriter(cr, 0);

ClassVisitor cv = new ChangeVersionAdapter(cw);

cr.accept(cv, 0);

return cw.toByteArray();

}

});

}


2.2.5. Removing class members


이전 섹션에서 클래스 버전을 변환하는 데 사용 된 메서드는 ClassVisitor의 메서드입니다. 예를 들어, visitField 및 visitMethod 메소드에서 access 또는 name 인수를 변경하기위해 ,필드나 메소드의 modifier나 이름을 변경할 수 있습니다. 또한 , 수정 된 인수를 사용하여 메서드 호출을 전달하는 대신, 이 호출에 전혀 전달하지 않도록 선택할 수 있습니다. 결과는 해당 클래스 요소가 제거 된 것입니다.


예를 들어, 다음 클래스 어댑터가 클래스의 컴파일 된 소스 파일의 이름뿐만 아니라 외부 및 내부 클래스에 대한 정보를 제거합니다 (결과 클래스는 디버깅 목적으로 만 사용되기 때문에 완전한 기능을 유지함). 이는 적절한 visit 메서드에서 아무 것도 전달되지 않게 수행됩니다.

즉, 아무것도 설정하지 class생성에 값이 전달되지않는다. 유지하기 위해서는 cv.visitXXX을 호출 해 줘야한다.


public class RemoveDebugAdapter extends ClassVisitor {

public RemoveDebugAdapter(ClassVisitor cv) {

super(ASM4, cv);

}

@Override

public void visitSource(String source, String debug) { }

@Override public void visitOuterClass(String owner, String name, String desc) { }

@Override public void visitInnerClass(String name, String outerName, String innerName, int access) { }

}


이 전략은 visitField 및 visitMethod 메서드가 결과를 반환해야하기 때문에 필드와 메서드에 대해서는 작동하지 않습니다. 필드 또는 메서드를 제거하려면 메서드 호출을 전달하지 말고 호출자에게 null을 반환해야합니다. 예를 들어, 다음 클래스 어댑터는 이름과 설명자로 지정된 단일 메소드를 제거합니다 (클래스는 동일한 이름이지만 매개 변수가 다른 여러 메소드를 포함 할 수 있기 때문에 이름은 메소드를 식별하기에 충분하지 않습니다).


public class RemoveMethodAdapter extends ClassVisitor {

private String mName;

private String mDesc;

public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) {

super(ASM4, cv); this.mName = mName;

this.mDesc = mDesc;

}

@Override

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

if (name.equals(mName) && desc.equals(mDesc)) {

// do not delegate to next visitor -> this removes the method

return null;

}

return cv.visitMethod(access, name, desc, signature, exceptions);

}

}


2.2.6. Adding class members


 당신은  클래스 element를 추가하여 더 많은 것을 전달할 수 있습니다. 원래의 메소드 호출 사이의 여러 곳에 새로운 호출을 삽입할 수 있으며, 다양한 visitXxx 메소드를 이용하면 된다. (섹션 2.2.1 참조).


예를 들어, 클래스에 필드를 추가하려면 원래 메서드 호출 사이에 새로운 visitField의 호출을 삽입해야 하며, 클래스 어댑터의 visit 메서드 중 하나로 이 새로운 호출을 추가해야 합니다. visit메소드에서 이 작업을 수행 할 수 없습니다. 예를 들어  visitField 호출 다음에의 visitSource, visitOuterClass, visitAnnotation 또는 visitAttribute 는 유효하지 않습니다. 같은 이유로 새로운 호출을 visitSource, visitOuterClass, visitAnnotation 또는 visitAttribute 메소드에 넣을 수 없습니다. 가능한 옵션은 visitInnerClass, visitField, visitMethod 또는 visitEnd 메소드입니다. 

>>visit는 순서가 있다.


visitEnd 메소드에 새 호출을 넣으면 이 메소드가 항상 호출되기 때문에 필드가 항상 추가됩니다 (명시 적 조건을 추가하지 않는 한).

visitField 또는 visitMethod에 입력하면 여러 필드가 추가됩니다. 원래 클래스의 필드 또는 메소드 당 하나씩 필드가 추가됩니다. 두 솔루션 모두 의미가 있습니다. 그것은 당신이 필요한 것에 달려 있습니다. 예를 들어 객체의 호출 수를 계산하는 단일 카운터 필드 또는 메소드 당 하나의 카운터를 추가하여 각 메소드의 호출을 개별적으로 계산할 수 있습니다.


참고 : 사실 유일한 해결책은 visitEnd 메서드에서 추가 호출을 만들어 새 멤버를 추가하는 것입니다. 실제로 클래스에는 중복 멤버가 없어야하며 새 멤버가 고유한지 확인하는 유일한 방법은 모든 기존 멤버와 비교하는 것입니다.이 멤버는 모두 방문한 후에 만 수행 할 수 있습니다 (예 : visitEnd 메서드). 이것은 다소 제한적입니다. _counter $ 또는 _4B7F_와 같이 프로그래머가 사용하지 않는 생성 된 이름을 사용하면 중복 된 멤버가 visitEnd에 추가하지 않고도 피할 수 있습니다. 첫 번째 장에서 설명한 것처럼 트리 API에는이 제한이 없습니다.이 API를 사용하여 변환 내에서 언제든지 새 멤버를 추가 할 수 있습니다.


위의 설명을 설명하기 위해이 필드가 이미 존재하지 않는 한 클래스에 필드를 추가하는 클래스 어댑터가 있습니다.


public class AddFieldAdapter extends ClassVisitor {

private int fAcc;

private String fName;

private String fDesc;

private boolean isFieldPresent;

public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {

super(ASM4, cv);

this.fAcc = fAcc;

this.fName = fName;

this.fDesc = fDesc;

}

@Override

public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {

if (name.equals(fName)) {

isFieldPresent = true;

}

return cv.visitField(access, name, desc, signature, value);

}

@Override

public void visitEnd() {

if (!isFieldPresent) {

FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);

if (fv != null) {

fv.visitEnd();

}

}

cv.visitEnd();

}

}


필드는 visitEnd 메소드에 추가됩니다. 기존 필드를 수정하거나 필드를 제거하기 위해 visitField 메소드를 재정의하지 않고 추가하려는 필드가 이미 존재하는지 여부를 감지합니다. fv.visitEnd ()를 호출하기 전에 visitEnd 메소드에서 fv! = null 테스트를 참고하십시오. 이전 섹션에서 보았듯이 class visitor는 visitField에서 null을 리턴 할 수 있기 때문입니다.


2.2.7. Transformation chains

지금까지 우리는 ClassReader, class adapter 및 ClassWriter로 이루어진 간단한 변환 체인을 보았습니다. 물론 더 복잡한 체인을 사용할 수도 있습니다. 여러 개의 클래스 어댑터가 함께 연결됩니다. 여러 어댑터를 연결하면 복잡한 변형을 수행하기 위해 여러 개의 독립적 인 클래스 변환을 작성할 수 있습니다. 변환 체인은 반드시 선형 적이지는 않습니다. 받은 모든 메소드 호출을 여러 ClassVisitor에 동시에 전달하는 ClassVisitor를 작성할 수있다.


public class MultiClassAdapter extends ClassVisitor {

protected ClassVisitor[] cvs;

public MultiClassAdapter(ClassVisitor[] cvs) {

super(ASM4);

this.cvs = cvs;

}

@Override public void visit(int version, int access, String name,

String signature, String superName, String[] interfaces) {

for (ClassVisitor cv : cvs) {

cv.visit(version, access, name, signature, superName, interfaces);

}

}

...

}


대칭 적으로 여러 클래스 어댑터가 동일한 ClassVisitor에 위임 할 수 있습니다 (예 : visit 및 visitEnd 메소드가이 ClassVisitor에서 한 번만 호출되도록하는 몇 가지 예방 조치가 필요함). 따라서 그림 2.8에 제시된 것과 같은 변형 체인이 완벽하게 가능합니다.


Figure 2.8 :  A complex transformation chain



2.3. Tools


ClassVisitor 클래스 및 관련 ClassReader 및 ClassWriter 구성 요소 외에도 ASM은 org.objectweb.asm.util 패키지에 클래스 생성기 또는 어댑터의 개발 중에 유용 할 수 있는 (runtime에서필요하지 않은) 여러 가지 도구를 제공합니다.  ASM은 런타임에 내부 이름, 유형 설명자 및 메소드 설명자를 조작하기위한 유틸리티 클래스도 제공합니다. 이 모든 도구는 다음과 같습니다.


2.3.1. Type


이전 섹션에서 보았 듯이, ASM API는 컴파일 된 클래스, 즉 내부 이름 또는 유형 설명자로 저장되는 Java 유형을 노출합니다. 소스 코드에 나타나는대로 코드를 노출하여 코드를 보다 쉽게 ​​읽을 수 있습니다. 그러나 이것은 ClassReader와 ClassWriter의 두 표현 사이의 체계적인 변환을 필요로 하며 이는 성능을 저하시킵니다. 그렇기 때문에 ASM은 내부 이름과 유형 설명자를 동등한 소스 코드 형식으로 투명하게 변환하지 않습니다. 그러나 필요한 경우 수동으로 수행하기위한 Type 클래스를 제공합니다.


Type object는 Java 타입을 나타내며, type descriptor 또는 Class object부터 구축 할 수 있습니다. Type class는 primitive type을 나타내는 정적 변수도 포함합니다. 예를 들어, Type.INT_TYPE는 int 유형을 나타내는 Type 객체입니다.


getInternalName 메소드는 Type의 내부 이름을 리턴합니다. 예를 들어, Type.getType (String.class) .getInternalName ()은 String 클래스의 내부 이름을 제공합니다 (예 : "java / lang / String"). 이 메소드는 클래스 또는 인터페이스 유형에만 사용해야합니다.


getDescriptor 메소드는 Type의 디스크립터를 리턴한다. 예를 들어, 대신 "Ljava / lang / String;"을 사용하십시오. 코드에서 Type.getType (String.class) .getDescriptor ()를 사용할 수 있습니다. 또는 I를 사용하는 대신 Type.INT_TYPE.getDescriptor ()를 사용할 수 있습니다.


Type object는 메서드 type을 나타낼 수도 있습니다. 이러한 객체는 메소드 설명자 또는 Method 객체에서 구성 할 수 있습니다.. 그런 다음 getDescriptor 메소드는이 유형에 해당하는 메소드 설명자를 리턴합니다. 또한 getArgumentTypes 및 getReturnType 메서드를 사용하여 메서드의 인수 형식 및 반환 형식에 해당하는 Type 개체를 가져올 수 있습니다. 예를 들어 Type.getArgumentTypes ( "(I) V")는 단일 요소 Type을 포함하는 배열을 반환합니다.


INT_TYPE. 같이, Type.getReturnType ( "(I) V")를 호출하면 (자), Type.VOID_TYPE object가 반환됩니다.



2.3.2. TraceClassVisitor


생성되거나 변환 된 클래스가 예상 한 것과 일치하는지 확인하려면, ClassWriter에 의해 반환 된 바이트 배열은 사람이 읽을 수 없으므로 별로 도움이 되지 않습니다. 텍스트 표현은 사용하기가 훨씬 쉬울 것입니다. 이것은 TraceClassVisitor 클래스가 제공하는 것입니다. 이 클래스는 이름에서 알 수 있듯이 ClassVisitor 클래스를 확장하고 visited class의 텍스트 표현을 작성합니다. 따라서, ClassWriter를 사용하여 클래스를 생성하는 대신 TraceClassVisitor를 사용하여 실제로 생성 된 내용을 읽을 수있는 추적을 얻을 수 있습니다. 또는 두 가지를 동시에 사용할 수 있습니다. 실제로 TraceClassVisitor는 기본 비헤이비어 외에 메소드에 대한 모든 호출을 다른 방문자 (예 : ClassWriter)에게 위임 할 수 있습니다.


ClassWriter cw = new ClassWriter(0);

TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);

cv.visit(...);

...

cv.visitEnd();

byte b[] = cw.toByteArray();


이 코드는 수신 한 모든 호출을 cw에 위임 한 TraceClassVisitor를 만들고이 호출의 텍스트 표현을 printWriter에 인쇄합니다.

예를 들어, 2.2.3 절의 예제에서 TraceClassVisitor를 사용하면 다음을 얻을 수 있습니다.


// class version 49.0 (49)

// access flags 1537

public abstract interface pkg/Comparable implements pkg/Mesurable {

// access flags 25

public final static I LESS = -1

// access flags 25

public final static I EQUAL = 0

// access flags 25

public final static I GREATER = 1

// access flags 1025

public abstract compareTo(Ljava/lang/Object;)I

}


TraceClassVisitor는 생성 또는 변형 체인의 어느 지점에서나 ClassWriter 바로 전에뿐만 아니라 체인의이 시점에서 어떤 일이 발생하는지 확인할 수 있습니다. 이 어댑터에 의해 생성 된 클래스의 텍스트 표현은 String.equals ()를 사용하여 클래스를 쉽게 비교하는 데 사용될 수 있습니다.


2.3.3. CheckClassAdapter


ClassWriter 클래스는 메서드가 올바른 순서와 유효한 인수로 호출되는지 확인하지 않습니다. 따라서 Java 가상 머신 검증 자에 의해 거부 될 유효하지 않은 클래스를 생성 할 수 있습니다.


가능한 빨리 이러한 오류 중 일부를 감지하기 위해 CheckClassAdapter 클래스를 사용할 수 있습니다. TraceClassVisitor와 마찬가지로이 클래스는 ClassVisitor 클래스를 확장하고 메서드에 대한 모든 호출을 다른 ClassVisitor (예 : TraceClassVisitor 또는 ClassWriter)에 위임합니다. 그러나 vistied class의 텍스트 표현을 인쇄하는 대신이 클래스는 다음 방문자에게 위임하기 전에 해당 메소드가 올바른 순서 및 유효한 인수로 호출되는지 확인합니다. 에러가 발생했을 경우, IllegalStateException 또는 IllegalArgumentException가 발생됩니다.


클래스를 검사하고이 클래스의 텍스트 표현을 출력하고 마지막으로 바이트 배열 표현을 만들려면 다음과 같이 사용해야합니다.


ClassWriter cw = new ClassWriter(0);

TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);

CheckClassAdapter cv = new CheckClassAdapter(tcv);

cv.visit(...);

...

cv.visitEnd();

byte b[] = cw.toByteArray();


이러한 클래스 visitor를 다른 순서로 연결하면 수행하는 작업도 다른 순서로 수행됩니다. 예를 들어 다음 코드를 사용하면 trace 후에 검사가 수행됩니다.


ClassWriter cw = new ClassWriter(0);

CheckClassAdapter cca = new CheckClassAdapter(cw);

TraceClassVisitor cv = new TraceClassVisitor(cca, printWriter);


TraceClassVisitor와 마찬가지로, 체인의 클래스를 확인하기 위해 ClassWriter 바로 전에뿐만 아니라 생성 또는 변형 체인의 어느 지점에서나 CheckClassAdapter를 사용할 수 있습니다.


2.3.4. ASMifier


이 클래스는 TraceClassVisitor 도구에 대한 대체 백엔드를 제공합니다 (기본적으로 Textifier 백엔드를 사용하여 위에 표시된 출력 유형을 생성합니다). 이 백엔드는 TraceClassVisitor 클래스의 각 메소드를 호출하는 데 사용 된 Java 코드를 인쇄합니다. 예를 들어 visitEnd () 메서드를 호출하면 cv.visitEnd ();가 인쇄됩니다. 결과적으로 ASMifier 백엔드가있는 TraceClassVisitor 방문자가 클래스를 방문하면 ASM을 사용하여이 클래스를 생성하는 소스 코드를 인쇄합니다. 이 방문자를 사용하여 이미 존재하는 클래스를 방문하는 경우에 유용합니다. 예를 들어 ASM을 사용하여 컴파일 된 클래스를 생성하는 방법을 모르는 경우 해당 소스 코드를 작성하고 javac을 사용하여 컴파일 한 다음 ASMifier를 사용하여 컴파일 된 클래스를 방문하십시오.

이 컴파일 된 클래스를 생성하는 ASM 코드를 얻게됩니다!


The ASMifier class can be used from the command line. For example using:


 java -classpath asm.jar:asm-util.jar \ org.objectweb.asm.util.ASMifier \ java.lang.Runnable


produces code that, after indentation, reads:


package asm.java.lang;

import org.objectweb.asm.*;

public class RunnableDump implements Opcodes {

public static byte[] dump() throws Exception {

ClassWriter cw = new ClassWriter(0);

FieldVisitor fv;

MethodVisitor mv;

AnnotationVisitor av0;

cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,

"java/lang/Runnable", null, "java/lang/Object", null);

{

mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "run", "()V",null, null);

mv.visitEnd();

}

cw.visitEnd();

return cw.toByteArray();

}

}




2018/09/11 - [분류 전체보기] - ASM User Guide 번역 3