태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

The Power of Byte Code Instrumentation in Java - Part 4

Enterprise Java 2007.08.05 23:50

Part3에 이어...

One Simple but Powerful Example of BCI

Part3에서 언급한 봐와 같이 java.lang.Exception 클래스의 바이트 코드를 직접 수정함으로써 모든 Exception의 발생을 캡쳐할 수 있다. Exception 클래스의 생성자들에서 다음과 같은 코드를 수행하게끔 바이트 코드를 수정하면 된다.

public Exception(String s)
{
super(s);
ExceptionCallBack.exceptionOccurred(this);
}

ASM이 제공하는 Library를 이용하면 이 작업을 매우 손쉽게 수행할 수 있다.

ASM 라이브러리의 사용법을 상세히 설명하는 것은 이 블로그의 범위를 벗어나며 http://asm.objectweb.org 에서 제공하는 매뉴얼과 문서에 이미 상세하게 설명이 되어 있다.

아래 소스 코드는 java.lang.Exception 클래스를 ASM Library를 이용해 읽고(Read),그대로(변형없이) Write하는 예제이다.

package flowlite.exception2;

import java.io.FileOutputStream;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;

public class ExceptionTransformer implements Opcodes {

// Convert class
public void transform(String newClassName) throws Exception {

System.out.println("Starting transformation of java.lang.Exception...");

// Reader
ClassReader reader = new ClassReader("java.lang.Exception");
// Writer

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);

// Class Adapter
ClassAdapter adapter = new ExceptionClassAdapter(writer);


reader.accept(adapter, ClassReader.SKIP_FRAMES);

byte[] b = writer.toByteArray();
FileOutputStream fos = new FileOutputStream(newClassName + ".class");
fos.write(b);
fos.flush();

}

public static void main(String[] args) {
try {
String newClassName = "Exception";
if(args.length >= 1) newClassName = args[0];
ExceptionTransformer fit = new ExceptionTransformer();
fit.transform(newClassName);
} catch(Exception ex) {
ex.printStackTrace();
}

}
}


class ExceptionClassAdapter extends ClassAdapter implements Opcodes {

public ExceptionClassAdapter(ClassVisitor visitor) {
super(visitor);

}

}

위의 소스 코드를 보면 바이트 코드 변환은 다음과 같은 과정을 통해 이루어진다는 것을 알 수 있다.

  • ClassReader 객체를 이용해 원래 클래스의 바이트 코드를 읽어들인다.
  • ClassAdapter 객체를 이용해 바이트 코드를 변경한다.
  • ClassWriter 객체를 이용해 변경된 바이트 코드를 얻는다.
  • 변경된 바이트 코드는 파일로 저장하거나 ClassLoader에게 넘겨 준다.

위의 코드를 실행하면 Exception.class 파일이 아래 그림과 같이 파일시스템에 저장된다.

-Xbootclasspath 옵션 사용하기

문제는 어떻게 하면 원래 JVM의 rt.jar 에서 제공하는 Exception 클래스 파일이 아닌 내가 생성한 Exception 클래스 파일을 쓰게 하느냐이다. rt.jar 파일을 직접 변경시키는 것은 매우 위험하고, 법적으로도(이건 농담이 아님) 문제가 될 수 있기 때문에 권장되지 않는다.

답은 -Xbootclasspath 옵션을 이용하는 것이다. "java -X" 명령을 수행하면 다음과 같은 결과를 확인할 수 있다.

즉, -Xbootclasspath/p:<내가 작성한 Exception Class의 패스>를 지정하면 JVM은 rt.jar보다 먼저(prepend) 내가 작성한 Exception Class 파일을 읽어 들인다. 이렇게 함으로써 rt.jar 파일에 대한 수정을 가하지 않아도 된다.

우선 아래와 같은 Test Class를 작성한 후

public class ExceptionTest2 {

public static void doException(boolean bException) throws RuntimeException {
if(bException)
throw new RuntimeException("test");

}

public static void main(String[] args) {

try {
ExceptionTest2.doException(true);<-- 여기서 Exception발생
} catch(Exception ex) {}

try {
ExceptionTest2.doException(false);
} catch(Exception ex) {}
}
}

아래와 같이 -Xbootclasspath 옵션을 이용해서 수행한다.

(./converted_classes/java/lang 디렉토리에 내가 만든 Exception Class가 있다...)

java -Xbootclasspath/p:./converted_classes ExcetpionTest2

java.lang.Exception에 내가 필요한 바이트 코드 삽입하기

우리가 java.lang.Exception 클래스의 생성자에 삽입하고자 하는 코드는 다음과 같다.

ExceptionCallBack.exceptionOccurred(this);

따라서 가장 먼저 알아야 할 것은 이 코드에 해당하는 바이트 코드가 무엇이냐이다. ASM Bytecode Outline Plugin을 이용하면 아래와 같이 손쉽게 알아낼 수 있다.

위의 그림에서 빨간 색으로 밑줄이 그어진 부분이 우리가 필요로 하는 부분이다. 간략하게 설명하면...

  • mv.visitVarInsn(ALOAD, 0) : 0번째 변수는 항상 나 자신(this)를 가리킨다.
  • mv.visitMethodInsns(INVOKESTATIC, [className], [methodName], [methodDescription]) : 메소드를 호출한다.
    • INVOKESTATIC은 Static Method를 실행하겠다는 의미이다. methodDescription은 메소드의 파라미터와 리턴값을 의미한다. (Ljava/lang/Exception;) 은 java.lang.Exception 클래스가 호출된 메소드의 파라미터 타입이라는 것을 의미한다. V는 리턴타입이 void라는 것을 의미한다.

이제 처리해야할 마지막 작업은 위와 같은 형태의 코드를 java.lang.Exception 클래스의 생성자마다 삽입해주는 것이다. 아래에 샘플 코드가 있다.

package flowlite.exception2;

import java.io.FileOutputStream;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;

public class ExceptionTransformer implements Opcodes {

// Convert class
public void transform(String newClassName) throws Exception {

System.out.println("Starting transformation of java.lang.Exception...");

ClassReader reader = new ClassReader("java.lang.Exception");
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter adapter = new ExceptionClassAdapter(writer);


reader.accept(adapter, ClassReader.SKIP_FRAMES);

byte[] b = writer.toByteArray();
FileOutputStream fos = new FileOutputStream(newClassName + ".class");
fos.write(b);
fos.flush();

}


public static void main(String[] args) {
try {
String newClassName = "Exception";
if(args.length >= 1) newClassName = args[0];
ExceptionTransformer fit = new ExceptionTransformer();
fit.transform(newClassName);
} catch(Exception ex) {
ex.printStackTrace();
}

}
}


class ExceptionClassAdapter extends ClassAdapter implements Opcodes {

public ExceptionClassAdapter(ClassVisitor visitor) {
super(visitor);

}

public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] exes) {
MethodVisitor mv = super.visitMethod(access, name, desc, sig, exes);

if(name.equals("")) { // Constructor
System.out.println("Redefine Constructor...");
ExceptionConstructorAdviceAdapter ecaa =

new ExceptionConstructorAdviceAdapter(access, name, desc, mv);
return ecaa;
}

return mv;

}
}


class ExceptionConstructorAdviceAdapter extends AdviceAdapter {

public ExceptionConstructorAdviceAdapter(int access, String name, String desc, MethodVisitor mv) {
super(mv, access, name, desc);
}

protected void onMethodEnter() {

}

protected void onMethodExit(int opcode) {
if(opcode == RETURN) {
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC,

"flowlite/exception2/ExceptionCallBack",

"exceptionOccurred", "(Ljava/lang/Exception;)V");
mv.visitEnd();
}
}

}

위의 소스 코드에 대한 설명은 다음과 같다.

  • ClassReader를 이용해 rt.jar의 원본 java.lang.Exception 클래스를 읽는다.
  • ExceptionClassAdapter(extends ClassAdapter)객체를 이용해 원본 바이트 코드에 대한 변경을 시도한다.
    • ExceptionClassAdpater는 visitMethod를 이용해 Exception 클래스의 생성자에 대한 변경을 시도한다.
    • ClassAdapter.visitMethod는 바이트 코드에서 특정 메소드에 대한 정의가 시작될 때 호출된다. 따라서 visitMethod를 재정의함으로써 원하는 메소드를 우리 입맞에 맞게 수정할 수 있다.
    • 생성자(Constructor)의 메소드 이름은 항상 이다.
  • ExceptionClassAdapter.visitMethod 메소드는 만일 메소드가 생성자()이면 ExceptionConstructorAdviceAdapter(extends AdviceAdapter) 객체를 이용해 생성자에 대한 재정의를 시도한다.
    • AdviceAdpater 객체는 onMethodEnter, onMethodExit(opcode)라는 두 메소드를 제공한다.
    • onMethodEnter는 메소드 시작 시점에, onMethodExit는 메소드 리턴 직전 시점에 수행될 코드를 정의할 공간을 마려해준다.
    • 즉, ExceptionConstructorAdviceAdapter 객체는 이 두 메소드를 재정의함으로써 메소드의 시작/끝 시점에 자신이 원하는 코드를 삽일할수 있다.

위의 설명과 같이 하나의 ClassAdapter와 AdviceAdpater를 이용해서 우리가 원하는 작업을 수행할 수 있다.

이 예제에서는 onMethodExit를 이용해서 생성자가 종료되기 직전에 우리가 원하는 바이트 코드를 삽입한다. ExceptionConstructorAdviceAdapter.onMethodExit 메소드를 좀 더 자세히 살펴보자.

protected void onMethodExit(int opcode) {
if(opcode == RETURN) {
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC,

"flowlite/exception2/ExceptionCallBack",

"exceptionOccurred", "(Ljava/lang/Exception;)V");
mv.visitEnd();
}
}

위의 코느는 생성자가 정상적으로 리턴될 때(opcode == RETURN) "ExceptionCallBack.exceptionOccurred(this)" 코드를 수정하게끔 바이트 코드를 변경한다.

ExceptionTransformer를 수행한 후 생성된 Exception Class의 바이트 코드를 보면 아래와 같이 원하는 결과를 얻었음을 알 수 있다.

// class version 49.0 (49)
// access flags 33
public class Exception extends Throwable {


// access flags 24
final static long serialVersionUID = -3387516993124229948

// access flags 1
public () : void
ALOAD 0
INVOKESPECIAL Throwable.() : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 1
MAXLOCALS = 1

// access flags 1
public (String) : void
ALOAD 0
ALOAD 1
INVOKESPECIAL Throwable.(String) : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 2
MAXLOCALS = 2

// access flags 1
public (String,Throwable) : void
ALOAD 0
ALOAD 1
ALOAD 2
INVOKESPECIAL Throwable.(String,Throwable) : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 3
MAXLOCALS = 3

// access flags 1
public (Throwable) : void
ALOAD 0
ALOAD 1
INVOKESPECIAL Throwable.(Throwable) : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 2
MAXLOCALS = 2
}

이제 마지막 단계로 ExceptionCallBack.exceptionOccurred의 클래스와 메소드를 다음과 같이 정의한다.

package flowlite.exception2;

public class ExceptionCallBack {


public static void exceptionOccurred(Exception ex) {
System.out.println("Oops~ " + ex + " has occurred...");
ex.printStackTrace();
}
}

이 작업을 마무리하고, 다음과 같이 -Xbootclasspath 옵션을 이용해서 Exception을 발생시키는 ExceptionTest2를 수행해보자.(새로 생성된 Exception.class 파일을 converted_classes/java/lang 디렉토리로 카피하는 것은 기본!!!)

java -Xbootclasspath/p:./converted_classes ExcetpionTest2

이제 Exception이 발생할 때마다 ExceptionCallBack.exceptionOccurred가 수행되므로 다음과 같은 수행 결과가 나타난다.

놀랍지 않은가?

바이트 코드에 대한 아주 간단한 조작만으로도 모든 Exception 발생에 대한 처리 로직을 추가할 수 있다.

이번 예제를 통해 ASM을 이용한 BCI의 개념이 이해되었기를 바라며, 좀 더 복잡한 예제를 통해 더 의미있는 논의를 해보기로 한다.

신고
Trackback 0 : Comment 1
  1. 마늘소스 2011.03.31 18:55 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment

티스토리 툴바