Javassistでランタイムでクラスを書き換える

イケてないクローズドなライブラリを使う際にどうしても現在のシステムと合わない箇所があり、なんとかクローズドなライブラリに手を入れられないか?ということでJavassistでランタイムでクラスを書き換えて対応しようというものです。

unk極まりないですが、にっちもさっちもいかない場合に使えるのと、自分も滅多に使わないのでまとめようと思います。

前提

コード例

コード簡略化のため、コマンドから実行するソースとしていますが、同様のコードでアプリケーションサーバ上でも動くはずです。

設計上の注意点として既にクラスローダーに読み込まれてしまったクラスは変更できないため、読み込まれる前に書き換えを行う必要があります。 ですのでWEBアプリであればListenerなど、とにかくデプロイの早い段階で読み込むことを保障することが重要です。

github.com

import javassist.*;

public class Sample {

    public static void main(String... args) throws NotFoundException, CannotCompileException {


        final ClassPool classPool = ClassPool.getDefault();

        // Webアプリではクラスローダーが複数あるので対象のクラスが存在するクラスローダーを
        // ClassPoolに登録しないとうまく動作しない可能性があるとのこと。
        // WARのクラスローダーが欲しいので自分のクラスのローダーを取得して登録している
        // http://jboss-javassist.github.io/javassist/tutorial/tutorial.html
        classPool.insertClassPath(new ClassClassPath(Sample.class));

        CtClass simpleLoggerClass = classPool.get("SimpleLogger");
        // 変更済みなら終える(一度変更済みだと再度の変更はできない)
        if (simpleLoggerClass.isModified()) {
            return;
        }

        // 全コンストラクタを削除
        final CtConstructor[] constructors = simpleLoggerClass.getDeclaredConstructors();
        for (CtConstructor constructor : constructors) {
            simpleLoggerClass.removeConstructor(constructor);
        }

        // デフォルトコンストラクタを生成して追加
        // デフォルトコンストラクタ1つは作っておかないとnewできない
        simpleLoggerClass.addConstructor(CtNewConstructor.defaultConstructor(simpleLoggerClass));


        CtMethod methods = simpleLoggerClass.getDeclaredMethod("log");

        // ここらへん参考に置き換えるソースを記述
        // http://jboss-javassist.github.io/javassist/tutorial/tutorial2.html#alter
        String newSrc = "{"
                + "System.out.println(\"[modified]\" + $1);"
                + "}";

        methods.setBody(newSrc);

        // 読み込みの時と同様の理由で変更後クラスを登録する際にもクラスローダーを指定してあげる
        // http://jboss-javassist.github.io/javassist/tutorial/tutorial.html#load
        simpleLoggerClass.toClass(Sample.class.getClassLoader(), null);

        final SimpleLogger simpleLogger = new SimpleLogger();

        simpleLogger.log("test");
    }
}
import java.util.logging.Logger;

public class SimpleLogger {

    private Logger logger;

    // 消される
    public SimpleLogger() {
        logger = Logger.getLogger(this.getClass().getName());
        logger.info("create SimpleLogger");
    }

    // 標準出力への出力に変更される
    public void log(String text) {
        logger.info(text);
    }
}

実行結果です。以下2点が確認できます。

  • コンストラクタによるログの出力がない
  • logメソッドによるログ出力が変更されている
~/git/javassistsample % ./gradlew run                                                                                                                       (git)-[master]
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
[modified]test

BUILD SUCCESSFUL

Total time: 6.625 secs

This build could be faster, please consider using the Gradle Daemon: https://docs.gradle.org/2.13/userguide/gradle_daemon.html