https://sundaland.tistory.com/75
[ ▶ Instrumentation API ]
Instrumentation API는 자바 플랫폼의 java.lang.instrument 패키지에서 제공되는 API로, JVM의 클래스 로딩 및 런타임 동작을 조작할 수 있도록하는 API이며, 이 API 자체는 인터페이스로 제공된다. 이 API는 주로 성능 모니터링, 프로파일링, 코드 커버리지 도구, 그리고 AOP (Aspect-Oriented Programming) 같은 기술을 구현할 때 사용한다. 구현체는 JVM 내부에서 이 API를 구현하고, 이를 통해 자바 에이전트와 같은 도구들이 해당 기능을 사용할 수 있도록 한다.
[ ▷ 주요기능 ]
1. ClassFileTransformer
- ClassFileTransformer는 클래스가 로드되기 전에 바이트코드를 변환할 수 있는 인터페이스이다. 이를 통해 클래스 파일을 로드하거나 정의하는 과정에서 코드를 삽입하거나 수정할 수 있다.
- 예를 들어, 메서드 호출전 후에 로그를 기록하는 기능을 추가하거나, 메서드 실행 시간을 측정하는 코드를 삽입할 수 있다.
2. 자바 에이전트
- Instrumentation API를 사용하는 주요 방법 중 하나는 자바 에이전트를 사용하는 것이다. 에이전트는 JVM이 시작될 때 클래스 로더에 ClassFileTransformer를 등록하여, 모든 클래스가 로드되기 전에 바이트코드를 변경할 수 있다.
- 에이전트는 JVM 시작 시에 -javaagent 옵션을 통해 지정되며, 런타임에 클래스를 조작하는 데 사용된다.
3. Retransformation
- Instrumentation API는 이미 로드된 클래스를 다시 변환 (retransform)할 수 있는 기능도 제공한다. 이 기능은 클래스가 이미 JVM에 의해 로드된 후에도 바이트코드를 수정할 수 있게 한다.
4. Redefinition
- RedefineClasses 메서드를 사용하여, JVM에서 실행 중인 클래스의 정의를 새롭게 바꿀 수 있다. 이 기능을 사용하면 기존 클래스의 메서드나 필드의 바이트코드를 새롭게 정의할 수 있다.
[ ▷ 구현체 ]
- JVM 자체가 Instrumentation API의 실제 구현이다. 즉 JVM은 Instrumentation 인터페이스를 구현하고, 자바 에이전트 또는 다른 도구가 이 API를 통해 클래스의 로딩 및 변환 작업을 수행할 수 있게 한다.
- 작성중
- 자바 에이전트는 JVM에 의해 제공된 Instrumentation 구현체에 접근하여 클래스 파일의 바이트코드를 조작하거나 변경할 수 있다. 자바 에이전트가 JVM에 로드될 때, JVM은 Instrumentation 구현체를 자바 에이전트의 permain 메서드로 전달된다. 이를 통해 자바 에이전트는 JVM이 관리하는 클래스 로딩 과정에 개입할 수 있다.
Instrumentation API의 구현체는 JVM 자체에 포함되어 있으며, 이 구현체를 통해 자바 에이전트와 같은 도구가 런타임에 클래스 로딩 및 변환 작업을 수행할 수 있다. 자바 에이전트는 JVM에서 제공하는 Instrumentation API 구현체를 활용하여, 애플리케이션의 동작을 런타임에 조작하는 것이다.
[ ▷ 사용사례 ]
- 프로파일링 및 성능 모니터링:
- 코드 커버리지 도구: 테스트 실행 중에 어떤 코드가 실행되었는지를 추적하기 위해 바이트코드 수준에서 커버리지 정보를 추가할 수 있다.
- AOP 구현: 특정 메서드 호출 전후에 횡단 관심사를 적용하기 위해, 메서드 실행 전에 추가 코드를 삽입하는 방식으로 AOP를 구현할 수 있다.
- 디버깅 도구: 실행 중인 애플리케이션에서 특정 코드 조작을 동적으로 변경하거나 추가하여 버그를 분석할 수 있다.
[ ▷ 예시 ]
자바 에이전트와 Instrumentation API를 사용하는 간단 예시는 아래와 같다.
이 예제는 클래스의 메서드 시작 부분에 간단한 로그를 추가하는 방식으로 바이트코드를 변환한다. 이를 위해, ASM 라이브러리를 사용하여 바이트코드를 조작한ㄴ 예시이다.
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
public class SimpleAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new SimpleTransformer());
}
}
class SimpleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("Transforming " + className);
if (!className.equals("MyTargetClass")) {
// 타겟 클래스를 확인하여 특정 클래스에만 변환 적용
return classfileBuffer;
}
try {
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new MyClassVisitor(Opcodes.ASM9, classWriter);
classReader.accept(classVisitor, 0);
return classWriter.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return classfileBuffer; // 오류가 발생하면 원본 바이트코드를 반환
}
}
}
class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(Opcodes.ASM9, mv);
}
}
class MyMethodVisitor extends MethodVisitor {
public MyMethodVisitor(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitCode() {
super.visitCode();
// 메서드의 시작 부분에 System.out.println("Method entered") 추가
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Method entered");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
}
}
- ClassReader: 기존 클래스 파일의 바이트코드를 읽는다.
- ClassWriter: 새로운 바이트코드를 생성한다.
- ClassVisitor: 클래스의 구조 (메서드, 필드 등)를 방문하여 수정할 수 있도록 하는 클래스이다.
- MethodVisitor: 메서트의 바이트코드를 수정할 수 있도록 지원하는 클래스이다.
- MyClassVisitor: 특정 클래스의 메서드를 수정하기 위해 MethodVisitor를 생성한다.
- MyMethodVisitor: 특정 클래스의 메서드를 수정하여, 메서드가 실행될 때마다 "Method enterd"라는 메시지를 출력한다.
[ ▷ 주의 사항 ]
이 코드는 ASM 라이브러리에 의존한다. ASM은 바이트코드 조작을 위한 프레임워크로, 이를 사용하기 위해서는 프로젝트에 ASM 라이브러리를 추가해야 한다. Maven이나 Gradle을 사용하는 경우, 의존성을 추가할 수 있다.
▼ Maven
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
▼ Gradle
implementation 'org.ow2.asm:asm:9.2'
이렇게 하면 MyTargetClass의 모든 메서드 부분에 "Method enterd"라는 로그가 출력되도록 바이트코드를 수정하게된다.
위 예시에서, SimpleAgent 클래스는 자바 에이전트로 사용되며, JVM이 시작될 때 모든 클래스를 로드하기 전에 SimpleTransformer를통해 클래스 바이트코드를 변경할 수 있다.
Instrumentation API는 자바 어플리케이션의 런타임 동작을 동적으로 변경할 수 있는 강력한 도구이다. 이를 통해 개발자는 성능 모니터링, 프로파일링, AOP 등 다양한 목적을 위해 클래스 바이트코드를 변환하고, 애플리케이션의 동작을 세밀하게 제어할 수 있다.
Java Instrument API vs ASM: https://blank001.tistory.com/60
Java Agent: https://blank001.tistory.com/61
Instrumentation API vs AspectJ: https://blank001.tistory.com/62
Spring instrument library: https://blank001.tistory.com/63
'스프링 AOP' 카테고리의 다른 글
Java Agent (1) | 2024.09.04 |
---|---|
Java Instrument API vs ASM(Abstract Syntax Manipulation) (0) | 2024.09.03 |
Declaring Advice, Introductions (1) | 2024.09.02 |
@AspectJ support (0) | 2024.08.31 |
Delegate (0) | 2024.08.28 |