【Java】クラス内で参照されているクラスの一覧を取得

2018.07.09

投稿者 :fuji

カテゴリ: プログラミング

一昔前にJavaの世界ではAOP(アスペクト指向プログラミング)が非常に流行りました。
もちろん、現在においても廃れたという事では無くSpringFrameworkを初めとして今でも多くのフレームワークで稼働し続けている技術ではあります。

AOP黎明期には業務でSpringFrameworkやSeasar2など幾つかのフレームワークを使い、共通基盤の構築など行ってきましたが、
「バイトコードを置き換えている」「バイトコード変換が出来る幾つかのライブラリが存在している」という事くらいは知っていたものの、それらのライブラリを直接扱う機会はありませんでした。

その後、早いもので10年ほど。
ようやくバイトコードコード置き換えライブラリを直接扱いたい場面に遭遇しました。

とある理由により、Javaソースの中で参照されている全てのクラスを「確実に」知りたい、
といった状況になりました。

例えば下記のようなクラスです。

package refclass;
import java.util.ArrayList;

public class TestCode {
    public static void main(String[] args) {

	new ArrayList<Object>().stream();

    }
}

このクラスが実際に利用しているクラスは
宣言部においてはmain引数のString, 暗黙のextendsのObject
そして処理部においては
java.util.ArrayListと
java.util.stream.Streamです。

Streamインタフェースについてはimport文に宣言されていない訳ですし、変数で受け取っている訳でも無いのでソースの中に型が記載される事はありません。
つまり、文字列としての構文解析のみで今回知りたい情報を得るのは、かなり困難という事ですね。

という事で、クラスバイナリの中身を覗き込む事で実現してみます。

ライブラリはJavassistを使います。
package refclass;
import javassist.ClassPool;
import javassist.CtClass;

public class RefClassCheck {

    public static void main(String[] args) throws Exception {
        
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("refclass.TestCode");
        ctClass.getRefClasses().stream().forEach(System.out::println);
        
    }
    
}

このコードにより、下記のような結果を得る事が出来ました。
java.util.stream.Stream
java.lang.Object
java.lang.String
refclass.TestCode
java.util.ArrayList
ArrayListだけでなくjava.util.stream.Streamがしっかり取れていますね。

Javassistではメソッドの前後に任意の処理を差し込む事も可能ですし、mainよりも先に実行されるpremain処理(Instrumentation)と組み合わせる事で、classファイルがロードされた(される)タイミングでこれらの処理を行う事も可能です。

尚、こういったバイトコード操作における定番?的な使い方としては下記のようにメソッドの前後に処理を挿入する事です。
package refclass;

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

public class InsertBeforeTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("refclass.Example");
        
        CtMethod method = ctClass.getDeclaredMethod("test1");
        method.insertBefore("System.out.println(\"挿入されたコード。\");");
        method.insertAfter("return -9;");
        ctClass.toClass();
        
        Example example= new Example();
        System.out.println(example.test1());
    }
}
class Example {
    public int test1() {
        System.out.println("test1");
        return 1;
    }
}

このコードでは、下記のような結果が出力されます。
実行してみるまでreturnまわりの挙動が気になっていましたが、Example#test1のreturn 1;が完全に無かった事になっています。
挿入されたコード。
test1
-9


正直、あまり開発業務でこのようなライブラリを直接利用する場面は無さそうな(というか、利用すべきでは無い)気もしますが、
ネタとして知っておくと数年に一回くらい役には立つかもしれません。