Java Instrumentation API

2023. 5. 16. 17:26Spring Framework/Spring AOP

Instrumentation API는 Java 플랫폼의 java.lang.instrument 패키지에서 제공되는 API로, JVM의 클래스 로딩 및 런타임 동작을 조작할 수 있도록 하는 API이며, 이 API 자체는 인터페이스로 제공됩니다. 이 API는 주로 성능 모니터링, 프로파일링, 코드 커버리지 도구, 그리고 AOP(Aspect-Oriented Programming) 같은 기술을 구현할 때 사용됩니다. 구현체는 JVM 내부에서 이 API를 구현하고, 이를 통해 Java Agent와 같은 도구들이 해당 기능을 사용할 수 있도록 합니다.

주요 기능

  1. ClassFileTransformer:
    • ClassFileTransformer는 클래스가 로드되기 전에 바이트코드를 변환할 수 있는 인터페이스입니다. 이를 통해 클래스 파일을 로드하거나 정의하는 과정에서 코드를 삽입하거나 수정할 수 있습니다.
    • 예를 들어, 메소드 호출 전후에 로그를 기록하는 기능을 추가하거나, 메소드 실행 시간을 측정하는 코드를 삽입할 수 있습니다.
  2. Java Agent:
    • Instrumentation API를 사용하는 주요 방법 중 하나는 Java 에이전트를 사용하는 것입니다. 에이전트는 JVM이 시작될 때 클래스 로더에 ClassFileTransformer를 등록하여, 모든 클래스가 로드되기 전에 바이트코드를 변경할 수 있습니다.
    • 에이전트는 JVM 시작 시에 -javaagent 옵션을 통해 지정되며, 런타임에 클래스를 조작하는 데 사용됩니다.
  3. Retransformation:
    • Instrumentation API는 이미 로드된 클래스를 다시 변환(retransform)할 수 있는 기능도 제공합니다. 이 기능은 클래스가 이미 JVM에 의해 로드된 후에도 바이트코드를 수정할 수 있게 합니다.
  4. 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

 

[ Spring instrument library ]