태터데스크 관리자

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

태터데스크 메시지

저장하였습니다.

The Power of Byte Code Instrumentation in Java - Part 5

Enterprise Java 2007.08.10 18:41

Part4에 이어...

The Power of Byte Code Instrumentation in Java - Part 5

Part4에서 Byte Code Instrumentation(이하 BCI)을 사용하는 간단한 예제를 살펴봤는데, 이제 보다 복잡한 예제를 통해 BCI가 어떤 정도의 위력을 가지고 있는지 알아보자.

시나리오

우리가 풀어야 할 문제는 다음과 같다.

"특정 Java 어플리케이션이 Socket을 통해 읽고 쓰는 데이터를 추적하고 싶다. 즉, 어떤 Socket을 통해 어떤 쓰레드가 얼마나 I/O를 일으키는가를 추적하고 싶다."

BCI를사용하지 않고 이것을 수행하는 방법은 OS에서 제공하는 Network Monitoring Tool이나 API를 사용해서 Java Application의 특정 Port를 모니터링하는 것이다. 하지만 이러 Tool이나 API를 이용해 우리 입맛에 맞는 분석 데이터를 만드는것은 대단히 어렵거나 혹은 불가능하다.

하지만 BCI를 사용하면 이 작업을 매우 손쉽게 처리할 수 있다. 어떻게 이것이 가능한가?

java.net..Socket을 어떻게 변경할 것인가?

우선 Socket Class에서 우리가 필요로 하는 API를 보자.

public InputStream getInputStream()throws IOException

public OutputStream getOutputStream()throws IOException

위의 두 메소드는 Socket을 통해서 데이터를 읽고 쓰는 InputStream과 OutputStream을 얻어온다. 논의를 간단하게 하기 위해 InputStream만을 살펴 보자. InputStream Class에서 우리가 관심을 가지는 API는 다음과 같다.

public int read(byte[]b,intoff,intlen)throws IOException

위의 메소드는 InputStream으로부터 특정 데이터(b[])를 읽어 들이고 실제로 읽어들인 바이트 수를 return하는 역할을 한다. 이 메소드에 우리가 원하는 모든 것이 있는 셈이다.

BCI를 이용해서 InputStream.read 메소드를 캡쳐 혹은 변경하는 방법은 무궁무진한데, 여기서 필자는 다음과 같은 방법을 사용하고자 한다.

1. 우선 Socket.getInputStream() 메소드의 이름을 __orig$getInputStream__으로 변경한다.

2. Socket.getInputStream() 메소드를 다음과 같이 재정의한다.

public InputStream getInputStream() {

// 원래의 getInputStream(이름이 __orig$getInputStream__으로 바뀐)을 호출해서 InputStream을 얻어온다.

InputStream is = __orig$getInputStream__();

// 이 InputStream을 이용해서 나만의 InputStream을 만들어준다.

FlowLiteSocketInputStream fsis = new FlowLiteSocketInputStream(this, is);

return fsis;

}

이렇게 함으로써 Socket과 관련된 InputStream에서 발생하는 모든 액션을 캡쳐할 수 있다. JDK가 제공하는 Socket용 InputStream을 대신할 나만의 InputStream, 즉 FlowLiteSocketInputStream은 다음과 같이 InputStream을 상속받으며, 각 메소드는 모든 액션을 캡쳐할 수 있도록 구현된다.

package java.net;

public class FlowLiteSocketInputStream extends InputStream {

Socket s = null;
InputStream is = null;

public FlowLiteSocketInputStream(Socket s, InputStream is) {
this.s = s;
this.is = is;
SocketIOCallBack.createCalled(this);
}

public int read() throws IOException {
int len = is.read();
SocketIOCallBack.readCalled(this, 4);

return len;

}

public int read(byte[] b) throws IOException{
int len = is.read(b);
SocketIOCallBack.readCalled(this, len);
return len;
}

public int read(byte[] b, int off, int len) throws IOException {
int len2 = is.read(b, off, len);
SocketIOCallBack.readCalled(this, len2);
return len2;
}

public int available() throws IOException {
return is.available();
}


public void close() throws IOException {
is.close();
}

public void mark(int readlimit) {
is.mark(readlimit);
}

public boolean markSupported() {
return is.markSupported();
}

public Socket getSocket() {
return this.s;
}
}

SocketIOCallBack 클래스는 FlowLiteInputStream으로부터 콜백받은 결과를 저장하고 그 결과를 보여주게끔 구현하면 된다.

ASM을 이용해 java.net.Socket의 바이트 코드 변경하기

ASM 라이브러리가 제공하는 라이브러리를 이용하면 우리가 원하는 대로 java.net.Socket 클래스의 바이트 코드를 변경할 수 있다. 아래에 그 샘플 코드가 있다.

package flowlite.net;

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


/**
* Net io class transformer.
* Execute byte code transformation to convert NetInputStream class
* You must convert native java.io.Socket class using this class
*
* The technique is the most powerful. So keep in mind~~
*
* Must use ASM 3.0 library
*
* @history
* 2007/07/17Dongwook ChoInitial Coding
*
*/
public class SocketTransformer implements Opcodes {

public SocketTransformer() {

}

// Convert class
public void transform(String newClassName) throws Exception {
System.out.println("Starting transformation of java.net.Socket...");
// Prepared reader, writer, adapter

ClassReader reader = new ClassReader("java.net.Socket");
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS+ClassWriter.COMPUTE_FRAMES);
ClassAdapter adapter = new SocketClassAdapter(writer);

reader.accept(adapter, ClassReader.SKIP_DEBUG);

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

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

}
}

// Socket Class 변환기
class SocketClassAdapter extends ClassAdapter implements Opcodes {

public SocketClassAdapter(ClassVisitor cv) {
super(cv);
}

// 각메소드를 방문하면서 필요하면 변경한다.
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {

if(name.equals("getInputStream")) {
//getInputStream을 __orig$getInputStreram__ 으로 변경한다.

System.out.println("Rename getInputStream to __orig$getInputStream__");
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC,

"__orig$getInputStream__", "()Ljava/io/InputStream;", null, new String[] { "java/io/IOException" });
mv.visitCode();
mv.visitEnd();

return mv;
}

return super.visitMethod(access, name, descriptor, signature, exceptions);
}

//새로운getInputStream 메소드를 추가한다.

public void visitEnd() {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getInputStream", "()Ljava/io/InputStream;",

null, new String[] { "java/io/IOException" });

mv.visitCode();
mv.visitTypeInsn(NEW, "java/net/FlowLiteSocketInputStream");
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);

// 원래메소드를 호출하고,
mv.visitMethodInsn(INVOKEVIRTUAL, "java/net/Socket", "__orig$getInputStream__", "()Ljava/io/InputStream;");
mv.visitMethodInsn(INVOKESPECIAL, "jav`a/net/FlowLiteSocketInputStream",

"", "(Ljava/net/Socket;Ljava/io/InputStream;)V");
mv.visitInsn(ARETURN);
mv.visitMaxs(4, 1);
mv.visitEnd();

}
}

아래 결과는 위에서 구현한 FlowLiteInputStream, Socket, SocketIOCallBack을 이용해 Socket 통신을 통해 주고받는 데이터를 보여주고 있다.

[Socket Info] Thread id = 20, Host name =localhost:10583, Status = 1, Access time = Fri Aug 10 18:11:06 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Fri Aug 10 18:11:06 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Fri Aug 10 18:11:06 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Fri Aug 10 18:11:06 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Fri Aug 10 18:11:06 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Fri Aug 10 18:11:06 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Fri Aug 10 18:11:06 KST 2007
....

[Net IO], Thread id = 20, Bytes read = 736, Access time = Fri Aug 10 18:11:06 KST 2007

놀랍지 않은가? 간단한 Byte code 수정만으로 손쉽게 Socket에서 발생하는 모든 I/O를 추적할 수 있다.

이러한 단순한 기법을 잘 발전시켜면 그 용도는 실로 무궁무진하다. File I/O, Network I/O, JDBC Request, Servlet Request, EJB Request, Struts Request, Spring Framework Request 등 Enterprise Java의 성능 모니터링과 분석에 필요한 데이터의 대부분을 수집하고 분석할 수 있다.

실제로 상업적으로 판매되는 대부분의 WAS 모니터링 툴들이 이런 비슷한 기법을 사용해서 성능 데이터를 수집하고 있다.

관심이 있는 사람이라면 이런 비싼 상용툴들을 도입하기 전에, 자신만의 Byte Code Instrumentation 기법을 이용해 WAS나 Java Application의 성능을 분석해보기를 권장한다.

이 BCI가 제공하는 기법이 너무나 강력하기 때문에, 한번 여기에 익숙해지면 다른 기법들은 모두 한 수 아래로 보일 것이다.

다음 Part에서 Java 5 (JDK 1.5)에서 BCI를 JDK 스펙 차원에서 지원하기 위해 새롭게 등장한 java.lang.instrument 패키지에 대해서 논의하고 Byte Code Instrumetation에 대한 논의를 마무리지을 것이다.

PS)

필자는 BCI를 이용해 Exception Tracking, Object Creation Tracking, File I/O Tracking, Network I/O Tracking, JDBC Tracking 등을 구현한 사례가 있는데, 사용할 수록 그 유용성에 놀라곤 한다. 이글을 읽는 여러분들도 이러한 놀라움을 나누지 않겠는가!!!

신고
Trackback 0 : Comment 0

Write a comment

티스토리 툴바