Javaのコードをフックする。

ここではJavaのバイトコード操作ライブラリ、Javassistとjava.lang.instrumentのpremainの仕組みを使用して、Javaアプリケーションにフックを設置する方法を、サンプルを交えながら解説します。

もっと短く言うと、Javaアプリに追加の処理を差し込んでみよう!という趣旨の記事です。

1.フックを設置するJavaアプリ

/* ファイル名:HelloWorld.java */

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class HelloWorld {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame();
            frame.add(new JLabel("Hello World!"));
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.setSize(200, 100);
            frame.setVisible(true);
        });
    }
}

実行すると「Hello World!」というウィンドウを表示するだけのシンプルなアプリです。このウィンドウに表示される「Hello World!」の後ろに「 by JavaHook」という文字列を付け足すように、処理を差し込んでみたいと思います。

C:\>javac HelloWorld.java
C:\>java HelloWorld

画像1

2.Javassistのダウンロード

Javassist(javassist.jar)はGitHubから最新バージョンを、HelloWorld.javaと同じ場所にダウンロードします。ただし最終的にすべてをひとつのJarファイルにまとめる予定なので、ファイル名はjavahook.jarに変更します。

3.フックを設置する準備

まずはフックを設置する場所の検討が必要になります。「Hello World!」という文字列はjavax.swing.JLabelのコンストラクタに渡されています。そしてこのコンストラクタは、内部で渡された文字列をそのままsetText​(String text)に渡しています。

よってフックは、javax.swing.JLabel#setText​(String text)に設置することにします。具体的にはjavaagentのpremainで変換処理を登録しておき、実際にクラスをロードする時にJavassistによるバイトコードの変換処理を走らせるようにします。

/* ファイル名:JavaHook.java */

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;


public class JavaHook implements ClassFileTransformer {
   public static void premain(String agentArgs,
                              Instrumentation inst) throws Exception {
       inst.addTransformer(new JavaHook());  // 変換処理の登録
   }

   public byte[] transform(ClassLoader loader,
                           String className,
                           Class<?> classBeingRedefined,
                           ProtectionDomain protectionDomain,
                           byte[] classfileBuffer) throws IllegalClassFormatException {

       if (className != null && className.equals("javax/swing/JLabel")) {
           // 変換処理の実行
           try {
               ClassPool classpool = ClassPool.getDefault();

               // バイトコードからCtClassオブジェクトを生成
               CtClass clazz = classpool.makeClass(new ByteArrayInputStream(classfileBuffer));

               // CtClassオブジェクトからsetTextのCtMethodオブジェクトを取り出し
               CtClass[] args = classpool.get(new String[] {"java.lang.String"});
               CtMethod method = clazz.getDeclaredMethod("setText", args);

               // setTextへの処理の差し込み:$1は1番目の引数という意味
               // 参考URL:https://www.javassist.org/tutorial/tutorial2.html#before
               method.insertBefore("$1 += \" by JavaHook\";");

               // 変換後のバイトコードを生成
               return clazz.toBytecode();
           } catch (Exception e) {
               return classfileBuffer;
           }
       } else {
           return classfileBuffer;
       }
   }
}

加えてpremainを実行するためのマニュフェストファイル(ファイル名:javahook.mf)も準備します。

Premain-Class: JavaHook

最後にこれらをjavahook.jarに追加します。

C:\>javac -cp javahook.jar JavaHook.java
C:\>jar umf javahook.mf javahook.jar JavaHook.class

ここまで、いろいろと複雑そうに見えますが、やっていることは、

a) JavaHook#premain()をHelloWorld#main()よりも先に実行するための準備
b) javax.swing.JLabel#setText()に追加の処理を差し込むための準備

の2点だけです。

4.フックを設置した状態での実行

C:\>java -javaagent:javahook.jar HelloWorld

-javaagent:javahook.jarというオプションをJavaコマンドへ渡す、もしくはJAVA_TOOL_OPTIONSという環境変数にその-javaagent:javahook.jarを設定した状態でHelloWorldを実行すると、

C:\>set JAVA_TOOL_OPTIONS=-javaagent:javahook.jar
C:\>java HelloWorld

「 by JavaHook」が付け足されたウィンドウが表示されます。

画像2

5.あとがき

ここに書いたセオリーを理解し、そしてそのキモとなるJavassistの使い方をマスターすれば、既存のJavaアプリをJAVA_TOOL_OPTIONSという環境変数を通して、自在にフック・アンフックできるようになります。

この技術のさらなる広がりに興味があるなら、Javassistのチュートリアルに一度目を通すことをおすすめします。3ページなので、そんなに量はありません。

ご健勝をお祈りします。

Let's きっちり納税。noteでの収益を励みに、皆さんへ有益な情報を届けます!