LoginSignup
5
3

More than 5 years have passed since last update.

【翻訳】Byte Buddy チュートリアル

Last updated at Posted at 2019-03-07

https://bytebuddy.net/#/tutorial bytebuddy のチュートリアルを google翻訳した

Why runtime code generation?

Java言語には比較的厳密な型システムが付属しています。 Javaでは、すべての変数とオブジェクトが特定の型である必要があり、互換性のない型を代入しようとすると常にエラーが発生します。これらのエラーは通常、型を不正にキャストしたときにJavaコンパイラによって、または少なくともJavaランタイムによって発行されます。このような厳密なタイピングは、たとえばビジネスアプリケーションを書くときなどに望ましいことがよくあります。ビジネスドメインは通常、任意のドメイン項目がそれ自体のタイプを表すという明示的な方法で記述できます。これにより、Javaを使用して、間違いが発生源に近い場所で検出された、非常に読みやすくて堅牢なアプリケーションを構築できます。とりわけ、エンタープライズプログラミングにおけるJavaの人気の原因となっているのは、Javaの型システムです。

ただし、その厳密な型システムを強制することによって、Javaは他のドメインでの言語の範囲を制限する制限を課します。例えば、他のJavaアプリケーションによって使用されることになっている汎用ライブラリーを書くとき、私達は私達のライブラリーがコンパイルされるときこれらのタイプが私達に知られていないのでユーザーのアプリケーションで定義されるタイプを参照できません。メソッドを呼び出すため、またはユーザーの未知のコードのフィールドにアクセスするために、JavaクラスライブラリにはリフレクションAPIが付属しています。リフレクションAPIを使用して、未知の型をイントロスペクトしたり、メソッドを呼び出したり、フィールドにアクセスしたりすることができます。残念ながら、リフレクションAPIの使用には2つの大きな欠点があります。

  • リフレクションAPIの使用は、ハードコーディングされたメソッド呼び出しよりも時間がかかります。まず、特定のメソッドを記述するオブジェクトを取得するために、かなり高価なメソッド検索を実行する必要があります。そしてメソッドが呼び出されるとき、これは直接呼び出しに比べて長い実行時間を必要とするネイティブコードを実行するためにJVMを必要とします。しかし、最近のJVMは、JNIベースのメソッド呼び出しが、動的に作成されたクラスに挿入される生成済みバイトコードに置き換えられる、インフレーションと呼ばれる概念を知っています。結局のところ、Javaのインフレーションシステムは非常に一般的なコードを生成するという欠点を抱えています。それは、たとえばボックス化されたプリミティブ型でのみ機能し、パフォーマンスの欠点は完全には解決されません。
  • リフレクションAPIは型保証を破ります:JVMはリフレクションによってコードを呼び出すことができますが、リフレクションAPI自体は型保証ではありません。ライブラリを書くとき、リフレクションAPIをライブラリのユーザに公開する必要がない限り、これは問題になりません。結局のところ、コンパイル時にユーザーコードがわからず、ライブラリコードをその型に対して検証できませんでした。時々、しかしながら、例えばライブラリに私たちのために私たち自身のメソッドの一つを呼び出させることによってリフレクションAPIをユーザーに公開することが必要とされます。 Javaコンパイラがプログラムの型安全性を検証するためのすべての情報を持っているので、リフレクションAPIの使用が問題になるところです。たとえば、メソッドレベルのセキュリティのためにライブラリを実装する場合、このライブラリのユーザは、セキュリティ制約を強制した後に初めてライブラリにメソッドを呼び出させることを望みます。このため、ライブラリは、ユーザがこのメソッドに必要な引数を渡した後に、メソッドを反射的に呼び出す必要があります。しかし、そうすることで、これらのメソッド引数がメソッドのリフレクティブ呼び出しと一致するかどうか、コンパイル時の型チェックは行われなくなります。メソッド呼び出しはまだ検証されていますが、チェックは実行時まで延期されます。そうすることで、Javaプログラミング言語の優れた機能を無効にしました。

これはランタイムコード生成が私達を助けることができるところです。これにより、Javaの静的型チェックを破棄することなく、動的言語でプログラミングするときにのみ通常アクセス可能ないくつかの機能をエミュレートできます。このようにして、両方の長所を最大限に引き出すことができ、さらにランタイムパフォーマンスを向上させることができます。この問題をよりよく理解するために、前述のメソッドレベルのセキュリティライブラリを実装する例を見てみましょう。

Writing a security library

ビジネスアプリケーションは大きくなる可能性があり、アプリケーション内でコールスタックの概要を把握するのが難しい場合があります。アプリケーション内で特定の条件下でのみ呼び出すべき重要なメソッドがある場合、これは問題になる可能性があります。アプリケーションのデータベースからすべてを削除できるリセット機能を実装するビジネスアプリケーションを想像してください。

class Service {
  void deleteEverything() {
    // delete everything ...
  }
}

そのようなリセットはもちろん管理者によってのみ実行されるべきであり、我々のアプリケーションの普通のユーザによって決して実行されるべきではありません。私たちのソースコードを分析することによって、もちろんこれが起こらないことを確かめることができます。しかし、私たちは自分のアプリケーションが成長し、将来変更されることを期待できます。したがって、メソッド呼び出しがアプリケーションの現在のユーザーに対する明示的なチェックによって保護されている、より厳格なセキュリティモデルを実装する必要があります。通常、セキュリティフレームワークを使用して、このメソッドが管理者以外の誰からも呼び出されないようにします。

この目的のために、以下のように、パブリックAPIを持つセキュリティフレームワークを使用していると仮定します:

@Retention(RetentionPolicy.RUNTIME)
@interface Secured {
  String user();
}

class UserHolder {
  static String user;
}

interface Framework {
  <T> T secure(Class<T> type);
}

このフレームワークでは、 Secured アノテーションを使用して、特定のユーザーだけがアクセスできるメソッドをマークする必要があります。 UserHolder は、どのユーザーが現在アプリケーションにログインしているかをグローバルに定義するために使用されます。Framework インタフェースは、与えられた型のデフォルトコンストラクタを呼び出すことによって保護されたインスタンスの作成を可能にします。もちろん、このフレームワークは非常に単純ですが、原則として、これは例えば人気のあるSpring Securityのようなセキュリティフレームワークがどのように機能するかです。このセキュリティフレームワークの特徴は、ユーザーの種類を保持することです。私たちの Framework インターフェースの契約により、私たちはユーザーが受け取る任意の型 T のインスタンスを返すことを約束します。これのおかげで、ユーザはあたかもセキュリティフレームワークが存在しなかったかのように彼自身のタイプと対話することができます。テスト環境では、ユーザーは自分の型の保護されていないインスタンスを作成し、保護されたインスタンスの代わりにこれらのインスタンスを使用することさえできます。あなたはこれが本当に便利であることに同意するでしょう!そのようなフレームワークは、POJO(普通のJavaオブジェクト)と対話することが知られています。これは、ユーザーに独自のタイプを課さない非侵入型フレームワークを記述するために造られた用語です。

今のところ、 Framework に渡される型は T = Service にしかならないことと、 deleteEverything メソッドに @Secured("ADMIN") というアノテーションが付けられていることがわかっていることを想像してください。このようにして、単純にサブクラス化することで、この特定の型の保護されたバージョンを簡単に実装できます。

class SecuredService extends Service {
  @Override
  void deleteEverything() {
    if(UserHolder.user.equals("ADMIN")) {
      super.deleteEverything();
    } else {
      throw new IllegalStateException("Not authorized");
    }
  }
}

この追加クラスを使用して、次のようにフレームワークを実装できます:

class HardcodedFrameworkImpl implements Framework {
  @Override
  public <T> T secure(Class<T> type) {
    if(type == Service.class) {
      return (T) new SecuredService();
    } else {
      throw new IllegalArgumentException("Unknown: " + type);
    }
  }
}

もちろん、この実装はあまり役に立ちません。安全なメソッドのシグネチャにより、このメソッドは任意のタイプのセキュリティを提供できることを示唆していましたが、実際には、既知のサービス以外に何かが発生した場合は例外をスローします。また、これは我々のセキュリティライブラリがライブラリがコンパイルされるときにこの特定のサービスタイプについて知ることを必要とするでしょう。明らかに、これはフレームワークを実装するための実行可能な解決策ではありません。では、どうすればこの問題を解決できるでしょうか。これはコード生成ライブラリのチュートリアルなので、答えは推測できます。必要に応じて、実行時に、 Service クラスが secure メソッドの呼び出しによってセキュリティフレームワークに認識されるようになるときにサブクラスを作成します。コード生成では、与えられた型を取り、実行時にそれをサブクラス化し、保護したいメソッドをオーバーライドすることができます。今回のケースでは、 @Secured でアノテーションが付けられているすべてのメソッドをオーバーライドし、アノテーションの user プロパティから必要なユーザーを読み取ります。多くの一般的なJavaフレームワークは、同様の方法で実装されています。

General information

コード生成とByte Buddyについてすべて学ぶ前に、コード生成を慎重に使用してください。 Javaの型はJVMにとってかなり特別なものであり、ガベージコレクトされていないことがよくあります。したがって、コード生成を使いすぎないようにし、生成コードが唯一の解決策である場合にのみ生成コードを使用して問題を解決してください。ただし、前の例のように未知の型を拡張する必要がある場合は、コード生成がおそらく唯一の選択肢です。セキュリティ、トランザクション管理、オブジェクトリレーショナルマッピング、またはモッキングのためのフレームワークは、コード生成ライブラリの典型的なユーザーです。

もちろん、Byte BuddyはJVM上でコードを生成するための最初のライブラリではありません。ただし、Byte Buddyは他のフレームワークでは適用できないいくつかのトリックを知っていると考えています。 Byte Buddyの全体的な目的は、そのドメイン固有の言語と注釈の使用の両方に焦点を当てることによって宣言的に作業することです。私たちが知っているJVM用の他のコード生成ライブラリは、この方法では動作しません。それにもかかわらず、コード生成のための他のいくつかのフレームワークを調べて、どれが自分に最も適しているかを見つけたいと思うかもしれません。とりわけ、Javaの分野では以下のライブラリーが普及しています。

  • Java proxies
    • Javaクラスライブラリには、特定のインタフェースセットを実装するクラスの作成を可能にするプロキシツールキットが付属しています。この組み込みプロキシサプライヤは便利ですが非常に限られています。上記のセキュリティフレームワークは、インタフェースではなくクラスを拡張したいので、たとえばこの方法では実装できません。
  • cglib
    • コード生成ライブラリはJavaの初期の頃に実装されていましたが、残念ながらJavaプラットフォームの開発に追いついていませんでした。それにもかかわらず、cglibは非常に強力なライブラリのままですが、その活発な開発はかなり曖昧になりました。このため、そのユーザーの多くはcglibから離れました。
  • Javassist
    • このライブラリには、アプリケーションの実行時にJavaバイトコードに変換されるJavaソースコードを含む文字列を受け取るコンパイラが付属しています。 Javaソースコードは明らかにJavaクラスを記述するための素晴らしい方法であるため、これは非常に野心的で、原則として素晴らしいアイデアです。ただし、Javassistコンパイラはその機能においてjavacコンパイラと比較されず、動的に文字列を構成してより複雑なロジックを実装するときに簡単なミスを許容します。さらに、JavassistにはJCLのプロキシユーティリティに似たプロキシライブラリが付属していますが、クラスを拡張することができ、インタフェースに限定されません。ただし、Javassistのプロキシツールの範囲は、そのAPIと機能において同等に制限されています。

あなた自身のためにフレームワークを評価しなさい、しかし我々はあなたがさもなければ無駄に検索するであろう機能と便利さをByte Buddyが提供すると信じます。 Byte Buddyには、プレーンなJavaコードを記述したり、独自のコードに強力な型指定を使用したりすることによって、非常にカスタムなランタイムクラスを作成できるようにする表現豊かなドメイン固有言語が付属しています。同時に、Byte Buddyは非常にカスタマイズの余地があり、箱から出してくる機能を制限することはありません。必要に応じて、実装したメソッドにカスタムバイトコードを定義することもできます。しかし、どんなバイトコードなのか、それがどのように機能するのかを知らなくても、フレームワークを深く掘り下げることなく多くのことを実行できます。たとえば、Hello Worldを見ましたか。例は? Byte Buddyを使うのはそれほど簡単です。

もちろん、コード生成ライブラリを選択するときに考慮する必要があるのは、快適なAPIだけではありません。多くのアプリケーションにとって、生成されたコードの実行時の特性が最良の選択を決定する可能性が高いです。また、生成されたコード自体の実行時間を超えて、動的クラスを作成するための実行時間も問題になる可能性があります。私たちは最速だと主張するそれは図書館の速度のための有効な測定基準を提供するのが難しいのと同じくらい簡単です。それでも、そのような測定基準を基本的な方向付けとして提供したいと思います。ただし、これらの結果は必ずしも個々の測定基準を実施する必要がある特定のユースケースに必ずしも変換されないことに注意してください。

メトリックについて説明する前に、生データを見てみましょう。次の表は、標準偏差が中括弧で囲まれている操作の平均実行時間をナノ秒単位で示しています。

baseline Byte Buddy cglib Javassist Java proxy
trivial class creation 0.003 (0.001) 142.772 (1.390) 515.174 (26.753) 193.733 (4.430) 70.712 (0.645)
interface implementation 0.004 (0.001) 1'126.364 (10.328) 960.527 (11.788) 1'070.766 (59.865) 1'060.766 (12.231)
stub method invocation 0.002 (0.001) 0.002 (0.001) 0.003 (0.001) 0.011 (0.001) 0.008 (0.001)
class extension 0.004 (0.001) 885.983 (7.901) 5'408.329 (52.437) 1'632.730 (52.737) 683.478 (6.735)
super method invocation 0.004 (0.001) 0.004 (0.001) 0.004 (0.001) 0.021 (0.001) 0.025 (0.001) -

静的コンパイラと同様に、コード生成ライブラリは高速コードの生成と高速コードの生成の間のトレードオフに直面します。これらの相反する目標の中から選択する場合、Byte Buddyの主な焦点は最小限の実行時間でコードを生成することにあります。通常、型の作成や操作はどのプログラムでも一般的な手順ではなく、長時間実行するアプリケーションに大きな影響を与えることはありません。特に、クラスのロードやクラスのインスツルメンテーションは、このようなコードを実行するときに最も時間がかかり避けられない手順です。

上の表の最初のベンチマークは、メソッドを実装またはオーバーライドすることなく、 Object をサブクラス化するためのライブラリの実行時間を測定しています。これは、コード生成におけるライブラリの一般的なオーバーヘッドの印象を与えてくれます。このベンチマークでは、常にインターフェースを拡張すると仮定した場合にのみ可能な最適化により、Javaプロキシーは他のライブラリーよりもパフォーマンスが良くなります。 Byte Buddyはジェネリック型とアノテーションのクラスもチェックし、追加のランタイムを引き起こします。このパフォーマンスのオーバーヘッドは、クラスを作成するための他のベンチマークにも見られます。ベンチマーク(2a)は、18個のメソッドを持つ単一のインターフェースを実装するクラスを作成(およびロード)するために測定されたランタイムを示し、(2b)はこのクラスに対して生成されたメソッドの実行時間を示します。同様に、(3a)は、実装されているものと同じ18のメソッドでクラスを拡張するためのベンチマークを示しています。 Byte Buddyは2つのベンチマークを提供しています。これは、スーパーメソッドを常に実行するインターセプターに対して可能な最適化のためです。クラス作成中の時間を犠牲にすると、Byte Buddyで作成したクラスの実行時間は通常ベースラインに達します。つまり、インストルメンテーションはオーバーヘッドをまったく発生させません。メタデータ処理が無効になっている場合、Byte Buddyはクラス作成中にも他のコード生成ライブラリよりも優れていることに注意してください。しかしながら、コード生成の実行時間はプログラムの全実行時間と比較して非常に少ないので、そのようなオプトアウトはライブラリコードを複雑にするという犠牲を払ってもほとんど性能を得られないので利用できない。

最後に、私たちの測定基準はJVMの ジャストインタイムコンパイラ によって以前に最適化されたJavaコードのパフォーマンスを測定することに注意してください。コードがたまにしか実行されない場合、パフォーマンスは上記のメトリックによって示唆されるよりも悪くなります。ただし、この場合、コードのパフォーマンスはそれほど重要ではありません。このメトリクスのコードはByte Buddyと一緒に配布されているので、これらのメトリクスを自分のコンピュータで実行して、マシンの処理能力に応じて上記の数値を調整できます。このため、上記の数字を絶対的に解釈するのではなく、それらを異なるライブラリを比較する相対的な尺度と見なしてください。 Byte Buddyをさらに開発するときには、新しい機能を追加するときにパフォーマンスが低下するのを避けるために、これらの指標を監視する必要があります。

次のチュートリアルでは、Byte Buddyの機能について徐々に説明します。大部分のユーザーが使用する可能性が最も高い、そのより一般的な機能から始めます。その後、ますます高度なトピックを検討し、Javaバイトコードとクラスファイル形式について簡単に紹介します。そして、この後の資料に早送りしても落胆しないでください。 Byte Buddyの標準APIを使用してJVMの詳細を理解しなくても、ほとんど何でもできます。標準のAPIについて学ぶために、読んでください。

Creating a class

Byte Buddyによって作成された型は、 ByteBuddy クラスのインスタンスによって発行されます。 new ByteBuddy() を呼び出して新しいインスタンスを作成するだけで準備完了です。うまくいけば、あなたはあなたがあなたがあなたが与えられたオブジェクトに対して呼び出すことができるメソッドに関する提案を得る開発環境を使用しています。このようにして、Byte BuddyのjavadocでクラスのAPIを手動で検索するのを避けながら、IDEを使ってプロセスを案内することができます。前述のように、Byte Buddyはできるだけ人間が読めるようにすることを目的としたドメイン固有の言語を提供しています。したがって、IDEのヒントによって、ほとんどの場合正しい方向に進むことができます。しかし、話は十分ですが、Javaプログラムの実行時に最初のクラスを作成しましょう。

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

明らかに明らかなように、上記のコード例は Object 型を拡張する新しいクラスを作成します。この動的に作成された型は、メソッド、フィールド、またはコンストラクタを明示的に実装しないで Object を拡張するだけのJavaクラスと同等です。あなたは、動的に生成された型にさえ名前を付けさえしなかったことに気付いたかもしれません、それは通常Javaクラスを定義するとき必要です。もちろん、あなたは簡単にあなたの型に明示的に名前をつけることができます:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();

しかし、明示的な名前なしで何が起こるのでしょうか。 Byte Buddyは、 設定を超えた慣習 に基づいて暮らしていて、便利なデフォルト設定を提供しています。型の名前に関しては、デフォルトのByte Buddy設定は動的型のスーパークラス名に基づいてクラス名をランダムに作成する NamingStrategy を提供します。さらに、名前はスーパークラスと同じパッケージ内にあるように定義されているため、直接スーパークラスのパッケージプライベートメソッドは常に動的型に認識されます。たとえば、 example.Foo という型をサブクラス化した場合、生成される名前は example.Foo$$ByteBuddy$$1376491271 のようになります。ここで、数値シーケンスはランダムです。 Object などの型が存在する java.lang パッケージから型をサブクラス化する場合、この規則の例外があります。 Javaのセキュリティモデルでは、この名前空間にカスタム型を含めることはできません。したがって、デフォルトの命名方法では、そのようなタイプ名の先頭に net.bytebuddy.renamed が付けられます。

このデフォルトの動作はあなたにとって都合が悪いかもしれません。また、設定原則に関する規約のおかげで、必要に応じていつでもデフォルトの動作を変更できます。これが、 ByteBuddy クラスが導入された場所です。new ByteBuddy()インスタンスを作成することによって、デフォルト設定を作成します。この設定でメソッドを呼び出すことで、個々のニーズに合わせてカスタマイズできます。これを試してみましょう:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    public String subclass(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

上記のコード例では、型命名戦略がデフォルト設定とは異なる新しい設定を作成しました。無名クラスは、文字列 i.love.ByteBuddy と基本クラスの単純名を単純に連結するように実装されています。したがって、Object 型をサブクラス化するとき、動的型は i.love.ByteBuddy.Object という名前になります。独自の命名方法を作成するときは注意してください。 Java仮想マシンは型を区別するために名前を使用しているため、命名の衝突を避けたいのです。命名動作をカスタマイズする必要がある場合は、Byte Buddyに組み込まれている NamingStrategy.SuffixingRandom を使用して、デフォルトよりもアプリケーションにとって意味のあるプレフィックスを含めるようにカスタマイズできます。

Domain specific language and immutability

Byte Buddyのドメイン固有の言語が実際に動作しているのを見た後、この言語がどのように実装されているかを簡単に見てみる必要があります。実装について知っておく必要がある1つの詳細は、言語は 不変オブジェクト を中心に構築されているということです。実際のところ、Byte Buddy名前空間に存在するほとんどすべてのクラスは不変にされていますが、場合によっては型を不変にすることができませんでした。 Byte Buddyにカスタム機能を実装する場合は、この原則に従うことをお勧めします。

前述の不変性の意味として、たとえば ByteBuddy インスタンスを構成するときは注意が必要です。たとえば、次のような間違いをするかもしれません。

ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.withNamingStrategy(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();

動的型は、(おそらく)定義されているカスタム命名戦略 new NamingStrategy.SuffixingRandom( "suffix") を使用して生成されるはずです。 byteBuddy 変数に格納されているインスタンスを変更する代わりに、withNamingStrategy メソッドを呼び出すと、カスタマイズされた ByteBuddy インスタンスが返されますが、これは失われます。その結果、動的タイプは最初に作成されたデフォルト構成を使用して作成されます。

Redefining and rebasing existing classes

これまでは、Byte Buddyを使用して既存のクラスのサブクラスを作成する方法を説明しました。ただし、既存のクラスを拡張するために同じAPIを使用することもできます。このような強化は、2つの異なるフレーバーで利用可能です。

type redefinition

クラスを再定義するとき、Byte Buddyはフィールドとメソッドを追加するか既存のメソッド実装を置き換えることによって既存のクラスの変更を可能にします。ただし、既存のメソッド実装は、他の実装に置き換えられた場合は失われます。たとえば、次のような型を再定義すると

class Foo {
  String bar() { return "bar"; }
}

bar メソッドから "qux" を返すために、このメソッドがもともと "bar" を返したという情報は完全に失われます。

type rebasing

クラスをリベースするとき、Byte Buddyはリベースされたクラスのすべてのメソッド実装を保持します。型の再定義を実行するときのようにオーバーライドされたメソッドを破棄する代わりに、Byte Buddyはそのようなすべてのメソッド実装を互換性のあるシグネチャを持つ名前が変更されたプライベートメソッドにコピーします。このようにして、実装が失われることはなく、リベースされたメソッドはこれらの名前が変更されたメソッドを呼び出すことで元のコードを呼び出し続けることができます。このように、上記のクラス Foo は以下のようにリベースできます。

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

bar メソッドが元々 "bar" を返したという情報は別のメソッド内に保存されているため、アクセス可能なままです。クラスをリベースするとき、Byte Buddyはサブクラスを定義した場合など、すべてのメソッド定義を扱います。つまり、リベースメソッドのスーパーメソッド実装を呼び出そうとすると、リベースメソッドが呼び出されます。しかし、代わりに、それは結局この仮想的なスーパークラスを上に表示されたリベースされたタイプに平坦化します。

リベース、再定義、またはサブクラス化は、 DynamicType.Builder インタフェースによって定義されているものと同じAPIを使用して実行されます。このようにして、例えばクラスをサブクラスとして定義し、後でその定義を変更して代わりにリベースされたクラスを表すことができます。これは、Byte Buddyのドメイン固有の言語の1語を変更するだけで達成されます。この方法では、可能なアプローチのいずれかを適用します

new ByteBuddy().subclass(Foo.class)
new ByteBuddy().redefine(Foo.class)
new ByteBuddy().rebase(Foo.class)

このチュートリアルの残りの部分で説明されている定義プロセスの他の段階では、透過的に処理されます。サブクラス定義はJava開発者にはよく知られた概念であるため、Byte Buddyのドメイン固有言語の以下の説明と例はすべて、サブクラスを作成することによって示されています。ただし、すべてのクラスは再定義またはリベースによって同様に定義できることに注意してください。

Loading a class

これまでのところ、動的型を定義して作成しただけですが、それを使用することはしていません。 Byte Buddyによって作成された型は、 DynamicType.Unloaded のインスタンスによって表されます。名前が示すように、これらの型はJava仮想マシンにはロードされません。代わりに、Byte Buddyによって作成されたクラスは、Javaクラスファイル形式のバイナリ形式で表されます。このように、生成された型に対して何をしたいのかはあなた次第です。たとえば、Javaアプリケーションをデプロイする前に拡張するクラスだけを生成するビルドスクリプトからByte Buddyを実行することができます。この目的のために、 DynamicType.Unloaded クラスは動的型を表すバイト配列を抽出することを可能にします。便宜上、この型にはクラスを特定のフォルダに保存できる saveIn(File) メソッドもあります。さらに、 inject(File) でクラスを既存のjarファイルに注入することもできます。

クラスのバイナリ形式に直接アクセスするのは簡単ですが、残念ながら型のロードはより複雑です。 Javaでは、すべてのクラスは ClassLoader を使用してロードされます。そのようなクラスローダの一例は、Javaクラスライブラリ内に出荷されているクラスのロードを担当するブートストラップクラスローダです。一方、システムクラスローダーは、Javaアプリケーションのクラスパスにクラスをロードする役割を果たします。明らかに、これらの既存のクラスローダーはどれも私たちが作成した動的クラスを認識していません。これを克服するには、ランタイム生成クラスをロードするための他の可能性を見つける必要があります。 Byte Buddyは、箱から出してすぐにさまざまなアプローチでソリューションを提供します。

  • 特定の動的に作成されたクラスの存在について明示的に伝えられる新しい ClassLoader を作成するだけです。 Javaクラスローダーは階層構造になっているため、このクラスローダーは、実行中のJavaアプリケーションにすでに存在する特定のクラスローダーの子として定義します。このようにして、実行中のすべてのタイプのJavaプログラムは、新しい ClassLoader でロードされた動的タイプから見えます。
  • 通常、Javaクラスローダーは、指定された名前の型を直接ロードしようとする前に、親の ClassLoader を照会します。これは、親クラスローダが同じ名前の型を認識している場合、クラスローダは通常型をロードしないことを意味します。この目的のために、Byte Buddyは、その親に問い合わせる前にそれ自身で型をロードしようと試みる、子供優先クラスローダーの作成を提供します。それ以外の点では、この方法は前述の方法と似ています。このアプローチは、親クラスローダーの型をオーバーライドするのではなく、この他の型をシャドウすることに注意してください。
  • 最後に、リフレクションを使って既存の ClassLoader に型を注入することができます。通常、クラスローダーは名前で与えられた型を提供するよう求められます。リフレクションを使用すると、クラスローダが実際にこの動的クラスを見つける方法を知らなくても、この原則を逆にして、保護されたメソッドを呼び出して新しいクラスをクラスローダに注入できます。

残念ながら、上記のアプローチには両方の欠点があります。

  • 新しい ClassLoader を作成すると、このクラスローダーは新しい名前空間を定義します。暗黙の内に、これらのクラスが2つの異なるクラスローダーによってロードされる限り、2つのクラスを同じ名前でロードすることは可能です。この2つのクラスは、たとえ両方のクラスが同一のクラス実装を表していても、Java仮想マシンによって同等と見なされることはありません。ただし、この同等性の規則はJavaパッケージにも当てはまります。つまり、両方のクラスが同じクラスローダでロードされていない場合、クラス example.Foo は別のクラス example.Bar のパッケージプライベートメソッドにアクセスできません。また、 example.Barexample.Foo を拡張すると、オーバーライドされたパッケージプライベートメソッドは機能しなくなりますが、元の実装に委譲されます。
  • クラスがロードされるときはいつでも、別の型を参照するコードセグメントが解決されると、そのクラスローダはこのクラスで参照される型を検索します。このルックアップは同じクラスローダーに委譲します。 2つのクラス example.Fooexample.Bar を動的に作成したシナリオを想像してください。 example.Foo を既存のクラスローダに注入した場合、このクラスローダは example.Bar を見つけようとするかもしれません。ただし、後者のクラスは動的に作成され、 example.Foo クラスを注入したクラスローダーには到達できないため、この検索は失敗します。したがって、リフレクティブアプローチは、クラスのロード中に有効になる循環依存関係を持つクラスには使用できません。幸い、ほとんどのJVM実装は、参照クラスを最初にアクティブに使用したときに遅延的に解決するため、クラスインジェクションは通常これらの制限なしに機能します。また、実際には、Byte Buddyによって作成されたクラスは通常、このような循環性の影響を受けません。
  • 一度に1つの動的タイプを作成しているので、循環依存関係に遭遇する可能性はあまり重要ではないと考えるかもしれません。ただし、型を動的に作成すると、いわゆる補助型が作成される可能性があります。これらのタイプは、作成中の動的タイプへのアクセスを提供するためにByte Buddyによって自動的に作成されます。次のセクションで補助型についてもっと学びます、今のところそれらについて心配しないでください。ただし、このため、動的に作成されたクラスを既存のクラスに注入するのではなく、特定の ClassLoader を作成してロードすることをお勧めします。

DynamicType.Unloaded を作成した後、この型は ClassLoadingStrategy を使ってロードできます。そのような戦略が提供されない場合、Byte Buddyは提供されたクラスローダーに基づいてそのような戦略を推論し、それ以外の場合はデフォルトであるリフレクションを使用して型を注入できないブートストラップクラスローダー専用の新しいクラスローダーを作成します。 Byte Buddyは、箱から出してすぐに使用できるいくつかのクラスローディング戦略を提供します。各ストラテジは、上記の概念の1つに従います。これらのストラテジーは ClassLoadingStrategy.Default で定義されています。 WRAPPER ストラテジーは新しいラッピング ClassLoader を作成します。ここで、 CHILD_FIRST ストラテジーは、子が最初のセマンティクスを持つ同様のクラスローダーを作成します。 WRAPPER ストラテジーと CHILD_FIRST ストラテジーの両方とも、クラスのロード後も型のバイナリ形式が保持される、いわゆるマニフェストバージョンでも利用できます。これらの代替バージョンは、 ClassLoader::getResourceAsStream メソッドを介してクラスローダのクラスのバイナリ表現にアクセスできるようにします。ただし、これを行うには、これらのクラスローダーがJVMのヒープ上のスペースを消費するクラスの完全なバイナリ表現への参照を維持する必要があることに注意してください。したがって、実際にバイナリ形式にアクセスする予定がある場合は、マニフェストバージョンのみを使用してください。 INJECTION 戦略はリフレクションを介して機能し、 ClassLoader::getResourceAsStream メソッドのセマンティクスを変更する可能性がないため、マニフェストバージョンでは当然使用できません。

そのようなクラスのロードを実際に見てみましょう。

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

上記の例では、クラスを作成してロードしました。前述したように、ほとんどの場合に適したクラスをロードするために WRAPPER ストラテジーを使用しました。最後に、 getLoaded メソッドは、現在ロードされている動的クラスを表すJavaクラスのインスタンスを返します。

クラスをロードするときは、現在の実行コンテキストの ProtectionDomain を適用することによって、事前定義されたクラスロード戦略が実行されます。あるいは、すべてのデフォルト戦略は withProtectionDomain メソッドを呼び出すことによって明示的な保護ドメインの指定を提供します。明示的な保護ドメインを定義することは、セキュリティマネージャを使用するとき、または署名付きjarで定義されているクラスを扱うときに重要です。

Reloading a class

前のセクションでは、既存のクラスを再定義またはリベースするために、Byte Buddyをどのように使用できるかを説明しました。ただし、Javaプログラムの実行中は、特定のクラスがまだロードされていないことを保証することは不可能です。 (さらに、Byte Buddyは現在、ロードクラスを引数としているだけです。将来のバージョンでは、既存のAPIを使用してアンロードクラスと同等に機能するようになります)それらがロードされた後でも。この機能は、Byte Buddyの ClassReloadingStrategy によってアクセス可能になります。クラスFooを再定義してこの戦略を実証しましょう。

class Foo {
  String m() { return "foo"; }
}

class Bar {
  String m() { return "bar"; }
}

Byte Buddyを使用して、 Foo クラスを Bar になるように簡単に再定義できます。 HotSwap を使用すると、この再定義は既存のインスタンスにも適用されます。

ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
assertThat(foo.m(), is("bar"));

HotSwapは、いわゆるJavaエージェントを使用してのみアクセス可能です。このようなエージェントは、Java仮想マシンの起動時に -javaagent パラメーターを使用して指定することによってインストールできます。パラメーターの引数は、Byte BuddyのBintrayページからダウンロードできるByte Buddyのエージェントjarです。ただし、JavaアプリケーションがJava仮想マシンのJDKインストールから実行される場合、Byte Buddyは ByteBuddyAgent.installOnOpenJDK() によってアプリケーションの起動後もJavaエージェントをロードできます。クラスの再定義は主にツールやテストの実装に使用されるため、これは非常に便利な方法です。 Java 9以降、JDKインストールなしで実行時にエージェントをインストールすることも可能です。

上の例では直感に反するように見えるかもしれないことの1つは、Byte BuddyがFoo型が最終的に再定義されるBar型を再定義するように指示されるという事実です。 Java仮想マシンは、名前とクラスローダによって型を識別します。したがって、Barの名前をFooに変更し、この定義を適用することで、最終的にBarの名前を変更した型を再定義します。もちろん、異なるタイプの名前を変更せずにFooを直接再定義することも同様に可能です。

ただし、JavaのHotSwap機能を使用すると、1つ大きな欠点があります。 HotSwapの現在の実装では、クラスの再定義の前後に、再定義されたクラスが同じクラススキーマを適用する必要があります。つまり、クラスをリロードするときにメソッドやフィールドを追加することはできません。クラスリベースが ClassReloadingStrategy では機能しないように、Byte Buddyがリベースクラスの元のメソッドのコピーを定義することは既に説明しました。また、明示的なクラス初期化メソッド(クラス内の静的ブロック)を持つクラスでは、クラスの再定義は機能しません。これは、この初期化メソッドも追加のメソッドにコピーする必要があるためです。しかしながら将来 HotSwap を拡張する計画があり、Byte Buddyはそれがうまく実行されれば、すぐにこの機能を使用する準備ができています。それまでの間、Byte Buddyの HotSwap サポートは、役に立つと思われるコーナーケースに使用できます。そうでなければ、クラスの再配置と再定義は、例えばビルドスクリプトから既存のクラスを拡張するときに便利な機能になります。

Working with unloaded classes

Javaの HotSwap 機能の限界についてのこの認識により、リベースおよび再定義命令の唯一の意味のある適用はビルド時にあると考えるかもしれません。ビルド時操作を適用することによって、処理されたクラスが最初のクラスロードの前にロードされないことをアサートすることができます。これは単にこのクラスローディングがJVMの別のインスタンスで行われるためです。 Byte Buddyは、まだロードされていないクラスでも同様に機能します。このために、Byte Buddyは、 Class インスタンスが例えば TypeDescription のインスタンスによって内部的に表されるように、JavaのリフレクションAPIを抽象化します。実際のところ、Byte Buddyは TypeDescription インタフェースを実装するアダプタによって提供されたクラスを処理する方法しか知りません。この抽象化に対する大きな利点は、クラスに関する情報が ClassLoader によって提供される必要はなく、他のどのソースによっても提供されることができるということです。

Byte Buddyは、 TypePool を使ってクラスの TypeDescription を取得するための標準的な方法を提供します。そのようなプールのデフォルト実装ももちろん提供されています。この TypePool.Default 実装はクラスのバイナリ形式を解析し、それを必須の TypeDescription として表します。 ClassLoader と同様に、表現可能なクラスのキャッシュも管理します。これもカスタマイズ可能です。また、通常は ClassLoader からクラスのバイナリ形式を取得しますが、このクラスをロードするように指示することはしません。

Java仮想マシンは、最初の使用時にクラスをロードするだけです。結果として、たとえば、次のようなクラスを安全に再定義できます。

package foo;
class Bar { }

他のコードを実行する前に、プログラムの起動時に実行します。

class MyApplication {
  public static void main(String[] args) {
    TypePool typePool = TypePool.Default.ofClassPath();
    new ByteBuddy()
      .redefine(typePool.describe("foo.Bar").resolve(), // do not use 'Bar.class'
                ClassFileLocator.ForClassLoader.ofClassPath())
      .defineField("qux", String.class) // we learn more about defining fields later
      .make()
      .load(ClassLoader.getSystemClassLoader());
    assertThat(Bar.class.getDeclaredField("qux"), notNullValue());
  }
}

アサーションステートメントで最初に使用する前に、再定義されたクラスを明示的にロードすることによって、JVMの組み込みクラスローディングを未然に防ぐことができます。このようにして、 foo.Bar の再定義された定義がアプリケーションの実行時を通してロードされ使用されます。ただし、 TypePool を使用して説明を提供するときは、クラスリテラルでクラスを参照しません。 foo.Bar にクラスリテラルを使用した場合、再定義するための変更が行われる前にJVMがこのクラスをロードしていたため、再定義の試みは無効になります。また、アンロードされたクラスを扱うときは、クラスのクラスファイルを見つけることができる ClassFileLocator を指定する必要があります。上記の例では、実行中のアプリケーションのそのようなファイルのクラスパスをスキャンするクラスファイルロケータを単に作成します。

Creating Java agents

アプリケーションが大きくなり、よりモジュール化されるようになると、そのような変換を特定のプログラムポイントで適用することは、当然、実施するのが面倒な制約になります。そして、そのようなクラスの再定義をオンデマンドで適用するためのより良い方法があります。 Javaエージェント を使用すると、Javaアプリケーション内で行われるクラスローディングアクティビティを直接傍受することが可能です。 Javaエージェントは、リンクされたリソースの下で説明されているように、このjarファイルのマニフェストファイルで指定されたエントリポイントを持つ単純なjarファイルとして実装されます。 Byte Buddyを使用すると、そのようなエージェントの実装は AgentBuilder を使用して簡単に行えます。 ToString という名前の単純なアノテーションを以前に定義したと仮定すると、単に以下のようにエージェントの premain メソッドを実装することによって、すべてのアノテーション付きクラスに対して toString メソッドを実装するのは簡単です。

class ToStringAgent {
  public static void premain(String arguments, Instrumentation instrumentation) {
    new AgentBuilder.Default()
        .type(isAnnotatedWith(ToString.class))
        .transform(new AgentBuilder.Transformer() {
      @Override
      public DynamicType.Builder transform(DynamicType.Builder builder,
                                              TypeDescription typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}

上記の AgentBuilder.Transformer を適用した結果、注釈付きクラスのすべての toString メソッドは変換済みを返すようになりました。 Byte Buddy の DynamicType.Builder については、今後のセクションで説明しますが、今のところこのクラスについては心配しないでください。上記のコードは、もちろん些細で無意味なアプリケーションになります。この概念を正しく使用すると、アスペクト指向プログラミングを簡単に実装するための強力なツールになります。

エージェントを使用するときにブートストラップクラスローダによってロードされたクラスをインスツルメントすることも可能です。ただし、これにはいくつかの準備が必要です。まず第一に、ブートストラップクラスローダーは null 値で表されているため、リフレクションを使用してこのクラスローダーにクラスをロードすることは不可能です。ただしこれは、クラスの実装をサポートするために、計測クラスのクラスローダーにヘルパークラスをロードするために必要な場合があります。クラスをブートストラップクラスローダーにロードするために、Byte Buddyはjarファイルを作成してこれらのファイルをブートストラップクラスローダーのロードパスに追加することができます。これを可能にするには、これらのクラスをディスクに保存する必要があります。これらのクラスのフォルダは、クラスを追加するために Instrumentation インターフェイスのインスタンスも取得する enableBootstrapInjection コマンドを使用して指定できます。インストルメンテーションクラスで使用されるすべてのユーザークラスも、インストルメンテーションインターフェイスを使用して可能なブートストラップ検索パスに配置する必要があります。

Loading classes in Android applications

Androidは、Javaクラスファイル形式のレイアウトにないdexファイルを使用して、異なるクラスファイル形式を使用します。さらに、Dalvik仮想マシンを継承したARTランタイムでは、Androidアプリケーションは、Androidデバイスにインストールされる前にネイティブのマシンコードにコンパイルされます。その結果、Byte Buddyは、解釈する中間コード表現がないため、アプリケーションがそのJavaソースと一緒に明示的にデプロイされていない限り、もはやクラスを再定義または再配置することはできません。しかし、Byte Buddyは、 DexClassLoader と組み込みのdexコンパイラを使って新しいクラスを定義することができます。この目的のために、Byte Buddyは、Androidアプリケーション内から動的に作成されたクラスのロードを可能にする AndroidClassLoadingStrategy を含むbyte-buddy-androidモジュールを提供しています。機能するためには、一時ファイルとコンパイルされたクラスファイルを書き込むためのフォルダが必要です。これはAndroidのセキュリティマネージャによって禁止されているので、このフォルダは異なるアプリケーション間で共有してはいけません。

Working with generic types

Byte Buddyは、Javaプログラミング言語によって定義されているとおりにジェネリック型を処理しています。ジェネリック型は、ジェネリック型の消去のみを処理するJavaランタイムでは考慮されません。ただし、ジェネリック型はまだ任意のJavaクラスファイルに埋め込まれており、JavaリフレクションAPIによって公開されています。したがって、ジェネリック型情報は他のライブラリやフレームワークの動作に影響を与える可能性があるため、ジェネリック情報を生成されたクラスに含めることは意味があります。ジェネリック型情報を埋め込むことは、クラスが永続化され、Javaコンパイラによってライブラリとして処理される場合にも重要です。

クラスをサブクラス化するとき、インタフェースを実装するとき、またはフィールドまたはメソッドを宣言するとき、Byte Buddyは消去された Class の代わりにJava Type を受け入れます。ジェネリック型はTypeDescription.Generic.Builder を使用して明示的に定義することもできます。型の消去に対するJavaのジェネリック型の1つの重要な違いは、型変数の文脈上の意味です。ある型で定義されている特定の名前の型変数は、別の型が同じ名前で同じ型変数を宣言している場合、必ずしも同じ型を表すわけではありません。したがって、Byte Buddyは、 Type インスタンスがライブラリに渡されると、生成された型またはメソッドのコンテキストで型変数を表すすべてのジェネリック型を再バインドします。

Byte Buddyは、型が作成されたときに ブリッジメソッド を透過的に挿入します。ブリッジメソッドは、 ByteBuddy インスタンスのプロパティである MethodGraph.Compiler によって解決されます。デフォルトのメソッドグラフコンパイラは、Javaコンパイラのように動作し、クラスファイルのジェネリック型情報を処理します。 Java以外の言語の場合は、微分法グラフ・コンパイラーが適している可能性があります。

Fields and methods

前のセクションで作成したほとんどの型は、フィールドやメソッドを定義していません。ただし、 Object をサブクラス化することによって、作成されたクラスはそのスーパークラスによって定義されているメソッドを継承します。このJavaのトリビアを確認して、動的型のインスタンスで toString メソッドを呼び出します。リフレクティブに作成したクラスのコンストラクタを呼び出すことでインスタンスを取得できます。

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance() // Java reflection API
  .toString();

Object#toString メソッドの実装は、インスタンスの完全修飾クラス名とインスタンスのハッシュコードの16進表現の連結を返します。そして実際には、作成したインスタンスで toString メソッドを呼び出すと、 example.Type@340d1fa5 のようなものが返されます。

もちろん、私たちはここでやっているわけではありません。動的クラスを作成する主な動機は、新しいロジックを定義する機能です。これがどのように行われるかを示すために、簡単なことから始めましょう。 toString メソッドをオーバーライドして、 Hello World! を返します。以前のデフォルト値の代わりに、

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .method(named("toString")).intercept(FixedValue.value("Hello World!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .toString();

コードに追加した行には、Byte Buddyのドメイン固有の言語による2つの命令が含まれています。最初の命令は、オーバーライドしたいメソッドをいくつでも選択できるメソッドです。この選択は、オーバーライド可能な各メソッドをオーバーライドするかどうかを決定する述語として機能する ElementMatcher を引き渡すことによって適用されます。 Byte Buddyには、 ElementMatchers クラスに集められた、定義済みのメソッドマッチャーが多数付属しています。通常は、このクラスを静的にインポートして、結果のコードがより自然に読めるようにします。そのような静的インポートは、正確な名前でメソッドを選択する名前付きメソッドマッチャーを使用した上記の例でも想定されていました。事前定義されたメソッドマッチャーは構成可能です。このようにして、以下のようにして方法の選択をさらに詳細に説明することができます。

named("toString").and(returns(String.class)).and(takesArguments(0))

この後者のメソッドマッチャーは、 toString メソッドを完全なJavaシグネチャで記述しているため、この特定のメソッドにのみ一致します。しかし、与えられたコンテキストでは、私たちのオリジナルのメソッドマッチャーで十分であるように異なるシグネチャを持つ toString という名前の他のメソッドがないことを知っています。

toString メソッドを選択した後、2番目の命令インターセプトは、指定された選択のすべてのメソッドをオーバーライドする実装を決定します。メソッドの実装方法を知るためには、この命令には実装タイプの単一の引数が必要です。上記の例では、Byte Buddyに同梱されている FixedValue 実装を利用しています。このクラスの名前が示すように、実装は常に特定の値を返すメソッドを実装しています。このセクションの少し後で、 FixedValue の実装について詳しく説明します。今は、メソッドの選択をもう少し詳しく見てみましょう。

これまでのところ、私たちはただ一つのメソッドを傍受しました。実際のアプリケーションでは、事態はもっと複雑になるかもしれず、異なるメソッドをオーバーライドするために異なるルールを適用したいかもしれません。そのようなシナリオの例を見てみましょう。

class Foo {
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}

Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

上記の例では、メソッドをオーバーライドするための3つの異なる規則を定義しました。コードを調べると、最初の規則は Foo によって定義されているメソッド、つまりサンプルクラスの3つのメソッドすべてに関係していることがわかります。 2番目のルールは、前の選択のサブセットである foo という名前の両方のメソッドに一致します。そして最後のルールは foo(Object) メソッドにのみマッチします。これは前者の選択をさらに減らしたものです。しかし、この選択が重複している場合、Byte Buddyはどの規則がどの方法に適用されるかをどのように決定するのでしょうか。

Byte Buddyは、メソッドをオーバーライドするための規則をスタック形式で編成します。つまり、メソッドをオーバーライドするための新しいルールを登録するときはいつでも、このスタックの一番上にプッシュされ、新しいルールが追加されるまで常に最初に適用されます。上記の例では、これは次のことを意味します。

  • bar() メソッドは、最初に named("foo").and(takesArguments(1)) と照合され、次に named("foo") と照合されます。両方の照合の試行は否定的になります。最後に、 isDeclaredBy(Foo.class) マッチャーは、 One! を返すために bar() メソッドをオーバーライドするための緑色の光を与えます。
  • 同様に、foo() メソッドは最初に named("foo").and(takesArguments(1)) に対して照合されます。引数がないと照合が失敗します。その後、 named("foo") マッチャーは、 foo() メソッドが上書きされて Two! を返すように、正の一致を決定します。
  • foo(Object) は、named( "foo").and(takesArguments(1)) マッチャーとすぐに一致し、オーバーライドされた実装は Three! を返します。

この組織のために、あなたは常により具体的なメソッドマッチャーを最後に登録するべきです。それ以外の場合は、後で登録される特定性の低いメソッドマッチャーによって、以前に定義したルールが適用されない可能性があります。 ByteBuddy 設定で ignoreMethod プロパティを定義できることに注意してください。このメソッドマッチャーに対してうまくマッチしたメソッドは決して上書きされません。デフォルトでは、Byte Buddyは合成メソッドをオーバーライドしません。

シナリオによっては、スーパータイプのメソッドやインタフェースをオーバーライドしない新しいメソッドを定義したい場合があります。これはByte Buddyを使っても可能です。この目的のために、署名を定義できるところで defineMethod を呼び出すことができます。メソッドを定義したら、メソッドマッチャーによって識別されたメソッドと同様に、実装を提供するように求められます。メソッドの定義後に登録されたメソッドマッチャーは、前に説明したスタッキングの原則によってこの実装よりも優先される可能性があります。

defineField を使用すると、Byte Buddyは特定のタイプのフィールドを定義できます。 Javaでは、フィールドはオーバーライドされることはなく、 シャドウイング されるだけです。このため、フィールドマッチングなどは利用できません。

メソッドの選択方法に関するこの知識を基にして、これらのメソッドをどのように実装できるかについて学習する準備が整いました。この目的のために、Byte Buddyに同梱されている定義済みの Implementation 実装を見てみましょう。カスタム実装の定義については、独自のセクションで説明していますが、非常にカスタムメソッドの実装を必要とするユーザーのみを対象としています。

A closer look at fixed values

FixedValue の実装はすでに実行中です。その名前が示すように、 FixedValue によって実装されるメソッドは単に提供されたオブジェクトを返します。クラスはそのようなオブジェクトを2つの異なる方法で記憶することができます。

  • 固定値は クラスの定数プール に書き込まれます。定数プールは、Javaクラスファイル形式内のセクションであり、クラスのプロパティを記述するステートレスな値を多数含みます。定数プールは主に、クラスの名前やそのメソッドの名前など、クラスのプロパティを記憶するために必要です。これらの反映特性に加えて、定数プールには、メソッドまたはクラスのフィールド内で使用される文字列またはプリミティブ値を格納するためのスペースがあります。文字列とプリミティブ値に加えて、クラスプールは他の型への参照も格納できます。
  • 値はクラスの静的フィールドに格納されます。ただし、これが行われるためには、クラスがJava仮想マシンにロードされた後でフィールドに指定の値が割り当てられる必要があります。この目的のために、動的に作成されたすべてのクラスは、そのような明示的な初期化を実行するように設定できる TypeInitializer を伴います。 DynamicType.Unloaded をロードするように指示すると、Byte Buddyは自動的にその型の初期化子を起動してクラスが使用可能になるようにします。したがって、通常は型初期化子について心配する必要はありません。ただし、動的クラスをロードしてByte Buddyの外部にロードする場合は、これらのクラスがロードされた後にそれらの型初期化子を手動で実行することが重要です。それ以外の場合、 StaticValue にはこの値が割り当てられなかったため、 FixedValue の実装では、たとえば必須値の代わりに null が返されます。しかし多くの動的型は明示的な初期化を必要としないかもしれません。そのため、クラスの型初期化子は、その isAlive メソッドを呼び出すことによって、その活発さについて照会することができます。 TypeInitializer を手動でトリガーする必要がある場合は、 DynamicType インターフェイスによって公開されていることがわかります。

FixedValue#value(Object) でメソッドを実装すると、Byte Buddyはパラメータの型を分析し、可能であれば動的型のクラスプールに格納されるように定義します。それ以外の場合は静的フィールドに値を格納します。ただし、値がクラスプールに格納されている場合、選択されたメソッドによって返されるインスタンスは、異なるオブジェクトIDのものになる可能性があります。したがって、 FixedValue#reference(Object) を使用して、Byte Buddyに常に静的フィールドにオブジェクトを格納するように指示できます。後者のメソッドは、フィールドの名前を2番目の引数として指定できるようにオーバーロードされています。それ以外の場合、フィールド名はオブジェクトのハッシュコードから自動的に導き出されます。この動作の例外は null 値です。 null 値はフィールドに格納されることはありませんが、単にそのリテラル式で表されます。

あなたはこの文脈で型安全について疑問に思うかもしれません。明らかに、無効な値を返すメソッドを定義することができます。

new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0))
  .make();

Javaの型システム内のコンパイラによるこの無効な実装を防ぐことは困難です。代わりに、Byte Buddyは型が作成されたときに IllegalArgumentException をスローし、 String を返すメソッドへの不正な整数の代入が有効になります。 Byte Buddyは、作成されたすべての型が正当なJava型であり、違法な型の作成中に例外をスローして高速に失敗することを保証するために全力を尽くします。

Byte Buddyの割り当て動作はカスタマイズ可能です。繰り返しになりますが、Byte Buddyは、Javaコンパイラの代入動作を模倣した正当なデフォルトのみを提供します。その結果、Byte Buddyはそのスーパータイプのいずれかへのタイプの割り当てを可能にし、プリミティブ値のボックス化またはそれらのラッパー表現のボックス化解除も考慮します。ただし、Byte Buddyは現在ジェネリック型を完全にはサポートしておらず、型の消去のみを考慮していることに注意してください。したがって、Byte Buddyが ヒープ汚染 を引き起こす可能性があります。事前定義済みアサイナを使用する代わりに、Javaプログラミング言語に暗黙的に含まれていない型変換が可能な独自のアサイナをいつでも実装できます。このチュートリアルの最後のセクションで、そのようなカスタム実装について調べます。今のところ、任意のFixedValue実装でwithAssignerを呼び出すことによって、そのようなカスタムアサイナを定義できることに言及しています。

Delegating a method call

多くのシナリオで、メソッドから固定値を返すことはもちろん不十分です。より柔軟にするために、Byte Buddyは MethodDelegation 実装を提供しています。これは、メソッド呼び出しに反応する際に最大限の自由度を提供します。メソッド委譲は、動的型の外側に存在する可能性がある別のメソッドに呼び出しを転送するために、動的に作成された型のメソッドを定義します。このように、動的クラスのロジックはプレーンJavaを使用して表すことができますが、コード生成では他のメソッドへのバインディングのみが実現されます。詳細を説明する前に、 MethodDelegation の使用例を見てみましょう。

class Source {
  public String hello(String name) { return null; }
}

class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}

String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

この例では、Source#hello(String) メソッドの呼び出しを Target メソッドに委任して、メソッドが null ではなく Hello World! を返すようにしています。 この目的のために、 MethodDelegation 実装は、 Target 型の呼び出し可能なメソッドを識別し、それらのメソッド間で最適な一致を識別します。上記の例では、 Target 型は単一の静的メソッドしか定義しておらず、メソッドのパラメータ、戻り型、および名前は Source#name(String) のものと同じであるため便利です。

実際には、委任対象メソッドの決定は、おそらくもっと複雑になります。では、実際に選択肢がある場合、Byte Buddyはどのようにして方法を決定するのでしょうか。このために、 Target クラスが次のように定義されているとします。

class Target {
  public static String intercept(String name) { return "Hello " + name + "!"; }
  public static String intercept(int i) { return Integer.toString(i); }
  public static String intercept(Object o) { return o.toString(); }
}

お気づきかもしれませんが、上記のメソッドはすべてインターセプトと呼ばれています。 Byte Buddyでは、ターゲットメソッドをソースメソッドと同じ名前にする必要はありません。私達はまもなくこの問題を詳しく調べます。さらに重要なことに、 Target の定義を変えて前の例を実行した場合は、 named(String) メソッドが intercept(String) にバインドされていることがわかります。しかし、それはなぜですか?明らかに、 intercept(int) メソッドはソースメソッドの String 引数を受け取ることができないため、一致する可能性もありません。ただし、これはバインド可能な intercept(Object) メソッドには当てはまりません。このあいまいさを解決するために、Byte Buddyはもう一度、最も具体的なパラメータタイプを持つメソッドバインディングを選択することによってJavaコンパイラを模倣します。 Javaコンパイラがオーバーロードメソッドのバインディングをどのように選択するかを覚えておいてください。 StringObject より具体的なので、最後に intercept(String) クラスが3つの選択肢の中から選択されます。

これまでの情報で、メソッドバインディングアルゴリズムはかなり硬い性質であると考えるかもしれません。しかし、私たちはまだ全文を語っていません。これまでのところ、デフォルトが実際の要件に合わない場合に変更が可能である、設定原則に関する規約の別の例を観察しただけです。実際には、 MethodDelegation の実装は、パラメータのアノテーションがどの値に割り当てられるべきかを決定するアノテーションと連携します。ただし、注釈が見つからない場合、Byte Buddyはパラメータを @Argument で注釈が付けられているかのように扱います。この後者のアノテーションはByte Buddyがソースメソッドの n 番目の引数をアノテーションを付けられたターゲットに割り当てるようにします。注釈が明示的に追加されていない場合、 n の値は注釈付きパラメータのインデックスに設定されます。このルールによると、Byte Buddyは次のように扱います。

void foo(Object o1, Object o2)

すべてのパラメータに次のように注釈が付けられているかのように

void foo(@Argument(0) Object o1, @Argument(1) Object o2)

その結果、インスツルメントされたメソッドの1番目と2番目の引数がインターセプターに割り当てられます。インターセプトされたメソッドが少なくとも2つのパラメータを宣言していない場合、または注釈付きのパラメータタイプがインスツルメントされたメソッドのパラメータタイプから割り当てられない場合、問題のインターセプタメソッドは破棄されます。

@Argument アノテーションの他に、 MethodDelegation で使用できる他の定義済みアノテーションがいくつかあります。

  • @AllArguments アノテーションを持つパラメータは配列型でなければならず、すべてのソースメソッドの引数を含む配列が割り当てられています。この目的のために、すべてのソースメソッドパラメータは配列のコンポーネント型に代入可能でなければなりません。そうでない場合、現在のターゲットメソッドは、ソースメソッドにバインドされる候補として見なされません。
  • @This アノテーションは、インターセプトされたメソッドが現在呼び出されている動的型のインスタンスの割り当てを誘導します。注釈付きパラメータが動的型のインスタンスに割り当てられない場合、現在のメソッドはソースメソッドにバインドされる候補としては見なされません。このインスタンスで任意のメソッドを呼び出すと、インストルメント化された可能性のあるメソッド実装が呼び出されることになります。オーバーライドされた実装を呼び出すためには、後述の @Super アノテーションを使用する必要があります。インスタンスのフィールドにアクセスするために @This アノテーションを使用する典型的な理由。
  • @Origin でアノテーションが付けられたパラメータは、 MethodConstructorExecutableClassMethodHandleMethodTypeString 、または int 型のいずれかで使用されなければなりません。パラメータの型に応じて、現在計測されている元のメソッドまたはコンストラクタへの Method または Constructor 参照、または動的に作成された Class への参照が割り当てられます。 Java 8を使用している場合は、インターセプターで Executable 型を使用して、メソッドまたはコンストラクターの参照を受け取ることもできます。注釈付きパラメータが String の場合、パラメータにはメソッドの toString メソッドが返すはずの値が割り当てられます。一般に、可能な限りメソッド識別子としてこれらの String 値を使用することをお勧めします。また、ルックアップによって大きなランタイムオーバーヘッドが発生するため、 Method オブジェクトの使用は推奨しません。このオーバーヘッドを回避するために、 @Origin アノテーションはそのようなインスタンスを再利用のためにキャッシュするためのプロパティも提供します。 MethodHandleMethodType はクラスの定数プールに格納されるため、これらの定数を使用するクラスは少なくともJavaバージョン7である必要があります。リフレクションを使用して他のオブジェクトの代行受信メソッドを反射的に呼び出すのではなく、このセクションで後述するパイプアノテーション。 int 型のパラメータに @Origin アノテーションを使用すると、インストルメント化メソッドの修飾子が割り当てられます。

事前定義された注釈を使用する以外に、Byte Buddyでは1つまたは複数の ParameterBinder を登録することによって独自の注釈を定義できます。このチュートリアルの最後のセクションで、そのようなカスタマイズについて調べます。

これまでに説明した4つのアノテーションの他に、動的型のメソッドのスーパー実装へのアクセスを許可する2つの他の事前定義アノテーションがあります。このようにして、動的型は例えばメソッド呼び出しのロギングのような アスペクト をクラスに追加することができます。次の例に示すように、 @SuperCall アノテーションを使用して、メソッドのスーパー実装の呼び出しを動的クラスの外部からでも実行できます。

class MemoryDatabase {
  public List<String> load(String info) {
    return Arrays.asList(info + ": foo", info + ": bar");
  }
}

class LoggerInterceptor {
  public static List<String> log(@SuperCall Callable<List<String>> zuper)
      throws Exception {
    System.out.println("Calling database");
    try {
      return zuper.call();
    } finally {
      System.out.println("Returned from database");
    }
  }
}

MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

上記の例から、superメソッドは、その呼び出しメソッドから、最初のオーバーライドされていない MemoryDatabase#load(String) の実装を呼び出す LoggerInterceptorCallable のインスタンスをインジェクトすることによって呼び出されることは明らかです。このヘルパークラスは、Byte Buddyの用語では AuxiliaryType と呼ばれています。補助型はByte Buddyによってオンデマンドで作成され、クラスが作成された後に DynamicType インタフェースから直接アクセスできます。このような補助型のため、1つの動的型を手動で作成すると、元のクラスの実装に役立ついくつかの追加型が作成される可能性があります。最後に、 @SuperCall アノテーションは、元のメソッドの戻り値が削除される Runnable 型でも使用できることに注意してください。

この補助型が他の型のスーパーメソッドをどのようにしてJavaで通常禁止されているかを呼び出すことができるのか、まだ疑問に思うかもしれません。詳しく調べると、この動作は非常に一般的であり、次のJavaソースコードスニペットがコンパイルされたときに生成されるコンパイル済みコードに似ています。

class LoggingMemoryDatabase extends MemoryDatabase {

  private class LoadMethodSuperCall implements Callable {

    private final String info;
    private LoadMethodSuperCall(String info) {
      this.info = info;
    }

    @Override
    public Object call() throws Exception {
      return LoggingMemoryDatabase.super.load(info);
    }
  }

  @Override
  public List<String> load(String info) {
    return LoggerInterceptor.log(new LoadMethodSuperCall(info));
  }
}

ただし、メソッドの元の呼び出しで割り当てられたものとは異なる引数を使用してスーパーメソッドを呼び出すことが必要な場合があります。これは @Super アノテーションを使うことでByte Buddyでも可能です。このアノテーションは、問題の動的型のスーパークラスまたはインターフェースを拡張する別の AuxiliaryType の作成を引き起こします。前と同様に、補助型は動的型のスーパー実装を呼び出すためにすべてのメソッドをオーバーライドします。このようにして、前の例のロガーインターセプターの例を実装して、実際の呼び出しを変更することができます。

class ChangingLoggerInterceptor {
  public static List<String> log(String info, @Super MemoryDatabase zuper) {
    System.out.println("Calling database");
    try {
      return zuper.load(info + " (logged access)");
    } finally {
      System.out.println("Returned from database");
    }
  }
}

@Super のアノテーションが付けられたパラメータに割り当てられているインスタンスは、動的型の実際のインスタンスとは異なるIDです。したがって、パラメータによってアクセス可能なインスタンスフィールドは、実際のインスタンスのフィールドを反映しません。さらに、補助インスタンスのオーバーライド不可能なメソッドは、その呼び出しを委任するのではなく、それらが呼び出されたときに不条理な動作を引き起こす可能性がある元の実装を保持します。最後に、 @Super でアノテーションが付けられたパラメータが、関連する動的型のスーパー型を表していない場合、そのメソッドはそのメソッドのバインディングターゲットとは見なされません。

@Super アノテーションは任意の型の使用を可能にするので、この型をどのように構成できるかについての情報を提供する必要があるかもしれません。デフォルトでは、Byte Buddyはクラスのデフォルトコンストラクタを使用しようとします。これは、暗黙的に Object 型を拡張するインタフェースに対して常に機能します。しかし、動的型のスーパークラスを拡張するとき、このクラスはデフォルトコンストラクタを提供しないかもしれません。このような場合、またはそのような補助型を作成するために特定のコンストラクタを使用する必要がある場合は、 @Super アノテーションを使用して、パラメータの型をアノテーションの constructorParameters プロパティとして設定することで、異なるコンストラクタを識別できます。このコンストラクタは、対応するデフォルト値を各パラメータに割り当てることによって呼び出されます。あるいは、コンストラクタを呼び出さずに補助型を作成するためにJavaの内部クラスを利用するクラスを作成するための Super.Instantiation.UNSAFE 戦略を使用することもできます。ただし、この方法は必ずしもOracle以外のJVMに移植できるわけではなく、将来のJVMリリースでは使用できなくなる可能性があります。今日の時点で、この危険なインスタンス生成方法で使用される内部クラスは、ほとんどすべてのJVM実装にあります。

さらに、上記の LoggerInterceptor がチェック例外を宣言していることにすでに気付いているかもしれません。一方、このメソッドを呼び出すインストルメント済みソースメソッドは、 チェック済み Exception を宣言しません。通常、Javaコンパイラはそのような呼び出しをコンパイルすることを拒否します。ただし、コンパイラとは対照的に、Javaランタイムはチェックされた例外をチェックされていないものと異なる扱いをせず、この呼び出しを許可します。このため、チェックされた例外を無視し、それらの使用に完全な柔軟性を与えることにしました。ただし、動的に作成されたメソッドから宣言されていないチェック済み例外をスローすると、アプリケーションのユーザーが混乱する可能性があるため注意してください。

メソッド委譲モデルには、もう1つ注意が必要な点があります。静的型付けはメソッドの実装には最適ですが、厳密型はコードの再利用を制限する可能性があります。その理由を理解するために、次の例を検討してください。

class Loop {
  public String loop(String value) { return value; }
  public int loop(int value) { return value; }
}

上記のクラスのメソッドは、互換性のない型を持つ2つの類似したシグネチャを記述しているので、通常は単一のインターセプタメソッドを使用して両方のメソッドをインスツルメントすることはできません。代わりに、静的型チェックを満たすためだけに、異なるシグネチャを持つ2つの異なるターゲットメソッドを提供する必要があります。この制限を克服するために、Byte Buddyは @RuntimeType でメソッドとメソッドパラメータに注釈を付けることを許可します。

class Interceptor {
  @RuntimeType
  public static Object intercept(@RuntimeType Object value) {
    System.out.println("Invoked method with: " + value);
    return value;
  }
}

上記のターゲットメソッドを使用して、両方のソースメソッドに対して単一のインターセプトメソッドを提供できるようになりました。 Byte Buddyでは、プリミティブ値をボックス化したり、ボックス化解除したりすることもできます。ただし、 @RunType の使用は型の安全性を放棄するという犠牲を払うため、互換性のない型が混在すると ClassCastException が発生する可能性があります。

@SuperCall と同等のものとして、Byte Buddyには、メソッドのスーパーメソッドを呼び出す代わりにデフォルトメソッドを呼び出すことができる @DefaultCall アノテーションが付属しています。このパラメータアノテーションを持つメソッドは、インターセプトされたメソッドが、インスツルメント化された型によって直接実装されているインタフェースによってデフォルトのメソッドとして宣言されている場合にのみ、バインディングと見なされます。同様に、 @SuperCall アノテーションは、インスツルメントされたメソッドが非抽象スーパーメソッドを定義していない場合、メソッドのバインディングを防ぎます。ただし、特定の型でデフォルトのメソッドを呼び出したい場合は、 @DefaultCalltargetType プロパティを特定のインタフェースで指定できます。この仕様では、Byte Buddyは、指定されたインタフェースタイプのデフォルトメソッドが存在する場合にそれを呼び出すプロキシインスタンスを挿入します。それ以外の場合、パラメータ注釈を持つターゲットメソッドは、委任先とは見なされません。明らかに、デフォルトのメソッド呼び出しは、 Java 8以降のクラスファイルバージョンで定義されているクラスに対してのみ使用可能です。同様に、 @Super アノテーションに加えて、特定のデフォルトメソッドを明示的に呼び出すためのプロキシを注入する @Default アノテーションがあります。

カスタム注釈を任意の MethodDelegation に定義して登録できることは既に述べました。 Byte Buddyには、すぐに使用できるようになっていますが、まだ明示的にインストールして登録する必要がある1つの注釈が付属しています。 @Pipe アノテーションを使用すると、インターセプトされたメソッド呼び出しを別のインスタンスに転送できます。 Javaクラスライブラリには、関数型を定義するJava 8より前の適切なインタフェース型が付属していないため、 @Pipe 注釈は MethodDelegation に事前登録されません。したがって、 Object を引数として受け取り、結果として別の Object を返す単一の非静的メソッドを使用して、型を明示的に指定する必要があります。メソッド型が Object 型によってバインドされている限り、ジェネリック型を使用することができます。もちろん、Java 8を使用しているのであれば、 Function 型は実行可能なオプションです。パラメータの引数でメソッドを呼び出すと、Byte Buddyはパラメータをメソッドの宣言型にキャストし、元のメソッド呼び出しと同じ引数を使用して代行受信メソッドを呼び出します。例を見る前に、Java 5以降で使用できるカスタム型を定義しましょう。

interface Forwarder<T, S> {
  T to(S target);
}

この型を使用して、メソッド呼び出しを既存のインスタンスに転送することで、上記の MemoryDatabase へのアクセスを記録する新しいソリューションを実装できます。

class ForwardingLoggerInterceptor {

  private final MemoryDatabase memoryDatabase; // constructor omitted

  public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) {
    System.out.println("Calling database");
    try {
      return pipe.to(memoryDatabase);
    } finally {
      System.out.println("Returned from database");
    }
  }
}

MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.withDefaultConfiguration()
    .withBinders(Pipe.Binder.install(Forwarder.class)))
    .to(new ForwardingLoggerInterceptor(new MemoryDatabase()))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

上記の例では、呼び出しはローカルに作成した別のインスタンスにのみ転送されます。ただし、型をサブクラス化してメソッドをインターセプトすることよりも優れている点は、この方法で既存のインスタンスを拡張できることです。さらに、通常はクラスレベルで静的インターセプターを登録するのではなく、インスタンスレベルでインターセプターを登録します。

これまでのところ、たくさんの MethodDelegation 実装を見てきました。しかし、先に進む前に、Byte Buddyがどのようにターゲットメソッドを選択するかについて、より詳細に検討します。 Byte Buddyがパラメータタイプを比較することによって最も具体的な方法をどのように解決するかについてはすでに説明しましたが、それ以外にもあります。 Byte Buddyが特定のソースメソッドへのバインディングに適した候補メソッドを特定した後、その解決策を AmbiguityResolvers のチェーンに委任します。繰り返しになりますが、Byte Buddyのデフォルトを補完したり置き換えたりすることができる独自のあいまいさ解決策を自由に実装できます。このような変更がないと、あいまいさ解決チェーンは、以下と同じ順序で以下の規則を適用することによって、固有のターゲットメソッドを識別しようとします。

メソッドに @BindingPriority という注釈を付けることで、メソッドに明示的な優先順位を割り当てることができます。あるメソッドが他のメソッドよりも優先順位が高い場合は、優先順位の高いメソッドが優先順位の低いメソッドよりも常に優先されます。さらに、 @IgnoreForBinding によってアノテーションが付けられたメソッドは、ターゲットメソッドとは見なされません。
ソースメソッドとターゲットメソッドが同じ名前を持つ場合、このターゲットメソッドは別の名前を持つ他のターゲットメソッドよりも優先されます。
2つのメソッドが @Argument を使用してソースメソッドの同じパラメーターをバインドする場合、最も特定のパラメーター型を持つメソッドが考慮されます。これに関連して、注釈がパラメータに注釈を付けないことによって明示的にまたは暗黙的に提供されるかどうかは問題ではない。解決アルゴリズムは、オーバーロードされたメソッドへの呼び出しを解決するためのJavaコンパイラのアルゴリズムと同様に機能します。 2つの型が同等に特定されている場合は、より多くの引数をバインドするメソッドがターゲットと見なされます。この解決段階でパラメータタイプを考慮せずにパラメータに引数を割り当てる必要がある場合は、アノテーションの bindingMechanic 属性を BindingMechanic.ANONYMOUS に設定することで可能になります。さらに、解決アルゴリズムが機能するためには、非ターゲットパラメータは各ターゲットメソッドのインデックス値ごとに一意である必要があります。
ターゲットメソッドが他のターゲットメソッドよりも多くのパラメータを持っている場合は、後者よりも前者の方が優先されます。
これまでのところ、 MethodDelegation.to(Target.class) のように特定のクラスを命名することによってメソッド呼び出しを静的メソッドに委譲しただけです。ただし、インスタンスメソッドまたはコンストラクタに委譲することもできます。

MethodDelegation.to(new Target()) を呼び出すことで、メソッド呼び出しを Target クラスの任意のインスタンスメソッドに委任することができます。これには、 Object クラスで定義されているメソッドを含め、インスタンスのクラス階層内の任意の場所で定義されているメソッドが含まれます。 MethodDelegationfilter(ElementMatcher) を呼び出してメソッド委任にフィルタを適用することで可能なことは、候補メソッドの範囲を制限したい場合があります。 ElementMatcher 型は、Byte Buddyのドメイン固有言語内でソースメソッドを選択するために以前使用されていたものと同じです。メソッド委譲の対象となるインスタンスは、静的フィールドに格納されます。固定値の定義と同様に、これには TypeInitializer の定義が必要です。静的フィールドに委任を格納する代わりに、 MethodDelegation.toField(String) によって任意のフィールドの使用を定義することもできます。引数は、すべてのメソッド委任の転送先のフィールド名を指定するものです。このような動的クラスのインスタンスでメソッドを呼び出す前に、必ずこのフィールドに値を割り当てるようにしてください。それ以外の場合、メソッドの委任は NullPointerException になります。
メソッド委譲を使用して、特定の型のインスタンスを構築できます。 MethodDelegation.toConstructor(Class) を使用することにより、インターセプトされたメソッドを呼び出すと、指定されたターゲット型の新しいインスタンスが返されます。
あなたが今学んだように、 MethodDelegation はそのバインディングロジックを調整するためにアノテーションを調べます。これらの注釈はByte Buddyに固有のものですが、これは注釈付きクラスが何らかの方法でByte Buddyに依存することを意味するのではありません。代わりに、Javaランタイムは、クラスがロードされたときにクラスパス上に見つからない注釈型を単に無視します。これは、動的クラスが作成された後にByte Buddyが不要になったことを意味します。つまり、クラスパスにByte Buddyがなくても、動的クラスとそのメソッド呼び出しを委任する型を別のJVMプロセスにロードできます。

MethodDelegation で使用できる定義済みの注釈がいくつかありますが、それらについて簡単に説明します。これらのアノテーションについてもっと知りたいのなら、コード内のドキュメントでさらなる情報を見つけることができます。これらの注釈は次のとおりです。

  • @Empty :この注釈を適用して、Byte Buddyはパラメータタイプのデフォルト値を挿入します。プリミティブ型の場合、これは数値ゼロと同等です。参照型の場合、これは null です。このアノテーションを使用することは、インターセプタのパラメータを無効にすることを意図しています。
  • @StubValue :このアノテーションでは、アノテーションを付けられたパラメータはインターセプトされたメソッドのスタブ値を注入されます。 reference-return-typesおよび void メソッドの場合は、 null 値が挿入されます。プリミティブ値を返すメソッドの場合は、同等のボクシングタイプ 0 が挿入されます。 @RuntimeType アノテーションを使用している間に Object 型を返す汎用インターセプターを定義するときに、これは組み合わせて役に立ちます。注入された値を返すことで、プリミティブな戻り型を正しく考慮しながら、メソッドはスタブとして動作します。
  • @FieldValue :このアノテーションは、インストルメント化された型のクラス階層内のフィールドを見つけ、そのフィールドの値をアノテーション付きパラメータに挿入します。注釈付きパラメーターに互換タイプの可視フィールドが見つからない場合、ターゲットメソッドはバインドされていません。
  • @FieldProxy :この注釈を使用して、Byte Buddyは特定のフィールドのアクセサを挿入します。アクセスされたフィールドは、その名前によって明示的に指定することも、取得メソッドまたは設定メソッドの名前から派生させることもできます。ただし、インターセプトされたメソッドは、そのようなメソッドを表します。この注釈を使用する前に、 @Pipe 注釈と同様に、明示的にインストールして登録する必要があります。
  • @Morph :このアノテーションは @SuperCall アノテーションと非常によく似た働きをします。ただし、このアノテーションを使用すると、スーパーメソッドを呼び出すために使用する引数を指定できます。 @Morph アノテーションを使用するにはすべての引数のボックス化とボックス化解除が必要となるため、このアノテーションは元の呼び出しとは異なる引数でスーパーメソッドを呼び出す必要がある場合にのみ使用してください。特定のスーパーメソッドを呼び出したい場合は、タイプセーフプロキシを作成するために @Super アノテーションを使用することを検討してください。この注釈を使用する前に、 @Pipe 注釈と同様に、明示的にインストールして登録する必要があります。
  • @SuperMethod :このアノテーションは Method から代入可能なパラメータタイプに対してのみ使用可能です。割り当てられたメソッドは、元のコードの呼び出しを可能にする合成アクセサメソッドに設定されています。このアノテーションを使用すると、セキュリティマネージャを通過せずにスーパーメソッドを外部から呼び出すことを可能にするプロキシクラス用のパブリックアクセサが作成されます。
  • @DefaultMethod : @SuperMethod と似ていますが、デフォルトのメソッド呼び出し用です。デフォルトのメソッドが呼び出される可能性が1つしかない場合は、デフォルトのメソッドが一意の型で呼び出されます。それ以外の場合は、注釈プロパティとして型を明示的に指定できます。

Calling a super method

名前が示すように、 SuperMethodCall 実装はメソッドのスーパー実装を呼び出すために使用できます。一見したところでは、スーパーインプリメンテーションの単独の呼び出しは、インプリメンテーションを変更するのではなく、既存のロジックを複製するだけなので、あまり役に立ちません。ただし、メソッドをオーバーライドすることで、メソッドのアノテーションとそのパラメータを変更できます。これについては次のセクションで説明します。ただし、Javaでスーパーメソッドを呼び出すもう1つの理由は、常にスーパータイプまたは独自のタイプの別のコンストラクタを呼び出す必要があるコンストラクタの定義です。

これまでのところ、動的型のコンストラクタは常にその直接のスーパー型のコンストラクタに似ていると単純に仮定しました。例として、我々は呼び出すことができます

new ByteBuddy()
  .subclass(Object.class)
  .make()

その直接のスーパーコンストラクタである Object のデフォルトコンストラクタを単に呼び出すように定義された単一のデフォルトコンストラクタで Object のサブクラスを作成するただし、この動作はByte Buddyによって規定されていません。代わりに、上記のコードは呼び出すためのショートカットです。

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_TYPE)
  .make()

ConstructorStrategy は、任意のクラスに対して事前定義コンストラクタのセットを作成します。動的型の直接スーパークラスの各可視コンストラクタをコピーする上記のストラテジーの他に、他に3つの事前定義済みストラテジーがあります。そのようなコンストラクタが存在しない場合、およびスーパー型のパブリックコンストラクタを模倣するコンストラクタだけが存在する場合は、例外がスローされます。

Javaクラスファイル形式の中では、一般にコンストラクタはメソッドと違いはありません。そのため、Byte Buddyはそれらをそのまま扱うことができます。ただし、コンストラクターは、Javaランタイムによって受け入れられるように、別のコンストラクターのハードコードされた呼び出しを含む必要があります。このため、 SuperMethodCall 以外のほとんどの定義済み実装は、コンストラクタに適用したときに有効なJavaクラスを作成できません。

ただし、カスタム実装を使用することで、カスタム ConstructorStrategy を実装するか、 defineConstructor メソッドを使用してByte Buddyのドメイン固有の言語で個々のコンストラクタを定義することで、独自のコンストラクタを定義できます。さらに、より複雑なコンストラクタをそのまま定義するための新しい機能をByte Buddyに追加する予定です。

クラスのリベースとクラスの再定義のために、コンストラクタはもちろん ConstructorStrategy の仕様を時代遅れにするものをそのまま保持します。代わりに、これらの保持されたコンストラクタ(およびメソッド)の実装をコピーするためには、これらのコンストラクタ定義を含む元のクラスファイルの検索を許可する ClassFileLocator を指定する必要があります。 Byte Buddyは、元のクラスファイルの場所をそれ自体で識別するために最善を尽くします。たとえば、対応する ClassLoader を照会することによって、またはアプリケーションのクラスパスを調べることによってです。慣習的なクラスローダーを扱うとき、ルックアップはしかしながら成功しないかもしれません。その後、カスタムの ClassFileLocator を提供できます。

Calling a default method

バージョン8リリースでは、Javaプログラミング言語はインタフェースのデフォルトメソッドを導入しました。 Javaでは、デフォルトのメソッド呼び出しは、スーパーメソッドの呼び出しと似た構文で表現されます。唯一の違いとして、デフォルトのメソッド呼び出しは、そのメソッドを定義するインターフェースを指定します。 2つのインタフェースが同一のシグネチャを持つメソッドを定義していると、デフォルトのメソッド呼び出しがあいまいになる可能性があるため、これが必要です。したがって、Byte Buddyの DefaultMethodCall 実装は優先順位付けされたインタフェースのリストを受け取ります。メソッドをインターセプトするとき、 DefaultMethodCall は最初に言及されたインタフェース上のデフォルトメソッドを呼び出します。例として、次の2つのインタフェースを実装したいとします。

interface First {
  default String qux() { return "FOO"; }
}

interface Second {
  default String qux() { return "BAR"; }
}

両方のインタフェースを実装するクラスを作成し、デフォルトのメソッドを呼び出すために qux メソッドを実装した場合、この呼び出しは First または Second インタフェースで定義されたデフォルトのメソッドの呼び出しの両方を表現できます。ただし、 DefaultMethodCall を指定して First インタフェースを優先させることで、Byte Buddyは代替インタフェースではなくこの後者のインタフェースのメソッドを呼び出す必要があることを認識します。

new ByteBuddy(ClassFileVersion.JAVA_V8)
  .subclass(Object.class)
  .implement(First.class)
  .implement(Second.class)
  .method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class))
  .make()

Java 8より前のバージョンのクラスファイルで定義されているJavaクラスは、デフォルトのメソッドをサポートしていません。さらに、Byte Buddyは、Javaプログラミング言語と比較して、デフォルトメソッドの呼び出し可能性に対してより弱い要件を課していることに注意する必要があります。 Byte Buddyは、型の階層の中で最も具体的なクラスによって実装されるデフォルトのメソッドのインターフェースのみを必要とします。 Javaプログラミング言語以外では、このインタフェースがスーパークラスによって実装される最も特定的なインタフェースである必要はありません。最後に、あいまいなデフォルトメソッドの定義を期待しないのであれば、あいまいなデフォルトメソッド呼び出しの発見で例外を投げる実装を受け取るために DefaultMethodCall.unambiguousOnly() を常に使うことができます。この同じ動作は、デフォルトのメソッド呼び出しが優先順位付けされていないインターフェース間であいまいで、互換性のあるシグネチャを持つメソッドを定義する優先順位付けされたインターフェースが見つからなかった場合の優先順位付け DefaultMethodCall でも表示されます。

Calling a specific method

場合によっては、上記の実装はより多くのカスタム動作を実装するのに十分ではありません。たとえば、明示的な振る舞いを持つカスタムクラスを実装したい場合があります。たとえば、同じ引数を持つスーパーコンストラクタを持たないコンストラクタを使って次のJavaクラスを実装することができます。

public class SampleClass {
  public SampleClass(int unusedValue) {
    super();
  }
}

Object クラスは int をパラメータとするコンストラクタを定義していないため、以前の SuperMethodCall の実装ではこのクラスを実装できませんでした。代わりに、 Object スーパーコンストラクタを明示的に呼び出すことができます。

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
  .defineConstructor(Arrays.<Class<?>>asList(int.class), Visibility.PUBLIC)
  .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()))
  .make()

上記のコードで、使用されていない単一の int パラメータをとる単一のコンストラクタを定義する Object の単純なサブクラスを作成しました。後者のコンストラクタは、 Object スーパーコンストラクタへの明示的なメソッド呼び出しによって実装されます。

MethodCall の実装は、引数を渡すときにも使用できます。これらの引数は、値として、手動で設定する必要があるインスタンスフィールドの値として、または指定されたパラメータ値として明示的に渡されます。また、実装では、インストルメントされているインスタンス以外のインスタンスでメソッドを呼び出すことができます。さらに、インターセプトされたメソッドから新しいインスタンスを構築することができます。 MethodCall クラスのドキュメントには、これらの機能に関する詳細情報が記載されています。

Accessing fields

FieldAccessor を使用して、フィールド値を読み書きするメソッドを実装することが可能です。この実装と互換性を持たせるために、メソッドは次のいずれかを実行する必要があります。

  • void setBar(Foo f) のようなシグネチャを使ってフィールドセッターを定義します。セッターは通常、 bar という名前のフィールドにアクセスします。これは、Java Bean仕様では一般的な方法です。このコンテキストでは、パラメータ型Fooはこのフィールドの型のサブタイプでなければなりません。
  • フィールドゲッターを定義するには、 Foo getBar() のような署名を付けます。セッターは通常、 bar という名前のフィールドにアクセスします。これは、Java Bean仕様では一般的な方法です。これが可能になるためには、メソッドの戻り型 Foo はフィールドの型のスーパー型でなければなりません。

そのような実装を作成するのは簡単です: FieldAccessor.ofBeanProperty() を呼び出すだけです。ただし、メソッドの名前からフィールドの名前を派生させたくない場合でも、 FieldAccessor.ofField(String) を使用してフィールド名を明示的に指定できます。このメソッドを使用すると、唯一の引数はアクセスされるべきフィールドの名前を定義します。必要に応じて、このようなフィールドがまだ存在しない場合でも、これを使用して新しいフィールドを定義できます。既存のフィールドにアクセスするときは、 in メソッドを呼び出すことによって、フィールドが定義されている型を指定できます。 Javaでは、階層のいくつかのクラスでフィールドを定義することは合法です。このプロセスでは、クラスのフィールドはそのサブクラスのフィールド定義によって隠されます。そのようなフィールドのクラスの明示的な場所がないと、Byte Buddyは、最も具体的なクラスから始めて、クラス階層をたどって最初に遭遇するフィールドにアクセスします。

FieldAccessor のアプリケーション例を見てみましょう。この例では、実行時にサブクラス化したい UserType を受け取ったとします。この目的のために、インターフェイスで表されるインスタンスごとにインターセプタを登録します。このようにして、私達は私達の実際の要求に従って異なる実装を提供することができます。この後者の実装は、対応するインスタンス上で InterceptionAccessor インタフェースのメソッドを呼び出すことによって交換可能になります。この動的型のインスタンスを作成するために、さらにリフレクションを使用したくないが、オブジェクトファクトリとして機能する InstanceCreator のメソッドを呼び出す。次の種類はこの設定に似ています。

class UserType {
  public String doSomething() { return null; }
}

interface Interceptor {
  String doSomethingElse();
}

interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}

interface InstanceCreator {
  Object makeInstance();
}

MethodDelegation を使ってクラスのメソッドを傍受する方法をすでに学びました。後者の実装を使用して、インスタンスフィールドへの委譲を定義し、このフィールドインターセプターに名前を付けることができます。さらに、 InterceptionAccessor インターフェイスを実装し、このフィールドのアクセサを実装するためにインターフェイスのすべてのメソッドをインターセプトします。 Bean プロパティアクセサを定義することで、 getInterceptorgettersetInterceptorsetter を実現します。

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toField("interceptor"))
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE)
  .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty())
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();

新しいdynamicUserTypeを使用すると、InstanceCreatorインターフェイスを実装してこの動的タイプのファクトリになることができます。繰り返しになりますが、動的型のデフォルトコンストラクタを呼び出すために、既知のMethodDelegationを使用しています。

InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.construct(dynamicUserType))
  .make()
  .load(dynamicUserType.getClassLoader())
  .getLoaded().newInstance();

ファクトリをロードするには dynamicUserType のクラスローダを使用する必要があることに注意してください。そうでなければ、この型はロード時にファクトリに表示されません。

これら2つの動的型を使用して、動的に拡張された UserType の新しいインスタンスを最終的に作成し、そのインスタンスのカスタムインターセプターを定義できます。作成したばかりのインスタンスに HelloWorldInterceptor を適用して、この例を終了しましょう。フィールドアクセサインタフェースとファクトリの両方のおかげで、反射を使用せずにこれを実行できるようになったことに注意してください。

class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}

UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());

Miscellaneous

これまでに説明した実装に加えて、Byte Buddyには他にもいくつかの実装が含まれています。

  • StubMethod は、それ以上の操作を行わずに単純にメソッドの戻り型のデフォルト値を返すメソッドを実装しています。このように、メソッド呼び出しは黙って抑制することができます。このアプローチは、例えばモック型を実装するために使用できます。どのプリミティブ型のデフォルト値も、それぞれゼロまたはゼロ文字です。参照型を返すメソッドは、デフォルトとして null を返します。
  • ExceptionMethod は、例外をスローするだけのメソッドを実装するために使用できます。前述のように、メソッドがこの例外を宣言していなくても、どのメソッドからでもチェック済み例外をスローすることは可能です。
  • Forwarding 実装では、インターセプトされたメソッドの宣言型と同じ型の別のインスタンスにメソッド呼び出しを単純に転送することができます。 MethodDelegation を使用しても同じ結果が得られます。ただし、 Forwarding により、ターゲットメソッドの検出が不要なユースケースをカバーできる、より単純な委任モデルが適用されます。
  • InvocationHandlerAdapterを使うと、Javaクラスライブラリに同梱されている プロキシクラス用 の既存のInvocationHandlerを使用できます。
  • InvokeDynamic実装では、Java 7以降からアクセス可能な ブートストラップメソッド を使用して、実行時にメソッドを動的にバインドできます。

Annotations

Byte Buddyがその機能の一部を提供するためにアノテーションにどのように依存しているかを学びました。そして、Byte Buddyは、アノテーションベースのAPIを持つ唯一のJavaアプリケーションではありません。動的に作成された型をそのようなアプリケーションと統合するために、Byte Buddyはその作成された型とそのメンバーに注釈を定義することを許可します。注釈を動的に作成された型に割り当てる方法の詳細を調べる前に、ランタイムクラスに注釈を付ける例を見てみましょう。

@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeDefinition { }

class RuntimeDefinitionImpl implements RuntimeDefinition {
  @Override
  public Class<? extends Annotation> annotationType() {
    return RuntimeDefinition.class;
  }
}

new ByteBuddy()
  .subclass(Object.class)
  .annotateType(new RuntimeDefinitionImpl())
  .make();

Javaの @interface キーワードに示唆されているように、アノテーションは内部的にはインターフェース型として表されます。結果として、アノテーションは普通のインターフェースのようにJavaクラスによって実装されることができます。インタフェースの実装との唯一の違いは、クラスが表す注釈型を決定する注釈の暗黙の annotationType メソッドです。後者のメソッドは通常、実装されている注釈型のクラスリテラルを返します。それ以外のアノテーションプロパティは、それがインターフェースメソッドであるかのように実装されます。ただし、注釈メソッドの実装によって注釈のデフォルト値を繰り返す必要があることに注意してください。

クラスが別のクラスのサブクラスプロキシとして機能する必要がある場合は、動的に作成されたクラスの注釈を定義することが特に重要になります。サブクラスプロキシは、サブクラスが元のクラスをできるだけ透過的に模倣する必要がある場合に、分野横断的な懸念を実装するためによく使用されます。ただし、アノテーションを @Inherited に定義することでこの動作が明示的に要求されている限り、クラスのアノテーションはそのサブクラスには保持されません。 Byte Buddyを使用して、Byte Buddyのドメイン固有言語の属性メソッドを呼び出すことで、基本クラスのアノテーションを保持するサブクラスプロキシを簡単に作成できます。このメソッドは、引数として TypeAttributeAppender を想定しています。型属性アペンダーは、その基本クラスに基づいて、動的に作成されたクラスの注釈を定義するための柔軟な方法を提供します。たとえば、 TypeAttributeAppender.ForSuperType を渡すと、クラスのアノテーションは動的に作成されたサブクラスにコピーされます。注釈と型属性アペンダーは加法的であり、どのクラスに対しても注釈型を複数回定義することはできません。

メソッドとフィールドの注釈は、今説明した型注釈と同様に定義されています。メソッド注釈は、メソッドを実装するためのByte Buddyのドメイン固有の言語における最終的なステートメントとして定義できます。同様に、フィールドには定義後に注釈を付けることができます。もう一度例を見てみましょう。

new ByteBuddy()
  .subclass(Object.class)
    .annotateType(new RuntimeDefinitionImpl())
  .method(named("toString"))
    .intercept(SuperMethodCall.INSTANCE)
    .annotateMethod(new RuntimeDefinitionImpl())
  .defineField("foo", Object.class)
    .annotateField(new RuntimeDefinitionImpl())

上記のコード例は toString メソッドをオーバーライドし、オーバーライドされたメソッドに RuntimeDefinition で注釈を付けます。さらに、作成された型は、同じ注釈を持つフィールド foo を定義し、作成された型自体に後者の注釈も定義します。

デフォルトでは、 ByteBuddy 構成は、動的に作成された型または型メンバーに注釈を事前定義しません。ただし、この動作は、デフォルトの TypeAttributeAppenderMethodAttributeAppender 、または FieldAttributeAppender を指定することで変更できます。このようなデフォルトのアペンダは加法的ではなく、以前の値に置き換わることに注意してください。

クラスを定義するときに、注釈型またはそのプロパティの型を読み込まないことが望ましい場合があります。この目的のために、クラスのロードをトリガーすることなく注釈を定義するための流暢なインターフェースを提供する AnnotationDescription.Builder を使用することができますが、型安全性が犠牲になります。ただし、すべての注釈プロパティは実行時に評価されます。

デフォルトでは、Byte Buddyは、デフォルト値によって暗黙的に指定されているデフォルトプロパティを含む、アノテーションのすべてのプロパティをクラスファイルに含めます。ただし、この動作は、 AnteationFilterByteBuddy インスタンスに提供することでカスタマイズできます。

Type annotations

Byte Buddyは、Java 8の一部として導入された型注釈を公開して書き込みます。型注釈は、 TypeDescription.Generic インスタンスによって宣言された注釈としてアクセスできます。型注釈をジェネリックフィールドまたはメソッドの型に追加する必要がある場合は、 TypeDescription.Generic.Builder を使用して注釈型を生成できます。

Attribute appenders

Javaクラスファイルには、いわゆる属性として任意のカスタム情報を含めることができます。このような属性は、タイプ、フィールド、またはメソッドに *AttributeAppender を使用することによってByte Buddyを使用して含めることができます。ただし、属性アペンダーは、インターセプトされた型、フィールド、またはメソッドによって提供される情報に基づいてメソッドを定義するためにも使用できます。たとえば、サブクラスのメソッドをオーバーライドするときに、インターセプトされたメソッドのすべての注釈をコピーすることが可能です。

class AnnotatedMethod {
  @SomeAnnotation
  void bar() { }
}

new ByteBuddy()
  .subclass(AnnotatedMethod.class)
  .method(named("bar"))
  .intercept(StubMethod.INSTANCE)
  .attribute(MethodAttributeAppender.ForInstrumentedMethod.INSTANCE)

上記のコードは AnnotatedMethod クラスの bar メソッドをオーバーライドしますが、オーバーライドされたメソッドのすべてのアノテーション(パラメーターまたは型のアノテーションを含む)をコピーします。

クラスが再定義されたりリベースされたりすると、同じ規則が適用されない場合があります。デフォルトでは、 ByteBuddy は、上記のようにメソッドがインターセプトされた場合でも、リベースまたは再定義されたメソッドのアノテーションを保持するように設定されています。ただし、この動作は、 AnnotationRetention ストラテジを DISABLED に設定することでByte Buddyが既存の注釈を破棄するように変更できます。

Custom method implementations

前のセクションでは、Byte Buddyの標準APIについて説明しました。これまでに説明した機能はどれも、知識またはJavaバイトコードの明示的な表現を必要としません。ただし、カスタムバイトコードを作成する必要がある場合は、その上にByte Buddyが構築されている低レベルのバイトコードライブラリである ASM のAPIに直接アクセスすることで作成できます。ただし、異なるバージョンのASMは他のバージョンと互換性がないため、コードをリリースするときにByte Buddyを自分のネームスペースに再パッケージする必要があります。そうでなければ、別の依存関係が異なるバージョンのASMに基づく異なるバージョンのByte Buddyを予期しているときに、アプリケーションがByte Buddyの他の用途に非互換性をもたらす可能性があります。あなたはByte Buddyへの依存を維持することに関する詳細な情報を フロントページ に見つけることができます。

ASMライブラリには、Javaバイトコードとライブラリの使用に関する 優れたドキュメント が付属しています。そのため、JavaバイトコードとASMのAPIについて詳しく知りたい場合に備えて、このドキュメントを参照してください。代わりに、JVMの実行モデルとByte BuddyによるASMのAPIの適応について簡単に紹介します。

どのJavaクラスファイルも複数のセグメントで構成されています。コアセグメントは、おおよそ次のように分類できます。

  • 基本データ: クラスファイルは、クラスの名前とそのスーパークラスおよびその実装されたインタフェースの名前を参照します。さらに、クラスファイルには、クラスのJavaバージョン番号、その注釈、またはクラスを作成するためにコンパイラが処理したソースファイルの名前など、さまざまなメタデータが含まれています。
  • 定数プール: クラスの定数プールは、このクラスのメンバーまたは注釈によって参照される値の集まりです。これらの値のうち、定数プールには、クラスのソースコード内のリテラル式によって作成されるプリミティブ値や文字列などが格納されます。さらに、定数プールには、クラス内で使用されるすべての型とメソッドの名前が格納されています。
  • フィールドリスト: Javaクラスファイルには、このクラスで宣言されているすべてのフィールドのリストが含まれています。フィールドの型、名前、および修飾子に加えて、クラスファイルは各フィールドの注釈を格納します。
  • メソッドリスト: フィールドのリストと同様に、Javaクラスファイルには宣言されたすべてのメソッドのリストが含まれています。フィールド以外に、メソッド本体を記述するバイトエンコード命令の配列によって、非抽象メソッドも追加的に記述されます。これらの命令は、いわゆるJavaバイトコードを表します。

幸いなことに、ASMライブラリはクラスを作成するときに適切な定数プールを確立する責任を全うします。これにより、唯一の自明でない要素は、それぞれが単一バイトとして符号化された実行命令の配列によって表される方法の実装の説明のままである。これらの命令は、メソッドの呼び出し時に仮想 スタックマシン によって処理されます。簡単な例として、2つのプリミティブ整数 1050 の合計を計算して返すメソッドを考えてみましょう。このメソッドのJavaバイトコードは次のようになります。

LDC     10  // stack contains 10
LDC     50  // stack contains 10, 50
IADD        // stack contains 60
IRETURN     // stack is empty

上記の Javaバイトコード配列のニーモニック は、 LDC 命令を使用して両方の数値をスタックにプッシュすることから始まります。この実行順序は、加算が中置記法 10 + 50 として書かれるJavaソースコードで表現される順序とはどう違うのかに注意してください。スタック上で現在見つかっている最上位の値この加算は IADD で表現され、両方ともプリミティブ整数であると予想される2つの最上位スタック値を消費します。その過程で、これら2つの値を加算し、結果をスタックの一番上にプッシュします。最後に、 IRETURN ステートメントはこの計算結果を消費してメソッドから返し、空のスタックを残します。

メソッド内で参照されるすべてのプリミティブ値は、クラスの定数プールに格納されることは既に述べました。これは、上記の方法で参照される番号 5010 にも当てはまります。定数プールのどの値にも、長さ2バイトのインデックスが割り当てられます。数値 1050 がインデックス 12 に格納されていたとしましょう。 LDC の場合は 0x12IADD の場合は 0x60IRETURN の場合は 0xAC である上記ニーモニックのバイト値とともに、上記の方法を次のように表す。生バイト命令:

12 00 01
12 00 02
60
AC

コンパイル済みクラスの場合、この正確なバイトシーケンスはクラスファイルにあります。ただし、この説明ではメソッドの実装を完全に定義するにはまだ十分ではありません。 Javaアプリケーションの実行時間を短縮するために、各メソッドはJava仮想マシンに実行スタックに必要なサイズを通知する必要があります。ブランチなしで来る上記のメソッドの場合、スタックには最大で2つの値があることをすでに見たので、これは決定がかなり簡単です。ただし、より複雑な方法では、この情報を提供することは簡単に複雑な作業になる可能性があります。さらに悪いことに、スタック値は異なるサイズにすることができます。 longdouble の値はどちらも2つのスロットを消費しますが、他の値は1つを消費します。これだけでは不十分であるかのように、Java仮想マシンはメソッド本体内のすべてのローカル変数のサイズに関する情報も必要とします。メソッド内のそのような変数はすべて、任意のメソッドパラメータと非静的メソッドの this 参照も含む配列に格納されます。この場合も、 long 値と double 値は2つのスロットを消費します。

明らかに、これらすべての情報を追跡すると、Javaバイトコードの手動によるアセンブリが面倒でエラーが発生しやすくなるため、Byte Buddyが単純化された抽象化を提供しています。 Byte Buddy内では、スタック命令は StackManipulation インターフェイスの実装に含まれています。スタック操作の実装はすべて、与えられたスタックを変更するための命令と、この命令のサイズへの影響に関する情報を組み合わせたものです。そのような命令をいくつでも簡単に共通の命令にまとめることができます。これを実証するために、まず IADD 命令の StackManipulation を実装しましょう。

enum IntegerSum implements StackManipulation {

  INSTANCE; // singleton

  @Override
  public boolean isValid() {
    return true;
  }

  @Override
  public Size apply(MethodVisitor methodVisitor,
                    Implementation.Context implementationContext) {
    methodVisitor.visitInsn(Opcodes.IADD);
    return new Size(-1, 0);
  }
}

上記の apply メソッドから、このスタック操作はASMのメソッドビジターで関連メソッドを呼び出すことによって IADD 命令を実行することがわかります。さらに、この方法は、命令が現在のスタックサイズを1スロット減らすことを表す。作成されたSizeインスタンスの2番目の引数は0です。これは、この命令が中間結果を計算するために特定の最小スタックサイズを必要としないことを表します。さらに、どのStackManipulationも無効であると表現できます。この振る舞いは、例えば型制約を破る可能性のあるオブジェクト割り当てのように、より複雑なスタック操作に使用することができます。このセクションの後半で、無効なスタック操作の例を見ます。最後に、スタック操作を シングルトン列挙 として説明していることに注意してください。このような不変で機能的なスタック操作の説明を使用することは、Byte Buddyの内部実装には良い習慣であることが証明されています。同じアプローチに従うことをお勧めします。

上記のIntegerSumを定義済みのIntegerConstantおよびMethodReturnスタック操作と組み合わせることで、メソッドを実装できます。 Byte Buddy内では、メソッド実装はByteCodeAppenderに含まれています。これは次のように実装されます。

enum SumMethod implements ByteCodeAppender {

  INSTANCE; // singleton

  @Override
  public Size apply(MethodVisitor methodVisitor,
                    Implementation.Context implementationContext,
                    MethodDescription instrumentedMethod) {
    if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) {
      throw new IllegalArgumentException(instrumentedMethod + " must return int");
    }
    StackManipulation.Size operandStackSize = new StackManipulation.Compound(
      IntegerConstant.forValue(10),
      IntegerConstant.forValue(50),
      IntegerSum.INSTANCE,
      MethodReturn.INTEGER
    ).apply(methodVisitor, implementationContext);
    return new Size(operandStackSize.getMaximalSize(),
                    instrumentedMethod.getStackSize());
  }
}

この場合も、カスタム ByteCodeAppender はシングルトン列挙として実装されています。

目的のメソッドを実装する前に、まずインスツルメントされたメソッドが実際にプリミティブ整数を返すことを検証します。そうでなければ、作成されたクラスはJVMのバリデータによって拒否されるでしょう。次に、2つの数値 1050 を実行スタックにロードし、これらの値の合計を適用して計算結果を返します。これらすべての命令を複合スタック操作でラップすることで、この一連のスタック操作を実行するために必要な集約スタックサイズを確実に取得できます。最後に、このメソッドの全体的なサイズ要件を返します。返された ByteCodeAppender.Size の最初の引数は、 StackManipulation.Size に含まれるように前述した実行スタックに必要なサイズを反映しています。さらに、2番目の引数は、ローカル変数配列に必要なサイズを反映しています。これは、ここでは単にメソッドのパラメータに必要なサイズ、およびローカル変数を定義していないためこの参照に似ています。

この集計メソッドの実装により、Byte Buddyのドメイン固有の言語に提供できるこのメソッドのカスタム実装を提供する準備ができました。

enum SumImplementation implements Implementation {

  INSTANCE; // singleton

  @Override
  public InstrumentedType prepare(InstrumentedType instrumentedType) {
    return instrumentedType;
  }

  @Override
  public ByteCodeAppender appender(Target implementationTarget) {
    return SumMethod.INSTANCE;
  }
}

どの実装も2段階で照会されます。まず、実装は prepare メソッドに追加のフィールドまたはメソッドを追加することによって、作成されたクラスを変更する機会を得ます。さらに、この準備により、実装で前のセクションで学習した TypeInitializer を登録することができます。そのような準備が不要な場合は、引数として提供されている未変更の InstrumentedType を返すだけで十分です。インプリメンテーションは通常、インストルメント化された型の個々のインスタンスを返すべきではなく、接頭辞がすべてであるインストルメンテーション型のアペンダメソッドを呼び出すべきです。特定のクラスを作成するための実装が準備された後、 ByteCodeAppender を取得するために appender メソッドが呼び出されます。次に、このアペンダーは、指定された実装による代行受信用に選択されたメソッド、および実装による prepare メソッドの呼び出し中に登録されたメソッドについても照会されます。

Byte Buddyは、各実装の prepare メソッドと appender メソッドを、クラスの作成プロセス中に1回だけ呼び出すことに注意してください。実装がクラスの作成で使用するために何回登録されても、これは保証されます。このように、実装はフィールドまたはメソッドがすでに定義されているかどうかを検証することを避けることができます。その過程で、Byte Buddyは、 Implementations インスタンスを hashCode および equals メソッドで比較します。一般に、Byte Buddyによって使用されるクラスはすべて、これらのメソッドの意味のある実装を提供する必要があります。列挙が定義ごとにそのような実装に付属しているという事実は、それらが使用されるもう1つの正当な理由です。

以上で、 SumImplementation の動作を見てみましょう。

abstract class SumExample {
  public abstract int calculate();
}

new ByteBuddy()
  .subclass(SumExample.class)
    .method(named("calculate"))
    .intercept(SumImplementation.INSTANCE)
  .make()

おめでとうございます。 Byte Buddyを拡張して、 1050 の合計を計算して返すカスタムメソッドを実装しました。もちろん、この実装例はあまり実用的ではありません。ただし、このインフラストラクチャの上に、より複雑な実装を簡単に実装できます。結局のところ、あなたが何か便利なものを作成したと感じたら、 あなたの実装に貢献する ことを検討してください。ご連絡をお待ちしております。

Byte Buddyの他のコンポーネントのカスタマイズに進む前に、ジャンプ命令の使用といわゆるJavaスタックフレームの問題について簡単に説明する必要があります。 Java 6以降、例えば if または while ステートメントを実装するために使用されるジャンプ命令は、JVMの検証プロセスを高速化するためにいくつかの追加情報を必要とします。この追加情報はスタックマップフレームと呼ばれます。スタックマップフレームには、ジャンプ命令の任意のターゲットの実行スタックで見つかったすべての値に関する情報が含まれています。この情報を提供することによって、JVMの検証者はいくらかの作業を節約できますが、今はそれを私たちに任せています。より複雑なジャンプ命令については、正しいスタックマップフレームを提供することはかなり難しい作業であり、多くのコード生成フレームワークは常に正しいスタックマップフレームを作成するのにかなりの問題を抱えています。それでは、どうやってこの問題に対処するのでしょうか。実際のところ、私たちは単純にそうではありません。 Byte Buddyの哲学は、コード生成はコンパイル時には未知の型階層と、これらの型に注入する必要があるカスタムコードとの間の接着剤としてのみ使用されるべきであるということです。したがって、生成される実際のコードはできる限り制限されたままにする必要があります。可能な場合はいつでも、条件付きステートメントは、選択したJVM言語で実装およびコンパイルしてから、最小限の実装を使用して特定のメソッドにバインドする必要があります。このアプローチの良い副作用は、Byte Buddyのユーザーが通常のJavaコードで作業したり、デバッガやIDEコードナビゲータなどの慣れ親しんだツールを使用できることです。ソースコード表現を持たない生成コードでは、これは不可能です。ただし、ジャンプ命令を使用してバイトコードを作成する必要がある場合は、Byte Buddyが自動的にそれらを含めないため、ASMを使用して正しいスタックマップフレームを追加するようにしてください。

Creating a custom assigner

前のセクションで、Byte Buddyの組み込み実装は、変数に値を割り当てるために Assigner に依存することを説明しました。このプロセスでは、 Assigner は適切な StackManipulation を発行することによって、ある値の変換を別の値に適用できます。そうすることで、Byte Buddyの組み込みアシスタントは、例えばプリミティブ値とそのラッパータイプの自動ボックス化を提供します。最も一般的なケースでは、値はそのまま変数に代入できます。しかし場合によっては、代入者から無効な StackManipulation を返すことで表現できることがまったく代入できないことがあります。無効な代入の正規実装はByte Buddyの IllegalStackManipulation クラスによって提供されます。

カスタムアサイナの使い方を説明するために、受け取った任意の値に対して toString メソッドを呼び出して、文字列型変数にのみ値を割り当てるアサイナを実装します。

enum ToStringAssigner implements Assigner {

  INSTANCE; // singleton

  @Override
  public StackManipulation assign(TypeDescription.Generic source,
                                  TypeDescription.Generic target,
                                  Assigner.Typing typing) {
    if (!source.isPrimitive() && target.represents(String.class)) {
      MethodDescription toStringMethod = new TypeDescription.ForLoadedType(Object.class)
        .getDeclaredMethods()
        .filter(named("toString"))
        .getOnly();
      return MethodInvocation.invoke(toStringMethod).virtual(sourceType);
    } else {
      return StackManipulation.Illegal.INSTANCE;
    }
  }
}

上記の実装では、最初に入力値がプリミティブ型ではないことと、ターゲット変数型が String 型であることを検証します。これらの条件が満たされていない場合、 AssignerIllegalStackManipulation を発行して試行された割り当てを無効にします。そうでなければ、オブジェクト型の toString メソッドをその名前で識別します。次に、Byte Buddyの MethodInvocation を使用して、このメソッドをソースタイプで仮想的に呼び出す StackManipulation を作成します。最後に、このカスタム Assigner を、たとえばByte Buddyの FixedValue 実装と次のように統合できます。

new ByteBuddy()
  .subclass(Object.class)
  .method(named("toString"))
    .intercept(FixedValue.value(42)
      .withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE),
                    Assigner.Typing.STATIC))
  .make()

toString メソッドが上記の型のインスタンスで呼び出されると、文字列値 42 が返されます。これは、 toString メソッドを呼び出して Integer 型を String に変換するカスタムアサインナを使用することによってのみ可能です。このラップされたプリミティブ値の割り当てをその内側のアサイナに委譲する前に、提供されたプリミティブ int のラッパー型への自動ボックス化を実行する組み込み PrimitiveTypeAwareAssigner でカスタムアサイナをさらにラップしたことに注意してください。他の組み込みのアサイナは VoidAwareAssignerReferenceTypeAwareAssigner です。カスタムアサイナには、意味のある hashCode および equals メソッドを必ず実装してください。これらのメソッドは、通常、特定のアサイナを使用している Implementation 内の対応するメソッドから呼び出されます。また、アサイナをシングルトン列挙として実装することで、これを手動で行わないようにします。

Creating a custom parameter binder

前のセクションで、 MethodDelegation 実装を拡張してユーザー定義の注釈を処理することが可能であることについてはすでに述べました。この目的のために、与えられたアノテーションをどのように扱うかを知っているカスタム ParameterBinder を提供する必要があります。例として、単に注釈付きパラメータに固定文字列を挿入するという目的で注釈を定義したいと思います。まず、このような StringValue アノテーションを定義します。

@Retention(RetentionPolicy.RUNTIME)
@interface StringValue {
  String value();
}

適切な RuntimePolicy を設定して、アノテーションが実行時に見えるようにする必要があります。そうでなければ、注釈は実行時に保持されず、Byte Buddyはそれを発見する機会がありません。そうすることで、上記の value プロパティは、注釈付きパラメータに値として割り当てられる文字列を含みます。

カスタムアノテーションを使って、このパラメータのバインディングを表す StackManipulation を作成できる、対応する ParameterBinder を作成する必要があります。このパラメータバインダーは毎回呼び出され、対応するアノテーションは MethodDelegation によってパラメータ上で発見されます。この例のアノテーション用のカスタムパラメータバインダーを実装するのは簡単です。

enum StringValueBinder
    implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> {

  INSTANCE; // singleton

  @Override
  public Class<StringValue> getHandledType() {
    return StringValue.class;
  }

  @Override
  public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loaded<StringValue> annotation,
                                                         MethodDescription source,
                                                         ParameterDescription target,
                                                         Implementation.Target implementationTarget,
                                                         Assigner assigner,
                                                         Assigner.Typing typing) {
    if (!target.getType().asErasure().represents(String.class)) {
      throw new IllegalStateException(target + " makes illegal use of @StringValue");
    }
    StackManipulation constant = new TextConstant(annotation.loadSilent().value());
    return new MethodDelegationBinder.ParameterBinding.Anonymous(constant);
  }
}

最初に、パラメータバインダは、 target パラメータが実際には String 型であることを確認します。そうでない場合は、アノテーションのユーザーにこのアノテーションの違法な配置を通知する例外をスローします。それ以外の場合は、定数スタック文字列を実行スタックにロードすることを表す TextConstant を単純に作成します。この StackManipulation は、最後にメソッドから返される無名の ParameterBinding としてラップされます。あるいは、 Unique または Illegal パラメータバインディングを指定した可能性があります。一意のバインディングは、 AmbiguityResolver からこのバインディングを取得することを許可する任意のオブジェクトによって識別されます。後のステップで、そのようなリゾルバは、パラメータバインディングが何らかの一意の識別子で登録されているかどうかを調べることができ、次にこのバインディングが他の正常にバインドされたメソッドより優れているかどうかを判断できます。違法な束縛では、 source メソッドと target メソッドの特定のペアは互換性がなく、一緒に束縛することはできないことをByte Buddyに指示することができます。

これはすでに、 MethodDelegation 実装でカスタム注釈を使用するために必要なすべての情報です。 ParameterBinding を受け取った後、その値が正しいパラメータにバインドされていることを確認するか、 source メソッドと target メソッドの現在のペアをバインド不能として破棄します。さらに、それは AmbiguityResolvers がユニークな束縛を調べることを可能にするでしょう。最後に、このカスタムアノテーションを実行しましょう。

class ToStringInterceptor {
  public static String makeString(@StringValue("Hello!") String value) {
    return value;
  }
}

new ByteBuddy()
  .subclass(Object.class)
  .method(named("toString"))
    .intercept(MethodDelegation.withDefaultConfiguration()
      .withBinders(StringValueBinder.INSTANCE)
      .to(ToStringInterceptor.class))
  .make()

StringValueBinder を唯一のパラメータバインダーとして指定することで、すべてのデフォルト値が置き換えられます。あるいは、既に登録されているものにパラメータバインダーを追加することもできます。 ToStringInterceptor に 1つのターゲットメソッドしかない場合、動的クラスのインターセプトされた toString メソッドは後者のメソッドの呼び出しにバインドされます。ターゲットメソッドが呼び出されると、Byte Buddyはアノテーションの文字列値をターゲットメソッドの唯一のパラメータとして割り当てます。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3