바이트 코드 조작

이연중·2021년 1월 22일
0

JAVA

목록 보기
4/20

바이트 코드 조작으로 클래스 로딩 직전에도 클래스 파일만 있으면, 정보를 빼오거나 새로운 클래스를 만드는 등 여러가지를 할 수 있음

코드 커버리지 측정

테스트 코드가 소스코드의 얼만큼을 테스트 했는지에 대한 측정

JaCoCo

pom.xml에 플러그인을 추가

<build>
    <plugins>
        <plugin>
        	<groupId>org.jacoco</groupId>
   			<artifactId>jacoco-maven-plugin</artifactId>
    		<version>0.8.4</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>

cmd창에 "mvn clean verify"를 입력해 메이븐 빌드

커버리지 만족 못하면 빌드 실패하도록 설정(min 50%)

<execution>
	<id>jacoco-check</id>
    <goals>
    	<goal>check</goal>
    </goals>
    <configuration>
    	<rules>
        	<rule>
            	<element>PACKAGE</element>
            	<limits>
                	<limit>
                    	<counter>LINE</counter>
                        <value>COVEREDRATIO</value>
                        <minimum>0.50</minimum>
                     </limit>
                </limits>
              </rule>
         </rules>
            </configuration>
</execution>

바이트 코드 조작으로 코드에 포인트를 심어 해당 포인트를 거칠때마다 카운트를 한다. 이제부터 이러한 툴이 어떤식으로 동작하는지 알아보려한다.

마술코드

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

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

출력

공백 출력

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

클래스 로더는 클래스를 읽을 때, 기존의 Moja라는 클래스를 읽고 ByteBuddy로 만든 Moja를 읽어들인다. 조작전 Moja가 먼저 로딩되었기에 그대로 실행하면 조작전 클래스의 메서드가 실행될 것이다.

ByteBuddy를 이용해 위 마술코드를 다시 작성한다.

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

우선, 위 의존성을 추가한다.

public class Masulsa{
	public static void main(String[] args){
        try{
			new ByteBuddy().redefine(Moja.class)
            .method(named("pullOut")).intercept(FixedValue.value("Rabbit!"))
            .make().saveIn();//saveIn안에는 Moja 클래스의 경로를 넣어주면 됨. 									   classes까지!
        }catch(IOException e){
			e.printStackTrace();
        }
        System.out.println(new Moja().pullOut());
	}
}

출력

Rabbit!

바이트코드 실행 -> 바이트코드 제거 후 모자 클래스의 pullOut 메소드 실행 -> 토끼등장!

=> 클래스 로딩 순서에 의존적

클래스 로딩 순서에 상관없이 이같이 구현하기 위해서는?

Javaagent

Agent 생성 -> .jar파일 만듦 -> JVM option에 .jar파일 경로 추가
클래스 로딩시에 적용되기 때문에 클래스 파일 자체가 바뀌지는 않는다.(기존 코드를 건드리지 않음) 로딩시 자바 에이전트를 거쳐 변경된 바이트 코드를 읽어오고, 이를 메모리에 적재한다.

Javaagent 붙여서 사용하기

  • 클래스 로더가 클래스를 읽어올 때 Javaagent를 거쳐 변경된 바이트 코드를 읽어들여 사용한다. 기존 코드의 변경을 하지 않고 변경 사항을 적용할 수 있는 것이다.

Javaagent를 사용한 마술코드

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);
    }
}

이렇게 Agent를 만들고, 이를 .jar로 패키징 하면서 특정 값을 넣어줘야 한다.

밑에와 같이 <build>를 추가하고 <manifestEntries>안에 <Premain-Class>이하 것들을 추가한다.(manifest를 jar 플러그인으로 만들기 위해)
pom.xml

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

  <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>me.alwayslee.MasulsaAgent</Premain-Class>
              <Can-Redefine-Classes>true</Can-Redefine-Classes>
              <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>

Javaagent JAR 파일 만들기

  • 붙이는 방식에 따라 premain(시작시), agentmain(런타임 중 동적으로)
  • Instrumentation 사용
  • mvn clean packaging

JVM option

  • VM option에 .jar 파일의 경로를 넣어준다
  • -javaagent: ~(.jar 파일 경로)

바이트 코드 조작 툴 활용 예

프로그램 분석

  • 코드에서 버그 찾기
  • 코드 복잡도 계산

클래스 파일 생성

  • 프록시
  • 특정 API 호출 접근 제한
  • 스칼라와 같은 언어의 컴파일러

자바 코드의 변경 없이 변경 사항 적용 가능

  • 프로파일러(newrelic: 성능 측정 툴이 자신이 원하는 값을 추출하게 함)
  • 최적화(불필요한, 중복되는 코드 제거)
  • 로깅

.......

스프링이 컴포넌트 스캔을 하는 방법(asm)

  • 컴포넌트 스캔으로 빈으로 등록할 후보 클래스 정보는 찾는데 사용
  • ClassPathScanningCandidateComponentProvider -> SimpleMetadateReader
  • ClassReader와 Visitor를 사용해 클래스에 있는 메타 정보를 읽어옴

참고 영상 및 툴

참고

https://www.inflearn.com/course/the-java-code-manipulation

profile
Always's Archives

0개의 댓글