bytekinの紹介
bytekinとは?
bytekinは、ASM上に構築された軽量で使いやすいJavaバイトコード変換フレームワークです。開発者がバイトコードレベルでJavaクラスをプログラム的に変更できるようにし、強力なコードインジェクション、メソッドインターセプション、変換機能を提供します。
bytekinは以下のように設計されています:
- シンプル: 直感的なアノテーションベースAPI
- 軽量: 最小限の依存関係(ASMのみ)
- 柔軟: 複数の変換戦略
- 強力: 複雑なバイトコード操作のサポート
主な機能
- インジェクション: メソッドの特定の位置(先頭、return文など)にカスタムコードを挿入
- インボケーション: メソッド呼び出しをインターセプトして動作を変更
- リダイレクト: メソッド呼び出しのターゲットを変更
- 定数の変更: バイトコード内の定数値を変更
- 変数の変更: ローカル変数を操作
- マッピングサポート: クラスとメソッド名を自動的に変換
- ビルダーパターン: トランスフォーマー構築のための流暢なAPI
ユースケース
bytekinは以下のようなシナリオに最適です:
- ログ記録や監視の追加 - ソースコードを変更せずに既存のクラスに追加
- AOP(アスペクト指向プログラミング)の実装 - フレームワークのオーバーヘッドなし
- セキュリティチェックの追加 - 実行時に追加
- サードパーティライブラリの動作を変更 - バイトコードレベルで
- モックやスタブの実装 - テスト用に
- コードの計装 - プロファイリングや分析のため
- 横断的関心事の適用 - 複数のクラスに
なぜbytekinなのか?
フル機能フレームワークとは異なり、bytekinは:
- 最小限: ASMのみに依存し、重い依存関係なし
- 直接的: リフレクションのオーバーヘッドなしでバイトコードを直接操作
- 柔軟: アノテーションベースとプログラマティックの両方のアプローチをサポート
- 高速: JVMロード時の効率的なバイトコード操作
プロジェクト構造
bytekinプロジェクトはいくつかのモジュールに分かれています:
- 変換エンジン: コアバイトコード操作ロジック
- インジェクションシステム: メソッドインジェクション機能
- マッピングシステム: クラスとメソッド名マッピングのサポート
- ユーティリティ: バイトコード操作のためのヘルパークラス
次のステップ
bytekinを始める
このセクションでは、bytekinのセットアップと最初のバイトコード変換の作成方法を説明します。
前提条件
- Java 8以上
- Javaの基本的な理解
- MavenまたはGradle(依存関係管理のため)
インストール
Maven
pom.xml
に以下を追加してください:
<dependency>
<groupId>io.github.brqnko.bytekin</groupId>
<artifactId>bytekin</artifactId>
<version>1.0</version>
</dependency>
Gradle
build.gradle
に以下を追加してください:
dependencies {
implementation 'io.github.brqnko.bytekin:bytekin:1.0'
}
最初の変換
ステップ1: フッククラスの作成
ターゲットクラスをどのように変換したいかを定義する@ModifyClass
アノテーションを持つクラスを作成します:
import io.github.brqnko.bytekin.injection.ModifyClass;
import io.github.brqnko.bytekin.injection.Inject;
import io.github.brqnko.bytekin.injection.At;
import io.github.brqnko.bytekin.data.CallbackInfo;
@ModifyClass("com.example.Calculator")
public class CalculatorHooks {
@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)
public static CallbackInfo onAddHead(int a, int b) {
System.out.println("Adding " + a + " + " + b);
return CallbackInfo.empty();
}
}
ステップ2: トランスフォーマーの作成
フッククラスでBytekinTransformer
をインスタンス化します:
BytekinTransformer transformer = new BytekinTransformer.Builder(CalculatorHooks.class)
.build();
ステップ3: クラスの変換
ターゲットクラスのバイトコードに変換を適用します:
byte[] originalBytecode = loadClassBytecode("com.example.Calculator");
byte[] transformedBytecode = transformer.transform("com.example.Calculator", originalBytecode);
ステップ4: 変換されたクラスの使用
カスタムClassLoader
を使用して、変換されたバイトコードをJVMにロードします:
ClassLoader loader = new ByteArrayClassLoader(transformedBytecode);
Class<?> transformedClass = loader.loadClass("com.example.Calculator");
結果
変換されたクラスには、add
メソッドにロギングが追加されます:
// 元のコード
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// 変換後のコード
public class Calculator {
public int add(int a, int b) {
System.out.println("Adding " + a + " + " + b); // インジェクション済み!
return a + b;
}
}
次のステップ
インストールガイド
Mavenを使用する場合
1. 依存関係を追加
pom.xml
ファイルを編集し、以下の依存関係を追加します:
<dependency>
<groupId>io.github.brqnko.bytekin</groupId>
<artifactId>bytekin</artifactId>
<version>1.0</version>
</dependency>
2. プロジェクトを更新
Mavenを実行して依存関係をダウンロードします:
mvn clean install
Gradleを使用する場合
1. 依存関係を追加
build.gradle
ファイルを編集し、以下を追加します:
dependencies {
implementation 'io.github.brqnko.bytekin:bytekin:1.0'
}
2. プロジェクトを同期
Android StudioまたはIntelliJ IDEAの場合、Gradleを手動で同期できます。コマンドラインの場合:
./gradlew build
依存関係の要件
bytekinは最小限の依存関係を持っています:
依存関係 | バージョン | 目的 |
---|---|---|
ASM | 9.7.1+ | バイトコード操作ライブラリ |
Java | 8+ | ランタイム環境 |
検証
インストール後、簡単なテストを実行してbytekinが正しくセットアップされていることを確認します:
import io.github.brqnko.bytekin.transformer.BytekinTransformer;
public class BytekinVer {
public static void main(String[] args) {
BytekinTransformer transformer = new BytekinTransformer.Builder()
.build();
System.out.println("bytekinは使用する準備ができています!");
}
}
インストールのトラブルシューティング
Maven: 依存関係が見つからない
- インターネットに接続されていることを確認
mvn clean
を実行してからmvn install
を再度実行- リポジトリにアクセスできるか確認
Gradle: ビルドが失敗する
- 最初に
./gradlew clean
を実行 - Gradleラッパーのバージョンを確認
- Javaバージョンの互換性を確認
次のステップ
最初の変換
このガイドでは、シンプルなバイトコード変換を実演する完全な例を作成します。
例: 計算機にロギングを追加する
ステップ1: ターゲットクラスの作成
まず、変換する簡単な計算機クラスを作成しましょう:
package com.example;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
}
ステップ2: フックメソッドの作成
インジェクションしたい内容を定義する@ModifyClass
アノテーション付きのクラスを作成します:
package com.example;
import io.github.brqnko.bytekin.injection.ModifyClass;
import io.github.brqnko.bytekin.injection.Inject;
import io.github.brqnko.bytekin.injection.At;
import io.github.brqnko.bytekin.data.CallbackInfo;
@ModifyClass("com.example.Calculator")
public class CalculatorHooks {
@Inject(
methodName = "add",
methodDesc = "(II)I",
at = At.HEAD
)
public static CallbackInfo logAdd(int a, int b) {
System.out.println("Computing: " + a + " + " + b);
return CallbackInfo.empty();
}
@Inject(
methodName = "multiply",
methodDesc = "(II)I",
at = At.HEAD
)
public static CallbackInfo logMultiply(int a, int b) {
System.out.println("Computing: " + a + " * " + b);
return CallbackInfo.empty();
}
}
ステップ3: トランスフォーマーの構築
package com.example;
import io.github.brqnko.bytekin.transformer.BytekinTransformer;
public class TransformerSetup {
public static BytekinTransformer createTransformer() {
return new BytekinTransformer.Builder(CalculatorHooks.class)
.build();
}
}
ステップ4: 変換の適用
package com.example;
import io.github.brqnko.bytekin.transformer.BytekinTransformer;
public class Main {
public static void main(String[] args) {
// 元のバイトコードを取得
byte[] originalBytecode = getClassBytecode("com.example.Calculator");
// トランスフォーマーを作成
BytekinTransformer transformer = TransformerSetup.createTransformer();
// 変換を適用
byte[] transformedBytecode = transformer.transform(
"com.example.Calculator",
originalBytecode
);
// 変換されたクラスをロード
Calculator calc = loadTransformedClass(transformedBytecode);
// 変換されたクラスを使用
int result = calc.add(5, 3);
// 出力: "Computing: 5 + 3" その後 "8"
result = calc.multiply(4, 7);
// 出力: "Computing: 4 * 7" その後 "28"
}
// クラスバイトコードを取得するヘルパー(疑似コード)
static byte[] getClassBytecode(String className) {
// 実装はクラスローダーのセットアップに依存します
return new byte[]{};
}
// 変換されたクラスをロードするヘルパー(疑似コード)
static Calculator loadTransformedClass(byte[] bytecode) {
// カスタムClassLoaderを使用してロード
return null;
}
}
何が起こったのか?
変換プロセス:
- スキャン:
@Inject
アノテーションを持つメソッドをCalculatorHooks
でスキャンしました - 発見:
com.example.Calculator
をターゲットとするインジェクションを見つけました - 変更: フックメソッドを呼び出すようにCalculatorクラスのバイトコードを変更しました
- 挿入: 指定されたメソッドの先頭にロギングコードを挿入しました
変換前と変換後
変換前:
public int add(int a, int b) {
return a + b;
}
変換後:
public int add(int a, int b) {
// インジェクションされたコード
com.example.CalculatorHooks.logAdd(a, b);
// 元のコード
return a + b;
}
次のステップ
- At列挙型で他のインジェクションポイントを探る
- インボケーション変換について学ぶ
- より多くの例をチェックする
コアコンセプト
このセクションでは、bytekinとバイトコード変換の背後にある基本的なコンセプトについて説明します。
バイトコードとは?
Javaソースコードはバイトコードにコンパイルされます - これはJava仮想マシン(JVM)上で実行されるプラットフォーム非依存の中間表現です。ソースコードとは異なり、バイトコードは人間が読める形式ではありませんが、標準化されておりプログラム的に操作できます。
バイトコードの構造
Javaバイトコードは以下で構成されています:
- 定数プール: 文字列リテラル、メソッド参照、フィールド参照
- メソッド: 命令シーケンスを持つコンパイル済みメソッド
- フィールド: クラスプロパティ
- 属性: 行番号、ローカル変数、例外などのメタデータ
バイトコード変換とは?
バイトコード変換は、コンパイル後でクラスロード前にバイトコードを読み取り、分析し、変更するプロセスです。これにより、ソースコードを変更せずにクラスの動作を変更できます。
ユースケース
- ランタイム監視: ソースを変更せずにログ記録を追加
- 横断的関心事: 複数のクラスに動作を適用
- テスト: メソッドをモックまたはスタブ化
- セキュリティ: セキュリティチェックをインジェクト
- パフォーマンス: プロファイリング計装を追加
bytekinの位置づけ
bytekinは以下によってバイトコード変換を簡素化します:
- ASMの複雑さを抽象化: 生のASM上にクリーンなAPIを提供
- アノテーションベース構成: Javaアノテーションを使用して変換を定義
- 複数の戦略: インジェクション、メソッドインターセプション、リダイレクトのサポート
- マッピングサポート: 難読化または名前変更されたクラスを処理
- 柔軟なビルダーパターン: プログラム的に変換を構成
変換パイプライン
ターゲットクラスバイトコード
↓
BytekinTransformer
↓
フッククラスをスキャン
↓
変換を適用
↓
変更されたバイトコード
主要コンポーネント
フッククラス
ターゲットクラスの変換方法を定義する@ModifyClass
でアノテートされたクラス。変換アノテーションを持つメソッドを含みます。
トランスフォーマー
バイトコードに変換を適用するオブジェクト。bytekinは変換プロセス全体を処理するBytekinTransformer
を提供します。
アノテーション
変換動作を定義するJavaコードの特別なマーカー:
@ModifyClass
: ターゲットクラスをマーク@Inject
: 特定の位置にコードを挿入@Invoke
: メソッド呼び出しをインターセプト- その他...
CallbackInfo
変換動作を制御するデータ構造:
cancelled
: 元のコードをスキップするかどうかreturnValue
: カスタム戻り値modifyArgs
: 変更されたメソッド引数
重要なコンセプト
メソッドディスクリプタ
メソッドディスクリプタはJVM形式でメソッドシグネチャを記述します:
(パラメータ型)戻り値型
例:
(II)I
- 2つのintを取り、intを返す(Ljava/lang/String;)V
- Stringを取り、voidを返す()Ljava/lang/String;
- 何も取らず、Stringを返す
クラス名
bytekinでは、クラス名はドット表記を使用します:
- Java表記:
java.lang.String
- 内部表記:
java/lang/String
- bytekinはJava表記を使用
次のステップ
- bytekinの仕組みについて学習
- すべての機能を探索
バイトコードの基礎
Javaバイトコードとは?
Javaソースコード(.java
ファイル)はJavaバイトコード(.class
ファイルに含まれる)にコンパイルされます。このバイトコードはJava仮想マシン(JVM)が理解するプラットフォーム非依存の中間言語です。
ソースコードとバイトコード
Javaソースコード:
public class Example {
public void greet(String name) {
System.out.println("Hello, " + name);
}
}
コンパイルされたバイトコード(概念的):
aload_0
aload_1
invokedynamic <concat>
getstatic System.out
swap
invokevirtual println
return
バイトコードバージョンはより冗長で、オペレーティングシステムに依存しません。
バイトコードが重要な理由
- プラットフォーム非依存: JVMがあればどのシステムでも動作
- 実行時の柔軟性: ロード前にバイトコードを変換可能
- セキュリティ: JVMはバイトコードの正しさを検証可能
- 最適化: JVMはネイティブコードにJITコンパイル可能
- イントロスペクション: ツールはソースコードなしでバイトコードを分析可能
バイトコードの読み方
検査ツール
- javap: 組み込みのJava逆アセンブラ
- Bytecode Viewer: バイトコード検査用のGUIツール
- ASM Tree View: IDE可視化プラグイン
例: javapの使用
javap -c Example.class
出力は各メソッドのバイトコード命令を表示します。
一般的なバイトコード命令
頻繁に遭遇するバイトコード命令:
命令 | 目的 |
---|---|
aload | オブジェクト参照をロード |
iload | 整数をロード |
invoke* | メソッドを呼び出す(静的、仮想など) |
return | メソッドから戻る |
getstatic | 静的フィールドを読む |
putstatic | 静的フィールドに書き込む |
new | 新しいオブジェクトを作成 |
メソッドディスクリプタ(シグネチャ)
バイトコードはメソッドシグネチャに特別な記法を使用します:
(パラメータ型)戻り値型
プリミティブ型
Z
- booleanB
- byteC
- charS
- shortI
- intJ
- longF
- floatD
- doubleV
- void
オブジェクト型
Ljava/lang/String;
- StringクラスL...classname...;
- 任意のクラス
配列型
[I
- int配列[Ljava/lang/String;
- String配列
例
(II)I
- 2つの整数を加算:int method(int a, int b) { return ...; }
(Ljava/lang/String;)V
- 文字列を受け取り、何も返さない:void method(String s) { ... }
()Ljava/lang/String;
- パラメータなし、文字列を返す:String method() { ... }
([Ljava/lang/String;)V
- 文字列配列を受け取る:void method(String[] args) { ... }
クラス参照
クラスは完全修飾名を/
区切りで参照されます:
java/lang/String
java/util/ArrayList
com/mycompany/MyClass
bytekinでは、通常、ドット付きの標準Java記法を使用します:
java.lang.String
java.util.ArrayList
com.mycompany.MyClass
クラスファイル形式
コンパイルされた.class
ファイルには以下が含まれます:
- マジックナンバー: クラスファイルとして識別(
0xCAFEBABE
) - バージョン: Javaバージョン情報
- 定数プール: 文字列、メソッド名、フィールド名、型情報
- アクセスフラグ: public、final、abstractなど
- このクラス: クラス名
- スーパークラス: 親クラス
- インターフェース: 実装されたインターフェース
- フィールド: クラスメンバー変数
- メソッド: バイトコード付きメソッド
- 属性: 追加のメタデータ
重要な注意事項
- バイトコードは人間が読めるものではないが、体系的で分析可能
- すべてのJavaソースの構造がバイトコードにマッピングされる
- バイトコードは検証可能 - JVMは実行前に正しさをチェック
- バイトコードは操作可能 - ソースコードなしでプログラム的に操作できる
次のステップ
- bytekinの動作を学ぶ
- コア概念を理解する
bytekinの動作
このドキュメントでは、bytekinの内部メカニズムとバイトコードをどのように変換するかを説明します。
変換プロセス
ステップ1: 初期化
フッククラスを定義
↓
BytekinTransformer.Builderを作成
↓
フッククラスをBuilderに渡す
例:
BytekinTransformer transformer = new BytekinTransformer.Builder(
CalculatorHooks.class,
StringHooks.class
).build();
ステップ2: 分析
build()
が呼び出されると、bytekinは:
- フッククラスのアノテーションをスキャン
- 変換ルールを抽出
- メソッドシグネチャを検証
- 変換戦略を準備
- BytekinClassTransformerインスタンスを作成
Builder.build()
↓
@ModifyClassアノテーションをスキャン
↓
@Inject、@Invokeなどを抽出
↓
トランスフォーマーマップを作成
↓
BytekinTransformerを返す
ステップ3: 変換
transform()
が呼び出されると:
byte[] transformed = transformer.transform("com.example.Calculator", bytecode);
bytekinは:
- ターゲットクラスを検索
- 一致するトランスフォーマーを見つける
- ASMを使用してバイトコードを解析
- 登録されたすべての変換を適用
- 変更されたバイトコードを返す
ターゲットバイトコード
↓
ASM ClassReader
↓
BytekinClassVisitor
↓
インジェクションを適用
↓
インボケーションを適用
↓
リダイレクトを適用
↓
定数の変更を適用
↓
変数の変更を適用
↓
ASM ClassWriter
↓
変更されたバイトコード
アーキテクチャ概要
コアコンポーネント
┌─────────────────────────────────────┐
│ BytekinTransformer (メインAPI) │
└──────────────┬──────────────────────┘
│
┌──────┴──────┐
↓ ↓
Builder transform()
│ │
└──────┬──────┘
↓
┌───────────────────────────┐
│ BytekinClassTransformer │
└───────────┬───────────────┘
↓
┌───────────────────────────┐
│ BytekinClassVisitor │
│ (ASM ClassVisitor) │
└───────────┬───────────────┘
↓
┌───────────────────────────┐
│ BytekinMethodVisitor │
│ (ASM MethodVisitor) │
└───────────────────────────┘
ビジターパターン
bytekinはビジターパターン(ASMから)を使用します:
┌─ ClassVisitor
│ └─ Method Visitor
│ └─ Code Visitor
│ └─ Instruction Handlers
ASMがバイトコードを解析する際、各要素(クラス、メソッド、フィールド、命令など)についてビジターのメソッドを呼び出して通知します。
変換タイプ
1. インジェクション(コード挿入)
目標: メソッドの特定箇所にコードを挿入
メソッドバイトコード
↓
インジェクションポイントを見つける
↓
フックメソッドへの呼び出しを挿入
↓
元のコードを続行
例の場所:
At.HEAD
: メソッド本体の前At.RETURN
: return文の前At.TAIL
: メソッドの終わり
2. インボケーション(メソッド呼び出しのインターセプト)
目標: メソッド内のメソッド呼び出しをインターセプト
メソッドバイトコード
↓
ターゲットメソッド呼び出しを見つける
↓
同じ引数でフックメソッドを呼び出す
↓
必要に応じて引数を変更
↓
メソッド呼び出しを行う(またはスキップ)
3. リダイレクト(呼び出し先の変更)
目標: どのメソッドが呼び出されるかを変更
A()へのメソッド呼び出し
↓
呼び出しをインターセプト
↓
B()にリダイレクト
4. 定数の変更
目標: 定数値を変更
定数Xをロード
↓
定数Yに置き換え
5. 変数の変更
目標: ローカル変数の値を変更
インデックスNのローカル変数
↓
スロットからロード
↓
変更を適用
↓
戻して格納
主要なデータ構造
Injection
コードのインジェクションを表します:
- ターゲットメソッド: どのメソッドにインジェクトするか
- フックメソッド: どのフックメソッドを呼び出すか
- 場所: どこにインジェクトするか(HEAD、RETURNなど)
- パラメータ: どのパラメータを渡すか
Invocation
メソッド呼び出しのインターセプトを表します:
- ターゲットメソッド: どのメソッドがターゲットを呼び出すか
- ターゲット呼び出し: どの呼び出しをインターセプトするか
- フックメソッド: どのフックを呼び出すか
- シフト: 呼び出しの前か後か
CallbackInfo
インジェクションの動作を制御します:
public class CallbackInfo {
public boolean cancelled; // 実行をキャンセル?
public Object returnValue; // カスタムリターン?
public Object[] modifyArgs; // 変更された引数?
}
マッピングシステム
bytekinは難読化されたコードのクラス/メソッド名マッピングをサポートしています:
元の名前 マップされた名前
↓ ↓
a.class ────→ com.example.Calculator
b(II)I ────→ add(II)I
マッピングは変換中に適用されます:
フッククラスが"com.example.Calculator"を参照
↓
マッピングを適用
↓
バイトコード内のマップされた名前を探す
↓
それに応じて変換
スレッドセーフティ
- BytekinTransformer:
build()
後はスレッドセーフ - Builder: 設定中はスレッドセーフではない
- transform(): 複数のスレッドから同時に呼び出し可能
パフォーマンスの考慮事項
効率
- 1回限りのコスト: トランスフォーマーのビルド
- 変換時間: バイトコードサイズに比例
- ランタイムオーバーヘッド: インジェクトされたコードのみが実行される
最適化のヒント
- トランスフォーマーを1回ビルドして再利用
- クラスロードの早い段階で変換を使用
- フックメソッドの複雑さを最小限に抑える
- ボトルネックを特定するためにプロファイル
次のステップ
機能概要
bytekinはJavaバイトコードを操作するためのいくつかの強力な変換機能を提供します。このセクションでは各機能の概要を説明します。
利用可能な機能
1. Inject - コードの挿入
ソースコードを変更せずに、メソッドの特定の箇所にカスタムコードを挿入します。
ユースケース:
- ログステートメントの追加
- 横断的関心事の実装
- セキュリティチェックの追加
- パラメータの検証
例:
@Inject(methodName = "calculate", methodDesc = "(II)I", at = At.HEAD)
public static CallbackInfo logStart(int a, int b) {
System.out.println("Starting calculation");
return CallbackInfo.empty();
}
詳細: Inject変換
2. Invoke - メソッド呼び出しのインターセプト
メソッド呼び出しをインターセプトし、必要に応じて引数や戻り値を変更します。
ユースケース:
- 特定のメソッド呼び出しのインターセプト
- メソッド引数の変更
- メソッドのモックやスタブ化
- 前処理/後処理の追加
例:
@Invoke(
targetMethodName = "process",
targetMethodDesc = "(Ljava/lang/String;)V",
invokeMethodName = "validate",
invokeMethodDesc = "(Ljava/lang/String;)V",
shift = Shift.BEFORE
)
public static CallbackInfo validateBefore(String input) {
return new CallbackInfo(false, null, new Object[]{input.trim()});
}
詳細: Invoke変換
3. Redirect - メソッド呼び出しのリダイレクト
実行時に呼び出されるメソッドを変更します。
ユースケース:
- 代替実装への呼び出しのリダイレクト
- メソッド動作のモック
- メソッド転送の実装
- 条件に基づく動作の変更
例:
@Redirect(
targetMethodName = "oldMethod",
targetMethodDesc = "(I)V",
redirectMethodName = "newMethod",
redirectMethodDesc = "(I)V"
)
public static void redirectCall(int value) {
System.out.println("Redirected to new method: " + value);
}
詳細: Redirect変換
4. 定数の変更
バイトコードに埋め込まれた定数値を変更します。
ユースケース:
- ハードコードされた設定値の変更
- 文字列リテラルの変更
- 数値定数の変更
- 実行時の定数のパッチ
例:
@ModifyConstant(
methodName = "getVersion",
oldValue = "1.0",
newValue = "2.0"
)
public static CallbackInfo updateVersion() {
return CallbackInfo.empty();
}
詳細: 定数の変更
5. 変数の変更
メソッド内のローカル変数の値を変更します。
ユースケース:
- 入力のサニタイゼーション
- データの変換
- 変数値のデバッグ
- カスタムロジックの実装
例:
@ModifyVariable(
methodName = "process",
variableIndex = 1
)
public static void transformVariable(int original) {
// 変換ロジック
}
詳細: 変数の変更
機能の組み合わせ
複雑な変換のために複数の機能を組み合わせて使用できます:
@ModifyClass("com.example.Service")
public class ServiceHooks {
// ロギングのインジェクション
@Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo logStart(String input) {
System.out.println("Processing: " + input);
return CallbackInfo.empty();
}
// 内部呼び出しのインターセプト
@Invoke(
targetMethodName = "handle",
targetMethodDesc = "(Ljava/lang/String;)V",
invokeMethodName = "validate",
invokeMethodDesc = "(Ljava/lang/String;)V",
shift = Shift.BEFORE
)
public static CallbackInfo validateInput(String input) {
return new CallbackInfo(false, null, new Object[]{sanitize(input)});
}
private static String sanitize(String input) {
return input.trim().toLowerCase();
}
}
適切な機能の選択
機能 | 目的 | 複雑さ |
---|---|---|
Inject | メソッドの特定箇所にコードを挿入 | 低 |
Invoke | 特定の呼び出しをインターセプト | 中 |
Redirect | 呼び出し先を変更 | 中 |
定数の変更 | ハードコードされた値を変更 | 低 |
変数の変更 | ローカル変数を変換 | 高 |
次のステップ
インジェクション変換
@Inject
アノテーションを使用すると、ターゲットメソッドの特定のポイントにカスタムコードを挿入できます。
基本的な使用法
@ModifyClass("com.example.Calculator")
public class CalculatorHooks {
@Inject(
methodName = "add",
methodDesc = "(II)I",
at = At.HEAD
)
public static CallbackInfo onAddStart(int a, int b) {
System.out.println("Adding: " + a + " + " + b);
return CallbackInfo.empty();
}
}
アノテーションパラメータ
methodName (必須)
インジェクション対象のメソッド名。
methodName = "add"
methodDesc (必須)
ターゲットメソッドシグネチャのJVMディスクリプタ。
methodDesc = "(II)I" // int add(int a, int b)
詳細はメソッドディスクリプタをご覧ください。
at (必須)
メソッド内のどこにコードをインジェクションするか。
At列挙型 - インジェクションポイント
At.HEAD
メソッドの最初、既存のコードの前にインジェクションします。
@Inject(methodName = "calculate", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo atMethodStart() {
System.out.println("Method started");
return CallbackInfo.empty();
}
結果:
public int calculate() {
System.out.println("Method started"); // インジェクション済み
// 元のコードはここ
}
At.RETURN
メソッド内のすべてのreturn文の前にインジェクションします。
@Inject(methodName = "getValue", methodDesc = "()I", at = At.RETURN)
public static CallbackInfo beforeReturn(CallbackInfo ci) {
System.out.println("Returning: " + ci.returnValue);
return CallbackInfo.empty();
}
結果:
public int getValue() {
if (condition) {
System.out.println("Returning: " + value); // インジェクション済み
return value;
}
System.out.println("Returning: " + defaultValue); // インジェクション済み
return defaultValue;
}
At.TAIL
メソッドの最後、すべてのコードの後、暗黙的なreturnの前にインジェクションします。
@Inject(methodName = "cleanup", methodDesc = "()V", at = At.TAIL)
public static CallbackInfo atMethodEnd() {
System.out.println("Cleanup complete");
return CallbackInfo.empty();
}
フックメソッドのパラメータ
フックメソッドは、ターゲットメソッドと同じパラメータに加えてCallbackInfo
を受け取ります:
// ターゲットメソッド:
public String process(String input, int count) { ... }
// フックメソッド:
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;I)Ljava/lang/String;", at = At.HEAD)
public static CallbackInfo processHook(String input, int count, CallbackInfo ci) {
// パラメータにアクセス可能
return CallbackInfo.empty();
}
CallbackInfo - 動作の制御
CallbackInfo
オブジェクトを使用して、インジェクションの動作を制御できます:
public class CallbackInfo {
public boolean cancelled; // 元のコードをスキップするか?
public Object returnValue; // カスタム値を返すか?
}
実行のキャンセル
元のメソッドをスキップして早期にreturnします:
@Inject(methodName = "authenticate", methodDesc = "(Ljava/lang/String;)Z", at = At.HEAD)
public static CallbackInfo checkPermission(String user, CallbackInfo ci) {
if (!user.equals("admin")) {
ci.cancelled = true;
ci.returnValue = false; // 元のコードを実行せずfalseを返す
}
return ci;
}
カスタム戻り値
元の結果の代わりにカスタム値を返します:
@Inject(methodName = "getCached", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo useCachedValue(CallbackInfo ci) {
Object cached = getFromCache();
if (cached != null) {
ci.cancelled = true;
ci.returnValue = cached;
}
return ci;
}
複数のインジェクション
同じメソッドに複数回インジェクションできます:
@ModifyClass("com.example.Service")
public class ServiceHooks {
@Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo logStart(String input) {
System.out.println("Start: " + input);
return CallbackInfo.empty();
}
@Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.RETURN)
public static CallbackInfo logEnd(String input) {
System.out.println("End: " + input);
return CallbackInfo.empty();
}
}
両方のインジェクションが適用されます。
インスタンスメソッド vs 静的メソッド
インスタンスメソッドの場合、最初のパラメータは通常this
(オブジェクトインスタンス)です:
// ターゲットインスタンスメソッド:
public class Calculator {
public int add(int a, int b) { return a + b; }
}
// フックは'this'を受け取れます:
@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)
public static CallbackInfo onAdd(Calculator self, int a, int b) {
System.out.println("Calculator instance: " + self);
return CallbackInfo.empty();
}
ベストプラクティス
- フックをシンプルに保つ: 複雑なロジックは別のメソッドに
- 例外を避ける: フックメソッド内で例外を処理する
- ガードにAt.HEADを使用: 早期に条件をチェック
- At.RETURNに注意: 複数のreturnには処理が必要
- 十分にテスト: インジェクションが正しく動作することを検証
例
完全な動作例については、例 - インジェクションをご覧ください。
次のステップ
インボケーション変換
@Invoke
アノテーションを使用すると、メソッド呼び出しをインターセプトし、実行前に引数をオプションで変更できます。
基本的な使用法
@ModifyClass("com.example.DataProcessor")
public class ProcessorHooks {
@Invoke(
targetMethodName = "process",
targetMethodDesc = "(Ljava/lang/String;)V",
invokeMethodName = "validate",
invokeMethodDesc = "(Ljava/lang/String;)V",
shift = Shift.BEFORE
)
public static CallbackInfo validateBeforeProcess(String data) {
if (data == null || data.isEmpty()) {
return new CallbackInfo(true, null, new Object[]{"default"});
}
return CallbackInfo.empty();
}
}
アノテーションパラメータ
targetMethodName (必須)
インターセプトする呼び出しを含むメソッド名。
targetMethodName = "process"
targetMethodDesc (必須)
呼び出しを含むメソッドのJVMディスクリプタ。
targetMethodDesc = "(Ljava/lang/String;)V"
invokeMethodName (必須)
呼び出されているメソッド(インターセプトしたいもの)の名前。
invokeMethodName = "helper"
invokeMethodDesc (必須)
呼び出されているメソッドのJVMディスクリプタ。
invokeMethodDesc = "(I)Ljava/lang/String;"
shift (必須)
メソッド呼び出しに対してフックを実行するタイミング。
Shift列挙型 - タイミング
Shift.BEFORE
メソッドが呼び出される前にフックを実行します。
@Invoke(
targetMethodName = "process",
targetMethodDesc = "(Ljava/lang/String;)V",
invokeMethodName = "validate",
invokeMethodDesc = "(Ljava/lang/String;)V",
shift = Shift.BEFORE
)
public static CallbackInfo beforeCall(String input) {
System.out.println("Before calling validate");
return CallbackInfo.empty();
}
結果:
public void process(String input) {
System.out.println("Before calling validate"); // インジェクション済み
validate(input);
// 残りのコード
}
Shift.AFTER
メソッドが呼び出された後にフックを実行します。
@Invoke(
targetMethodName = "process",
targetMethodDesc = "(Ljava/lang/String;)V",
invokeMethodName = "save",
invokeMethodDesc = "()V",
shift = Shift.AFTER
)
public static CallbackInfo afterCall() {
System.out.println("After calling save");
return CallbackInfo.empty();
}
結果:
public void process(String input) {
// 何らかのコード
save();
System.out.println("After calling save"); // インジェクション済み
}
引数の変更
CallbackInfo
を使用して、インターセプトされた呼び出しに渡される引数を変更します:
@Invoke(
targetMethodName = "processData",
targetMethodDesc = "(Ljava/lang/String;I)V",
invokeMethodName = "transform",
invokeMethodDesc = "(Ljava/lang/String;I)V",
shift = Shift.BEFORE
)
public static CallbackInfo sanitizeInput(String data, int count, CallbackInfo ci) {
// 引数を変更
String sanitized = data.trim().toLowerCase();
int newCount = Math.max(0, count);
ci.modifyArgs = new Object[]{sanitized, newCount};
return ci;
}
結果:
public void processData(String data, int count) {
// 元: transform(data, count);
// フック後: transform(data.trim().toLowerCase(), max(0, count));
transform(data, count);
}
メソッド呼び出しのキャンセル
メソッドが呼び出されないようにします:
@Invoke(
targetMethodName = "deleteFile",
targetMethodDesc = "(Ljava/lang/String;)Z",
invokeMethodName = "delete",
invokeMethodDesc = "(Ljava/io/File;)Z",
shift = Shift.BEFORE
)
public static CallbackInfo preventDeletion(File file, CallbackInfo ci) {
if (isSystemFile(file)) {
// delete()を呼び出さず、falseを返す
ci.cancelled = true;
ci.returnValue = false;
}
return ci;
}
戻り値の処理
戻り値にアクセスして変更します(Shift.AFTER
の場合):
@Invoke(
targetMethodName = "getValue",
targetMethodDesc = "()I",
invokeMethodName = "compute",
invokeMethodDesc = "()I",
shift = Shift.AFTER
)
public static CallbackInfo modifyReturnValue(CallbackInfo ci) {
// 戻り値にアクセス
int original = (int) ci.returnValue;
// 変更する
ci.returnValue = original * 2;
return ci;
}
複雑な例
@ModifyClass("com.example.UserService")
public class UserServiceHooks {
// ログイン試行をインターセプト
@Invoke(
targetMethodName = "authenticate",
targetMethodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z",
invokeMethodName = "validateCredentials",
invokeMethodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z",
shift = Shift.BEFORE
)
public static CallbackInfo logAuthAttempt(
String username, String password, CallbackInfo ci
) {
// 試行をログに記録
System.out.println("Auth attempt for: " + username);
// 特定のユーザー名をブロック
if (username.equals("blocked")) {
ci.cancelled = true;
ci.returnValue = false;
}
return ci;
}
}
ベストプラクティス
- 呼び出しフローを理解する: メソッドがどこで呼び出されるかを知る
- タイミングを考慮する: 入力検証には
BEFORE
、出力変換にはAFTER
- 引数の変更をテスト: 型が一致することを確認
- キャンセルを慎重に処理: 呼び出し元のコードがキャンセルされた呼び出しを処理することを確認
- パフォーマンスをプロファイル: フックはすべての呼び出しで実行される
制限事項
- 明示的なメソッド呼び出しのみインターセプト可能、バイトコードからの仮想メソッド呼び出しは不可
- コンストラクタへの呼び出しを直接インターセプトできない
- すべての呼び出しでパフォーマンスへの影響が発生する
例
完全な動作例については、例 - インボケーションをご覧ください。
次のステップ
リダイレクト変換
@Redirect
アノテーションを使用すると、実行時に実際に呼び出されるメソッドを変更できます。
基本的な使用法
@ModifyClass("com.example.LegacyService")
public class LegacyServiceHooks {
@Redirect(
targetMethodName = "oldMethod",
targetMethodDesc = "(I)V",
redirectMethodName = "newMethod",
redirectMethodDesc = "(I)V"
)
public static void redirectCall(int value) {
System.out.println("Redirecting call with value: " + value);
}
}
アノテーションパラメータ
targetMethodName (必須)
リダイレクトする呼び出しを含むメソッドの名前。
targetMethodName = "process"
targetMethodDesc (必須)
ターゲットメソッドのJVMディスクリプタ。
targetMethodDesc = "(Ljava/lang/String;)V"
redirectMethodName (必須)
代わりに呼び出す新しいメソッドの名前。
redirectMethodName = "newImplementation"
redirectMethodDesc (必須)
リダイレクトメソッドのJVMディスクリプタ。
redirectMethodDesc = "(Ljava/lang/String;)V"
リダイレクトの仕組み
前:
public class LegacyAPI {
public void oldMethod(int value) {
// 古い実装
}
}
public class Client {
public void use() {
api.oldMethod(42); // oldMethodを呼び出す
}
}
リダイレクト後:
public class Client {
public void use() {
api.newMethod(42); // newMethodにリダイレクト!
}
}
実用的な例
移行戦略
古いAPIから新しいAPIに段階的に移行:
@ModifyClass("com.example.Application")
public class APIRedirection {
@Redirect(
targetMethodName = "main",
targetMethodDesc = "([Ljava/lang/String;)V",
redirectMethodName = "legacySearch",
redirectMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
from = "oldSearch",
to = "modernSearch"
)
public static void upgradeSearch() {
// 検索呼び出しが最新の実装にルーティングされる
}
}
テストのためのモッキング
実際の実装をテストダブルで置き換え:
@ModifyClass("com.example.DataAccess")
public class TestRedirection {
@Redirect(
targetMethodName = "query",
targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
redirectMethodName = "fetchFromDatabase",
redirectMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
from = "realDB",
to = "mockDB"
)
public static void useMockDatabase() {
// すべてのデータベース呼び出しがモック実装を使用
}
}
パフォーマンス最適化
最適化された実装にルーティング:
@ModifyClass("com.example.Processing")
public class PerformanceOptimization {
@Redirect(
targetMethodName = "processLargeList",
targetMethodDesc = "(Ljava/util/List;)Ljava/util/List;",
redirectMethodName = "slowImplementation",
redirectMethodDesc = "(Ljava/util/List;)Ljava/util/List;",
from = "bruteForce",
to = "optimized"
)
public static void useOptimizedAlgorithm() {
// 遅いアルゴリズムの代わりに高速アルゴリズムを使用
}
}
他の変換との違い
機能 | インジェクション | インボケーション | リダイレクト |
---|---|---|---|
何をするか | コードを挿入 | 呼び出しをインターセプト | ターゲットを変更 |
呼び出しは発生するか | はい | はい | はい、ただし異なるターゲット |
実行をスキップできるか | はい | はい | はい |
ユースケース | ロギングの追加 | 動作を変更 | API移行 |
型の互換性
リダイレクトメソッドは互換性のあるシグネチャを持つ必要があります:
// 元の呼び出し
search(String query); // (Ljava/lang/String;)Ljava/util/List;
// 互換性のあるシグネチャにリダイレクトする必要がある
newSearch(String query); // (Ljava/lang/String;)Ljava/util/List;
型の不一致は問題を引き起こします:
// 間違い - パラメータの型が異なる
@Redirect(..., from = "process(int)", to = "process(String)")
パフォーマンスの考慮事項
リダイレクトは通常のメソッド呼び出しと比較して最小限のオーバーヘッドです:
- 直接的なバイトコード置換である
- ラッパーやプロキシは作成されない
- JVMは通常通りインライン化と最適化が可能
制限事項
- 両方のメソッドは互換性のあるシグネチャを持つ必要がある
- finalメソッドにリダイレクトできない
- コンストラクタ呼び出しをリダイレクトできない(代わりに
@Invoke
を使用) - リダイレクトは静的 - すべての呼び出しで同じターゲット
ベストプラクティス
- 互換性を確保: メソッドシグネチャが完全に一致することを確認
- リダイレクトを文書化: なぜかを説明するコメントを残す
- リダイレクトをテスト: リダイレクト後の動作を検証
- 移行に使用: 古いAPIから新しいAPIへの移行に最適
- 注意する: 混乱を避けるためにすべてのリダイレクトを追跡
高度なパターン: 条件付きリダイレクト
@Redirect
は静的ですが、@Invoke
と組み合わせて条件付き動作を実現できます:
@Invoke(
targetMethodName = "search",
targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
invokeMethodName = "getResults",
invokeMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
shift = Shift.BEFORE
)
public static CallbackInfo selectImplementation(String query, CallbackInfo ci) {
if (query.length() > 100) {
// 大きなクエリには最適化された検索を使用
ci.returnValue = optimizedSearch(query);
ci.cancelled = true;
}
return ci;
}
次のステップ
定数の変更
ソースコードを再コンパイルすることなく、バイトコード内のハードコードされた定数値を変更します。
基本的な使用方法
@ModifyClass("com.example.Config")
public class ConfigHooks {
@ModifyConstant(
methodName = "getVersion",
oldValue = "1.0.0",
newValue = "2.0.0"
)
public static CallbackInfo updateVersion() {
return CallbackInfo.empty();
}
}
変更可能な定数
- 文字列リテラル
- 数値定数(int、long、float、double)
- ブール定数
- 定数プール内の定数
文字列定数
@ModifyClass("com.example.App")
public class AppHooks {
@ModifyConstant(
methodName = "getAPIEndpoint",
oldValue = "http://localhost:8080",
newValue = "https://api.production.com"
)
public static CallbackInfo updateEndpoint() {
return CallbackInfo.empty();
}
}
変更前:
public String getAPIEndpoint() {
return "http://localhost:8080";
}
変更後:
public String getAPIEndpoint() {
return "https://api.production.com";
}
数値定数
@ModifyClass("com.example.Limits")
public class LimitsHooks {
@ModifyConstant(
methodName = "getMaxConnections",
oldValue = 10,
newValue = 100
)
public static CallbackInfo increaseLimit() {
return CallbackInfo.empty();
}
}
同じメソッド内の複数の定数
@ModifyClass("com.example.Config")
public class ConfigHooks {
@ModifyConstant(
methodName = "initialize",
oldValue = "DEBUG",
newValue = "PRODUCTION"
)
public static CallbackInfo updateMode() {
return CallbackInfo.empty();
}
@ModifyConstant(
methodName = "initialize",
oldValue = "localhost",
newValue = "example.com"
)
public static CallbackInfo updateHost() {
return CallbackInfo.empty();
}
}
両方の変更が同じメソッドに適用されます。
ユースケース
環境設定
環境固有の値を変更:
@ModifyClass("com.example.Environment")
public class EnvironmentHooks {
@ModifyConstant(
methodName = "getDatabaseURL",
oldValue = "jdbc:mysql://dev.local:3306/db",
newValue = "jdbc:mysql://prod.remote:3306/db"
)
public static CallbackInfo updateDatabase() {
return CallbackInfo.empty();
}
}
フィーチャーフラグ
再コンパイルなしで機能を有効/無効化:
@ModifyClass("com.example.Features")
public class FeatureHooks {
@ModifyConstant(
methodName = "isNewFeatureEnabled",
oldValue = false,
newValue = true
)
public static CallbackInfo enableFeature() {
return CallbackInfo.empty();
}
}
APIバージョニング
APIエンドポイントを更新:
@ModifyClass("com.example.API")
public class APIHooks {
@ModifyConstant(
methodName = "getAPIVersion",
oldValue = "v1",
newValue = "v3"
)
public static CallbackInfo updateAPIVersion() {
return CallbackInfo.empty();
}
}
パフォーマンスへの影響
定数の変更は最小限のランタイムオーバーヘッドです:
- 変更はバイトコード変換時に行われる(1回のみ)
- ランタイムパフォーマンスは再コンパイルされたコードと同一
- JVMは変更された定数を最適化できる
制限事項
変更できないもの
- ローカル変数の初期化(一部のケース)
- 実行時に作成される定数
- JVMによってすでに最適化された定数
型の一致
古い値と新しい値は互換性のある型である必要があります:
// 正しい
@ModifyConstant(
methodName = "getCount",
oldValue = 10, // int
newValue = 20 // int - 互換性あり
)
// 誤り
@ModifyConstant(
methodName = "getCount",
oldValue = 10, // int
newValue = "20" // String - 互換性なし!
)
ベストプラクティス
- 設定に使用する: ロジックの変更には使用しない
- 明確に文書化する: 定数が変更される理由を説明する
- 値の一貫性を保つ: 正確な古い値を使用する
- すべてのパスをテストする: 新しい値でコードが動作することを確認する
- 型の変更を避ける: 型の互換性を保つ
応用: 条件付き変更
より詳細な制御のために、他の変換と組み合わせる:
@ModifyClass("com.example.Service")
public class ServiceHooks {
@Inject(
methodName = "initialize",
methodDesc = "()V",
at = At.HEAD
)
public static CallbackInfo checkEnvironment() {
String env = System.getProperty("environment");
if ("production".equals(env)) {
// プロダクション用の追加セットアップ
}
return CallbackInfo.empty();
}
@ModifyConstant(
methodName = "getTimeout",
oldValue = 5000,
newValue = 30000
)
public static CallbackInfo productionTimeout() {
return CallbackInfo.empty();
}
}
次のステップ
変数の変更
バイトコード変換中にメソッド内のローカル変数の値を変更します。
基本的な使用方法
@ModifyClass("com.example.Processor")
public class ProcessorHooks {
@ModifyVariable(
methodName = "process",
variableIndex = 1
)
public static void sanitizeInput(String original) {
// 変換ロジック
}
}
変数インデックスの理解
メソッド内のローカル変数はスロットに格納され、0から始まるインデックスが付けられます:
インスタンスメソッド
public void process(String name, int count) {
String result;
// ...
}
変数インデックス:
0
:this
(暗黙的)1
:name
(第1引数)2
:count
(第2引数)3
:result
(ローカル変数)
静的メソッド
public static void process(String name, int count) {
String result;
// ...
}
変数インデックス:
0
:name
(第1引数)1
:count
(第2引数)2
:result
(ローカル変数)
アノテーションパラメータ
methodName(必須)
対象メソッドの名前。
methodName = "process"
variableIndex(必須)
変更するローカル変数のインデックス。
variableIndex = 1
パラメータの変更
メソッドパラメータを変換:
@ModifyClass("com.example.UserService")
public class UserServiceHooks {
@ModifyVariable(
methodName = "createUser",
variableIndex = 1 // 第1引数: email
)
public static void normalizeEmail(String original) {
// 元のメールアドレスが正規化される
// 例: "USER@EXAMPLE.COM" が "user@example.com" になる
}
}
変更前:
public void createUser(String email) {
// email = "USER@EXAMPLE.COM"
// ...
}
変更後:
public void createUser(String email) {
// email = "user@example.com" (正規化済み)
// ...
}
ローカル変数の変更
メソッド内で作成された変数を変換:
@ModifyClass("com.example.Calculator")
public class CalculatorHooks {
@ModifyVariable(
methodName = "calculateTotal",
variableIndex = 2 // ローカル変数: total
)
public static void applyTaxToTotal(int original) {
// total に1.1を掛けて税を適用
}
}
ユースケース
入力のサニタイゼーション
メソッド入力をクリーンアップ:
@ModifyClass("com.example.WebService")
public class WebServiceHooks {
@ModifyVariable(
methodName = "handleRequest",
variableIndex = 1 // request パラメータ
)
public static void sanitizeRequest(String original) {
// 悪意のある文字を削除
}
@ModifyVariable(
methodName = "handleRequest",
variableIndex = 2 // path パラメータ
)
public static void validatePath(String original) {
// パスがルートディレクトリから逸脱しないことを確認
}
}
データ変換
データ形式を変換:
@ModifyClass("com.example.DateProcessor")
public class DateProcessorHooks {
@ModifyVariable(
methodName = "processDate",
variableIndex = 1 // date パラメータ
)
public static void convertToUTC(String original) {
// ローカル時間からUTCに変換
}
}
型変換
データ型を変更:
@ModifyClass("com.example.Converter")
public class ConverterHooks {
@ModifyVariable(
methodName = "process",
variableIndex = 1 // number パラメータ
)
public static void convertToPercentage(int original) {
// 生の数値をパーセンテージに変換
}
}
高度なパターン
複数の変数
同じメソッド内の複数の変数を変更:
@ModifyClass("com.example.Transfer")
public class TransferHooks {
@ModifyVariable(
methodName = "transfer",
variableIndex = 1 // 送信元アカウント
)
public static void validateFromAccount(String original) {
// 送信元アカウントを検証
}
@ModifyVariable(
methodName = "transfer",
variableIndex = 2 // 送信先アカウント
)
public static void validateToAccount(String original) {
// 送信先アカウントを検証
}
@ModifyVariable(
methodName = "transfer",
variableIndex = 3 // 金額
)
public static void validateAmount(long original) {
// 金額が正であることを確認
}
}
3つすべての変更が同じメソッドに適用されます。
型の保持
変更中に変数の型は保持されます:
@ModifyClass("com.example.Data")
public class DataHooks {
// String パラメータの変更
@ModifyVariable(methodName = "processName", variableIndex = 1)
public static void transformName(String original) { }
// int パラメータの変更
@ModifyVariable(methodName = "processCount", variableIndex = 1)
public static void transformCount(int original) { }
// List パラメータの変更
@ModifyVariable(methodName = "processItems", variableIndex = 1)
public static void transformItems(List<?> original) { }
}
各フックは自動的に正しい型を受け取ります。
制限事項
変更できないもの
- 使用されない変数
- JVMによって最適化されて削除された値を持つ変数
- 初期化後に複雑な方法で変更される変数
課題
- インデックスの計算: 変数インデックスを正しく識別する必要がある
- 型安全性: パラメータの型が一致する必要がある
- スコープ: 変更はそのメソッド内でのみ有効
- デバッグ: 変更の追跡が困難な場合がある
正しい変数インデックスの特定
javap
を使用して変数レイアウトを検査:
javap -c -private MyClass.class | grep -A 50 "methodName"
変数の位置を示すLocalVariableTableを探してください。
ベストプラクティス
- インデックスを文書化する: どの変数がどのインデックスかを明確にコメントする
- 変換をシンプルに保つ: 複雑なロジックは別にすべき
- セマンティクスを保持する: 変更された値が意味をなすことを確認する
- 徹底的にテストする: 変更された変数での動作を検証する
- インスペクタを使用する: 適用前にインデックスが正しいことを確認する
他の機能との組み合わせ
変数の変更をインジェクションと併用:
@ModifyClass("com.example.Service")
public class ServiceHooks {
@Inject(
methodName = "handle",
methodDesc = "(Ljava/lang/String;)V",
at = At.HEAD
)
public static CallbackInfo validateInput(String input) {
if (input == null || input.isEmpty()) {
return new CallbackInfo(true, null, null);
}
return CallbackInfo.empty();
}
@ModifyVariable(
methodName = "handle",
variableIndex = 1 // input パラメータ
)
public static void normalizeInput(String original) {
// 入力も正規化
}
}
次のステップ
高度な使用方法
このセクションでは、bytekinを効果的に使用するための高度なパターンとテクニックについて説明します。
プログラマティックAPI(アノテーションベース以外)
アノテーションは便利ですが、プログラマティックAPIを使用することもできます:
BytekinTransformer transformer = new BytekinTransformer.Builder()
.inject("com.example.Calculator", new Injection(
"add",
"(II)I",
At.HEAD,
Arrays.asList(Parameter.THIS, Parameter.INT, Parameter.INT)
))
.build();
複数のフッククラス
フックを複数のクラスに整理し、すべてを渡します:
BytekinTransformer transformer = new BytekinTransformer.Builder(
LoggingHooks.class,
AuthenticationHooks.class,
PerformanceHooks.class,
SecurityHooks.class
).build();
クラスの再マッピング
マッピングを使用して難読化されたコードを処理します:
class MyMappingProvider implements IMappingProvider {
@Override
public String getClassName(String name) {
// a.classをcom.example.Calculatorにマップ
if ("a".equals(name)) return "com.example.Calculator";
return name;
}
@Override
public String getMethodName(String className, String methodName, String descriptor) {
// bをaddにマップ
if ("com.example.Calculator".equals(className) && "b".equals(methodName)) {
return "add";
}
return methodName;
}
@Override
public String getFieldName(String className, String fieldName) {
return fieldName;
}
}
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.mapping(new MyMappingProvider())
.build();
変換のチェーン化
同じクラスに複数の変換を適用します:
byte[] original = getClassBytecode("com.example.Service");
// 最初の変換
byte[] step1 = transformer1.transform("com.example.Service", original);
// 2番目の変換
byte[] step2 = transformer2.transform("com.example.Service", step1);
// 最終結果をロード
Class<?> clazz = loadFromBytecode(step2);
条件付きフックロジック
条件に基づいてフックを実行します:
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo conditionalHook(String input, CallbackInfo ci) {
// 特定の入力に対してのみインジェクト
if (input.startsWith("test_")) {
System.out.println("テストモード: " + input);
}
// 特定の環境に対してのみインジェクト
String env = System.getProperty("app.env", "dev");
if ("prod".equals(env)) {
// 本番環境の場合の異なる動作
}
return ci;
}
ステートフルフック
フックインボケーション間で状態を維持します:
@ModifyClass("com.example.RequestHandler")
public class RequestHooks {
private static final Map<String, Integer> callCounts = new ConcurrentHashMap<>();
@Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo trackCalls(String id, CallbackInfo ci) {
int count = callCounts.getOrDefault(id, 0);
callCounts.put(id, count + 1);
if (count > 100) {
System.out.println("高い呼び出し回数: " + id);
}
return ci;
}
}
複数の変換の組み合わせ
同じメソッドに異なる変換タイプを使用します:
@ModifyClass("com.example.DataService")
public class ServiceHooks {
// エントリをログ記録
@Inject(methodName = "query", methodDesc = "(Ljava/lang/String;)Ljava/util/List;",
at = At.HEAD)
public static CallbackInfo logEntry(String sql) {
System.out.println("クエリ: " + sql);
return CallbackInfo.empty();
}
// データベース呼び出しをインターセプト
@Invoke(
targetMethodName = "query",
targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
invokeMethodName = "execute",
invokeMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
shift = Shift.BEFORE
)
public static CallbackInfo cacheLookup(String sql, CallbackInfo ci) {
List<?> cached = getFromCache(sql);
if (cached != null) {
ci.cancelled = true;
ci.returnValue = cached;
}
return ci;
}
// 定数データベースURLを変更
@ModifyConstant(methodName = "getConnection", oldValue = "localhost",
newValue = "db.production.com")
public static CallbackInfo updateDbLocation() {
return CallbackInfo.empty();
}
}
パフォーマンス最適化パターン
効率的なパフォーマンス監視のためにフックを使用します:
@ModifyClass("com.example.CriticalPath")
public class PerformanceHooks {
private static final int SLOW_THRESHOLD = 1000; // ms
@Inject(methodName = "criticalOperation", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo startTimer() {
TIMER.set(System.currentTimeMillis());
return CallbackInfo.empty();
}
@Inject(methodName = "criticalOperation", methodDesc = "()V", at = At.RETURN)
public static CallbackInfo checkTimer() {
long duration = System.currentTimeMillis() - TIMER.get();
if (duration > SLOW_THRESHOLD) {
System.out.println("遅い操作: " + duration + "ms");
}
return CallbackInfo.empty();
}
private static final ThreadLocal<Long> TIMER = ThreadLocal.withInitial(() -> 0L);
}
セキュリティパターン - 入力検証
エントリポイントですべての入力を検証します:
@ModifyClass("com.example.WebController")
public class SecurityHooks {
@Inject(methodName = "handleRequest", methodDesc = "(Ljava/lang/String;)V",
at = At.HEAD)
public static CallbackInfo validateRequest(String request, CallbackInfo ci) {
if (request == null || isMalicious(request)) {
ci.cancelled = true; // リクエストをブロック
return ci;
}
return ci;
}
private static boolean isMalicious(String request) {
// SQLインジェクション、XSSなどをチェック
return request.contains("DROP") || request.contains("<script>");
}
}
テストパターン - モックオブジェクト
テストで依存性インジェクションのためにフックを使用します:
@ModifyClass("com.example.UserService")
public class TestHooks {
private static UserRepository mockRepository = new MockUserRepository();
@Inject(methodName = "getUserById", methodDesc = "(I)Lcom/example/User;",
at = At.HEAD)
public static CallbackInfo useMockRepository() {
// モックリポジトリをインジェクト
return CallbackInfo.empty();
}
}
A/Bテストパターン
ユーザーに基づいて異なる実装にルーティングします:
@ModifyClass("com.example.Algorithm")
public class ABTestingHooks {
@Invoke(
targetMethodName = "process",
targetMethodDesc = "(Ljava/lang/Object;)Ljava/lang/Object;",
invokeMethodName = "compute",
invokeMethodDesc = "(Ljava/lang/Object;)Ljava/lang/Object;",
shift = Shift.BEFORE
)
public static CallbackInfo selectImplementation(Object data, CallbackInfo ci) {
// ユーザーに基づいて新しいまたは古い実装にルーティング
if (useNewImplementation(data)) {
ci.returnValue = computeNew(data);
ci.cancelled = true;
}
return ci;
}
private static boolean useNewImplementation(Object data) {
String userId = extractUserId(data);
int hash = userId.hashCode();
return hash % 2 == 0; // 50/50分割
}
}
フィーチャーフラグパターン
デプロイなしで機能を有効/無効にします:
@ModifyClass("com.example.Features")
public class FeatureFlagHooks {
private static final Map<String, Boolean> flags = new ConcurrentHashMap<>();
@Inject(methodName = "newFeature", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo checkFeatureFlag(CallbackInfo ci) {
if (!isFeatureEnabled("newFeature")) {
ci.cancelled = true; // このメソッドをスキップ
}
return ci;
}
private static boolean isFeatureEnabled(String feature) {
return flags.getOrDefault(feature, false);
}
public static void setFeatureFlag(String feature, boolean enabled) {
flags.put(feature, enabled);
}
}
遅延初期化パターン
高コストな初期化を延期します:
@ModifyClass("com.example.Config")
public class ConfigHooks {
private static volatile Configuration config;
@Inject(methodName = "getConfig", methodDesc = "()Lcom/example/Configuration;",
at = At.HEAD)
public static CallbackInfo lazyInitialize(CallbackInfo ci) {
if (config == null) {
synchronized (ConfigHooks.class) {
if (config == null) {
config = loadConfiguration(); // 高コストな操作
}
}
}
ci.cancelled = true;
ci.returnValue = config;
return ci;
}
}
次のステップ
マッピング
bytekinは難読化または名前変更されたコードを扱うためのクラス名とメソッド名のマッピングをサポートしています。
マッピングとは?
マッピングは人間が読める名前とバイトコード名の間を変換します。これは以下の場合に便利です:
- 難読化されたコードを扱う場合
- 名前変更されたクラスに変換を適用する場合
- バージョンの違いを処理する場合
- 複数の命名規則をサポートする場合
マッピングプロバイダーの作成
IMappingProvider
インターフェースを実装:
public class MyMappingProvider implements IMappingProvider {
@Override
public String getClassName(String name) {
// クラス名をマップ
if ("OriginalName".equals(name)) {
return "MappedName";
}
return name;
}
@Override
public String getMethodName(String className, String methodName, String descriptor) {
// クラスとシグネチャに基づいてメソッド名をマップ
if ("MyClass".equals(className) && "oldMethod".equals(methodName)) {
return "newMethod";
}
return methodName;
}
@Override
public String getFieldName(String className, String fieldName) {
// フィールド名をマップ
if ("MyClass".equals(className) && "oldField".equals(fieldName)) {
return "newField";
}
return fieldName;
}
}
マッピングの使用
ビルダーにマッピングプロバイダーを渡す:
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.mapping(new MyMappingProvider())
.build();
一般的なマッピングパターン
シンプルな名前変更
public String getClassName(String name) {
return name.replace("OldPrefix", "NewPrefix");
}
ルックアップテーブル
private static final Map<String, String> classMap = new HashMap<>();
static {
classMap.put("a", "com.example.ClassA");
classMap.put("b", "com.example.ClassB");
}
public String getClassName(String name) {
return classMap.getOrDefault(name, name);
}
ファイルベースのマッピング
public String getClassName(String name) {
// 設定ファイルから読み込み
Properties props = loadMappings("mappings.properties");
return props.getProperty(name, name);
}
難読化されたコードのマッピング
難読化されたコードを扱う場合:
public class ObfuscationMapping implements IMappingProvider {
@Override
public String getClassName(String name) {
// a.class -> com.example.MyClass
switch (name) {
case "a": return "com.example.MyClass";
case "b": return "com.example.OtherClass";
default: return name;
}
}
@Override
public String getMethodName(String className, String methodName, String descriptor) {
// a.b() -> MyClass.process()
if ("com.example.MyClass".equals(className)) {
switch (methodName) {
case "b": return "process";
case "c": return "validate";
default: return methodName;
}
}
return methodName;
}
}
マッピングを使用したフック設定
人間が読める名前を使用してフックを書く:
@ModifyClass("com.example.UserService") // 読みやすい名前を使用
public class UserServiceHooks {
@Inject(methodName = "getUser", methodDesc = "(I)Lcom/example/User;", at = At.HEAD)
public static CallbackInfo hook() { }
}
マッピングプロバイダーがバイトコード内の実際のクラス名に変換します。
デフォルト(何もしない)マッピング
変更されない名前のために空のマッピングを使用:
public class EmptyMappingProvider implements IMappingProvider {
@Override
public String getClassName(String name) {
return name; // 変更なし
}
@Override
public String getMethodName(String className, String methodName, String descriptor) {
return methodName; // 変更なし
}
@Override
public String getFieldName(String className, String fieldName) {
return fieldName; // 変更なし
}
}
応用: バージョン固有のマッピング
複数のバージョンをサポート:
public class VersionAwareMappingProvider implements IMappingProvider {
private final String version;
public VersionAwareMappingProvider(String version) {
this.version = version;
}
@Override
public String getClassName(String name) {
if ("1.0".equals(version)) {
return mapToV1(name);
} else if ("2.0".equals(version)) {
return mapToV2(name);
}
return name;
}
private String mapToV1(String name) {
// バージョン1のマッピング
return name;
}
private String mapToV2(String name) {
// バージョン2のマッピング
return name;
}
}
次のステップ
ビルダーパターン
bytekinは、トランスフォーマーをプログラム的に構築するための流暢なビルダーAPIを提供します。
基本的な使用方法
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.build();
マッピングの使用
ビルダー構築時に名前マッピングを適用します:
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.mapping(new CustomMappingProvider())
.build();
プログラマティック変換の追加
アノテーションベースとプログラマティック構成を混在させます:
BytekinTransformer transformer = new BytekinTransformer.Builder(AnnotationHooks.class)
.inject("com.example.Extra", new Injection(...))
.invoke("com.example.Another", new Invocation(...))
.build();
複数のフッククラス
BytekinTransformer transformer = new BytekinTransformer.Builder(
LoggingHooks.class,
SecurityHooks.class,
PerformanceHooks.class
)
.mapping(myMappings)
.build();
ビルダーメソッド
mapping(IMappingProvider)
クラス/メソッド名変換のためのマッピングプロバイダーを設定します。
inject(String, Injection)
インジェクション変換をプログラム的に追加します。
invoke(String, Invocation)
インボケーション変換をプログラム的に追加します。
redirect(String, RedirectData)
リダイレクト変換をプログラム的に追加します。
modifyConstant(String, ConstantModification)
定数変更をプログラム的に追加します。
modifyVariable(String, VariableModification)
変数変更をプログラム的に追加します。
build()
トランスフォーマーをビルドして返します。このメソッドは:
- すべてのフッククラスをスキャン
- アノテーションを抽出
- プログラマティック変換を追加
- 内部トランスフォーマーマップを作成
- 使用可能なトランスフォーマーを返す
ベストプラクティス
- 一度だけビルド: 初期化時にトランスフォーマーを作成
- 再利用: 複数の変換に同じトランスフォーマーを使用
- パターンを組み合わせる: アノテーションとプログラマティックAPIを混在
- 構成を文書化: 特定の変換が適用される理由をコメント
パフォーマンスのヒント
// 良い: 一度だけビルド
BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class).build();
for (String className : classes) {
byte[] transformed = transformer.transform(className, bytecode);
}
// 悪い: 複数回ビルド
for (String className : classes) {
BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class).build();
byte[] transformed = transformer.transform(className, bytecode);
}
次のステップ
カスタムトランスフォーマー
アノテーション以外にも、bytekinは高度なユースケースのためにカスタムトランスフォーマーを作成できます。
カスタムトランスフォーマーの作成
カスタムロジックを実装して変換システムを拡張できます:
public class CustomTransformer implements IBytekinMethodTransformer {
@Override
public byte[] transform(byte[] bytecode) {
// カスタム変換ロジック
return bytecode;
}
}
高度なカスタマイゼーション
より複雑なシナリオの場合、ASMビジターパターンを直接使用します:
public class AdvancedCustomTransformer extends ClassVisitor {
public AdvancedCustomTransformer(ClassVisitor cv) {
super(ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// カスタムメソッドビジターを返す
return new MethodVisitor(ASM9, mv) {
@Override
public void visitCode() {
// カスタムメソッド計装
super.visitCode();
}
};
}
}
カスタムと組み込み変換の組み合わせ
カスタムトランスフォーマーとbytekinの組み込み機能を混在させます:
BytekinTransformer transformer = new BytekinTransformer.Builder(BuiltInHooks.class)
.build();
// 後でカスタム変換を適用
byte[] original = getClassBytecode("com.example.MyClass");
byte[] withBuiltIn = transformer.transform("com.example.MyClass", original);
byte[] withCustom = applyCustom(withBuiltIn);
パフォーマンスの考慮事項
- カスタムトランスフォーマーを効率的に保つ
- 可能な場合は変換結果をキャッシュ
- ホットスポットのためにカスタムコードをプロファイル
次のステップ
APIリファレンス
このセクションではbytekinの詳細なAPIドキュメントを提供します。
コアクラス
BytekinTransformer
バイトコード変換のメインエントリーポイント。
public class BytekinTransformer {
public byte[] transform(String className, byte[] bytes, int api);
public static class Builder {
public Builder(Class<?>... classes);
public Builder mapping(IMappingProvider mapping);
public Builder inject(String className, Injection injection);
public Builder invoke(String className, Invocation invocation);
public Builder redirect(String className, RedirectData redirect);
public Builder modifyConstant(String className, ConstantModification modification);
public Builder modifyVariable(String className, VariableModification modification);
public BytekinTransformer build();
}
}
CallbackInfo
フックメソッド内で変換動作を制御します。
public class CallbackInfo {
public boolean cancelled;
public Object returnValue;
public Object[] modifyArgs;
public static CallbackInfo empty();
}
アノテーション
@ModifyClass
バイトコード変換のフックコンテナとしてクラスをマークします。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyClass {
String className();
}
@Inject
メソッドの特定の箇所にコードをインジェクトします。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
String methodName();
String methodDesc();
At at();
}
@Invoke
メソッド呼び出しをインターセプトします。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invoke {
String targetMethodName();
String targetMethodDesc();
String invokeMethodName();
String invokeMethodDesc();
Shift shift();
}
@Redirect
メソッド呼び出しを別のターゲットにリダイレクトします。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Redirect {
String targetMethodName();
String targetMethodDesc();
String redirectMethodName();
String redirectMethodDesc();
}
@ModifyConstant
バイトコード内の定数値を変更します。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyConstant {
String methodName();
Object oldValue();
Object newValue();
}
@ModifyVariable
ローカル変数の値を変更します。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyVariable {
String methodName();
int variableIndex();
}
列挙型
At
コードをインジェクトする場所を指定します。
public enum At {
HEAD, // メソッド本体の前
RETURN, // return文の前
TAIL // メソッドの終わり
}
Shift
メソッド呼び出しに対する相対的なタイミングを指定します。
public enum Shift {
BEFORE, // 呼び出しの前
AFTER // 呼び出しの後
}
インターフェース
IMappingProvider
クラス名とメソッド名をマッピングします。
public interface IMappingProvider {
String getClassName(String name);
String getMethodName(String className, String methodName, String descriptor);
String getFieldName(String className, String fieldName);
}
データクラス
Injection
インジェクション変換を表します。
public class Injection {
// コンストラクタとメソッド
}
Invocation
インボケーション変換を表します。
public class Invocation {
// コンストラクタとメソッド
}
RedirectData
リダイレクト変換を表します。
public class RedirectData {
// コンストラクタとメソッド
}
ConstantModification
定数変更変換を表します。
public class ConstantModification {
// コンストラクタとメソッド
}
VariableModification
変数変更変換を表します。
public class VariableModification {
// コンストラクタとメソッド
}
一般的な例外
VerifyError
変換されたバイトコードが無効な場合にスローされます。
ClassNotFoundException
ターゲットクラスが見つからない場合にスローされます。
ClassFormatException
バイトコード形式が無効な場合にスローされます。
ユーティリティクラス
DescriptorParser
メソッドディスクリプタの解析と検証のためのユーティリティ。
BytecodeManipulator
低レベルのバイトコード操作ユーティリティ。
スレッディング
すべてのpublicメソッドは初期化後はスレッドセーフです:
BytekinTransformer.transform()
は複数のスレッドから呼び出し可能Builder
は設定中はスレッドセーフではないCallbackInfo
は各フック呼び出しにローカル
パフォーマンス特性
操作 | 複雑さ |
---|---|
Builder.build() | O(n) n = フックメソッド数 |
transform() | O(m) m = バイトコードサイズ |
フックの実行 | 平均O(1) |
次のステップ
- アノテーションを詳しく確認する
- クラスとインターフェースを確認する
- 例を探索する
アノテーションリファレンス
bytekinアノテーションの完全なリファレンスです。
@ModifyClass
目的
バイトコード変換のためのフックメソッドを含むクラスをマークします。
使用方法
@ModifyClass("com.example.TargetClass")
public class MyHooks {
// ここにフックメソッド
}
パラメータ
パラメータ | 型 | 必須 | 説明 |
---|---|---|---|
className | String | はい | ターゲットクラスの完全修飾名 |
スコープ
クラス型にのみ適用されます。
@Inject
目的
メソッドの特定の位置にコードをインジェクトします。
使用方法
@Inject(
methodName = "myMethod",
methodDesc = "(I)Ljava/lang/String;",
at = At.HEAD
)
public static CallbackInfo hook(int param) { }
パラメータ
パラメータ | 型 | 必須 | 説明 |
---|---|---|---|
methodName | String | はい | ターゲットメソッド名 |
methodDesc | String | はい | メソッドディスクリプタ(JVM形式) |
at | At | はい | コードをインジェクトする位置 |
スコープ
メソッドにのみ適用されます。
戻り値の型
CallbackInfo
を返す必要があります。
@Invoke
目的
メソッド呼び出しをインターセプトします。
使用方法
@Invoke(
targetMethodName = "parentMethod",
targetMethodDesc = "()V",
invokeMethodName = "childMethod",
invokeMethodDesc = "(I)V",
shift = Shift.BEFORE
)
public static CallbackInfo hook() { }
パラメータ
パラメータ | 型 | 必須 | 説明 |
---|---|---|---|
targetMethodName | String | はい | 呼び出しを含むメソッド |
targetMethodDesc | String | はい | ターゲットメソッドのディスクリプタ |
invokeMethodName | String | はい | 呼び出されるメソッドの名前 |
invokeMethodDesc | String | はい | 呼び出されるメソッドのディスクリプタ |
shift | Shift | はい | 呼び出しの前か後か |
スコープ
メソッドにのみ適用されます。
@Redirect
目的
メソッド呼び出しを別のターゲットにリダイレクトします。
使用方法
@Redirect(
targetMethodName = "oldMethod",
targetMethodDesc = "()V",
redirectMethodName = "newMethod",
redirectMethodDesc = "()V"
)
public static void hook() { }
パラメータ
パラメータ | 型 | 必須 | 説明 |
---|---|---|---|
targetMethodName | String | はい | 呼び出しを含むメソッド |
targetMethodDesc | String | はい | ターゲットメソッドのディスクリプタ |
redirectMethodName | String | はい | リダイレクトメソッドの名前 |
redirectMethodDesc | String | はい | リダイレクトメソッドのディスクリプタ |
@ModifyConstant
目的
バイトコード内の定数値を変更します。
使用方法
@ModifyConstant(
methodName = "getConfig",
oldValue = "dev",
newValue = "prod"
)
public static CallbackInfo hook() { }
パラメータ
パラメータ | 型 | 必須 | 説明 |
---|---|---|---|
methodName | String | はい | 定数を含むメソッド |
oldValue | Object | はい | 元の定数値 |
newValue | Object | はい | 新しい定数値 |
@ModifyVariable
目的
ローカル変数の値を変更します。
使用方法
@ModifyVariable(
methodName = "process",
variableIndex = 1
)
public static void hook(String param) { }
パラメータ
パラメータ | 型 | 必須 | 説明 |
---|---|---|---|
methodName | String | はい | ターゲットメソッド名 |
variableIndex | int | はい | ローカル変数スロットのインデックス |
列挙型: At
値
値 | 説明 |
---|---|
HEAD | メソッドの開始位置、すべてのコードの前 |
RETURN | 各return文の前 |
TAIL | メソッドの終了位置 |
列挙型: Shift
値
値 | 説明 |
---|---|
BEFORE | メソッド呼び出しの前にフックを実行 |
AFTER | メソッド呼び出しの後にフックを実行 |
次のステップ
- クラスとインターフェースをレビュー
- APIリファレンスをチェック
- 例を探索
クラスとインターフェース
bytekinのクラスとインターフェースのリファレンスドキュメント。
コアクラス
BytekinTransformer
バイトコード操作のためのメイントランスフォーマークラス。
メソッド:
byte[] transform(String className, byte[] bytes, int api)
- クラスのバイトコードを変換byte[] transform(String className, byte[] bytes)
- 変換(デフォルトAPI)
ビルダー:
new BytekinTransformer.Builder(Class<?>... classes)
- ビルダーを作成
CallbackInfo
変換動作を制御するためのデータ構造。
フィールド:
boolean cancelled
- 元のコード実行をスキップObject returnValue
- カスタム戻り値Object[] modifyArgs
- 変更されたメソッド引数
メソッド:
static CallbackInfo empty()
- 空のコールバックを作成CallbackInfo(boolean cancelled, Object returnValue, Object[] modifyArgs)
- コンストラクタ
ビルダークラス
BytekinTransformer.Builder
トランスフォーマーを構築するための流暢なビルダー。
コンストラクタ:
Builder(Class<?>... classes)
- フッククラスで初期化
メソッド:
Builder mapping(IMappingProvider)
- マッピングプロバイダーを設定Builder inject(String, Injection)
- インジェクションを追加Builder invoke(String, Invocation)
- インボケーションを追加Builder redirect(String, RedirectData)
- リダイレクトを追加Builder modifyConstant(String, ConstantModification)
- 定数変更を追加Builder modifyVariable(String, VariableModification)
- 変数変更を追加BytekinTransformer build()
- トランスフォーマーをビルド
データクラス
Injection
インジェクションポイントを表します。
目的: インジェクション設定データを格納。
Invocation
インボケーションポイントを表します。
目的: インボケーション設定データを格納。
RedirectData
リダイレクトターゲットを表します。
目的: リダイレクト設定データを格納。
ConstantModification
定数の変更を表します。
目的: 定数変更データを格納。
VariableModification
変数の変更を表します。
目的: 変数変更データを格納。
インターフェース
IMappingProvider
名前マッピングのためのインターフェース。
メソッド:
String getClassName(String name)
- クラス名をマップString getMethodName(String className, String methodName, String descriptor)
- メソッド名をマップString getFieldName(String className, String fieldName)
- フィールド名をマップ
実装例
EmptyMappingProvider - 何もしないマッピング(変更されない名前を返す)
カスタムマッピング:
public class CustomMapping implements IMappingProvider {
@Override
public String getClassName(String name) {
// カスタムマッピングロジック
return name;
}
@Override
public String getMethodName(String className, String methodName, String descriptor) {
// カスタムマッピングロジック
return methodName;
}
@Override
public String getFieldName(String className, String fieldName) {
// カスタムマッピングロジック
return fieldName;
}
}
ユーティリティクラス
DescriptorParser
メソッドディスクリプタを解析して検証します。
メソッド:
static String parseDescriptor(String desc)
- ディスクリプタ形式を解析
BytecodeManipulator
低レベルのバイトコードユーティリティ。
目的: バイトコード操作のための内部ユーティリティ。
継承階層
Object
├── BytekinTransformer
│ └── BytekinTransformer.Builder
├── CallbackInfo
├── Injection
├── Invocation
├── RedirectData
├── ConstantModification
└── VariableModification
インターフェースの実装
IMappingProvider
├── EmptyMappingProvider
└── (カスタム実装)
次のステップ
例
例 - 基本的な使用方法
このセクションには、一般的なbytekinのユースケースの完全で動作する例が含まれています。
例1: ログの追加
問題
ソースコードを変更せずにメソッドにログを追加する。
解決策
ターゲットクラス:
package com.example;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
フッククラス:
package com.example;
import io.github.brqnko.bytekin.injection.*;
import io.github.brqnko.bytekin.data.CallbackInfo;
@ModifyClass("com.example.Calculator")
public class CalculatorLoggingHooks {
@Inject(
methodName = "add",
methodDesc = "(II)I",
at = At.HEAD
)
public static CallbackInfo logAddition(int a, int b) {
System.out.println("Adding: " + a + " + " + b);
return CallbackInfo.empty();
}
}
使用方法:
public class Main {
public static void main(String[] args) {
BytekinTransformer transformer = new BytekinTransformer.Builder(
CalculatorLoggingHooks.class
).build();
byte[] original = getClassBytecode("com.example.Calculator");
byte[] transformed = transformer.transform("com.example.Calculator", original);
Calculator calc = loadTransformed(transformed);
int result = calc.add(5, 3);
// 出力:
// Adding: 5 + 3
// 8
}
}
例2: パラメータ検証
問題
実行前にメソッドパラメータを検証する。
解決策
フッククラス:
@ModifyClass("com.example.UserService")
public class UserValidationHooks {
@Inject(
methodName = "createUser",
methodDesc = "(Ljava/lang/String;I)V",
at = At.HEAD
)
public static CallbackInfo validateUser(String name, int age, CallbackInfo ci) {
if (name == null || name.isEmpty()) {
System.out.println("ERROR: Name cannot be empty");
ci.cancelled = true;
return ci;
}
if (age < 18) {
System.out.println("ERROR: User must be 18 or older");
ci.cancelled = true;
return ci;
}
System.out.println("Valid user: " + name + ", age " + age);
return CallbackInfo.empty();
}
}
例3: キャッシング
問題
キャッシングを実装するためにメソッド呼び出しをインターセプトする。
解決策
フッククラス:
@ModifyClass("com.example.DataRepository")
public class CachingHooks {
private static final Map<String, Object> cache = new ConcurrentHashMap<>();
@Invoke(
targetMethodName = "fetch",
targetMethodDesc = "(Ljava/lang/String;)Ljava/lang/Object;",
invokeMethodName = "queryDatabase",
invokeMethodDesc = "(Ljava/lang/String;)Ljava/lang/Object;",
shift = Shift.BEFORE
)
public static CallbackInfo checkCache(String key, CallbackInfo ci) {
Object cached = cache.get(key);
if (cached != null) {
System.out.println("Cache hit for: " + key);
ci.cancelled = true;
ci.returnValue = cached;
} else {
System.out.println("Cache miss for: " + key);
}
return ci;
}
}
例4: セキュリティ - 認証チェック
問題
すべての機密メソッドに認証が必要であることを確認する。
解決策
フッククラス:
@ModifyClass("com.example.PaymentService")
public class AuthenticationHooks {
@Inject(
methodName = "transfer",
methodDesc = "(Ljava/lang/String;J)Z",
at = At.HEAD
)
public static CallbackInfo checkAuthentication(String account, long amount, CallbackInfo ci) {
if (!isUserAuthenticated()) {
System.out.println("ERROR: Authentication required");
ci.cancelled = true;
ci.returnValue = false;
return ci;
}
System.out.println("Authenticated transfer: " + amount);
return CallbackInfo.empty();
}
private static boolean isUserAuthenticated() {
// 認証状態を確認
return true;
}
}
例5: 監視 - メソッド呼び出しカウンター
問題
特定のメソッドが何回呼び出されたかをカウントする。
解決策
フッククラス:
@ModifyClass("com.example.UserService")
public class MonitoringHooks {
private static final AtomicInteger callCount = new AtomicInteger(0);
@Inject(
methodName = "getUser",
methodDesc = "(I)Lcom/example/User;",
at = At.HEAD
)
public static CallbackInfo countCalls(int userId) {
int count = callCount.incrementAndGet();
if (count % 100 == 0) {
System.out.println("getUser() called " + count + " times");
}
return CallbackInfo.empty();
}
}
例6: 戻り値の変換
問題
メソッドの戻り値を変更する。
解決策
フッククラス:
@ModifyClass("com.example.PriceCalculator")
public class PriceHooks {
@Inject(
methodName = "getPrice",
methodDesc = "()D",
at = At.RETURN
)
public static CallbackInfo applyDiscount(CallbackInfo ci) {
double originalPrice = (double) ci.returnValue;
double discounted = originalPrice * 0.9; // 10%割引
ci.returnValue = discounted;
return ci;
}
}
Invokeの例
例: メソッド呼び出しのインターセプト
フッククラス:
@ModifyClass("com.example.DataProcessor")
public class ProcessorHooks {
@Invoke(
targetMethodName = "process",
targetMethodDesc = "(Ljava/lang/String;)Ljava/lang/String;",
invokeMethodName = "validate",
invokeMethodDesc = "(Ljava/lang/String;)Ljava/lang/String;",
shift = Shift.BEFORE
)
public static CallbackInfo sanitizeBeforeValidation(String data, CallbackInfo ci) {
String sanitized = data.trim().toLowerCase();
ci.modifyArgs = new Object[]{sanitized};
return ci;
}
}
組み合わせ例: 包括的な変換
完全なフッククラス:
@ModifyClass("com.example.UserRepository")
public class ComprehensiveHooks {
@Inject(
methodName = "save",
methodDesc = "(Lcom/example/User;)V",
at = At.HEAD
)
public static CallbackInfo validateBeforeSave(Object user, CallbackInfo ci) {
// 入力を検証
if (user == null) {
System.out.println("ERROR: Cannot save null user");
ci.cancelled = true;
}
return ci;
}
@Invoke(
targetMethodName = "save",
targetMethodDesc = "(Lcom/example/User;)V",
invokeMethodName = "validateUser",
invokeMethodDesc = "(Lcom/example/User;)Z",
shift = Shift.BEFORE
)
public static CallbackInfo modifyValidation(Object user, CallbackInfo ci) {
// 検証を強化
System.out.println("Validating user...");
return ci;
}
@Inject(
methodName = "save",
methodDesc = "(Lcom/example/User;)V",
at = At.RETURN
)
public static CallbackInfo logSuccess(Object user) {
System.out.println("User saved successfully");
return CallbackInfo.empty();
}
}
次のステップ
高度な例
bytekinの高度なユースケースとパターン。
例1: カスタムClassLoader
変換を適用するカスタムClassLoaderを実装:
public class TransformingClassLoader extends ClassLoader {
private final BytekinTransformer transformer;
private final ClassLoader parent;
public TransformingClassLoader(BytekinTransformer transformer, ClassLoader parent) {
super(parent);
this.transformer = transformer;
this.parent = parent;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classBytes = loadBytesFromClasspath(name);
byte[] transformed = transformer.transform(name, classBytes);
return defineClass(name, transformed, 0, transformed.length);
} catch (IOException e) {
throw new ClassNotFoundException("Cannot find " + name, e);
}
}
private byte[] loadBytesFromClasspath(String className) throws IOException {
String path = className.replace('.', '/') + ".class";
try (InputStream is = parent.getResourceAsStream(path)) {
return is.readAllBytes();
}
}
}
// 使用方法
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
ClassLoader loader = new TransformingClassLoader(transformer, ClassLoader.getSystemClassLoader());
Class<?> clazz = loader.loadClass("com.example.MyClass");
例2: Javaエージェント
バイトコード変換のためのJavaエージェントを作成:
public class BytekinAgent {
public static void premain(String agentArgs, Instrumentation inst) {
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
inst.addTransformer((loader, className, klass, pd, bytecode) -> {
return transformer.transform(className, bytecode);
});
}
}
起動: java -javaagent:bytekin-agent.jar MyApplication
例3: アスペクト指向プログラミング(AOP)
横断的関心事を実装:
@ModifyClass("com.example.UserService")
public class AuditingAspect {
@Inject(methodName = "save", methodDesc = "(Lcom/example/User;)V", at = At.HEAD)
public static CallbackInfo auditBefore(Object user) {
System.out.println("Audit: save() started");
return CallbackInfo.empty();
}
@Inject(methodName = "delete", methodDesc = "(I)V", at = At.HEAD)
public static CallbackInfo auditDelete(int id) {
System.out.println("Audit: delete(" + id + ") started");
return CallbackInfo.empty();
}
}
例4: 遅延初期化
遅延ローディングパターンを実装:
@ModifyClass("com.example.Repository")
public class LazyLoadingHooks {
private static Object resource;
@Inject(methodName = "initialize", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo lazyInit(CallbackInfo ci) {
if (resource == null) {
synchronized (LazyLoadingHooks.class) {
if (resource == null) {
resource = loadExpensiveResource();
}
}
}
ci.cancelled = true;
return ci;
}
private static Object loadExpensiveResource() {
// 高コストな初期化
return new Object();
}
}
例5: 動的設定
設定に基づいて動作を変更:
@ModifyClass("com.example.Service")
public class DynamicConfigHooks {
private static final Properties config = new Properties();
static {
try {
config.load(new FileInputStream("config.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo checkConfig(String input, CallbackInfo ci) {
boolean enabled = Boolean.parseBoolean(config.getProperty("feature.enabled", "false"));
if (!enabled) {
ci.cancelled = true;
}
return ci;
}
}
例6: 多層変換
複数のトランスフォーマーを順次適用:
public class MultiLayerTransformation {
public static void main(String[] args) {
BytekinTransformer logging = new BytekinTransformer.Builder(LoggingHooks.class).build();
BytekinTransformer security = new BytekinTransformer.Builder(SecurityHooks.class).build();
BytekinTransformer caching = new BytekinTransformer.Builder(CachingHooks.class).build();
byte[] original = getClassBytecode("com.example.Service");
// 層ごとに適用
byte[] withLogging = logging.transform("com.example.Service", original);
byte[] withSecurity = security.transform("com.example.Service", withLogging);
byte[] withCaching = caching.transform("com.example.Service", withSecurity);
Class<?> transformed = loadClass(withCaching);
}
}
例7: パフォーマンスプロファイリング
ソース変更なしでプロファイリングを追加:
@ModifyClass("com.example.CriticalPath")
public class ProfilingHooks {
private static final ThreadLocal<Long> timer = ThreadLocal.withInitial(() -> 0L);
@Inject(methodName = "compute", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo startProfiling() {
timer.set(System.nanoTime());
return CallbackInfo.empty();
}
@Inject(methodName = "compute", methodDesc = "()Ljava/lang/Object;", at = At.RETURN)
public static CallbackInfo endProfiling() {
long duration = System.nanoTime() - timer.get();
System.out.println("Duration: " + (duration / 1_000_000.0) + "ms");
return CallbackInfo.empty();
}
}
例8: レジリエンスパターン
リトライロジックを追加:
@ModifyClass("com.example.HttpClient")
public class ResilienceHooks {
private static final int MAX_RETRIES = 3;
@Inject(methodName = "request", methodDesc = "(Ljava/lang/String;)Ljava/lang/String;",
at = At.HEAD)
public static CallbackInfo addRetry(String url, CallbackInfo ci) {
String result = null;
int attempt = 0;
while (attempt < MAX_RETRIES) {
try {
result = executeRequest(url);
break;
} catch (Exception e) {
attempt++;
if (attempt >= MAX_RETRIES) throw e;
}
}
ci.cancelled = true;
ci.returnValue = result;
return ci;
}
private static String executeRequest(String url) throws Exception {
// HTTPリクエストを実行
return "";
}
}
例9: 可観測性
メトリクスを収集:
@ModifyClass("com.example.DataStore")
public class ObservabilityHooks {
private static final AtomicLong callCount = new AtomicLong(0);
private static final AtomicLong errorCount = new AtomicLong(0);
@Inject(methodName = "query", methodDesc = "(Ljava/lang/String;)Ljava/util/List;",
at = At.HEAD)
public static CallbackInfo trackCall(String query) {
callCount.incrementAndGet();
return CallbackInfo.empty();
}
@Invoke(
targetMethodName = "query",
targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
invokeMethodName = "throwException",
invokeMethodDesc = "()V",
shift = Shift.BEFORE
)
public static CallbackInfo trackError() {
errorCount.incrementAndGet();
return CallbackInfo.empty();
}
public static void printMetrics() {
System.out.println("Calls: " + callCount.get());
System.out.println("Errors: " + errorCount.get());
}
}
例10: 移行戦略
古いAPIから新しいAPIへ段階的に移行:
@ModifyClass("com.example.Application")
public class MigrationHooks {
private static final boolean USE_NEW_API = true;
@Redirect(
targetMethodName = "main",
targetMethodDesc = "([Ljava/lang/String;)V",
redirectMethodName = "oldSearch",
redirectMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
from = "search",
to = USE_NEW_API ? "newSearch" : "oldSearch"
)
public static void migrateAPI() {
// 徐々に新しい実装にルーティング
}
}
次のステップ
- ベストプラクティスを確認する
- トラブルシューティングを確認する
- 高度な使用方法を探索する
ベストプラクティス
このガイドは、bytekinを効果的かつ安全に使用するためのベストプラクティスをカバーしています。
設計原則
1. フックをシンプルに保つ
フックメソッドは集中的でシンプルに保ちます:
良い:
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo log() {
System.out.println("Starting process");
return CallbackInfo.empty();
}
避けるべき:
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo complexLogic() {
// 複数のデータベース呼び出し
// 複雑な計算
// ファイルI/O操作
// これはフックには多すぎます!
return CallbackInfo.empty();
}
2. 複雑なロジックを抽出
複雑なロジックは別のメソッドに移動:
@Inject(methodName = "validate", methodDesc = "(Ljava/lang/String;)Z", at = At.HEAD)
public static CallbackInfo onValidate(String input, CallbackInfo ci) {
if (!isValidInput(input)) {
ci.cancelled = true;
ci.returnValue = false;
}
return ci;
}
private static boolean isValidInput(String input) {
// ここに複雑な検証ロジック
return !input.isEmpty() && input.length() < 256;
}
パフォーマンスガイドライン
1. フックオーバーヘッドを最小化
フックは頻繁に実行されます。高速に保ちます:
良い:
@Inject(methodName = "getData", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo checkCache() {
if (cacheHit()) {
// 高速なキャッシュルックアップ
return new CallbackInfo(true, getFromCache(), null);
}
return CallbackInfo.empty();
}
避けるべき:
@Inject(methodName = "getData", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo expensiveCheck() {
// データベース全体をスキャン
List<Item> results = database.queryAll();
// 結果を処理
// ...これは遅すぎます!
return CallbackInfo.empty();
}
2. Builderを再利用
トランスフォーマーを1回ビルドして再利用:
良い:
// 初期化コード内
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.build();
// トランスフォーマーを複数回使用
byte[] transformed1 = transformer.transform("com.example.Class1", bytes1);
byte[] transformed2 = transformer.transform("com.example.Class2", bytes2);
避けるべき:
// ループ内でこれをしないでください!
for (String className : classNames) {
// 各クラスごとにトランスフォーマーを作成するのは無駄
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.build();
byte[] transformed = transformer.transform(className, bytes);
}
エラーハンドリング
1. フック内で例外を処理
フック内の例外は変換を壊す可能性があります:
良い:
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo safeLogging() {
try {
System.out.println("Processing started");
} catch (Exception e) {
// 優雅に処理し、伝播させない
e.printStackTrace();
}
return CallbackInfo.empty();
}
避けるべき:
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo unsafeLogging() {
// これがスローされると、変換が壊れます!
Path path = Paths.get("/invalid/path");
Files.writeString(path, "log");
return CallbackInfo.empty();
}
2. 戻り値を検証
CallbackInfoを変更する場合、型が正しいことを確認:
良い:
@Inject(methodName = "getValue", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo returnCustomValue() {
CallbackInfo ci = new CallbackInfo();
ci.cancelled = true;
ci.returnValue = 42; // Integerは戻り値型と一致
return ci;
}
避けるべき:
@Inject(methodName = "getValue", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo wrongType() {
CallbackInfo ci = new CallbackInfo();
ci.cancelled = true;
ci.returnValue = "42"; // Stringはint戻り値型と一致しない!
return ci;
}
ドキュメント
1. 変換をドキュメント化
各フックが何をするかを明確にドキュメント化:
/**
* すべてのデータアクセスメソッドに認証チェックを追加。
* ユーザーが認証されていない場合、メソッドをキャンセルしてfalseを返す。
*/
@ModifyClass("com.example.DataStore")
public class DataStoreHooks {
/**
* 読み取り操作の開始時に認証チェックをインジェクト。
*
* @param ci CallbackInfo - 認証されていない場合cancelled=trueを設定
*/
@Inject(methodName = "read", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo ensureAuthenticated(CallbackInfo ci) {
if (!isAuthenticated()) {
ci.cancelled = true;
ci.returnValue = null;
}
return ci;
}
}
2. パラメータをドキュメント化
どのパラメータがメソッド引数に対応するかを明確に示す:
/**
* 処理前にユーザー入力をサニタイズ。
*
* @param userId ユーザーID(ターゲットメソッドの第1引数)
* @param action 要求されたアクション(第2引数)
*/
@Inject(methodName = "execute", methodDesc = "(Ljava/lang/String;Ljava/lang/String;)V",
at = At.HEAD)
public static CallbackInfo sanitizeInput(String userId, String action) {
// userIdとactionはターゲットメソッドのパラメータから
return CallbackInfo.empty();
}
テスト
1. 変換をテスト
常に変換をテスト:
public class TransformationTest {
@Test
public void testInjectionWorks() {
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.build();
byte[] original = getClassBytecode("com.example.Target");
byte[] transformed = transformer.transform("com.example.Target", original);
// 変換されたクラスをロードしてテスト
Class<?> clazz = loadFromBytecode(transformed);
Object instance = clazz.newInstance();
// 変換が適用されたことを確認
assertNotNull(instance);
}
}
2. リグレッションがないことを確認
元の動作が保持されていることを確認:
@Test
public void testOriginalBehaviorPreserved() {
// 変換なしでテスト
Calculator calc1 = new Calculator();
int result1 = calc1.add(3, 4);
// 変換ありでテスト
byte[] transformed = applyTransformation(Calculator.class);
Calculator calc2 = loadTransformed(transformed);
int result2 = calc2.add(3, 4);
// 結果は同じであるべき
assertEquals(result1, result2);
}
互換性
1. バージョン互換性
サポートされているJavaバージョンをドキュメント化:
/**
* これらのフックはJava 8+で動作
* バージョン間で互換性のある標準メソッドディスクリプタを使用
*/
@ModifyClass("com.example.Service")
public class CompatibleHooks {
// ...
}
2. ライブラリ互換性
他のバイトコードツールとの非互換性を確認:
// 他のバイトコード操作との競合をドキュメント化
// 例: Spring、Mockito、AspectJなど
セキュリティ
1. 入力検証
フック内で常に入力を検証:
@Inject(methodName = "processFile", methodDesc = "(Ljava/lang/String;)V",
at = At.HEAD)
public static CallbackInfo validatePath(String path, CallbackInfo ci) {
if (path != null && isPathTraversal(path)) {
// ディレクトリトラバーサル攻撃を防ぐ
ci.cancelled = true;
}
return ci;
}
private static boolean isPathTraversal(String path) {
return path.contains("..") || path.startsWith("/");
}
2. 機密データの露出を避ける
機密情報をログに記録したり露出させたりしない:
良い:
@Inject(methodName = "login", methodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z",
at = At.HEAD)
public static CallbackInfo logAttempt(String user) {
System.out.println("Login attempt by: " + user);
return CallbackInfo.empty();
}
避けるべき:
@Inject(methodName = "login", methodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z",
at = At.HEAD)
public static CallbackInfo logAttempt(String user, String password) {
// パスワードをログに記録しないでください!
System.out.println("Login attempt: " + user + " / " + password);
return CallbackInfo.empty();
}
デバッグのヒント
1. バイトコード検査
生成されたバイトコードを検査して変換を確認:
# javapを使用して変換されたクラスを検査
javap -c TransformedClass.class
# インジェクトされたメソッド呼び出しを探す
2. ログを追加
ログを使用して変換の実行を追跡:
@Inject(methodName = "critical", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo logEntry() {
System.out.println("[DEBUG] Entering critical method");
System.out.println("[DEBUG] Stack trace: " + Arrays.toString(Thread.currentThread().getStackTrace()));
return CallbackInfo.empty();
}
メンテナンス
1. フックのバージョン管理
フックのバージョンを追跡:
/**
* バージョン2.0の変換フック
*
* v1.0からの変更点:
* - 認証チェックを追加
* - キャッシング戦略を最適化
* - レガシーコードのnullポインタ問題を修正
*/
@ModifyClass("com.example.Service")
public class ServiceHooksV2 {
// ...
}
2. 記録を保持
各変換が存在する理由をドキュメント化:
変換: Calculator.add()のログ記録
作成日: 2025-01-15
理由: デバッグビルドのパフォーマンス監視
ステータス: アクティブ
注記: プロファイリングフェーズ後に削除可能
よくある落とし穴
1. 間違ったメソッドディスクリプタ
❌ 誤り:
@Inject(methodName = "add", methodDesc = "(I I)I", at = At.HEAD) // ディスクリプタ内のスペース!
✅ 正しい:
@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)
2. 型の不一致
❌ 誤り:
@Invoke(..., invokeMethodDesc = "(I)V", shift = Shift.BEFORE)
public static CallbackInfo hook(String param) { // 型の不一致!
}
✅ 正しい:
@Invoke(..., invokeMethodDesc = "(I)V", shift = Shift.BEFORE)
public static CallbackInfo hook(int param) { // 正しい型
}
3. 不変データの変更
❌ 誤り:
@ModifyVariable(methodName = "process", variableIndex = 1)
public static void modify(String str) {
str = str.toUpperCase(); // Stringは不変、機能しない!
}
✅ 正しい:
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo modifyByReplacing(String str, CallbackInfo ci) {
ci.modifyArgs = new Object[]{str.toUpperCase()};
return ci;
}
次のステップ
FAQ - よくある質問
一般的な質問
bytekinとは何ですか?
bytekinはASM上に構築された軽量なJavaバイトコード変換フレームワークです。ソースコードに触れることなく、バイトコードレベルでJavaクラスを変更できます。
なぜバイトコード変換が必要なのですか?
一般的なユースケース:
- ソースコードを変更せずにログを追加
- 横断的関心事の実装
- テストとモック
- パフォーマンスプロファイリング
- セキュリティ強化
bytekinは他のツールとどう違いますか?
ツール | サイズ | 複雑さ | ユースケース |
---|---|---|---|
bytekin | 小 | シンプル | 直接的なバイトコード操作 |
Spring AOP | 大 | 複雑 | エンタープライズフレームワーク |
Mockito | 中 | 中 | テスト/モック |
Aspect | 中 | 複雑 | アスペクト指向プログラミング |
bytekinは本番環境対応ですか?
はい、bytekinは本番環境での使用を目的として設計されています。最小限の依存関係(ASMのみ)で、徹底的にテストされています。
技術的な質問
bytekinはどのJavaバージョンをサポートしていますか?
bytekinはJava 8以降が必要です。
bytekinをSpring Bootと一緒に使用できますか?
はい! bytekinはSpring Bootと並行して動作できます。通常、カスタムClassLoader
セットアップ中またはビルド時に変換を適用します。
bytekinは難読化されたコードで動作しますか?
はい、マッピングで! 難読化されたクラス名とメソッド名を処理するためにマッピングシステムを使用します。
複数の変換を組み合わせることはできますか?
はい! 同じクラスに複数の@Inject
、@Invoke
、その他のアノテーションを使用できます。すべてが適用されます。
使用方法に関する質問
メソッドのメソッドディスクリプタを見つけるにはどうすればよいですか?
javap
を使用:
javap -c MyClass.class
メソッドシグネチャを確認し、JVMディスクリプタ形式に変換します:
int add(int a, int b)
→(II)I
String process(String s)
→(Ljava/lang/String;)Ljava/lang/String;
InjectとInvokeの違いは何ですか?
- Inject: メソッドの特定の箇所にコードを挿入
- Invoke: メソッド内のメソッド呼び出しをインターセプトし、必要に応じて引数を変更
メソッドの実行をキャンセルできますか?
はい、フックメソッドでci.cancelled = true
を設定します。ただし、これは特定の変換タイプでのみ機能します。
メソッド引数を変更するにはどうすればよいですか?
CallbackInfo.modifyArgs
を使用:
ci.modifyArgs = new Object[]{ modifiedArg1, modifiedArg2 };
フックメソッドから静的フィールドにアクセスできますか?
はい、フッククラスから静的フィールドを参照できます:
@Inject(...)
public static CallbackInfo hook() {
// 静的フィールドにアクセス
if (cacheEnabled) {
// ...
}
}
パフォーマンスに関する質問
bytekinを使用するオーバーヘッドは何ですか?
- 変換時間: 最小限、クラスロード時に1回のみ発生
- ランタイムオーバーヘッド: ゼロ! 変換されたバイトコードは手書きのコードと同じ速度で実行
各変換ごとにトランスフォーマーを再ビルドする必要がありますか?
いいえ! 1回ビルドして再利用:
// 良い
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
for (String className : classNames) {
byte[] transformed = transformer.transform(className, bytecode);
}
// 悪い
for (String className : classNames) {
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
byte[] transformed = transformer.transform(className, bytecode);
}
バイトコード変換は起動時間にどれだけ影響しますか?
変換がシンプルで、必要なクラスにのみ適用される場合、影響は最小限です。
トラブルシューティングの質問
変換が適用されていません
一般的な原因:
- 間違ったクラス名:
@ModifyClass
の値が正確に一致することを確認 - 間違ったメソッドディスクリプタ:
methodDesc
パラメータを確認 - クラスが読み込まれていない: 変換前にクラスがロードされることを確認
ClassCastExceptionが発生します
これは通常、以下を意味します:
CallbackInfo.returnValue
の型不一致- フックメソッドシグネチャの間違った型
- 互換性のない型への引数の変更
フックメソッドが呼び出されていません
確認事項:
- フッククラスがBuilderに渡されていますか?
- メソッド名とディスクリプタは正しいですか?
- ターゲットクラス名は正しいですか?
java.lang.VerifyError
これは変換されたバイトコードが無効であることを意味します。一般的な原因:
- 不正なバイトコード変更
- 型の不一致
- 無効なメソッドシグネチャ
変換後のパフォーマンス低下
変換が遅い場合:
- フックメソッドを簡素化
- フック内の高コストな操作を避ける
- 条件ロジックを使用して不要な作業をスキップ
- JVMプロファイラーでプロファイル
高度な質問
カスタムトランスフォーマーを作成できますか?
はい! トランスフォーマークラスを拡張するか、アノテーションの代わりにプログラマティックAPIを使用できます。
bytekinはメソッドのオーバーロードをサポートしていますか?
はい、パラメータ型と戻り値型を含む完全なメソッドディスクリプタを使用することでサポートしています。
同じクラスを複数回変換できますか?
はい、異なる変換を順次適用できます。
bytekinはスレッドセーフですか?
ビルド後、BytekinTransformer.transform()
はスレッドセーフで、複数のスレッドから同時に呼び出すことができます。
bytekinをJavaエージェントと一緒に使用できますか?
はい! bytekinはJavaエージェントとうまく動作します。エージェントのtransform()
メソッド内で使用します。
移行とアップグレードに関する質問
他のバイトコードツールから移行するにはどうすればよいですか?
概念は似ています:
- ターゲットクラスを定義
- 変換アノテーション付きフックメソッドを作成
- トランスフォーマーをビルド
- 変換を適用
コードを変更せずにbytekinをアップグレードできますか?
はい、bytekinは下位互換性を維持しています。アップグレード前に常にリリースノートを確認してください。
ライセンスと法的質問
bytekinはどのライセンスの下にありますか?
bytekinはApache License 2.0の下でライセンスされています。
bytekinを商用プロジェクトで使用できますか?
はい! Apache 2.0は商用利用を許可しています。
bytekinを使用する場合、コードをオープンソース化する必要がありますか?
いいえ、Apache 2.0はコードのオープンソース化を要求しません。ライセンス通知を含めるだけです。
コミュニティに関する質問
バグを報告するにはどうすればよいですか?
GitHub Issuesページでバグを報告してください。
どのように貢献できますか?
貢献は歓迎します! 貢献ガイドラインについてはGitHubリポジトリを参照してください。
どこで助けを得ることができますか?
- ドキュメントを確認
- GitHub Issuesを検索
- 例を確認
まだ質問がありますか?
質問がここで回答されていない場合:
次のステップ
- 例を探索する
- ベストプラクティスを確認する
- トラブルシューティングを確認する
トラブルシューティングガイド
このガイドは、bytekin使用時の一般的な問題を解決するのに役立ちます。
変換が適用されない
症状
- フックメソッドが呼び出されない
- 元のコードが変更なしで実行される
- フック内のブレークポイントが到達されない
原因と解決策
1. 不正なクラス名
@ModifyClass
の値はバイトコードのクラス名と正確に一致する必要があります。
問題:
@ModifyClass("Calculator") // 誤り!
public class CalcHooks { }
解決策:
@ModifyClass("com.example.Calculator") // 正しい
public class CalcHooks { }
確認方法:
# JAR内のすべてのクラスをリスト
jar tf myapp.jar | grep -i calculator
2. 間違ったメソッドディスクリプタ
methodDesc
はバイトコード内のメソッドシグネチャと正確に一致する必要があります。
問題:
// バイトコード内のメソッド: public int add(int a, int b)
@Inject(methodName = "add", methodDesc = "(int, int)int", at = At.HEAD) // 誤り!
public static CallbackInfo hook() { }
解決策:
@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD) // 正しい
public static CallbackInfo hook() { }
正しいディスクリプタの見つけ方:
# javapを使用してメソッドシグネチャを確認
javap -c com.example.Calculator | grep -A 5 "public int add"
3. フッククラスがBuilderに渡されていない
フッククラスはBuilderに渡す必要があります。
問題:
BytekinTransformer transformer = new BytekinTransformer.Builder()
.build(); // フックはどこ?
解決策:
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
.build(); // フッククラスを渡す
4. クラスがまだロードされていない
変換は、JVMがクラスをロードする前に適用する必要があります。
問題:
// クラスがすでにロード済み
Class<?> clazz = Class.forName("com.example.MyClass");
// 今変換しようとしている - 遅すぎる!
byte[] transformed = transformer.transform("com.example.MyClass", bytecode);
解決策:
- ロード中に変換を適用するカスタム
ClassLoader
を使用 - またはクラスロードをインターセプトするJava instrumentation/agentsを使用
型不一致エラー
症状
java.lang.ClassCastException
- メソッドから誤った値が返される
- 型の非互換性エラー
一般的な原因
1. CallbackInfoの間違った戻り値型
問題:
@Inject(methodName = "getCount", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo wrongReturn() {
CallbackInfo ci = new CallbackInfo();
ci.cancelled = true;
ci.returnValue = "42"; // intの代わりにString!
return ci;
}
解決策:
@Inject(methodName = "getCount", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo correctReturn() {
CallbackInfo ci = new CallbackInfo();
ci.cancelled = true;
ci.returnValue = 42; // 正しい: int
return ci;
}
2. フックメソッドの間違ったパラメータ型
問題:
// ターゲットメソッド: void process(int count, String name)
@Inject(methodName = "process", methodDesc = "(ILjava/lang/String;)V", at = At.HEAD)
public static CallbackInfo wrongParams(String name, int count) { // 逆!
return CallbackInfo.empty();
}
解決策:
@Inject(methodName = "process", methodDesc = "(ILjava/lang/String;)V", at = At.HEAD)
public static CallbackInfo correctParams(int count, String name) { // 正しい順序
return CallbackInfo.empty();
}
3. 引数を間違った型に変更
問題:
@Invoke(..., shift = Shift.BEFORE)
public static CallbackInfo wrongArgType() {
CallbackInfo ci = new CallbackInfo();
ci.modifyArgs = new Object[]{"100"}; // intの代わりにString
return ci;
}
解決策:
@Invoke(..., shift = Shift.BEFORE)
public static CallbackInfo correctArgType() {
CallbackInfo ci = new CallbackInfo();
ci.modifyArgs = new Object[]{100}; // 正しい: int
return ci;
}
ヌルポインタ例外
症状
- 変換中のNPE
- 変換されたメソッド呼び出し時のNPE
- バイトコードから発生するスタックトレース
原因と解決策
1. インジェクションからnullを返す
問題:
@Inject(methodName = "getValue", methodDesc = "()Ljava/lang/String;", at = At.HEAD)
public static CallbackInfo returnNull() {
CallbackInfo ci = new CallbackInfo();
ci.cancelled = true;
ci.returnValue = null; // オブジェクトには有効だが、期待されていない可能性
return ci;
}
解決策:
- nullが返される可能性があることを文書化
- または代わりにデフォルト値を返す:
ci.returnValue = ""; // nullの代わりに空文字列
2. フック内でnullパラメータにアクセス
問題:
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo unsafeAccess(String input) {
System.out.println(input.length()); // inputがnullの場合NPE!
return CallbackInfo.empty();
}
解決策:
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo safeAccess(String input) {
if (input != null) {
System.out.println(input.length());
}
return CallbackInfo.empty();
}
パフォーマンスの問題
症状
- アプリケーションの起動が遅い
- メモリ使用量が多い
- 応答時間が低下
原因と解決策
1. 複雑なフックメソッド
問題:
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo slowHook() {
// データベースクエリ
List<Item> items = database.queryAll();
// ファイルI/O
Files.write(Paths.get("log.txt"), data);
// 高コストな計算
// ...
return CallbackInfo.empty();
}
解決策:
- フックをシンプルで高速に保つ
- 高コストな作業をバックグラウンドスレッドに延期
- リソースの遅延初期化を使用
2. トランスフォーマーの繰り返しビルド
問題:
for (String className : classNames) {
// 各クラスごとに新しいトランスフォーマーを作成!
BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class)
.build();
transformer.transform(className, bytecode);
}
解決策:
// 1回ビルドして、何度も再利用
BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class)
.build();
for (String className : classNames) {
transformer.transform(className, bytecode);
}
3. 不要なクラスの変換
問題:
// 必要ない場合でも、すべてのクラスに変換を適用
for (String className : allClasses) {
byte[] transformed = transformer.transform(className, bytecode);
}
解決策:
- 必要な特定のクラスのみを変換
- フィルタリング/命名パターンを使用
- ホットスポットを特定するためにプロファイル
バイトコード検証エラー
症状
- クラスロード時の
java.lang.VerifyError
- 「Illegal type at offset X」エラー
- スタックトレースが解釈困難
一般的な原因
1. 無効なバイトコード変更
これは通常、変換が無効なバイトコードを作成したことを意味します。
デバッグ方法:
javap
を使用して変換されたバイトコードを検査- 異常な命令シーケンスを探す
- 戻り値型が一致することを確認
2. 不正なメソッドディスクリプタ
不正なディスクリプタは検証失敗を引き起こす可能性があります。
解決策:
- すべてのメソッドディスクリプタを再確認
- 確認のためにオンラインディスクリプタコンバータを使用
javap
出力と比較
メソッドが見つからない
症状
- 特定のメソッドが変換されていない
- オーバーロードされたメソッドが問題を引き起こす
- コンストラクタ変換が失敗
原因と解決策
1. オーバーロードされたメソッド
オーバーロードされたメソッドは完全なディスクリプタで区別する必要があります。
問題:
// クラスに複数のadd()メソッドがある
// add(int, int) と add(double, double)
@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD) // intバージョンのみに一致
public static CallbackInfo hook() { }
解決策:
- パラメータと戻り値型を含む完全なディスクリプタを使用
- ディスクリプタが自動的にオーバーロードを区別
2. プライベートまたは内部メソッド
一部のプライベートメソッドはアクセスできない可能性があります。
問題:
@Inject(methodName = "internalMethod", methodDesc = "()V", at = At.HEAD) // プライベートメソッド
public static CallbackInfo hook() { }
解決策:
- メソッドが合成またはブリッジメソッドでないことを確認
- メソッド名とディスクリプタが正確に正しいことを確認
変換されたクラスをロードできない
症状
- 変換後のClassNotFoundException
- クラスが見つからないようだ
- カスタムClassLoaderの問題
原因と解決策
1. 不正なClassLoaderセットアップ
問題:
// デフォルトのクラスローダーで変換されたバイトコードを使用しようとしている
byte[] transformed = transformer.transform("com.example.MyClass", bytecode);
Class<?> clazz = Class.forName("com.example.MyClass"); // 変換されたバイトコードを使用しない!
解決策:
- 変換されたバイトコードを使用するカスタムClassLoaderを作成
- またはinstrumentation/agentsを使用してロードをインターセプト
2. バイトコードの破損
変換が無効なバイトコードを生成した可能性があります。
解決策:
- 変換がバイトコードを破損していないことを確認
- バイトコードのサイズ/整合性を確認
- バイトコード検査ツールを使用
デバッグのヒント
1. 詳細な出力を有効にする
// フックにデバッグログを追加
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo debug() {
System.out.println("[DEBUG] Hook executed");
System.out.println("[DEBUG] Stack: " + Arrays.toString(Thread.currentThread().getStackTrace()));
return CallbackInfo.empty();
}
2. バイトコードを検査
# 変換されたバイトコードを表示
javap -c -private TransformedClass.class
# インジェクトされた呼び出しを探す
3. バイトコードビューアを使用
Bytecode ViewerやIDEAプラグインなどのツールがバイトコードの可視化に役立ちます。
4. パフォーマンスをプロファイル
# JProfilerまたはYourKitを使用してボトルネックを特定
# メモリ使用量とCPU時間を監視
よくある質問
Q: ブートストラップクラスを変換できますか? A: 標準のクラスローダーでは簡単ではありません。instrumentation APIでJavaエージェントを使用してください。
Q: 変換はシリアライゼーションに影響しますか? A: 変換されたクラスは異なるバイトコードを持ちますが、フィールドを変更しなければ同じシリアライゼーション形式です。
Q: bytekinをSpring Bootで使用できますか? A: はい、ただしカスタムクラスロードを設定するか、エージェントを使用する必要があります。