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.xml 에 ByteBuddyAgent를 추가해 보겠습니다 .
<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에서 찾을 수 있습니다 .