문서의 이전 판입니다!
BCI - The Power of Bytecode Instrumentation in Java
BCI(Bytecode Instrumenation)와 관련하여 잘 정리된 글
The Power of Byte Code Instrumentation in Java - Part1
Byte Code Instrumentation이란?
Java에서 가장 원초적이고 강력한 프로그래밍 기법은?
나의 대답은 단연 BCI, 즉 Byte Code Instrumentation이다. (또는 Byte Code Insertion)
Byte Code Instrumenation이란 Java의 Byte Code에 대해 직접 수정을 가해서, 소스 파일의 수정없이 원하는 기능을 부여하는 기법을 말한다.
이러한 특징 때문에 Java 프로파일러나 모니터링 툴들이 대부분 BCI 기능을 이용하고 있으며, BCI를 통해 모니터링 대상이 되는 어플리케이션의 수정없이 성능 측정에 필요한 요소들을 삽입할 수 있다. Bytecode를 직접 수정할 수 있기 때문에 BCI를 통해서 구현할 수 있는 기능은 그야말로 무궁무진하다고 할 수 있다.
이 블로그에서 [AOP(Aspected Oriented Programming) In Java]라는 주제로 블로깅이 진행 중인데, AOP를 구현하는 핵심 기술이 바로 BCI이다. AOP 컴포넌트들이 컴파일 시간이나 로딩 시간, 또는 런타임 시간에 Aspect와 Business Logic을 Weaving할 수 있는 이유가 바로 BCI, 즉 Java 바이트 코드를 직접 수정할 수있는 기술을 사용하기 때문이다.
따라서 만일 AOP를 어떤 식으로든 사용한 적이 있다면 이미 암묵적으로 BCI를 사용하고 있다고 할 수 있다.
요즘 Jennifer, Performizer와 같은 WAS 모니터링 툴들이 많은 인기를 얻고 있는데, 이들 제품들이 성능 데이터를 수집하기 위해 가장 보편적으로 사용하는 기술이 바로 BCI이다.앞으로 몇 차례에 걸친 연재에서 이런 툴들이 어떻게 BCI를 이용해 성능 데이터를 수집하는지 몇가지 예를 보게 될 것이다.
Java Bytecode
Java가 Bytecode라는 일종의 기계어(머신코드)를 사용한다는 것은 익히 알려진 사실이다.
전통적인 기계어가 특정 OS/하드웨어에 의존적인데 반해 Java Bytecode는 JVM(Java Virtual Machine)에만 의존적이라는 중요한 차이가 있다. 따라서 JVM만 동일하다면 어떤 OS/하드웨어에서든 동일한 Bytecode가 구동가능하다. Java가 오늘날 지배적인 언어가 된 것은 바로 OS 중립적인 기계어인 Bytecode 때문이다.
아래에 간단한 Java Bytecode의 예가 있다.
public getValue()I L0 (0) LINENUMBER 28 L0 SIPUSH 1000 ISTORE 1 L1 (3) LINENUMBER 29 L1 ILOAD 1 IRETURN L2 (6) LOCALVARIABLE this Lflowlite/io/ASMTest; L0 L2 0 LOCALVARIABLE value I L1 L2 1 MAXSTACK = 1 MAXLOCALS = 2
위의 약간 암호같은 Bytecode는 아래의 Java Source코드가 컴파일된 것이다.
public int getValue() { int value = 1000; return value; }
다행히 Sun에서 JVM의 스펙을 정할 때 Bytecode의 문법을 명확하게 정의하기 때문에, 약간의 노력을 기울이면 Bytecode를 직접 읽고 쓸 수 있다.(사실은 많은 노력이 필요하다)
http://java.sun.com/docs/books/jvms/에서 Java Virtual Machine의 상세한 스펙을 얻을 수 있다. 이 문서를 참조하면 Java Class File의 포맷과 Class File을 이루는 Bytecode에 대한 상세한 정보를 얻을 수 있다.
하지만 이 문서를 실제로 보는 사람은 거의 없을 것으로 믿는다. ^^)
비록 Bytecode를 직접 읽고 쓰는 것이 이론적으로는 가능하지만, 대단히 성가시고 복잡하다. 이런 이유로 Bytecode를 쉽게 조작할 수 있는 컴포넌트를 개발하는 프로젝트들이 진행되었으며 그 결과로 현재는 다양한 라이브러리 중 마음에 드는 것을 선택할 수 있게 되었다.
BCI를 지원하는 라이브러리들
많은 오픈 소스 커뮤니티들이 Bytecode 조작을 가능하게 하는 라이브러리들을 제공하고 있다.
- ASM : Object Web에서 제공 http://asm.objectweb.org/
- BCEL : Apache 프로젝트에서 제공 http://jakarta.apache.org/bcel/
- SERP : Sourceforge 프로젝트에서 제공 http://serp.sourceforge.net/
- Javassist : JBoss 프로젝트에서 제공 http://www.csg.is.titech.ac.jp/~chiba/javassist/(놀랍게도 일본 사람이 개발… ㅠㅠ)
이 연재글에서는 ASM을 이용한 간단한 샘플들을 통해 BCI가 얼마나 강력한 프로그래밍 기법인지 공감하는 시간을 가질 것이다.
The Power of Byte Code Instrumenation in Java - Part2
Byte Code Instrumentation(이하 BCI)의 예제를 논의하기 전에 우선 이 시리즈에서 사용할 BCI 라이브러리인 ASM의 사용법에 대해 간략하게 알아보자
ASM
ASM은 http://asm.objectweb.org/에서 설치에 필요한 라이브러리와 개발 도구(Eclipse Plugin - AMS Bytecode Outline Plugin)들을 다운받을 수 있다. Eclipse Plugin을 이용하면 ASM을 활용하는데필요한 학습 시간을 극적으로 줄일 수 있다.
아래 그림은 Eclipse ASM Bytecode Outline Plugin을 이용해서 특정 자바 소스 파일의 Bytecode를 확인한 결과이다.
Bytecode를 직접 본 적이 없다면 아마 상당히 생소할 것이다. 하지만 앞으로 몇 가지 예제를 거치다 보면 자연스럽게 Bytecode에 익숙해지게 된다.
ASM Bytecode Outline Plugin의 가장 강력한 기능은 ASM화된코드를 손쉽게 확인할 수 있다는 것이다. 아래와 같이 [ASM]라는 이름의 아이콘을 선택하면 흉물스런 Bytecode가세련된 Java Code로 변환되는 것을 확인할 수 있다.
ASM화된 코드(ASMified Code)란 ASM 라이브러리가 제공하는 API를 이용해서 Bytecode를 생성하는 코드
를 말한다. 이 ASM화된코드를 적절히Copy/Paste/Edit해서 필요한 거의 모든 Bytecode를 직접 생성하고 조작할 수 있다.
굉장하지 않은가?
나의 느낌은 전율 그 자체였다. 이 원리를 이용하면 어떠한 Java어플리케이션이라도 내가원하는 코드를 어플리케이션의 코드 변경없이 삽입할 수있다. 응용처는 그야말로 무궁무진하다.
이런 강력함때문에 Java Monitor/Profiler/Analyzer 등 성능과 관련된 대부분의 툴들이 어떤 식으로든 BCI를 이용하고 있다. 이런 기능의 예를 앞으로 다양하게 살펴볼 것이다.
ASM 라이브러리에 대한 자세한 설명은 http://asm.objectweb.org/에서 제공하는 매뉴얼과 Java Doc을 통해 얻을 수 있다. 반드시 매뉴얼을 읽어볼 것을 권장한다. ASM은 Visitor Pattern을 이용해 구현되어 있어서 직관적인 프로그래밍이 가능하다는 장점이 있다.
The Power of Byte Code Instrumentation in Java - Part 3
One Simple but Powerful Example of BCI
ASM 라이브러리를 이용한 Byte Code Instrumentation의 아주 간단한 예제를 하나 작성해보자.
비록 매우 심플한 예제이지만, 실제로는 매우 강력한 방법이다.
이 방법에 익숙해지면 아마 그 용도의 무궁무진함에 놀라게 될 것이다.
이런 질문에 한번 답해보자.
“우리 회사에서 사용 중인 WAS Application에서 발생하는 Exception을 체계적으로 수집하고 정리하고 싶다. 그 Exception이 Catch되어서 처리되고 있는지에 무관하게…. ”
(사실은 이 문제에 대해 블로그의 “Aspected Oriented Programming in Java”에서 다룬 바 있다)
대부분의 사람들이 본능적으로 다음과 같은 대답을 떠올린다.
즉, Application에서 사용 중인 모든 소스 코드에 try catch 구문을 사용해 Exception을 Catch해서 적절히 조작하는 것이다. 이 접근법은 다음과 같은 문제점들이 있다.
- 그 많은 소스들을 언제 다 바꿀 것인가?
- 내가 직접 작성하지 않은 Java Core Library(java.*, javax.*, …)나 3rd Party Library는 어떻게 할 것인가?
- Exception 처리 정책이 바뀌었을 때는 또 어떻게 할 것인가?
- 무엇보다 엄청나게 지저분해질 코드는 또 어떻게 할 것인가?
Business Logic을 처리하는 코드에 Exception 수집을 위한 코드를 넣는 것은 가장 위험하고 비효율적인 방법임이 분명하다.
이런 문제를 해결하기 위해 AOP와 같은 방법론이 등장했을 정도로…
이 문제를 해결하는 가장 간단한 방법을 생각해보자….
나의 머리 속에 떠오른 방법은 이것이다.
- “java.lang.Excetpion” 클래스가 모든 Exception의 부모 클래스 아닌가?“
- “즉, 어떤 종류의 Exception이라도 반드시 java.lang.Exception이 제공하는 생성자와 메소드를 공유하고 있다”
- “따라서 Java.lang.Exception의 생성자에 Exception 수집 코드를 삽입하면 모든 것이 해결된다”
빙고!!
문제는 java.lang.Exception 클래스의 생성자를 어떻게 조작하느냐는 것이다.
여기가 바로 BCI, 즉 Byte Code Instrumentation이 등장하는 곳이다.
우선 java.lang.Exception 클래스가 어떤 코드로 이루어져 있는지 Java Decompiler를 이용해 코드를 살펴 보자.
Decompiler를 통해서 Exception 클래스의 소스 파일은 다음과 같다.
package java.lang; // Referenced classes of package java.lang: // Throwable, String public class Exception extends Throwable { public Exception() { } public Exception(String s) { super(s); } public Exception(String s, Throwable throwable) { super(s, throwable); } public Exception(Throwable throwable) { super(throwable); } static final long serialVersionUID = 0xd0fd1f3e1a3b1cc4L; }
ASM Bytecode Outline Plugin을 통해서 본 Byte Code는 아래와 같다.
/ 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 RETURN MAXSTACK = 1 MAXLOCALS = 1 // access flags 1 public (String) : void ALOAD 0 ALOAD 1 INVOKESPECIAL Throwable.(String) : void RETURN MAXSTACK = 2 MAXLOCALS = 2 // access flags 1 public (String,Throwable) : void ALOAD 0 ALOAD 1 ALOAD 2 INVOKESPECIAL Throwable.(String,Throwable) : void RETURN MAXSTACK = 3 MAXLOCALS = 3 // access flags 1 public (Throwable) : void ALOAD 0 ALOAD 1 INVOKESPECIAL Throwable.(Throwable) : void RETURN MAXSTACK = 2 MAXLOCALS = 2 }
만일 Exception 클래스의 생성자에서 다음과 같은 코드가 삽입되도록 한다면?
public class Exception extends Throwable { public Exception() { ExceptionCallBack.exceptionOccurred(this); } public Exception(String s) { super(s); ExceptionCallBack.exceptionOccurred(this); } public Exception(String s, Throwable throwable) { super(s, throwable); ExceptionCallBack.exceptionOccurred(this); } public Exception(Throwable throwable) { super(throwable); ExceptionCallBack.exceptionOccurred(this); } static final long serialVersionUID = 0xd0fd1f3e1a3b1cc4L; }
이렇게 되면 어떤 Exception이 발생하든지 항상 ExceptionCallBack.exceptionOccurred(this) 코드에 의해 수집이 이루어진다.심지어 Java Core Library를 사용하는 과정에서 내부적으로 발생하는 Exception도 다 수집할 수 있다.
Part3에서 ASM을 이용해 java.lang.Exception 클래스의 바이트 코드를 직접 수정하는 예제를 보게 될 것이다.
참조
이런 반문을 할 지 모르겠다. “java.lang.Exception” 클래스를 디컴파일한 소스를 직접 수정해서 새로운 java.lang.Exception 클래스를 직접 만들면 안되나?”
가능한 방법이다. 하지만 이런 문제점이 있다.
JVM의 Version이나 Vendor에 따라 소스는 모두 다를 수 있다. 따라서 타겟이 바뀔 때마다 디컴파일을 수행해서 소스를 생성한 후 작업을 해야 한다.
디컴파일러가 모든 클래스 파일을 다 완벽하게 디컴파일해내는 것은 아니다.
이런 이유들 때문에 소스 코드를 직접 수정하는 방법은 정말 특별한 경우가 아니면 권장되지 않는다. 더구나 Java Core Library의 소스 코드를 변경해서 사용하는 것은 법적(?)인 문제를 일으킬 수도 있다.
The Power of Byte Code Instrumentation in Java - Part 4
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에게 넘겨 준다.
-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의 개념이 이해되었기를 바라며, 좀 더 복잡한 예제를 통해 더 의미있는 논의를 해보기로 한다.