ClassReader 사용 예제와 ClassWriter 사용 예제는 이미 보았으니, 이젠 둘을 함께 사용하는 하는 경우를 보자.
사실, 두 클래스를 함께 사용하는 경우가 일반적으로 쓰이는 경우라 볼 수 있다.
소스 코드의 변경없이 사용하는 모든 클래스에 어떤 변경을 하려고 하는 경우 java.lang.instrument 패키지의 ClassFileTransformer를 사용한다. (참고 : example of java class transform with java agent and BCI)
첫번째로 아무 작업 없이 읽은 그대로 반환하는 코드이다.
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 { | |
byte[] ret; | |
/* | |
do somthing and fill ret | |
*/ | |
return ret; | |
} | |
}); | |
} |
위 코드의 내용을 ClassReader 와 ClassWriter를 이용하여 채워보자.
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 { | |
ClassWriter cw = new ClassWriter(0); | |
ClassReader cr = new ClassReader(b); | |
cr.accept(cw, 0); | |
return cw.toByteArray(); // ret represents the same class as b | |
} | |
}); | |
} |
이 코드에서는 ClassReader(event producer) 와 ClassWriter(event consumer) 를 accept 메소드를 통해 직접 연결하고 있으며, 아무 변환 없이 읽은 값을 그대로 반환한다.
이번엔 ClassVisitor(event filter) 클래스를 사용해보자. 역시 아무 변환도 하지 않는다.
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 { | |
ClassWriter cw = new ClassWriter(0); | |
// cv forwards all events to cw | |
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {}; | |
ClassReader cr = new ClassReader(b); | |
cr.accept(cv, 0); | |
return cw.toByteArray(); // ret represents the same class as b | |
} | |
}); | |
} |
단지, 이전에는 ClassReader(event producer) 와 ClassWriter(event consumer) 를 직접 연결했지만, 이번에는 ClassVisitor(event filter) 라는 filter를 통해 연결한 코드이다.
이제 간단한 변환을 해보자.
버전만 변환하는 간단한 ClassVisitor 의 자식 클래스
import org.objectweb.asm.ClassVisitor; | |
import org.objectweb.asm.Opcodes; | |
public class ChangeVersionAdapter extends ClassVisitor { | |
/** | |
* Constructs a new {@link org.objectweb.asm.ClassVisitor}. | |
* | |
* @param api the ASM API version implemented by this visitor. Must be one | |
* of {@link Opcodes#ASM4} or {@link Opcodes#ASM5}. | |
* @param cv the class visitor to which this visitor must delegate method | |
*/ | |
public ChangeVersionAdapter(int api, ClassVisitor cv) { | |
super(api, cv); | |
} | |
@Override | |
public void visit(int version, int access, String name, String signature, | |
String superName, String[] interfaces) { | |
if (cv != null) { | |
cv.visit(Opcodes.V1_5, access, name, signature, superName, interfaces); | |
} | |
} | |
} |
이 클래스는 visit 메소드 호출 시 버전을 바꾸는 것 이외에는 아무 작업 없이 단순히 호출을 전달하기만 한다.
물론, 다른 인자들을 이용해 변환을 할 수 있지만, 클래스의 나머지 부분도 바꿔 주어야하는 까다로움이 남아 있다.
이를 이용한 premain 코드는 다음과 같다.
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 { | |
ClassWriter cw = new ClassWriter(0); | |
ChangeVersionAdapter ca = new ChangeVersionAdapter(Opcodes.ASM5, cw); | |
ClassReader cr = new ClassReader(b); | |
cr.accept(ca, 0); | |
return cw.toByteArray(); | |
} | |
}); | |
} |
위의 예에서는 단순히 4바이트의 버전 정보만 바꾸지만, 모든 바이트 코드가 파싱되고 변환이 필요없는 곳에서도 이벤트가 발생한다. 이런 비효율성을 없애기 위해 다음과 같이 코드를 바꿔서 최적화한다.
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); | |
ChangeVersionAdapter ca = new ChangeVersionAdapter(Opcodes.ASM5, cw); | |
cr.accept(ca, 0); | |
return cw.toByteArray(); | |
} | |
}); | |
} |
최적화는 ClassReader 가 accept 메소드의 인자로 넘어온 ClassVisitor(위의 예에서는 ChangeVersionAdapter)에 의해 반환된 MethodVisitor 가 ClassWriter 의 것일 경우 변환이 필요없다는 것을 감지해서 파싱 이나 이벤트의 발생없이 해당 메소드의 바이트 코드를 ClassWriter 에 다이렉트로 복사한다.
최적화가 문제가 되는 경우도 존재한다.
위와 같이 최적화를 적용한 코드의 경우 원본 constant pool의 내용은 무조건 변환 후의 클래스에 복사되어 있어야 한다.필드, 메소드, 인스트럭션을 추가할 경우는 constant pool 에 내용을 추가하므로 문제가 되지 않는다. 하지만, 제거하거나 이름을 바꿀 경우 문제가 된다. 복사된 constant pool 에 세로운 요소는 추가되지만, 사용되지 않는 요소가 삭제되지는 않기 때문이다.
따라서, 최적화는 삭제가 많은 경우에는 원본 클래스보다 큰 클래스를 만들어내게 되므로, 추가가 많은 경우 사용하는 것이 좋다.
'프로그래밍 > BCI' 카테고리의 다른 글
[Java] ASM을 이용한 클래스 멤버 추가 (0) | 2015.03.20 |
---|---|
[Java] ASM 을 이용한 클래스 멤버 삭제하기 (0) | 2015.03.20 |
[Java] ASM을 이용한 클래스 생성 (0) | 2015.03.19 |
[Java] ASM을 이용한 .class 파일 파싱 (0) | 2015.03.19 |
[Java] javap 프로그램을 이용하여 .class파일 출력 (0) | 2015.03.18 |