ByteBuddy 라이브러리를 통해 특정 클래스에 대해 새롭게 정의된 클래스 파일을 덮어씌우면, 이후 코드에서 객체를 생성하고 매소드를 호출할 때, 원래 코드와는 전혀 다른 값이 반환될 수 있다.
//Main
package _02_bytecode.masul;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.FixedValue;
import java.io.File;
import java.io.IOException;
import static net.bytebuddy.matcher.ElementMatchers.named;
public class Masulsa {
public static void main(String[] args) {
//1. Moja.class 내의 pullOut 매소드가 고정값인 Rabbit! 을 반환하도록 조작한 새로운 .class 파일로 덮어 씌우기
try {
new ByteBuddy()
.redefine(Moja.class)
.method(named("pullOut"))
.intercept(FixedValue.value("Rabbit!"))
.make()
.saveIn(new File("/Users/mhson/Documents/github-miniminis/deep-dive-in-java/deep-java/build/classes/java/main/"));
} catch (IOException e) {
e.printStackTrace();
}
//2. 위의 코드 실행 후 주석처리. 아래 코드 실행 -> Rabbit! 이 반환된다.
System.out.println(new Moja().pullOut()); //return Rabbit!
}
}
//Moja
package _02_bytecode.masul;
public class Moja {
public String pullOut() {
return "default";
}
}
두 코드를 동시에 실행할 수 없는 이유는?
이미 1번 구간의 코드를 실행할 때, Moja.class 를 한번 읽어들인다. 읽는 시점에는 해당 클래스의 pullOut() 매소드는 바뀌기 전 값인 "default" 값을 가지고 있다.
이미 클래스 로더에서 Moja 클래스를 읽어들였기 때문에, 1번 구간 수행 후, Moja 의 클래스 파일이 변경되었다고 하더라도 2번 구간을 수행할 때, 다시 새롭게 클래스 파일을 읽어들이지 않는다.
결과적으로 1번 구간에서 최초 로드 후 클래스 파일은 변경되었지만 (Rabbit 반환코드) 2번 구간에서는 바뀐 클래스 파일을 새롭게 읽어들이지 않아서 이전에 읽어들인 Moja 클래스 파일 (default 반환코드) 을 실행하게 되는 것이다.
아래처럼 Moja.class 를 읽지 않는 경우에는 동시에 실행할 수 있다.
//Main
package _02_bytecode.masul2;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.pool.TypePool;
import java.io.File;
import java.io.IOException;
import static net.bytebuddy.matcher.ElementMatchers.named;
public class Masulsa {
public static void main(String[] args) {
//1. 클래스 파일을 로드하지 않고 클래스 파일을 변경
ClassLoader classLoader = Masulsa.class.getClassLoader();
TypePool typePool = TypePool.Default.of(classLoader);
try {
new ByteBuddy()
.redefine(typePool.describe("_02_bytecode.masul2.Moja").resolve(), ClassFileLocator.ForClassLoader.of(classLoader))
.method(named("pullOut"))
.intercept(FixedValue.value("Rabbit!"))
.make()
.saveIn(new File("/Users/mhson/Documents/github-miniminis/deep-dive-in-java/deep-java/build/classes/java/main/"));
} catch (IOException e) {
e.printStackTrace();
}
//2. 변경된 클래스 파일을 로드 후 실행 : Rabbit 반환
System.out.println(new Moja().pullOut()); //Rabbit!
}
}
//Moja
package _02_bytecode.masul2;
public class Moja {
public String pullOut() {
return "default";
}
}
하지만, 위의 코드는 결국 실행 순서에 따라 결과가 달라지므로, 아직 한계점이 있다. 다른 곳에서 Moja 클래스를 먼저 로드해서 읽어버리는 경우에는 먹히지 않는 방법이다.
완벽하게 바이트 코드를 조작하려면, 결국 다른 곳에서 먼저 위의 조치를 취해주어야 한다.
javaagent 를 이용해서 완벽하게 바이트코드를 조작해보자.
4. javaagent 사용하여 어플리케이션 실행시에 바이트 코드 조작하기
4-1. 새로운 프로젝트 생성
masulsa-agent
4-2. premain() 함수 작성하기
package me.flash;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
import static net.bytebuddy.matcher.ElementMatchers.named;
public class MasulsaAgent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform(
(builder, typeDescription, classLoader, module, protectionDomain)
-> builder.method(named("pullOut")).intercept(FixedValue.value("Rabbit!"))
).installOn(instrumentation);
}
}
4-3. jar manifest 변경하기
plugins {
id 'java'
}
//jar file manifest change
jar {
manifest.attributes(
'Premain-Class' : 'me.flash.MasulsaAgent',
'Can-Redefine-Classes' : true,
'Can-Retransform-Classes' : true
)
}
group 'me.flash'
version '1.0-SNAPSHOT'
4-4. 터미널에서 gradle jar 실행하여 build 결과물 확인
gradle jar
jar 파일은 확장자를 .zip 으로 수정하면 내부 내용을 살펴볼 수 있다.
4-5. 기존 application 에서 VM 옵션 추가
agent 프로젝트를 다시 jar 로 빌드한 뒤, 원래의 어플리케이션 코드로 돌아와 기존의 bytebuddy 코드를 모두 제거한다.
//Main
package _02_bytecode.masul3;
public class Masulsa {
public static void main(String[] args) {
//javaagent changes bytecode in premain()
System.out.println(new Moja().pullOut()); //Rabbit!
}
}
//Moja
package _02_bytecode.masul3;
public class Moja {
public String pullOut() {
return "default";
}
}