소스코드에 대한 테스트 케이스가 얼마나 작성되어 코드를 얼마나 커버했는지 측정하는 라이브러리인데 아래와 같이 if 문 실행까지 체크가 가능하다.
어떻게 플러그인하나를 추가했다고 우리가 작성한 코드를 인식하고 그에 대한 테스트가 이루어졌는지 알 수 있을까?
심지어 우리 코드(.java 파일,.class 파일)는 변경이 되지 않았는데 말이다!
이는 바로 JVM 힙영역(메모리)에 클래스정보가 올라가기전에 클래스로드 시점에 인터셉트 하여 바이트코드를 조작하기 때문이다.
때문에 프로그램을 실행하면서 컴파일한 .class 파일(바이트코드)도 변경되지 않고 Jacoco 의 로직이 추가되는 것이다!
JVM 힙 메모리영역에 올라간 바이트코드는 우리가 컴파일한 .class 파일과는 다르다. 바이트코드는 클래스가 로딩되기 직전에도 이미 컴파일단계에서 .class 파일이 만들어지기 때문에 조작가능하다.
이렇게 기존 코드를 건드리지 않으며 추가 목적을 달성하는 것을 Transparent 하다고 한다.
Java Agent 를 사용하여 클래스로드 시점에 바이트코드 조작하기
바이트코드를 조작당할 Client 프로젝트 - [Project 1]
아래와 같이 간단한 구조로 만들었다.
public class HomePage {
public String render(){
System.out.println("HomePage 가 렌더링 되었습니다.");
return "";
}
}
public class MyApp {
public static void main(String[] args) {
HomePage homePage = new HomePage();
System.out.println(homePage.render());
System.out.println(homePage.render());
}
}
조작 전 실행결과 - [Project 1]
콘솔에 빨간색으로 강조한 영역이 보이는가? 사실 우리의 코드는 이미 조작당하고 있다. (ㄷㄷㄷ) IntelliJ 도 JavaAgent 를 사용하여 우리의 코드를 조작하고 있었다...
바이트코드를 조작할 jar 파일 만들기 - [Project 2]
새로운 프로젝트를 생성해 바이트코드를 조작할 jar 파일을 만들어야 한다. premain 은 JavaAgent를 로딩할 때 필요한 메서드이며, JavaAgent 는 ClassLoad 시점에 실행된다.
부족하지만 코드 커버리지를 측정하듯이 해당 함수가 호출될때 호출이 몇번되었는지 체크하도록 바이트코드를 조작해보았다.
MyAgent.java - [Project 2]
package com;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst){
new AgentBuilder.Default()
.type(ElementMatchers.any()) // 모든 타입에 대하여
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
return builder.method(named("render")) // render() 메서드에 대해
.intercept(MethodDelegation.to(MyInterceptor.class));
}
}).installOn(inst);
}
public static class MyInterceptor {
static int callCount;
public static String intercept(@SuperCall Callable<String> zuper) throws Exception {
zuper.call();
callCount++;
return callCount + " 번 호출하셨네요. 나는 다 알아요 ㅎㅎ";
}
}
}
pom.xml 추가 - [Project 2]
// 바이트코드를 조작하는 라이브러리
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.1</version>
</dependency>
</dependencies>
// jar 로 만들 플러그인
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<index>true</index>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<mode>development</mode>
<url>${project.url}</url>
<key>value</key>
<Premain-Class>com.MyAgent</Premain-Class> // 해당프로젝트에서 premain을 사용하는 클래스 위치
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
.jar 파일로 만들기 - [Project 2]
해당 명령어를 입력하여 jar 파일로 만든다.
mvn clean package
바이트코드를 조작하기 - [Project 1]
VM 옵션 추가 - [Project 1]
-javaagent:{jar파일경로} 를 입력한다
pom.xml 추가 - [Project 1]
bytebuddy 를 추가하는 이유는 agent 코드가 bytebuddy 로 만들어졌기 때문이다.
Spring, Hibernate 등 유명라이브러리는 본인들 라이브러리내에 bytebuddy 같은 바이트코드조작 라이브러리를 포함시켜놓았다.