Dev 달팽이 @_''

Java - 바이트코드 조작 본문

Java&Spring/더 자바, 코드를 조작하는 다양한 방법

Java - 바이트코드 조작

다본죽 2022. 1. 9. 16:12

Java - 바이트코드 조작

모자에서 토끼를 꺼내는 마술

 

Moja.java

public class Moja {

    public String pullOut(){
        return "";
    }
}

Masulsa.java

public class Masulsa {
	public static void main(String[] args) {
		System.out.println(new Moja().pullOut());
	}
}

Masulsa를 실행하면 아무것도 없는 빈 문자열이 출력될 것이다. 하지만 여기서 우리는 Rabbit 이라는 문자열을 출력하도록 바이트코드를 조작하려고 한다.

 

바이트코드 조작 라이브러리

  • ASM
  • Javassist
  • ByteBuddy

여기서 우리는 ByteBuddy를 사용한다.

 

바이트 버디를 사용하기 위하여 pom.xml에 dependency를 추가한다.

ByteBuddy : https://bytebuddy.net/#/

<dependency>
	<groupId>net.bytebuddy</groupId>
	<artifactId>byte-buddy</artifactId>
	<version>LATEST</version>
</dependency>

다음은 ByteBuddy를 이용하여 Moja 클래스를 재정의 해준다.

public class Masulsa {

    public static void main(String[] args) throws IOException {

        new ByteBuddy().redefine(Moja.class)
                        .method(ElementMatchers.named("pullOut")).intercept(FixedValue.value("Rabbit!"))
                        .make().saveIn(new File("C:\\Users\\Jang\\Desktop\\더자바_코드를 조작하는 다양한 방법\\target\\classes\\"));

//        System.out.println(new Moja().pullOut());
    }
}

이 때, 기존에 있던 프린트문은 주석처리를 해준다. 그 이유는 클래스가 재정의 되기 전에 프린트문에 의해 Moja 클래스가 클래스 로더에 의해 로드되기 때문이다. 

 

다음으로 이를 실행시키고 재정의 부분을 주석처리, 프린트문의 주석을 제거하여 실행시키면 토끼를 꺼낼 수 있다.

 

위에 코드는 클래스를 재정의하고 다시 실행시켜야 토끼를 꺼낼 수 있었다. 이번 방법은 javaagent를 이용하여 한번에 토끼를 꺼내는 방법이다.

 

Javaagent JAR 파일 만들기

 

새로운 프로젝트를 생성하여 MasulsaAgent.java클래스를 생성한다.

다음은 위와 같이 ByteBuddy를 pom.xml에 추가하고  build에 maven jar plugin을 추가한다.

maven jar plugin : https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html

  <dependencies>
    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy</artifactId>
      <version>1.10.22</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
          <archive>
            <index>true</index>
            <manifest>
              <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
              <mode>development</mode>
              <url>${project.url}</url>
              <key>value</key>
              <Premain-Class>me.jdb4497.MasulsaAgent</Premain-Class>
              <Can-Redefine-Classes>true</Can-Redefine-Classes>
              <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>

premain 방식은 시작시 붙이는 방식, agentmain은 런타임 중 동적으로 붙이는 방식이다.

 

Javaagent 붙여서 사용하기

 

클래스 로더가 클래스를 읽어올 때 javaagent를 거쳐서 변경된 바이트 코드를 읽어드려 사용한다.

 

MasulsaAgent.java

public class MasulsaAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        new AgentBuilder.Default()
                .type(ElementMatchers.any())
                .transform((builder, typeDescription, classLoader, javaModule) ->
                        builder.method(named("pullOut")).intercept(FixedValue.value("Rabbit!"))).installOn(inst);
    }
}

이를 패키징해준다

Javaagent 적용

 

이제 기존의 Masulsa 코드로 돌아와서 Javaagent를 추가해준다.

프로젝트 configuration에 들어가서 VM options 에 jar 파일의 경로를 넣어준다.

-javaagent : C:\Users\Jang\Desktop\me.jdb4497\target\me.jdb4497-1.0-SNAPSHOT.jar

이 후 Masulsa를 실행하면 토끼가 꺼내지는 것을 볼 수 있다.

 

첫번째 방법과 두번째 방법의 차이

  • 첫번째 방법은 클래스 파일 자체를 건드림
  • 두번째 방법은 클래스 로딩할 때 일어나서 클래스 파일이 변경되지 않음.
  • 읽어 올 때 일어나므로 메모리 내부가 바뀌어있음

 

바이트 코드 조작 예

  • 프로그램 분석
    • 코드에서 버그 찾는 툴
    • 코드 복잡도 계산
  • 클래스 파일 생성
    • 프록시
    • 특정 API 호출 접근 제한
    • 스칼라 같은 언어의 컴파일러
  • 그 밖에도 자바 소스 코드 건드리지 않고 코드 변경이 필요한 여러 경우에 사용할 수 있다.
    • 프로파일러
    • 최적화
    • 로킹
    • ...
  • 스프링이 컴포넌트 스캔을 하는 방법(ASM)
    • 컴포넌트 스캔으로 빈으로 등록할 후보 클래스 정보를 찾는데 사용
    • ClassPathScanningCandidateComponentProvider -> SimpleMetadataReader
    • ClassReader와 Visitor 사용해서 클래스에 있는 메타 정보를 읽어옴