2023. 5. 16. 17:26ㆍSpring Framework/Spring AOP
Instrumentation API는 Java 플랫폼의 java.lang.instrument
패키지에서 제공되는 API로, JVM의 클래스 로딩 및 런타임 동작을 조작할 수 있도록 하는 API이며, 이 API 자체는 인터페이스로 제공됩니다. 이 API는 주로 성능 모니터링, 프로파일링, 코드 커버리지 도구, 그리고 AOP(Aspect-Oriented Programming) 같은 기술을 구현할 때 사용됩니다. 구현체는 JVM 내부에서 이 API를 구현하고, 이를 통해 Java Agent와 같은 도구들이 해당 기능을 사용할 수 있도록 합니다.
주요 기능
- ClassFileTransformer:
ClassFileTransformer
는 클래스가 로드되기 전에 바이트코드를 변환할 수 있는 인터페이스입니다. 이를 통해 클래스 파일을 로드하거나 정의하는 과정에서 코드를 삽입하거나 수정할 수 있습니다.- 예를 들어, 메소드 호출 전후에 로그를 기록하는 기능을 추가하거나, 메소드 실행 시간을 측정하는 코드를 삽입할 수 있습니다.
- Java Agent:
- Instrumentation API를 사용하는 주요 방법 중 하나는 Java 에이전트를 사용하는 것입니다. 에이전트는 JVM이 시작될 때 클래스 로더에
ClassFileTransformer
를 등록하여, 모든 클래스가 로드되기 전에 바이트코드를 변경할 수 있습니다. - 에이전트는 JVM 시작 시에
-javaagent
옵션을 통해 지정되며, 런타임에 클래스를 조작하는 데 사용됩니다.
- Instrumentation API를 사용하는 주요 방법 중 하나는 Java 에이전트를 사용하는 것입니다. 에이전트는 JVM이 시작될 때 클래스 로더에
- Retransformation:
- Instrumentation API는 이미 로드된 클래스를 다시 변환(retransform)할 수 있는 기능도 제공합니다. 이 기능은 클래스가 이미 JVM에 의해 로드된 후에도 바이트코드를 수정할 수 있게 합니다.
- Redefinition:
RedefineClasses
메소드를 사용하여, JVM에서 실행 중인 클래스의 정의를 새롭게 바꿀 수 있습니다. 이 기능을 사용하면 기존 클래스의 메소드나 필드의 바이트코드를 새롭게 정의할 수 있습니다.
구현체
- JVM 자체가 Instrumentation API의 실제 구현체입니다. 즉, JVM은 Instrumentation 인터페이스를 구현하고, Java Agent 또는 다른 도구가 이 API를 통해 클래스의 로딩 및 변환 작업을 수행할 수 있게 합니다.
- Instrumentation 인터페이스는
java.lang.instrument
패키지에 정의되어 있습니다. 이 인터페이스의 메서드들은 클래스를 변환하거나 재정의할 수 있는 기능을 제공하며, 이러한 기능은 JVM의 클래스 로딩 메커니즘에 깊이 통합되어 있습니다. - Java Agent는 JVM에 의해 제공된 Instrumentation 구현체에 접근하여 클래스 파일의 바이트코드를 조작하거나 변경할 수 있습니다. Java Agent가 JVM에 로드될 때, JVM은 Instrumentation 구현체를 Java Agent의
premain
메서드로 전달합니다. 이를 통해 Java Agent는 JVM이 관리하는 클래스 로딩 과정에 개입할 수 있습니다.
Instrumentation API의 구현체는 JVM 자체에 포함되어 있으며, 이 구현체를 통해 Java Agent와 같은 도구가 런타임에 클래스 로딩 및 변환 작업을 수행할 수 있습니다. Java Agent는 JVM에서 제공하는 Instrumentation API 구현체를 활용하여, 애플리케이션의 동작을 런타임에 조작하는 것입니다.
사용 사례
- 프로파일링 및 성능 모니터링: 런타임 동안 메소드 호출 횟수, 실행 시간 등을 기록하기 위해 Instrumentation API를 활용하여 코드를 삽입합니다.
- 코드 커버리지 도구: 테스트 실행 중에 어떤 코드가 실행되었는지를 추적하기 위해 바이트코드 수준에서 커버리지 정보를 추가할 수 있습니다.
- AOP 구현: 특정 메소드 호출 전후에 횡단 관심사를 적용하기 위해, 메소드 실행 전에 추가 코드를 삽입하는 방식으로 AOP를 구현할 수 있습니다.
- 디버깅 도구: 실행 중인 애플리케이션에서 특정 코드 조각을 동적으로 변경하거나 추가하여 버그를 분석할 수 있습니다.
예시
Java 에이전트와 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 entered"라는 메시지를 출력하도록 합니다.
주의사항:
이 코드는 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 entered"라는 로그가 출력되도록 바이트코드를 수정하게 됩니다.
위 예시에서, SimpleAgent
클래스는 Java 에이전트로 사용되며, JVM이 시작될 때 모든 클래스를 로드하기 전에 SimpleTransformer
를 통해 클래스 바이트코드를 변경할 수 있습니다.
Instrumentation API는 Java 애플리케이션의 런타임 동작을 동적으로 변경할 수 있는 강력한 도구입니다. 이를 통해 개발자는 성능 모니터링, 프로파일링, AOP 등 다양한 목적을 위해 클래스 바이트코드를 변환하고, 애플리케이션의 동작을 세밀하게 제어할 수 있습니다.
[ Java Instrument API vs ASM(Abstract Syntax Manipulation) ]
[ Java Agent ]
[ Instrumentation API vs AspectJ ]