1概要

簡単に言うと、http://bytebuddy.net/#/[ByteBuddy]は実行時に動的にJavaクラスを生成するためのライブラリです。

この最新の記事では、フレームワークを使用して既存のクラスを操作し、必要に応じて新しいクラスを作成し、さらにメソッド呼び出しをインターセプトすることもします。


2依存関係

まずプロジェクトに依存関係を追加しましょう。 Mavenベースのプロジェクトでは、この依存関係を

pom.xml

に追加する必要があります。

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

Gradleベースのプロジェクトでは、

build.gradle

ファイルに同じ成果物を追加する必要があります。

compile net.bytebuddy:byte-buddy:1.7.1

最新版はhttps://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22net.bytebuddy%22%20AND%20a%3A%22byte-buddy%22[Maven Centralにあります]。


3実行時にJavaクラスを作成する

既存のクラスをサブクラス化して動的クラスを作成することから始めましょう。古典的な

Hello World

プロジェクトを見てみましょう。

この例では、

Object.class

のサブクラスであるtype(

Class

)を作成し、

toString()

メソッドをオーバーライドします。

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

次に、

ByteBuddyのインスタンスを作成しました。次に、

sub.class()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リフレクションメソッドです。引数なしのコンストラクタで

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に再ロードすることで実行できます。

まず、

pom.xmlに

ByteBuddyAgent__を追加しましょう。

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

最新版はhttps://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22net.bytebuddy%22%20AND%20a%3A%22byte-buddy-agent%22[foundここにあります]。

それでは、先ほど

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

ライブラリの機能と、動的クラスを効率的に作成するための使い方について詳しく説明しました。

そのhttp://bytebuddy.net/#/tutorial[documentation]では、内部の仕組みやライブラリのその他の側面について詳しく説明しています。

そしていつものように、このチュートリアルの完全なコードスニペットはhttps://github.com/eugenp/tutorials/tree/master/libraries[over Github]で見つけることができます。