1. 개요

간단히 말해서 ByteBuddy는 런타임에 동적으로 Java 클래스를 생성하기 위한 라이브러리입니다.

이 요점 기사에서는 프레임워크를 사용하여 기존 클래스를 조작하고, 필요에 따라 새 클래스를 만들고, 메서드 호출을 차단할 것입니다.

2. 의존성

먼저 프로젝트에 의존성을 추가해 보겠습니다. Maven 기반 프로젝트의 경우 이 의존성을 pom.xml 에 추가해야 합니다 .

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

Gradle 기반 프로젝트의 경우 build.gradle 파일에 동일한 아티팩트를 추가해야 합니다.

compile net.bytebuddy:byte-buddy:1.12.13

최신 버전은 Maven Central 에서 찾을 수 있습니다 .

3. 런타임 시 Java 클래스 생성

기존 클래스를 서브클래싱하여 동적 클래스를 만드는 것으로 시작하겠습니다. 고전적인 Hello World 프로젝트를 살펴보겠습니다 .

이 예제에서는 Object.class 의 하위 클래스인 유형( Class )을 만들고 toString() 메서드를 재정의합니다 .

DynamicType.Unloaded unloadedType = new ByteBuddy()
  .subclass(Object.class)
  .method(ElementMatchers.isToString())
  .intercept(FixedValue.value("Hello World ByteBuddy!"))
  .make();

우리가 방금 한 것은 ByteBuddy 의 인스턴스를 만드는 것이었습니다 . 그런 다음 subclass() API를 사용하여 Object.class를 확장하고 ElementMatchers를 사용하여 상위 클래스( Object.class ) 의 toString()을 선택했습니다 .

마지막으로, intercept() 메서드를 사용하여 toString() 구현을 제공 하고 고정 값을 반환합니다.

make () 메서드는 새 클래스 생성을 트리거합니다.

이 시점에서 클래스는 이미 생성되었지만 아직 JVM에 로드되지 않았습니다. 생성된 형식의 이진 형식인 DynamicType.Unloaded 의 인스턴스로 표시됩니다 .

따라서 생성된 클래스를 사용하기 전에 JVM에 로드해야 합니다.

Class<?> dynamicType = unloadedType.load(getClass()
  .getClassLoader())
  .getLoaded();

이제 dynamicType 을 인스턴스화 하고 toString() 메서드를 호출할 수 있습니다.

assertEquals(
  dynamicType.newInstance().toString(), "Hello World ByteBuddy!");

dynamicType.toString() 호출은 ByteBuddy.class 의 toString() 구현 만 호출하므로 작동하지 않습니다 .

newInstance () 는 이 ByteBuddy 객체 가 나타내는 유형의 새 인스턴스를 생성하는 Java 리플렉션 메서드입니다 . no-arg 생성자와 함께 new 키워드를 사용하는 것과 비슷한 방식으로 .

지금까지 우리는 동적 유형의 슈퍼 클래스에 있는 메서드를 재정의하고 우리 고유의 고정 값을 반환할 수 있었습니다. 다음 섹션에서는 사용자 지정 논리로 메서드를 정의하는 방법을 살펴보겠습니다.

4. 메소드 Delegation 및 커스텀 로직

이전 예제에서는 toString() 메서드에서 고정 값을 반환했습니다.

실제로 애플리케이션에는 이보다 더 복잡한 논리가 필요합니다. 사용자 지정 논리를 촉진하고 동적 형식에 프로비저닝하는 효과적인 방법 중 하나는 메서드 호출 Delegation입니다.

sayHelloFoo() 메서드 가 있는 Foo.class를 하위 클래스로 만드는 동적 유형을 만들어 보겠습니다 .

public String sayHelloFoo() { 
    return "Hello in Foo!"; 
}

또한 sayHelloFoo() 와 동일한 서명 및 반환 유형의 정적 sayHelloBar ()를 사용하여 또 다른 클래스 Bar를 생성해 보겠습니다 .

public static String sayHelloBar() { 
    return "Holla in Bar!"; 
}

이제 ByteBuddy 의 DSL을 사용하여 sayHelloFoo() 의 모든 호출을 sayHelloBar() 에 Delegation해 보겠습니다. 이를 통해 런타임 시 새로 생성된 클래스에 순수 Java로 작성된 사용자 지정 논리를 제공할 수 있습니다.

String r = new ByteBuddy()
  .subclass(Foo.class)
  .method(named("sayHelloFoo")
    .and(isDeclaredBy(Foo.class)
    .and(returns(String.class))))        
  .intercept(MethodDelegation.to(Bar.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .sayHelloFoo();
        
assertEquals(r, Bar.sayHelloBar());

sayHelloFoo()를 호출하면 그에 따라 sayHelloBar()가 호출됩니다 .

ByteBuddy는 Bar.class 에서 호출할 메서드를 어떻게 압니까 ? 메서드 서명, 반환 유형, 메서드 이름 및 어노테이션에 따라 일치하는 메서드를 선택합니다.

sayHelloFoo ()sayHelloBar() 메서드는 이름이 동일하지 않지만 메서드 서명 및 반환 유형은 동일합니다.

서명 및 반환 유형이 일치하는 Bar.class 에 호출 가능한 메서드가 두 개 이상 있는 경우 @BindingPriority 어노테이션을 사용하여 모호성을 해결할 수 있습니다.

@BindingPriority는 정수 인수를 사용합니다. 정수 값이 높을수록 특정 구현을 호출하는 우선 순위가 높아집니다. 따라서 아래 코드 스니펫에서는 sayHelloBar()가 sayBar() 보다 선호됩니다 .

@BindingPriority(3)
public static String sayHelloBar() { 
    return "Holla in Bar!"; 
}

@BindingPriority(2)
public static String sayBar() { 
    return "bar"; 
}

5. 방법 및 필드 정의

우리는 동적 유형의 슈퍼 클래스에서 선언된 메서드를 재정의할 수 있었습니다. 클래스에 새 메서드(및 필드)를 추가하여 더 나아가 보겠습니다.

Java 리플렉션을 사용하여 동적으로 생성된 메서드를 호출합니다.

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .name("MyClassName")
  .defineMethod("custom", String.class, Modifier.PUBLIC)
  .intercept(MethodDelegation.to(Bar.class))
  .defineField("x", String.class, Modifier.PUBLIC)
  .make()
  .load(
    getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

Method m = type.getDeclaredMethod("custom", null);
assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar());
assertNotNull(type.getDeclaredField("x"));

Object.class 의 하위 클래스인 MyClassName 이라는 이름의 클래스를 만들었습니다 . 그런 다음 문자열을 반환 하고 공용 액세스 수정자를 갖는 사용자 지정 메서드를 정의합니다 .

이전 예제에서와 마찬가지로 메서드에 대한 호출을 가로채서 이 사용방법(예제)의 앞부분에서 생성한 Bar.class 에 Delegation하여 메서드를 구현했습니다.

6. 기존 클래스 재정의

동적으로 생성된 클래스로 작업했지만 이미 로드된 클래스로도 작업할 수 있습니다. 이는 기존 클래스를 재정의(또는 리베이스)하고 ByteBuddyAgent를 사용하여 JVM으로 다시 로드함으로써 수행할 수 있습니다.

먼저 pom.xmlByteBuddyAgent를 추가해 보겠습니다 .

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

최신 버전은 여기에서 찾을 수 있습니다 .

이제 이전에 Foo.class 에서 생성한 sayHelloFoo() 메서드를 다시 정의해 보겠습니다 .

ByteBuddyAgent.install();
new ByteBuddy()
  .redefine(Foo.class)
  .method(named("sayHelloFoo"))
  .intercept(FixedValue.value("Hello Foo Redefined"))
  .make()
  .load(
    Foo.class.getClassLoader(), 
    ClassReloadingStrategy.fromInstalledAgent());
  
Foo f = new Foo();
 
assertEquals(f.sayHelloFoo(), "Hello Foo Redefined");

7. 결론

이 정교한 사용방법(예제)에서는 ByteBuddy 라이브러리 의 기능과 이를 사용하여 동적 클래스를 효율적으로 생성하는 방법을 광범위하게 살펴보았습니다 .

해당 설명서는 라이브러리의 내부 작업 및 기타 측면에 대한 자세한 설명을 제공합니다.

그리고 항상 그렇듯이 이 예제의 전체 코드 스니펫은 Github에서 찾을 수 있습니다 .

res – REST with Spring (eBook) (everywhere)