- 投稿日:2019-03-07T23:12:51+09:00
[Java]コレクションクラスの重複判定メモ
コレクションクラス(List・Map・Set)
List
- 「ArrayList」「LinkedList」
インデックス番号 要素 0 りんご 1 なし 2 ぶどう 3 オレンジ 4 パイナップル Map
- 「HashMap」「TreeMap」
キー 値 name ごろう age 40 tel 09011112222 address 東京都ごろう5-6 goro@gmail.com 以下processingのソースコード
import java.util.*; HashMap<Point, Boolean> hashMap; void setup() { Point p1 = new Point(0.0, 1.0); Point p2 = new Point(1.0, 2.0); Point p3 = new Point(2.0, 3.0); hashMap = new HashMap<Point, Boolean>(); hashMap.put(p1, true); hashMap.put(p2, true); hashMap.put(p3, true); redundanciesMap(hashMap, p1); for(Iterator it = hashMap.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry)it.next(); Point p = (Point)entry.getKey(); boolean isUnique = ((Boolean)entry.getValue()).booleanValue(); if (isUnique) { println(p); } } } // 重複判定 public void redundanciesMap(HashMap<Point, Boolean> map, Point p) { if (map.containsKey(p)) { map.put(p, false); } else { map.put(p, true); } } class Point { float x, y; public Point(float x, float y) { this.x = x; this.y = y; } public void draw() { stroke(0); strokeWeight(5); point(x, y); } public String toString() { return "[x: " + x + ", y :" + y + "]"; } }Set
- 「HashSet」「TreeSet」
- 要素の重複は付加
- オブジェクトで重複判定する場合、hashCode、equalsをオーバーライドする必要がある
要素 りんご なし ぶどう オレンジ パイナップル 以下processingのソースコード
import java.util.*; HashSet<Point> hashSet; HashSet<Point> result; void setup() { hashSet = new HashSet<Point>(); hashSet.add(new Point(1.0, 2.0)); hashSet.add(new Point(1.0, 2.0)); hashSet.add(new Point(1.0, 3.0)); hashSet.add(new Point(1.0, 4.0)); result = deleteDuplicateObj(hashSet); for(Point p : result) { println(p.toString(), p.hashCode()); } } HashSet<Point> deleteDuplicateObj(HashSet<Point> hSet) { HashSet<Point> result = new HashSet<Point>(); for(Iterator<Point> it = hSet.iterator(); it.hasNext();) { Point p = it.next(); if(result.contains(p)) { it.remove(); } else { result.add(p); } } return result; } class Point { float x, y; public Point(float x, float y) { this.x = x; this.y = y; } public void draw() { stroke(0); strokeWeight(5); point(x, y); } // ハッシュ表で管理できるよう、hashCodeをオーバーライド public int hashCode() { return 0; } // ハッシュ表で管理できるよう、equalsをオーバーライド public boolean equals(Object o) { Point p = (Point)o; return (x == p.x && y == p.y); } public String toString() { return "[x: " + x + ", y :" + y + "]"; } }
- 投稿日:2019-03-07T18:00:49+09:00
SpringBootを使ってSwaggerに入門してみた
チーム内でバックエンドとフロントエンドを担当分けることになり、
僕はフロント側を担当することになりました。APIだけ先に用意してほしいな〜ってことで、試しにSwaggerを使ってみたら、だいぶ楽だった。
使い方調べてたら、いくつも方法があるみたいなので、
整理を含めて書き残しておこうと思い立った次第です。
今回はSpring Bootを利用してボトムアップアプローチで設計書を書き起こしてみます。ボトムアップ・アプローチ:ソースコードから書き起こす
ボトムアップアプローチというのは、ソースコードをベースにSwaggerを作成します。
ボトムアップアプローチの良いところは以下2点ですね。
- ソースコードベースで設計書が出来上がるので、ドキュメントとコードが乖離しにくくなる
- 設計書作ってからソースコード書くという二度手間がなくなる
「手戻り発生したら困るじゃないですか〜」みたいな話もありますが、
あくまで最初にAPIの口だけを定義するので、
ドキュメントとして作るのと作業時間は大差ないよねって思ってます。今回調べてみたところ、SpringBootとSwaggerで大抵のことはできそうなので、
今後は積極的に使っていきたいなと思いました。今回作成したコードはこちらに置いてあります。
Spring Bootのセッティング
Spring Foxの導入
Spring Foxを使うことでソースコードからAPIの設計書を書き起こしてくれます。
Spring Foxを導入するには、以下の依存関係を解決してあげればOKです。
build.gradlerepositories { jcenter() } dependencies { compile "io.springfox:springfox-swagger2:2.9.2" compile 'io.springfox:springfox-swagger-ui:2.9.2' // Swagger UIを利用するため }Spring BootでSpring Foxを有効にする
SpringBootSwaggerApplication.javapackage com.example.springbootswagger; // import文省略 @SpringBootApplication @EnableSwagger2 public class SpringBootSwaggerApplication { public static void main(String[] args) { SpringApplication.run(SpringBootSwaggerApplication.class, args); } // DocketはSpring Foxが提供するAPI。Swaggerで書き起こすために設定が必要 @Bean public Docket petApi() { return new Docket(DocumentationType.SWAGGER_2) .select() // ApiSelector : Swaggerで書き起こすAPIを選択する。 .paths(PathSelectors.ant("/pets/**")) // 指定したパスに一致するものだけをSwaggerに起こしてくれる .build() // ApiSelectorを作成 .useDefaultResponseMessages(false) // 定義していないステータスコードを自動で付与してくれる。今回は自動付与をOFFに .host("springbootswagger.example.com") .apiInfo(apiInfo()); // APIのインフォメーションを設定 } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Pet Service") .description("This is a pet service api") .version("1.0.0") .build(); } }公式ガイドを見ると、ここで他にも共通のレスポンスメッセージを返す設定や、
セキュリティに関する設定などができる。
今回はボリュームが多くなってしまうため割愛。プロダクションにする際には、閲覧制限かける必要あるよなぁ。
どうやるんだろう。今度調べてみよう。アノテーションを貼っていく
Spring Boot, Spring Foxの設定は完了したので、次は実際にSwaggerに書き起こしてもらうように
各種Resourceにアノテーションを貼っていく。一番シンプルな状態で以下の通りに書けばひとまず仕様書が見れる。
PetResource.javapackage com.example.springbootswagger; // import文省略 @RestController @RequestMapping("/pets") public class PetResource { @GetMapping public List<Map<String, String>> pets() { return new ArrayList<>(); } @GetMapping("{id}") public Map<String, String> pet(@PathVariable String id) { return new HashMap<>(); } @PutMapping("{id}") public void updatePet(@PathVariable String id) { return; } @PostMapping public int insertPet() { return 1; } @DeleteMapping public void deletePet() { return; } }通常、APIの各リソースに貼るアノテーションをつけてあげれば、
それだけでSwaggerに書き起こしてくれる。すごい。Swagger UIの確認方法
アプリケーションを起動して、
http://localhost:8080/swagger-ui.htmlにアクセスすると
以下の画面が表示されます。ちなみに、
swagger-ui.htmlは定義せずとも
gradleでio.springfox:springfox-swagger-ui:2.9.2の依存関係を解決していれば、
勝手に生成してくれるみたい。もっと細かくSwaggerを定義する
Springが提供しているController層のアノテーションであれば、
それに従ってSwaggerに反映してくれます。
例えば、@PathVariableをつけてあげれば、以下のように反映されます。しっかり必須項目も付いています。
その他、SpringではなくSwaggerが用意してくれているアノテーションをつけると、
より親切なAPI設計書が出来上がります。各リソースの概要を記載する
PetResource.java// import文省略 @RestController @RequestMapping("/pets") public class PetResource { // @ApiOperationでリソースの概要を設定 @ApiOperation(value = "This Resource fetch all reserved pets") @GetMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public List<PetDto> pets() { return new ArrayList<>(); } }レスポンスのパターンを追加する
PetResource.java// import文省略 @RestController @RequestMapping("/pets") public class PetResource { @ApiOperation(value = "This Resource fetch a pet by id") // ApiResponsesで複数のレスポンスを定義できる。codeとmessageが必須項目。 @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid Id supplied", response = ErrorDto.class), @ApiResponse(code = 404, message = "Pet not found")}) @GetMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public PetDto pet(@PathVariable String id) { return new PetDto(); } }あとがき
今回、セキュリティ周りのことはボリュームの都合で割愛したので、
別記事でまとめようかな〜と思います。
あとは、途中で書いた通りプロダクションでは見せないような工夫が必要なので、
そのやり方が気になっています。SpringBoot使ってるから、SpringSecurityでなんやかんやするべきなのかな?
参考文献
導入する際の設定もろもろはこちら。
http://springfox.github.io/springfox/docs/current/#getting-startedSwaggerのアノテーションはこちら。
https://github.com/swagger-api/swagger-core/wiki/annotations
- 投稿日:2019-03-07T16:30:07+09:00
オンラインカードゲームのサーバーサイドを作った④
Deckクラスを作る
はい、今回はやっとデッキを作っていきます。
前回、折角作った紙束インターフェースをしっかり使っていきましょう。まずはClassを作る
public class Deck{ private Stack<Card> deck = new Stack(); }はい、デッキができました。
というのは流石に冗談ですが、カードをStackとして持つDeckクラスができました。
ちなみにJavaのStackはListのサブクラスです。デッキは基本的にはトップから取り出す操作しか行わないため、配列や線形リストではなくスタックを利用しました。
CardSheafを実装する。
public class Deck implements CardSheaf { private Stack<Card> deck = new Stack<>(); public Deck(List<Card> deck) { this.deck.addAll(deck); } public Deck(Card... deck) { this.deck.addAll(Arrays.asList(deck)); } @Override public int indexOf(Card card) { return CardSheaf.indexOf(deck, card); } //CardSheafクラスではcardSizeの標準実装を提供していないため、実装した。 //要素数を取り出すだけ。 @Override public int cardSize() { return deck.size(); } @Override public void addCard(Card card) { CardSheaf.addCard(deck, card); } @Override public void removeCard(Card card) { CardSheaf.removeCard(deck, card); } @Override public void removeCard(int index) { CardSheaf.removeCard(deck, index); } @Override public Card find(int number, Card.Mark mark) { return CardSheaf.find(deck, number, mark); } @Override public int indexOf(int number, Card.Mark mark) { return CardSheaf.indexOf(deck, number, mark); } @Override public Card getCard(int index) { return CardSheaf.getCard(deck, index); } @Override public Card takeCard(int index) { return CardSheaf.takeCard(deck, index).getValue(); } }Deckクラスでは、カードをListで扱うため、CardSheafのstaticメソッドをそのまま利用した。
Deckに必要なメソッドを定義する
import java.util.*; public class Deck implements CardSheaf { private Stack<Card> deck = new Stack<>(); /** * リスト内のカードでデッキを初期化する。 * @param deck 初めにデッキ内に置きたいカードすべてのリスト */ public Deck(List<Card> deck) { this.deck.addAll(deck); } /** * 配列内のカードでデッキを初期化する。 * @param deck 初めにデッキ内に置きたいカードすべての配列 */ public Deck(Card... deck) { this.deck.addAll(Arrays.asList(deck)); } //略 /** * デッキの一番上のカードを取り出す。 */ public Card take_top() { return deck.pop(); } /** * ゲーム開始時の先攻判定に用いる。 * @return デッキの一番上のカード */ public Card first_step() { return this.take_top(); } /** * カードをドローする。 * @return デッキの一番上のカード */ public Card draw() { return this.take_top(); } /** * ダメージを受けたときに呼ばれる。 * @return デッキの一番上のカード */ public Card damage() { return this.take_top(); } /** * デッキをシャッフルする。 */ public void shuffle() { Collections.shuffle(deck); } /** * デッキ内にカードが存在するかどうかを判定する。 * @return デッキ内にカードがあるならtrue、ないならfalse */ public boolean hasCard() { return cardSize() > 0; } }では、今回はここまで。
次回は手札を実装予定です。
- 投稿日:2019-03-07T14:53:35+09:00
【翻訳】Byte Buddy チュートリアル
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.Barがexample.Fooを拡張すると、オーバーライドされたパッケージプライベートメソッドは機能しなくなりますが、元の実装に委譲されます。- クラスがロードされるときはいつでも、別の型を参照するコードセグメントが解決されると、そのクラスローダはこのクラスで参照される型を検索します。このルックアップは同じクラスローダーに委譲します。 2つのクラス
example.Fooとexample.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の代わりにJavaTypeを受け入れます。ジェネリック型は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コンパイラがオーバーロードメソッドのバインディングをどのように選択するかを覚えておいてください。StringはObjectより具体的なので、最後に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でアノテーションが付けられたパラメータは、Method、Constructor、Executable、Class、MethodHandle、MethodType、String、またはint型のいずれかで使用されなければなりません。パラメータの型に応じて、現在計測されている元のメソッドまたはコンストラクタへのMethodまたはConstructor参照、または動的に作成されたClassへの参照が割り当てられます。 Java 8を使用している場合は、インターセプターでExecutable型を使用して、メソッドまたはコンストラクターの参照を受け取ることもできます。注釈付きパラメータがStringの場合、パラメータにはメソッドのtoStringメソッドが返すはずの値が割り当てられます。一般に、可能な限りメソッド識別子としてこれらのString値を使用することをお勧めします。また、ルックアップによって大きなランタイムオーバーヘッドが発生するため、Methodオブジェクトの使用は推奨しません。このオーバーヘッドを回避するために、@Originアノテーションはそのようなインスタンスを再利用のためにキャッシュするためのプロパティも提供します。MethodHandleとMethodTypeはクラスの定数プールに格納されるため、これらの定数を使用するクラスは少なくとも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)の実装を呼び出すLoggerInterceptorにCallableのインスタンスをインジェクトすることによって呼び出されることは明らかです。このヘルパークラスは、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アノテーションは、インスツルメントされたメソッドが非抽象スーパーメソッドを定義していない場合、メソッドのバインディングを防ぎます。ただし、特定の型でデフォルトのメソッドを呼び出したい場合は、@DefaultCallのtargetTypeプロパティを特定のインタフェースで指定できます。この仕様では、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クラスで定義されているメソッドを含め、インスタンスのクラス階層内の任意の場所で定義されているメソッドが含まれます。MethodDelegationでfilter(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プロパティアクセサを定義することで、getInterceptorのgetterとsetInterceptorのsetterを実現します。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構成は、動的に作成された型または型メンバーに注釈を事前定義しません。ただし、この動作は、デフォルトのTypeAttributeAppender、MethodAttributeAppender、またはFieldAttributeAppenderを指定することで変更できます。このようなデフォルトのアペンダは加法的ではなく、以前の値に置き換わることに注意してください。クラスを定義するときに、注釈型またはそのプロパティの型を読み込まないことが望ましい場合があります。この目的のために、クラスのロードをトリガーすることなく注釈を定義するための流暢なインターフェースを提供する
AnnotationDescription.Builderを使用することができますが、型安全性が犠牲になります。ただし、すべての注釈プロパティは実行時に評価されます。デフォルトでは、Byte Buddyは、デフォルト値によって暗黙的に指定されているデフォルトプロパティを含む、アノテーションのすべてのプロパティをクラスファイルに含めます。ただし、この動作は、
AnteationFilterをByteBuddyインスタンスに提供することでカスタマイズできます。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つのプリミティブ整数
10と50の合計を計算して返すメソッドを考えてみましょう。このメソッドの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ステートメントはこの計算結果を消費してメソッドから返し、空のスタックを残します。メソッド内で参照されるすべてのプリミティブ値は、クラスの定数プールに格納されることは既に述べました。これは、上記の方法で参照される番号
50と10にも当てはまります。定数プールのどの値にも、長さ2バイトのインデックスが割り当てられます。数値10と50がインデックス1と2に格納されていたとしましょう。LDCの場合は0x12、IADDの場合は0x60、IRETURNの場合は0xACである上記ニーモニックのバイト値とともに、上記の方法を次のように表す。生バイト命令:12 00 01 12 00 02 60 ACコンパイル済みクラスの場合、この正確なバイトシーケンスはクラスファイルにあります。ただし、この説明ではメソッドの実装を完全に定義するにはまだ十分ではありません。 Javaアプリケーションの実行時間を短縮するために、各メソッドはJava仮想マシンに実行スタックに必要なサイズを通知する必要があります。ブランチなしで来る上記のメソッドの場合、スタックには最大で2つの値があることをすでに見たので、これは決定がかなり簡単です。ただし、より複雑な方法では、この情報を提供することは簡単に複雑な作業になる可能性があります。さらに悪いことに、スタック値は異なるサイズにすることができます。
longとdoubleの値はどちらも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つの数値
10と50を実行スタックにロードし、これらの値の合計を適用して計算結果を返します。これらすべての命令を複合スタック操作でラップすることで、この一連のスタック操作を実行するために必要な集約スタックサイズを確実に取得できます。最後に、このメソッドの全体的なサイズ要件を返します。返された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を拡張して、
10と50の合計を計算して返すカスタムメソッドを実装しました。もちろん、この実装例はあまり実用的ではありません。ただし、このインフラストラクチャの上に、より複雑な実装を簡単に実装できます。結局のところ、あなたが何か便利なものを作成したと感じたら、 あなたの実装に貢献する ことを検討してください。ご連絡をお待ちしております。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型であることを検証します。これらの条件が満たされていない場合、AssignerはIllegalStackManipulationを発行して試行された割り当てを無効にします。そうでなければ、オブジェクト型の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でカスタムアサイナをさらにラップしたことに注意してください。他の組み込みのアサイナはVoidAwareAssignerとReferenceTypeAwareAssignerです。カスタムアサイナには、意味のある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はアノテーションの文字列値をターゲットメソッドの唯一のパラメータとして割り当てます。
- 投稿日:2019-03-07T11:47:02+09:00
デザインパターン ~Visitor~
1. はじめに
GoFのデザインパターンにおける、Visitorパターンについてまとめます。
2. Visitorパターンとは
- Visitorという英単語は、訪問者という意味になります。
- Visitorパターンは、データ構造と処理を分離する方式です。
- データ構造の中をめぐり歩く訪問者クラスを用意し、訪問者クラスに処理を任せます。すると、新しい処理を追加したいときは新しい訪問者を作ればよいことになります。そして、データ構造の方は、訪問者を受け入れてあげればよいのです。
- GoFのデザインパターンでは、振る舞いに関するデザインパターンに分類されます。
3. サンプルクラス図
4. サンプルコード
4-1. Elementインターフェース
Visitorクラスのインスタンスを受け入れるデータ構造を表すインターフェースです。
Element.javapublic interface Element { public abstract void accept(Visitor v); }4-2. Entryクラス
FileやDirectoryの基底となるクラスです。Elementインターフェースを実装します。
Entry.javapublic abstract class Entry implements Element { public abstract String getName(); public String toString() { return getName(); } }4-3. Fileクラス
ファイルを表すクラスです。Visitorの受け入れ役になります。
File.javapublic class File extends Entry { private String name; public File(String name) { this.name = name; } public String getName() { return name; } public void accept(Visitor v) { v.visit(this); } }4-4. Directoryクラス
ディレクトリを表すクラスです。Visitorの受け入れ役になります。
Directory.javaimport java.util.ArrayList; import java.util.Iterator; public class Directory extends Entry { private String name; private ArrayList<Entry> dir = new ArrayList<Entry>(); public Directory(String name) { this.name = name; } public String getName() { return name; } public Entry add(Entry entry) { dir.add(entry); return this; } public Iterator<Entry> iterator() { return dir.iterator(); } public void accept(Visitor v) { v.visit(this); } }4-5. Visitorクラス
ファイルやディレクトリを訪れる訪問者を表す抽象クラスです。
Visitor.javapublic abstract class Visitor { public abstract void visit(File file); public abstract void visit(Directory directory); }4-6. ListVisitorクラス
ファイルやディレクトリの一覧を表示するクラスです。
ListVisitor.javaimport java.util.Iterator; public class ListVisitor extends Visitor { // 現在注目しているディレクトリ名 private String currentdir = ""; // ファイルを訪問したときに呼ばれる public void visit(File file) { System.out.println(currentdir + "/" + file); } // ディレクトリを訪問したときに呼ばれる public void visit(Directory directory) { System.out.println(currentdir + "/" + directory); String savedir = currentdir; currentdir = currentdir + "/" + directory.getName(); Iterator<Entry> it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); entry.accept(this); } currentdir = savedir; } }4-7. Mainクラス
メイン処理を行うクラスです。
Main.javapublic class Main { public static void main(String[] args) { Directory workspaceDir = new Directory("workspace"); Directory compositeDir = new Directory("Visitor"); Directory testDir1 = new Directory("test1"); Directory testDir2 = new Directory("test2"); workspaceDir.add(compositeDir); workspaceDir.add(testDir1); workspaceDir.add(testDir2); File element = new File("Element.java"); File entity = new File("Entity.java"); File file = new File("file.java"); File directory = new File("Directory.java"); File visitor = new File("Visitor.java"); File listVisitor = new File("ListVisitor.java"); File main = new File("main.java"); compositeDir.add(element); compositeDir.add(entity); compositeDir.add(file); compositeDir.add(directory); compositeDir.add(visitor); compositeDir.add(listVisitor); compositeDir.add(main); workspaceDir.accept(new ListVisitor()); } }4-8. 実行結果
/workspace /workspace/Visitor /workspace/Visitor/Element.java /workspace/Visitor/Entity.java /workspace/Visitor/file.java /workspace/Visitor/Directory.java /workspace/Visitor/Visitor.java /workspace/Visitor/ListVisitor.java /workspace/Visitor/main.java /workspace/test1 /workspace/test25. メリット
Visitorパターンは処理を複雑にしているだけで、「繰り返しの処理が必要ならデータ構造の中にループ処理を書けばいいのではないか?」と感じます。
Visitorパターンの目的は、データ構造と処理を分離することです。データ構造は、要素を集合としてまとめたり、要素間を繋いだりしてくれるものです。その構造を保持しておくことと、その構造を基礎とした処理を書くことは別のものです。
訪問者役(ListVisitor)は、受け入れ役(File、Directory)とは独立して開発することができます。つまり、Visitorパターンは、受け入れ役(File、Directory)クラスの部品としての独立性を高めていることになります。もし、処理の内容をFileクラスやDirectoryクラスのメソッドとして実装してしまうと、新しい処理を追加して機能拡張するたびにFileクラスやDirectoryクラスを修正しなければならなくなります。6. 参考
- 投稿日:2019-03-07T09:04:07+09:00
無限ループ∞最短選手権
さぁみんな無限ループしよう。
最近、無限ループが流行りらしいので
各プログラミング言語(その辺にあった10個の言語)の
無限ループを比べてみます。
果たしてどの言語が1位に輝くのか!?※改行は1文字としてカウント。
(一応、全て実行してチェックしています)C (24文字)
int main(void){for(;;);}C⋕ (37文字)
class a{static void Main(){for(;;);}}C++ (20文字)
int main(){for(;;);}D (22文字)
void main(){for(;;){}}Go (31文字)
package main func main(){for{}}Java (54文字)
class a{public static void main(String[] a){for(;;);}}JavaScript (8文字)
for(;;);PHP (14文字)
<?php for(;;);Python (9文字)
while 1:0Ruby (11文字)
while 1 end【優勝】JavaScript
チャンピオンは"JavaScript"でした。さすが"JS"。
"JavaScript"は無限ループ界において最有力候補であると考えられますね。???「JSが優勝だと思っていたのか。」
【真の優勝】Ruby ※追記
Ruby(6文字)loop{}Ruby、6文字で無限ループが出来るとは……。
恐るびー最後に
もっと文字数減らせるよ!とか
もっと文字数が少ない言語あるぜ!最強だぜ!などなどありましたらコメントまたは編集リクエストでお願い致します。
- 投稿日:2019-03-07T09:04:07+09:00
?♀️無限ループ∞最短選手権?♂️
さぁみんな無限ループしよう。
最近、無限ループが流行りらしいので
各プログラミング言語(その辺にあった10個の言語)の
無限ループを比べてみます。
果たしてどの言語が1位に輝くのか!?※改行は1文字としてカウント。
(一応、全て実行してチェックしています)C (24文字)
int main(void){for(;;);}C⋕ (37文字)
class a{static void Main(){for(;;);}}C++ (20文字)
int main(){for(;;);}D (22文字)
void main(){for(;;){}}Go (31文字)
package main func main(){for{}}Java (53文字)
class a{public static void main(String[]a){for(;;);}}JavaScript (8文字)
for(;;);PHP (14文字)
<?php for(;;);Python (9文字)
while 1:0Ruby (11文字)
while 1 end【優勝】JavaScript
チャンピオンは"JavaScript"でした。さすが"JS"。
"JavaScript"は無限ループ界において最有力候補であると考えられますね。???「JSが優勝だと思っていたのか。」
【真の優勝】Ruby ※追記
Ruby(6文字)loop{}Ruby、6文字で無限ループが出来るとは……。
恐るびー???「6文字ごときが優勝だと思っていたのか。」
【本当の真の優勝】L00P ※追記(番外編)
L00P(0文字)0文字……圧巻です。
言語名からして、無限ループ界の頂点に君臨していると思われる風貌をしてますね……。このように上記で比べていた10個の言語以外の言語では、もっと文字数が少ないものがありました。
無限ループは奥が深い。最後に
もっと文字数減らせるよ!とか
もっと文字数が少ない言語あるぜ!最強だぜ!などなどありましたらコメントまたは編集リクエストでお願い致します。
- 投稿日:2019-03-07T02:16:52+09:00
【Android】画像をカルーセル表示するときにハマった話
RecyclerViewを使うと簡単に画像をカルーセル表示することができます。
ですが、ImageViewの扱いでハマった部分があったので、その際に調査した内容をまとめました。TL;DR
ImageViewの
AdjustViewBoundsにtrueをセットすることで、ImageViewのサイズを画像サイズに合わせて調整することができます。前提条件
カルーセル、画像のサイズおよび表示方法に関する条件は以下の通りです。
- カルーセルの高さは固定とする
- カルーセルに表示する各画像のサイズはすべて異なる可能性がある
- 各画像の高さはカルーセルの高さと一致するように拡大縮小する
- 各画像のアスペクト比は維持する
- 各画像はクロップしない
ハマった内容
以前作成したScaleTypeと表示画像の対応表を参考に、
ScaleTypeは一旦FIT_CENTER(デフォルト値のためxml上の指定なし)、またlayout_widthはwrap_content、layout_heightは100dpとしました。RecyclerViewの各要素に表示する画面のxmlは以下の通りです。
item_main.xml<?xml version="1.0" encoding="utf-8"?> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="100dp" />正直どの
ScaleTypeも今回の条件を満たさないと感じつつも、layout_widthをwrap_contentにすることで、FIT_CENTERであれば画像の高さをカルーセルの高さと一致させ、それに合わせて画像全体が含まれるようにアスペクト比を維持しながら幅を調整してくれるのでは?というわずかな希望を抱きながら試してみました。結果は以下のようになりました。案の定、希望通りの表示になりませんでした。
これを見て、画像が小さいときの結果(左)は納得できるのですが、画像が大きいときの結果(右)に関しては何で左右に余白ができるのかが理解できませんでした。解決方法
冒頭でも触れましたが、ImageViewの
AdjustViewBoundsをtrueにすることで解決できました。これは文字通り「Viewの境界を調整する」ための属性です。
公式ドキュメントはこちらにあります。
AdjustViewBoundsをtrueにすると、結果は以下のようになりました。上が先ほどの結果(デフォルト値はfalse)で、下が今回の結果です。
これで、ImageViewのサイズを画像サイズに合わせて調整することができました。AdjustViewBoundsをセットしたときの内部処理
結果としてはこれで問題ないのですが、画像が大きいときの結果に関してはこれだけでは理解ができなかったので、ImageViewの処理を追ってみました。
ImageView.javaのソースコードはこちらにあります。Androidにおいて、Viewが表示されるまでの大まかな流れは以下の通りになります。今回の肝となるのは1.の
onMeasure()です。
- サイズを決める(
onMeasure())- 場所を決める(
onLayout())- 描画する(
onDraw())これに関してはこちらの記事を参考にさせていただきました。
では、ここからはImageViewのonMeasure()の処理を追っていきます。最初は各変数を宣言しているだけだったので割愛します。
以下の処理が1つ目のポイントになります。ImageView.java#LL.1088-1089final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
widthMeasureSpecとheightMeasureSpecはonMeasure()の引数で、そこからMeasureSpecのModeを取得しています。
ただ、widthMeasureSpecとheightMeasureSpecがどのように決定されるのかは正直よく分かりませんでした。
MeasureSpecは親Viewから子Viewに対して課される制約を表しており、以下の3種類のModeがあります。公式ドキュメントはこちらにあります。
MeasureSpec.Mode 制約条件 UNSPECIFIED 親Viewによって子Viewのサイズが決定されない EXACTLY 親Viewによって子Viewの正確なサイズが決定される AT_MOST 親Viewによって子Viewの最大のサイズが決定される したがって、
widthSpecModeはMeasureSpec.UNSPECIFIED、heightSpecModeはMeasureSpec.EXACTLYとなります。
そして、以下の処理が2つ目のポイントになります。
AdjustViewBoundsがtrueの場合のみ以下の処理が行われます。ImageView.java#LL.1104-1109if (mAdjustViewBounds) { resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; desiredAspect = (float) w / (float) h; }
resizeWidthとresizeHeightは幅・高さのリサイズを行うか否かを制御する変数です。
desiredAspectは画像のアスペクト比(wは幅、hは高さ)を表す変数です。今回の場合、
widthSpecModeはMeasureSpec.UNSPECIFIED、heightSpecModeはMeasureSpec.EXACTLYなので、AdjustViewBoundsがtrueのときはresizeWidthのみがtrueとなります。
そして、以下の処理が3つ目のポイントになります。
長いので詳細は省略しますが、おおむね以下のような処理が行われています。
AdjustViewBoundsがtrueのとき
ImageViewのサイズを画像サイズに合わせて調整するAdjustViewBoundsがfalseのとき
ImageViewの幅を画像の幅と一致するよう調整するImageView.java#LL.1120-1190int widthSize; int heightSize; if (resizeWidth || resizeHeight) { // Get the max possible width given our constraints widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec); // Get the max possible height given our constraints heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec); if (desiredAspect != 0.0f) { // See what our actual aspect ratio is final float actualAspect = (float)(widthSize - pleft - pright) / (heightSize - ptop - pbottom); if (Math.abs(actualAspect - desiredAspect) > 0.0000001) { boolean done = false; // Try adjusting width to be proportional to height if (resizeWidth) { int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright; // Allow the width to outgrow its original estimate if height is fixed. if (!resizeHeight && !sCompatAdjustViewBounds) { widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec); } if (newWidth <= widthSize) { widthSize = newWidth; done = true; } } // Try adjusting height to be proportional to width if (!done && resizeHeight) { ... } } } } else { w += pleft + pright; h += ptop + pbottom; w = Math.max(w, getSuggestedMinimumWidth()); h = Math.max(h, getSuggestedMinimumHeight()); widthSize = resolveSizeAndState(w, widthMeasureSpec, 0); heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); } setMeasuredDimension(widthSize, heightSize);
ここで、else句の最後にあるresolveSizeAndState()がMeasureSpec.UNSPECIFIEDのときは引数に指定した画像サイズそのものを返していたので、ImageViewの幅が画像の幅と同じサイズで表示されるということが分かりました。スッキリ。
View.javaのソースコードはこちらにあります。View#LL.23463-23483public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; switch (specMode) { case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }サンプルアプリ
上記の画像キャプチャを撮影したアプリです。
GitHub - AdjustViewBoundsChecker
- 投稿日:2019-03-07T01:18:49+09:00
Apache Arrow と JSON を Yosegi に変換する処理時間を比較しました
この記事では Apache Arrow と JSON をカラムナフォーマット Yosegi に変換する処理時間を比較してみたいと思います。
※ そもそもの Yosegi の書き込みの処理性能は前回書いた記事で ORC, Parquet と比較していますので参考にしてください。
Yosegi の準備
Yosegi には CLI があるので、こちらを利用します。
はじめに GitHub から clone します。
この jar のコンパイル後にセットアップコマンドを実行します。$ git clone https://github.com/yahoojapan/yosegi-tools $ mvn clean package $ ./bin/setup.shこれにより、コマンドが利用可能になります。
サンプルの JSON ファイルがありますので、こちらで動作を確認できます。$ ./bin/yosegi.sh create -i ./etc/sample_json.txt -o /tmp/a.json -f json $ ./bin/yosegi.sh cat -i /tmp/a.json -o "-" {"summary":{"total_price":550,"total_weight":412},"number":5,"price":110,"name":"apple","class":"fruits"} {"summary":{"total_price":800,"total_weight":600},"number":10,"price":80,"name":"orange","class":"fruits"}比較に利用するデータ
TPC-H の lineitem を JSON に変換したデータを利用します。データサイズは約 3.5GB です。
$ ls -l /tmp/lineitem.json -rwxrwxrwx 1 hoge hoge 3665538085 Mar 6 14:24 /tmp/lineitem.json検証作業
JSON から Yosegi への変換
JSON から Yosegi に変換します。
処理時間は約2分41秒でした。$ time HEAP_SIZE=1g ./bin/yosegi.sh create -i /tmp/lineitem.json -o /tmp/lineitem.yosegi.gz -f json real 2m41.488s user 1m47.595s sys 0m44.432sYosegi から Apache Arrow への変換
出力された Yosegi ファイルを Apache Arrow フォーマットに変換します。
処理時間は約16秒でした。$ time HEAP_SIZE=1g ./bin/yosegi.sh to_arrow -i /tmp/lineitem.yosegi.gz -o /tmp/lineitem.arrow real 0m16.134s user 0m9.927s sys 0m2.392sデータサイズは約 1.5GB となります。
Key の情報が少ないので JSON と比較すると大幅に少なくなります。$ ls -l /tmp/lineitem.arrow -rwxrwxrwx 1 hoge hoge 1566760242 Mar 6 16:24 /tmp/lineitem.arrowApache Arrow から Yosegi への変換
出力された Apache Arrow ファイルを Yosegi フォーマットに変換します。
処理時間は約1分9秒でした。$ time HEAP_SIZE=1g ./bin/yosegi.sh from_arrow -i /tmp/lineitem.arrow -o /tmp/lineitem_from_arrow.yosegi.gz real 1m9.046s user 1m4.562s sys 0m1.558sYosegi から JSON への変換
最後に Yosegi から JSON への変換を実行します。
処理時間は約14分43秒でした。$ time HEAP_SIZE=1g ./bin/yosegi.sh cat -i /tmp/lineitem.yosegi.gz -f json -o /tmp/lineitem_from_yosegi.json real 14m42.576s user 0m10.492s sys 8m23.839s書き込み先がディスクなので、/dev/null にして比較をする事にします。
JSON の変換では約46秒、Apache Arrow の変換は約10秒かかりました。
$ time HEAP_SIZE=1g ./bin/yosegi.sh cat -i /tmp/lineitem.yosegi.gz -f json -o "-" > /dev/null real 0m45.748s user 0m44.851s sys 0m0.429s $ time HEAP_SIZE=1g ./bin/yosegi.sh to_arrow -i /tmp/lineitem.yosegi.gz -o "-" > /dev/null real 0m10.141s user 0m9.431s sys 0m0.347s比較のまとめ
処理 JSON の時間 Apache Arrow の時間 Apache Arrow の時間 / JSON の時間 Yosegi への書き込み 161s 69s 0.43 Yosegi から読み込み 46s 10s 0.22 Apache Arrow は JSON と比較し、書き込み処理については約2.3倍、読み込みについては4.55倍速い結果となりました。
まとめ
書き込み処理において、JSON はカラムのデータ構造でメモリに保存する処理が加わる。
Apache Arrow はカラムのデータ構造をしているので、そのまま変換できる。そのため、メモリに一時的に保存するための処理が省略される。
読み込みと比較して差が少ないのは圧縮の処理時間が大きいためであると推測される。読み込み処理において、JSON はカラムのデータ構造でメモリにロードしてからメッセージ単位で読み込み、JSON へと変換する。
Apache Arrow はカラムのデータ構造をしているので、そのまま ロードする事ができる。そのため、メッセージ単位で読み込む処理と変換処理が省略される。この事から Apache Arrow を経由して相互に変換を行うのは効率的であると言えます。
今後は Apache Arrow が広まっていく事を想定すると、入出力は Apache Arrow を前提に設計した方が効率が良いかもしれないです。
この記事ではデータフォーマットについて記載したが、言語間でのデータの交換についても理解を深めたいと思います。おまけ
Apache Arrow のバイナリは python などで簡単にデータ処理ができます。
Yosegi 自体は Java しか対応していないのですが、Apache Arrow を経由する事により、簡単に連携ができます。import pyarrow as pa reader = pa.RecordBatchFileReader( pa.OSFile( "/tmp/lineitem.arrow" ) ) rb = reader.get_record_batch(0) df = rb.to_pandas() print( df["l_linestatus"].value_counts() )上記のプログラムの実行結果は以下となります。
$ time python a.py F 25129 O 24871 Name: l_linestatus, dtype: int64 real 0m0.327s user 0m0.269s sys 0m0.042sさいごに
現在 OSS として開発を行っている Yosegi では、利用者、開発者を募集しています!
もし、Yosegi に興味を少しでも持った方がいましたら、気軽にご連絡ください!






