ベストプラクティス
このガイドは、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;
}