본문 바로가기

프로젝트/APM prototype 개발

[Agent] Agent 동작 구성 및 이해

목차

    Agent 개발

     

    Instrumentation 인터페이스

    API reference for Java Platform, Instrumentation

     

    Instrumentation (Java SE 17 & JDK 17)

    public interface Instrumentation This class provides services needed to instrument Java programming language code. Instrumentation is the addition of byte-codes to methods for the purpose of gathering data to be utilized by tools. Since the changes are pur

    docs.oracle.com

     

    이 인터페이스는 Java 가상 머신이 제공하는 다양한 서비스를 Agent 프로그램에 제공한다.

     

    가장 중요한 기능 중 하나는 클래스의 바이트코드를 변형할 수 있는 기능이다.

    즉, Java 어플리케이션 클래스 로딩  실행 과정에 개입하여 모니터링이 가능하게 할 수 있다.

     

    Agent의 동작

    Agent는 Java의 기본 Instrumentation를 사용하여 제어할 것이다.

    그렇다면 내가 원하는 Agent가 Java Application이 시작될 때 같이 로딩되어 모니터링을 가능하게 하려면 어떻게 해야 할까?

     

    원래 Java 파일의 main이 호출되기 전, 더 쉽게 말하자면 원래 실행할 Java application의 main()을 실행하기 전,

    (즉, JVM이 시작되기 전) Agent의 코드를 먼저 실행해야 한다! (정적 로딩)

     

    premain은 JVM이 시작될 때 호출되며, agentmain은 JVM이 이미 실행 중일 때 호출된다.

    그러므로 이 premain 메소드를 이용할 것이다.

     

    premain

    premain 메서드의 시그니처는 정확히 public static void premain(String agentArgs, Instrumentation inst), 또는 public static void premain(String agentArgs) 형태여야 한다.

     

    package org.agent;
    
    import java.lang.instrument.Instrumentation;
    import java.time.LocalDateTime;
    
    public class MyAgent {
    
        //JVM이 시작될때 premain 메소드가 실행되고, Transformar 클래스를 클래스 변환자로 등록한다.
        //클래스 변환자는 ClassFileTransformer 인터페이스를 구현한 객체로, 클래스의 바이트 코드를 수정할 수 있다.
        public static void premain(String agentArgs, Instrumentation inst) {
            System.out.println("[Custom MyAgent Run]");
            System.out.println("nowTime : " + LocalDateTime.now() + " my Agent has been invoked with args: " + agentArgs);
            inst.addTransformer(new SimpleClassTransformer());
        }
    }

     

     

    SimpleClassTransformer

     ClassFileTransformer 인터페이스는 Java의 Instrumentation API의 일부로,
     Java 가상 머신(JVM)이 클래스를 로드할 때 그 클래스의 바이트코드를 변형할 수 있는 기능을 제공한다.

     1. '바이트코드 변형' 
     더 쉽게 이야기하면, .class파일이 JVM에 실행되기 전에 class 파일의 내용을 수정하거나, 추가하는 것이다.

     2. 오버라이드
     인터페이스를 구현하기 위해 transform 메서드를 오버라이드 해야한다.
     이 과정에서 parameter를 받는데, 각 인수의 의미는 다음과 같다.

     - ClassLoader loader: 클래스를 로드하는 클래스 로더.
     - String className: 현재 변형하고 있는 클래스의 이름.
     - Class<?> classBeingRedefined: 이미 정의된 경우, 재정의되고 있는 클래스.
     - ProtectionDomain protectionDomain: 클래스의 보호 도메인.
     - byte[] classfileBuffer: 원본 클래스 파일의 바이트코드.

     3. 바이트코드 적용
     transform 메서드의 반환값을 통해 바이트코드가 반환되면
     JVM은 이 코드를 사용하여 Class를(바이트코드에 의해 변경된 클래스를) 정의한다.

     

    package org.agent;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.security.ProtectionDomain;
    
    public class SimpleClassTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            // 여기에서 클래스 바이트코드를 조작하는 로직을 구현할 수 있다.
            return classfileBuffer;
        }
    }

     

     

    JAR 파일 패키징과 MAINFEST.MF

    Agent 프로그램의 코드를 작성 후 JAR 파일로 패키징을 해야 한다. (그래야 일반적으로 라이브러리나 종속성 등으로 가져오기 편하니까)

     

    하지만 이 패키징 과정에서 MANIFEST.MF 파일에 Premain-Class 속성을 설정해야 한다. 이 설정은 JVM에게 premain 메서드를 포함하는 클래스를 알려주는 것이다. 본 서버 역시 이 jar 파일이 premain 메서드가 어디 있는지 알아야 하니까.

     

    Manifest-Version: 1.0
    Premain-Class: MyAgent

     

    내 premain은 MyAgent Class이니 이를 패키징 과정에 넣어주자.

     

    Agent 실행

    원본의 파일에 실행 시 내 Agent 파일을 동봉하여 실행할 수 있도록 java 실행 시 명령어를 동봉하자.

    java -javaagent:'내 Agent 파일'.jar -jar '원래 실행하려고 했던 파일'.jar
    
    # -javaagent 옵션을 통해 jar 파일을 삽입할 수 있다.

     

     

    실습 : 실제 Springboot 안에 적용하기

     

    이러한 베이스를 가지고 JAR 파일 압축 후 내 다른 Springboot 파일에 삽입하여 적용해보자!

     

    1. Manifest 파일 만들기

    이전에 설명했듯, Manifest 파일(MANIFEST.MF)은 JAR 파일의 메타데이터를 포함하는 텍스트 파일이다.

    이 파일은 JAR 파일 내 META-INF 디렉토리에 위치해야 한다.

     

    1-1. 수동으로 파일 생성하기

     

    텍스트 에디터를 사용하여 MANIFEST.MF 파일을 생성하자.

    일반적으로 ' src/main/resources/META-INF' 경로가 사용된다고 한다.

     

    Manifest-Version: 1.0
    Premain-Class: org.agent.MyAgent
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true

     

    프로젝트 아래, 다음과 같은 경로이다.

     

     

    1-2. build.gradle로 자동으로 생성하기

    Gradle 빌드 스크립트에서 해당 파일을 JAR 파일에 포함시키도록 설정할 수 있다.

    build.gradle에서 다음과 같이 스크립트를 작성한다면, 이전과 달리 기본적으로 MF파일이 만들어진다.

     

    plugins {
        id 'java'
    }
    
    group = 'org.example'
    version = '1.0-SNAPSHOT'
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        testImplementation platform('org.junit:junit-bom:5.9.1')
        testImplementation 'org.junit.jupiter:junit-jupiter'
    }
    
    test {
        useJUnitPlatform()
    }
    
    //Build.gradle에서 다음과 같이 지정하면, 이전처럼 수동으로 만들 필요 없이 기본적으로 MANIFEST.MF 파일을 만들어준다.
    jar {
        manifest {
            attributes(
                    'Manifest-Version': '1.0',
                    'Premain-Class': 'org.agent.MyAgent',
                    'Can-Redefine-Classes': 'true',
                    'Can-Retransform-Classes': 'true'
            )
        }
    }

     

    2. JAR 파일 빌드

    파일 생성 준비가 끝났다면 파일을 빌드하자. gradlew를 사용해도 상관없다.

     

    gradle - task - build - jar

     

     

    빌드된 jar 파일을 보면 MANIFEST.MF가 자동으로 생성된 것을 확인할 수 있다.

     

    3. Springboot 서버에 적용하기

    직접 Tomcat에 설정으로 넣거나,

    Spring Boot 애플리케이션을 실행할 때 커맨드 java 명령을 사용할 수도 있다.

     

    java -javaagent:/full/path/to/agent.jar -jar your-spring-boot-application.jar

     

     

    InteliJ 적용의 경우 아래의 포스팅을 참조하자.

    https://januaryman.tistory.com/120

     

    [IntelliJ] IntelliJ 외부 라이브러리 추가하기(SpringBoot 외부 라이브러리 추가)

    [IntelliJ] IntelliJ 외부 라이브러리 추가하기(SpringBoot 외부 라이브러리 추가) 안녕하세요. 갓대희 입니다. 이번 포스팅은 [ [IntelliJ] IntelliJ 외부 라이브러리 추가하기(gradle 외부 라이브러리 추가) ]

    januaryman.tistory.com

     

     

     

    JVM 인수를 통한 Agent 등록

     

    의존성을 넣었으나, Java 에이전트를 사용하기 위해서는, -javaagent JVM 인수를 통해 명시적으로 JVM에 에이전트를 등록해야 한다.

     

    다음 단계를 따라 실행 구성에 -javaagent 인수를 추가하자.

     

    Run/Debug Configurations에 접근한다.

    VM options 필드를 찾는다 (없다면, 'Modify options' 를 클릭하여 추가할 수 있다).

     

     

    VM 옵션 필드에 인수를 추가하자.

    -javaagent:full/path/to/AgentV1_0201-1.0-SNAPSHOT.jar
    
    여기서 말하는 full path는 jar파일이 정말 물리적으로 존재하는 위치를 이야기한다.

     

    이를 적용하고 실행 시, spring boot 로고 이전 premain에 있던 로그가 실행됨을 확인할 수 있다.