티스토리 뷰
2018/09/11 - [ASM] - ASM User Guide 번역 3
4. Metadata
이 장에서는 핵심 API를 사용하여 주석과 같은 컴파일 된 Java 클래스 메타 데이터를 생성하고 변환하는 방법에 대해 설명합니다. 각 섹션은 한 가지 유형의 메타 데이터의 프리젠 테이션으로 시작한 다음 몇 가지 예시적인 예와 함께 해당 메타 데이터를 생성하고 변환하는 해당 ASM 인터페이스, 구성 요소 및 도구를 제공합니다.
4.1. Generics
List <E>와 같은 일반 클래스와이를 사용하는 클래스에는 선언하거나 사용하는 제네릭 형식에 대한 정보가 들어 있습니다. 이 정보는 런타임시 바이트 코드 명령어에 의해 사용되지 않지만 리플렉션 API를 통해 액세스 할 수 있습니다. 또한 별도의 컴파일을 위해 컴파일러에서 사용됩니다.
4.1.1. Structure
이전 버전과의 호환성을 위해 제네릭 형식에 대한 정보는 형식 또는 메서드 설명자 (Java 5에서 제네릭 소개 이전에 정의 된)에 저장되지 않았지만 형식, 메서드 및 클래스 서명이라는 유사한 구문으로 저장되었습니다. 이러한 서명은 제네릭 형식이 관련된 경우 클래스, 필드 및 메서드 선언의 설명자와 함께 저장됩니다 (제네릭 형식은 메서드의 바이트 코드에 영향을주지 않습니다. 컴파일러는 정적 형식 검사를 수행하기 위해이를 사용하지만 메서드를 컴파일 한 것처럼 필요한 경우 유형 캐스트를 다시 도입하여 사용하지 않음).
형식 및 메서드 설명자와 달리 generic 형식의 재귀 적 특성 때문에 (generic 형식은 generic 형식으로 매개 변수화 될 수 있습니다. 예를 들어 List <List <E>?>를 고려하십시오.) 형식 시그니처의 문법은 상당히 복잡합니다. 이것은 다음 규칙에 따라 제공됩니다 (이 규칙에 대한 자세한 설명은 Java Virtual Machine Specification 참조).
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 ;
첫 번째 규칙은 형식 서명이 원시 형식 설명자 또는 필드 형식 서명 중 하나임을 나타냅니다. 두 번째 규칙은 클래스 형식 서명, 배열 형식 서명 또는 형식 변수로 필드 형식 서명을 정의합니다. 세 번째 규칙은 클래스 유형 시그니처를 정의합니다. 가능한 유형 인수를 갖는 클래스 유형 설명자, 꺾쇠 괄호 사이, 주 클래스 이름 뒤 또는 내부 클래스 이름 뒤 (점 앞에 접두사가 있음)입니다. 나머지 규칙은 유형 인수 및 유형 변수를 정의합니다. 타입 인자는 타입 인자가있는 완전한 필드 타입 시그니쳐가 될 수있다. 그러므로 타입 시그니쳐는 매우 복잡 할 수있다 (그림 4.1 참조).
Java type and corresponding type signature |
List |
List <?> Ljava/util/List<*>; |
List<? extends Number> Ljava/util/List<+Ljava/lang/Number;>; |
List<? super Integer> Ljava/util/List<-Ljava/lang/Integer;>; |
List<List<String>[]> Ljava/util/List<[Ljava/util/List<Ljava/lang/String;>;>; |
HashMap<K, V>.HashIterator<K> Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>; |
Figure 4.1.: Sample type signatures
메서드 서명은 형식 시그니처와 같은 메서드 설명자를 확장하여 형식 설명자를 확장합니다. 메소드 서명은 메소드 매개 변수의 유형 서명과 리턴 유형의 서명을 설명합니다. 메소드 서술자와는 달리 메소드 앞에 던져진 예외의 서명을 포함하며 꺽쇠 괄호 사이에 선택적 형식 매개 변수도 포함 할 수 있습니다.
MethodTypeSignature:
TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception*
Exception: ^ClassTypeSignature | ^TypeVar
TypeParams: < TypeParam+ >
TypeParam: Id : FieldTypeSignature? ( : FieldTypeSignature )*
예를 들어 다음과 같은 제네릭 정적 메서드의 메서드 시그니처는 형식 변수 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*
For example the class signature of a class declared as C<E> extends List<E>
is <E:Ljava/lang/Object;>Ljava/util/List<TE;>;.
4.1.2. Interfaces and components
Descriptor디스크립터와 동일한 효율성을 이유로 (2.3.1 절 참조), ASM API는 컴파일 된 클래스에 저장된 서명을 노출합니다
(서명의 주된 발생은 선택적 클래스, 유형 또는 메소드 서명 인수로서 각각 ClassVisitor 클래스의 visitField 및 visitMethod 메소드에 있습니다). 바라건대 SignatureVisitor 추상 클래스 (그림 4.2 참조)를 기반으로 org.objectweb.asm.signature 패키지에서 서명을 생성하고 변환하는 도구를 제공하기를 바랍니다.
public abstract class SignatureVisitor {
public final static char EXTENDS = ’+’;
public final static char SUPER = ’-’;
public final static char INSTANCEOF = ’=’;
public SignatureVisitor(int api);
public void visitFormalTypeParameter(String name);
public SignatureVisitor visitClassBound();
public SignatureVisitor visitInterfaceBound();
public SignatureVisitor visitSuperclass();
public SignatureVisitor visitInterface();
public SignatureVisitor visitParameterType();
public SignatureVisitor visitReturnType();
public SignatureVisitor visitExceptionType();
public void visitBaseType(char descriptor);
public void visitTypeVariable(String name);
public SignatureVisitor visitArrayType();
public void visitClassType(String name);
public void visitInnerClassType(String name);
public void visitTypeArgument();
public SignatureVisitor visitTypeArgument(char wildcard);
public void visitEnd();
}
Figure 4.2.: The SignatureVisitor class
이 추상 클래스는 type 시그니처, 메소드 서명과 클래스 서명를 방문하는 데 사용됩니다. 형식 시그니처를 방문하는 데 사용되는 메서드는 굵게 표시되어 이전 문법 규칙을 반영하는 다음 순서로 호출해야합니다 (두 개는 SignatureVisitor를 반환 함 : 이는 형식 시그니처의 재귀 적 정의 때문 임).
visitBaseType | visitArrayType | visitTypeVariable |
( visitClassType visitTypeArgument*
( visitInnerClassType visitTypeArgument* )* visitEnd ) )
The methods used to visit method signatures are the following:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )*
visitParameterType* visitReturnType visitExceptionType*
Finally the methods used to visit class signatures are:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )*
visitSuperClass visitInterface*
이러한 메서드의 대부분은 SignatureVisitor를 반환합니다. type 서명을 방문하기위한 것입니다. ClassVisitor에 의해 리턴 된 MethodVisitors와는 달리, SignatureVisitor에 의해 반환 된 SignatureVisitor는 null이 아니어야하며 순차적으로 사용해야합니다. 실제로 중첩 된 서명이 완전히 방문되기 전에 부모 방문자의 메서드를 호출해야합니다.
클래스와 마찬가지로 ASM API는이 API를 기반으로 두 가지 구성 요소를 제공합니다. SignatureReader 구성 요소는 서명을 파싱하고 주어진 서명 방문객에 대해 적절한 방문 메소드를 호출하며 SignatureWriter 구성 요소는 수신 한 메소드 호출을 기반으로 서명을 작성합니다. 이 두 클래스는 클래스 및 메서드와 동일한 원칙을 사용하여 시그니처를 생성하고 변환하는 데 사용할 수 있습니다. 예를 들어, 일부 서명에 나타나는 클래스 이름의 이름을 바꾸려고한다고 가정 해 보겠습니다. 이는 visitClassType 및 visitInnerClassType 메소드를 제외하고 변경되지 않은 모든 메소드 호출을 전달하는 다음 서명 어댑터로 수행 할 수 있습니다 (여기서 sv 메소드는 항상 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();
}
}
Then the result of the following code is "LA
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();
4.1.3. Tools
2.3 절에 제시된 TraceClassVisitor와 ASMifier 클래스는 클래스 파일에 포함 된 서명을 내부 형식으로 인쇄합니다. 이 클래스는 특정 제네릭 형식에 해당하는 서명을 찾기 위해 사용할 수 있습니다. 일부 일반 형식으로 Java 클래스를 작성하고 컴파일 한 다음 해당 명령 줄 도구를 사용하여 해당 서명을 찾습니다.
4.2. Annotations
@Deprecated 또는 @Override와 같은 클래스, 필드, 메서드 및 메서드 매개 변수 주석은 보유 정책이 RetentionPolicy.SOURCE가 아닌 경우 컴파일 된 클래스에 저장됩니다. 이 정보는 런타임시 바이트 코드 명령어에 의해 사용되지 않지만 보존 정책이 RetentionPolicy.RUNTIME 인 경우 리플렉션 API를 통해 액세스 할 수 있습니다. 컴파일러에서 사용할 수도 있습니다.
4.2.1. Structure
소스 코드의 annotation은 다양한 형식을 가질 수 있지만,
@ Retention (RetentionPolicy.CLASS) 또는 @Task (desc = "refactor", id = 1)와 같은 속성을 사용합니다. 그러나 내부적으로 모든 주석은 동일한 형식을 가지며 주석 유형과 이름 값 쌍으로 지정됩니다. 여기서 값은 다음으로 제한됩니다.
• primitives, Strng 또는 Vlass 값
• enum 값,
• annotation 값,
• 위의 값의 배열.
주석은 다른 주석 또는 심지어 주석 배열을 포함 할 수 있습니다. 따라서 어노테이션은 매우 복잡 할 수 있습니다.
4.2.2. Interfaces and components
어노테이션 생성 및 변환을위한 ASM API는 AnnotationVisitor 추상 클래스를 기반으로합니다 (그림 4.3 참조).
public abstract class AnnotationVisitor {
public AnnotationVisitor(int api);
public AnnotationVisitor(int api, AnnotationVisitor av);
public void visit(String name, Object value);
public void visitEnum(String name, String desc, String value);
public AnnotationVisitor visitAnnotation(String name, String desc);
public AnnotationVisitor visitArray(String name);
public void visitEnd();
}
Figure 4.3.: The AnnotationVisitor class
이 클래스의 메소드는 annotation의 이름와 값 쌍을 방문하는 데 사용됩니다. (annnotation 형식은 이 형식을 반환하는 메서드, 즉 visitAnnotation 메서드에서 방문됩니다. 첫 번째 메서드는 primitive, String 및 Class 값 (나중에 Type 개체로 나타냄)에 사용되며 다른 메서드는 열거 형, annotation 및 배열 값에 사용됩니다. visitEnd를 제외한 모든 순서로 호출 할 수 있습니다.
( visit | visitEnum | visitAnnotation | visitArray )* visitEnd
두 가지 메소드가 AnnotationVisitor를 리턴한다는 점에 유의하십시오.
annotation은 다른 annotation을 포함 할 수 있기 때문입니다. 또한 ClassVisitor에서 반환 한 MethodVisitors와 달리 이 두 메서드에 의해 반환 된 AnnotationVisitors는 순차적으로 사용해야합니다. 실제로 중첩 된 주석을 완전히 방문하기 전에 상위 방문자에 대한 메서드를 호출해야합니다. visitArray 메서드는 AnnotationVisitor를 반환하여 배열의 요소를 방문합니다. 그러나 배열의 요소 이름이 지정되지 않았으므로 이름 인수는 visitArray가 반환 한 방문자의 메서드에 의해 무시되며 null로 설정할 수 있습니다.
Adding, removing and detecting annotations
필드 및 메소드와 마찬가지로 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);
}
}
ClassVisitor 클래스의 메서드를 호출해야하는 제약 때문에 클래스 주석을 추가하는 것이 더 어렵습니다. 실제로 visitAnnotation을 따르는 모든 메소드는 모든 annotation이 방문되었을 때를 감지하기 위해 무시되어야합니다 (메소드 주석은 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보다 작은 클래스의 주석을 무시하기 때문에 필요합니다.
클래스 및 메소드 어댑터에서 annotation를 사용한 가장 자주 사용되는 사례는 annotation를 사용하여 변형을 매개 변수화하는 것입니다. 예를 들어 @Persistent 주석이있는 필드에 대해서만 필드 액세스를 변환하고 @Log 주석이있는 메소드에만 로깅 코드를 추가 할 수 있습니다. 주석을 먼저 방문해야하기 때문에 이러한 모든 사례를 쉽게 구현할 수 있습니다. 필드 및 메소드와 메소드 및 매개 변수 주석을 코드보다 먼저 방문해야 클래스 annotation을 먼저 방문해야합니다. 그러므로 isAnnotationPresent 플래그로 위의 예에서 수행 된 것처럼 원하는 annotation이 감지 될 때 플래그를 설정하고 변환에서 나중에 사용하는 것으로 충분합니다.
4.2.3. Tools
4.3.1. Structure
클래스의 소스 파일 이름은 전용 클래스 파일 구조 섹션에 저장됩니다 (그림 2.1 참조).
소스 행 번호와 바이트 코드 명령 간의 매핑은 메소드의 컴파일 된 코드 섹션에 (행 번호, 레이블) 쌍의 목록으로 저장됩니다. 예를 들어, l1, l2 및 l3이이 순서로 나타나는 세 개의 레이블이면 다음 쌍이됩니다.
(n1, l1)
(n2, l2)
(n3, l3)
l1과 l2 사이의 명령어가 n1 라인에서 나오고, l2와 l3 사이의 명령어는 n2 라인에서 나오고, l3 이후의 명령어는 n3 라인에서 옵니다. 주어진 줄 번호는 여러 쌍으로 나타날 수 있습니다. 단일 소스 행에 나타나는 표현식에 해당하는 명령어가 바이트 코드에서 연속적이지 않을 수 있기 때문입니다. 예를 들어 (init; cond; incr) 문; 일반적으로 다음 순서로 컴파일됩니다 : init statement incr cond.
소스 코드의 로컬 변수 이름과 바이트 코드의 로컬 변수 슬롯 간의 매핑은 메소드의 컴파일 된 코드 섹션에있는 (이름, type 설명자, type 서명, 시작, 끝, 색인) 튜플의 목록으로 저장됩니다. 이러한 튜플은 두 레이블 시작과 끝 사이에서 슬롯 인덱스의 로컬 변수가 소스 코드의 이름과 유형이 처음 세 튜플 요소에 의해 주어진 로컬 변수에 해당 함을 의미합니다. 컴파일러는 동일한 지역 변수 슬롯을 사용하여 서로 다른 범위의 고유 한 소스 로컬 변수를 저장할 수 있습니다. 반대로 고유 한 소스 로컬 변수는 인접하지 않은 범위가있는 로컬 변수 슬롯으로 컴파일 될 수 있습니다. 예를 들어 다음과 같은 상황이있을 수 있습니다.
l1:
... // here slot 1 contains local variable i
l2:
... // here slot 1 contains local variable j
l3:
... // here slot 1 contains local variable i again
end:
해당 튜플은 다음과 같습니다.
("i", "I", null, l1, l2, 1)
("j", "I", null, l2, l3, 1)
("i", "I", null, l3, end, 1)
4.3.2. Interfaces and components
디버그 정보는 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 메소드를 호출해야합니다. 이전 섹션에서 제시된 쌍 및 튜플에 해당하는 예제 메서드 호출은 다음과 같습니다.
visitLineNumber(n1, l1);
visitLineNumber(n2, l2);
visitLineNumber(n3, l3);
visitLocalVariable("i", "I", null, l1, l2, 1);
visitLocalVariable("j", "I", null, l2, l3, 1);
visitLocalVariable("i", "I", null, l3, end, 1);
Ignoring debug information
줄 번호와 지역 변수 이름을 방문하기 위해 ClassReader 클래스는 점프 지침에서는 필요하지 않지만 디버그 정보 만 나타낼 수 있다는 의미에서 "인공"Label 객체를 도입해야 할 수도 있습니다. 3.2.5 절에서 설명 된 것과 같이 명령 시퀀스 중간의 레이블이 점프 대상으로 간주되어이 시퀀스가 제거되지 않도록하는 상황에서 오탐 (false positive)이 발생할 수 있습니다.
이러한 잘못된 긍정을 피하기 위해 ClassReader.accept 메서드에서 SKIP_DEBUG 옵션을 사용할 수 있습니다. 이 옵션을 사용하면 클래스 판독기가 디버그 정보를 방문하지 않고 인공 지능 레이블을 생성하지 않습니다. 물론 디버그 정보는 클래스에서 제거되므로이 옵션은 응용 프로그램에 문제가되지 않는 경우에만 사용할 수 있습니다.
참고 : ClassReader 클래스는 컴파일 된 코드 (클래스 구조 만 있으면 유용 할 수 있음), 스택 맵 프레임을 건너 뛰는 SKIP_FRAMES 및 이러한 프레임의 압축을 해제하는 EXPAND_FRAMES의 방문을 건너 뛰는 SKIP_CODE와 같은 다른 옵션을 제공합니다.
4.3.3. Tools
제네릭 형식 및 annotation과 마찬가지로 TraceClassVisitor, CheckClassAdapter 및 ASMifier 클래스를 사용하여 디버그 정보로 작업하는 방법을 찾을 수 있습니다.
'ASM' 카테고리의 다른 글
ASM User Guide 번역 3 (0) | 2018.09.11 |
---|---|
ASM User Guide 번역 2 (0) | 2018.09.10 |
ASM User Guide 번역 1 (0) | 2018.09.10 |
- Total
- Today
- Yesterday
- #번역
- chrome
- chatbot
- 번역
- native image
- user guide
- Java
- ASM
- bytecode
- 도서
- 대출
- #Dynamic Query
- 텔레그램
- nexacro
- cors
- #SPRING
- javaagent
- #BCI
- GraalVM
- telegram
- native-image
- #IOC
- #ASM
- #MyBatos
- #Bean
- 챗봇
- #JAVA
- #User Guide
- javaaent
- #Transaction
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |