1. 概要

簡単に言えば、 ByteBuddy は、実行時にJavaクラスを動的に生成するためのライブラリです。

この要点の記事では、フレームワークを使用して既存のクラスを操作し、オンデマンドで新しいクラスを作成し、メソッド呼び出しをインターセプトします。

2. 依存関係

まず、プロジェクトに依存関係を追加しましょう。 Mavenベースのプロジェクトの場合、この依存関係をpom.xmlに追加する必要があります。

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

Gradleベースのプロジェクトの場合、同じアーティファクトをbuild.gradleファイルに追加する必要があります。

compile net.bytebuddy:byte-buddy:1.11.20

最新バージョンはMavenCentralにあります。

3. 実行時のJavaクラスの作成

既存のクラスをサブクラス化して動的クラスを作成することから始めましょう。 従来のHelloWorldプロジェクトを見ていきます。

この例では、 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 、およびを選択しました toString() スーパークラスの( Object.class )使用 ElementMatchers

最後に、 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.classtoString()実装のみが呼び出されるため、機能しないことに注意してください。

newInstance()は、このByteBuddyオブジェクトで表されるタイプの新しいインスタンスを作成するJavaリフレクションメソッドです。 newキーワードを引数なしのコンストラクターで使用するのと同様の方法で。

これまでのところ、動的型のスーパークラスのメソッドをオーバーライドして、独自の固定値を返すことしかできませんでした。 次のセクションでは、カスタムロジックを使用してメソッドを定義する方法について説明します。

4. メソッドの委任とカスタムロジック

前の例では、 toString()メソッドから固定値を返します。

実際には、アプリケーションにはこれよりも複雑なロジックが必要です。 カスタムロジックを促進して動的タイプにプロビジョニングする効果的な方法の1つは、メソッド呼び出しの委任です。

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()に委任しましょう。 これにより、実行時に新しく作成したクラスに、純粋な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という名前のクラスを作成しました。 次に、 String を返し、 publicアクセス修飾子を持つメソッドcustom、を定義します。

前の例で行ったように、メソッドへの呼び出しをインターセプトし、このチュートリアルの前半で作成したBar.classに委任することでメソッドを実装しました。

6. 既存のクラスの再定義

動的に作成されたクラスを操作してきましたが、すでにロードされているクラスも操作できます。 これは、既存のクラスを再定義(またはリベース)し、ByteBuddyAgentを使用してそれらをJVMにリロードすることで実行できます。

まず、ByteBuddyAgentpom.xmlに追加しましょう。

<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にあります。