Java/개념

바이트코드?

ByteCode

바이트코드란 가상컴퓨터에서 돌아가는 실행프로그램을 위한 이진 표현법입니다.

자바의 JVM을 학습하게 되면서 알게 된 개념으로, 기계어로 변환되기 이전에 JVM에서 동작하기위해 생성되는 .class 파일이 이 바이트코드에 해당합니다.

D:\IdeaProjects\JavaLearn\src\main\java\me\ddings73>javap -c App.class
Compiled from "App.java"
public class me.ddings73.App {
  public me.ddings73.App();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13                 // String Hello Java!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

바이트코드는  커맨드라인에서 javap -c 명령을 이용하여 확인이 가능합니다.



//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package me.ddings73;

public class App {
    public App() {
    }

    public static void main(String[] args) {
        System.out.println("Hello Java!");
    }
}

이 외에 intellij에서는 target/classes/에서 사람이 알아보기 쉽게 디컴파일된 바이트코드를 확인할 수 있습니다.



바이트코드로 뭘 할수 있을까?

자바에서 바이트코드는 사용자가 입력한 소스코드가 반드시 거쳐야하는 경유지에 해당합니다.
어떤 소스코드를 작성하든 JVM에서 실행되기 위해서는 .class파일로의 변환이 이루어져야 하기 때문입니다.

 

만약 바이트코드를 조작할 수 있다면 어떤일이 가능할까? 라는 생각을 해볼만한 가치는 충분하다고 생각합니다.

 

바이트코드가 생성되는 그 순간에 코드를 조작하는 것이 가능하다면, 프로그램의 실행흐름을 변경하거나 기존에 존재하지않던 코드를 추가하는 등의 작업이 가능할 것입니다.

그 예시로 코드 커버리지를 들 수 있습니다.

 

코브 커버리지?

코드 커버리지란, 내가 작성한 테스트가 실제 코드를 얼마나 검증하는지를 나타내는 것을 의미합니다.

관련된 툴들은 다음과 같은 것들이 존재합니다.

 

 

어떤 동작을 할까?

저는 현재 자바 공부를 하고있기 떄문에 자바의 코드 커버리지 툴인 JaCoCo를 사용해서 간단한 코드를 대상으로 실행해보았습니다.

public class Univ {
    private String name;
    private int Student_no;

    public Univ(String name, int student_no) {
        this.name = name;
        Student_no = student_no;
    }

    public boolean Student_no_Check(int Student_no){
        if(this.Student_no != Student_no){
            return false;
        }else{
            return true;
        }
    }
}
public class UnivTest {

    @Test
    public void teststudent_no_Check() {
        Univ univ = new Univ("ddings", 1111111);
        assertTrue(univ.Student_no_Check(1111111));
    }
}

먼저 간단한 코드와 테스트코드를 작성하였습니다.

 

 

 

  <build>
    <plugins>
      <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.8.6</version>
        <executions>
          <execution>
            <goals>
              <goal>prepare-agent</goal>
            </goals>
          </execution>
          <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
              <goal>report</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

이후에 JaCoCo를 사용하기 위하여 플러그인을 pom.xml 파일에 설정해주고나서 메이븐에서 clean -> verify를 하고나면 target/site에 jacoco관련 디렉토리가 생기는 것을 확인할 수 있습니다.

 

 



 

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

앞서 살펴본 코드 커버리지로 바이트코드 조작을 통해 어떤 작업을 할 수 있는지 살펴볼 수 있었습니다.

이러한 바이트코드를 조작하는 라이브러리는 여러가지 종류가 존재합니다.

이러한 라이브러리등을 이용하여 바이트코드를 조작할 수 있습니다.



한번 살펴보자

public class People {
    public String name;
    public int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String WhoAmI(){
        return "";
    }
}
public class Introduce
{
    public static void main( String[] args )
    {
        People people = new People("ddings", 24);
        System.out.println(people.WhoAmI());
    }
}

바이트코드 라이브러리를 간단하게 사용해보기 위하여 People, Introduce 클래스를 만들고, WhoAmI 메소드를 실행하면 이름 : 나이 의 꼴로 출력 되는것을 목표로 진행해 봤습니다.


어떤 라이브러리를 사용해야 할까?

어떤 라이브러리를 사용해야 하는가에 있어서는 내부적으로 ASM을 의존하고있으며 배우기 쉬운 Bytebuddy를 사용하기로 결정했습니다.

 

 


진짜 부딪혀보기

<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.10.19</version>
</dependency>

먼저 Bytebuddy를 사용하기 위하여 의존성을 추가해줬습니다.

그 후에는 사용법을 알아야하기 때문에 공식문서 API를 조금 살펴보았습니다.

 

Byte Buddy - runtime code generation for the Java virtual machine

 

bytebuddy.net

 

 

목표설정

public class PeopleTest {

    @Test
    public void test_whoAmI() {
        People people = new People("ddings", 25);
        assertEquals(people.name + " : " + people.age , people.WhoAmI());
    }
}

먼저 ByteBuddy를 이용하여 최종적으로 통과되기를 원하는 테스트를 작성해 줬습니다.
물론 지금의 WhoAmI()는 아무것도 반환하지 않기때문에 테스트가 통과하지 않습니다.



 

목표에 다가가기

테스트에 통과하기 위해서는 WhoAmI() 의 리턴값을 바꿔주어야하며, 그 방법은 다음과 같습니다.

 

people = new People("ddings", 25);

try {
    new ByteBuddy().redefine(People.class)
            .method(named("WhoAmI")).intercept(FixedValue.value(people.name + " : " + people.age))
            .make()
            .saveIn(new File("target/classes/"));
} catch (IOException e) {
    e.printStackTrace();
}

ByteBuddy()를 이용하여 People.class의 WhoAmI 메소드를 재정의하여 생성되는 target/classes/경로에 저장하겠다는 코드입니다.

 

 

 

public class PeopleTest {
    People people;
    @Before
    public void setup(){
        people = new People("ddings", 25);

        try {
            new ByteBuddy().redefine(People.class)
                    .method(named("WhoAmI")).intercept(FixedValue.value(people.name + " : " + people.age))
                    .make()
                    .saveIn(new File("target/classes/"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test_whoAmI() {
        assertEquals(people.name + " : " + people.age , people.WhoAmI());
    }
}

해당하는 코드를 테스트 실행이전에 먼저 실행해줘서 .class파일을 생성해주면 성공적으로 테스트를 통과할 수 있습니다.

 

이를 통하여 ByteBuddy를 이용하여 Runtime에 코드를 수정하고 클래스파일을 생성해내는 모습을 볼 수 있었습니다.

 

 

public class Introduce
{
    public static void main( String[] args )
    {
        People people = new People("ddings", 24);
        try {
            new ByteBuddy().redefine(People.class)
                    .method(named("WhoAmI")).intercept(FixedValue.value(people.name + " : " + people.age))
                    .make()
                    .saveIn(new File("target/classes/"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        System.out.println(people.WhoAmI());
    }
}

바이트코드를 이용한 이 코드에서 원하는 결과를 얻을 수 없습니다.
그 이유는 ByteBuddy를 이용하여 메소드를 재정의하기 전에 이미 WhoAmI()호출로 인하여 해당 클래스파일이 만들어지기 때문입니다.

이 문제는 ByteBuddyAgent 의존성을 추가하여 클래스를 reload 시키는 방법과 직접 나만의 javaagent를 만들어서 의존성을 추가시키는 방법으로 해결이 가능합니다.

 

 

ByteBuddyAgent

<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy-agent -->
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>

먼저 해당 의존성을 추가해줍니다.

 

 

package me.ddings73;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.implementation.FixedValue;

import java.io.File;
import java.io.IOException;

import static net.bytebuddy.matcher.ElementMatchers.named;

public class Introduce
{
    public static void main( String[] args )
    {
        People people = new People("ddings", 24);

        ByteBuddyAgent.install();
        new ByteBuddy().redefine(People.class)
                .method(named("WhoAmI")).intercept(FixedValue.value(people.name + " : " + people.age))
                .make()
                .load(People.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());


        System.out.println(people.WhoAmI());
    }
}

그 후에 코드에 클래스로더를 이용하여 reload를 시켜주게되면 원하는 대로 결과가 출력됩니다.



 

javaagent

javaagent를 만들기위해서 관련 문서 백기선님의 더 자바, 코드를 조작하는 다양한 방법을 참조하였습니다.

아까 ByteBuddyAgent 도 그렇고 지금의 javaagent도 그렇고 일단 이게 정확히 무엇인지 모르기때문에 사용하는데 거부감이 생겼었습니다.

 

javaagent는 JVM내에서 실행중인 자바 프로그램에 간섭할 수 있는 서비스를 제공합니다.
즉, 실행중인 애플리케이션을 가로채서 바이트코드를 조작할 수 있는 기능을 제공합니다.
javaagent는 Java Instrumentation API의 일부입니다.

즉, 실행중에 프로그램에 간섭하여 바이트코드를 조작하기 위해서 javaagent를 만들어야했고 그를 위해서 java Instrumentation을 참조하였습니다.

 

 

 

package me.ddings73;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.Instrumentation;

import static net.bytebuddy.matcher.ElementMatchers.named;

public class PeopleAgent {
    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("WhoAmI")).intercept(FixedValue.value("transformed"));
                    }
                }).installOn(inst);
    }
}

 

 

agent를 만들기 위한 코드

이 코드를 패키징하여 생성되는 .jar파일을 의존성으로 넘겨줍니다.

 

VM Option에 생성된 jar파일 경로를 설정해주고 실행합니다.

 

 

 

package me.ddings73;

public class Introduce
{
    public static void main( String[] args )
    {
        People people = new People("ddings", 24);
        System.out.println(people.WhoAmI());
    }
}
transformed

원래 클래스내의 필드값을 참조하여 출력하게 만들고 싶었으나 필드값 참조를 어떻게 해야할지 잘 모르겠어서 문자열로 출력하였습니다.

 

 

 

 

출처 및 참고자료