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
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」が付け足されたウィンドウが表示されます。
5.あとがき
ここに書いたセオリーを理解し、そしてそのキモとなるJavassistの使い方をマスターすれば、既存のJavaアプリをJAVA_TOOL_OPTIONSという環境変数を通して、自在にフック・アンフックできるようになります。
この技術のさらなる広がりに興味があるなら、Javassistのチュートリアルに一度目を通すことをおすすめします。3ページなので、そんなに量はありません。
ご健勝をお祈りします。
Let's きっちり納税。noteでの収益を励みに、皆さんへ有益な情報を届けます!