こんにちは。 キャスレーコンサルティング SI(システム・インテグレーション)部の満石です。

今回は、バイトコードを操作することでEnumの挙動を変える方法を簡単な例で紹介します。

予めお断りしておきますが、バイトコードを操作してEnumを書き換えることは基本的におすすめできません。
なぜなら、ソースコードから読み解けるものとは違う挙動をさせることになり、特に保守の観点でリスクが大きいからです。

しかし、大量のコード定義用のEnumが存在しているけれど、それをリリース無しで書き換える必要があるような
特殊な条件の場合に有効な手段になることもあります。

書き換える内容

以前、再帰的ジェネリクスを活用したJava8時代のコード定義Enum用インタフェースを作成してみたで作成した、F1Constructorを書き換え対象のEnumとします。

書き換え内容は2017年第4戦終了時のコンストラクターズポイントランキング順を持つ状態から
2017年第11戦終了時のコンストラクターズポイントランキング順への変更です。

とは言っても、ザウバーが9位、マクラーレンが10位から逆転して、
9位がマクラーレン、10位がザウバーに入れ替わるだけです。

実行環境

今回はバイトコード操作ライブラリとして、Javassistを使います。
バージョンは現時点での最新の安定バージョンである、3.21.0-GAです。
Javaのバージョンも最新の1.8.0_144で実行します。

バイトコードを書き換えるプログラムの作成

バイトコードを書き換えるプログラムは、書き換え対象となるプログラムとは別物(別のjarファイル)として
作成する必要があります。

なぜなら、バイトコードを書き換えるのはクラスがロードされる前に限られているからです。
クラスがロードされた後に書き換えようとすると、java.lang.LinkageErrorが発生します。

そのため、mainメソッドよりも前に実行される、premainというメソッドを実装したクラスを作成する必要があります。

premainメソッドの宣言は

public static void premain(String agentArgs, Instrumentation inst)

と決められています。

Javaにpremainなんてメソッドがあるのか!と驚く人いると思います。
実はJava SE 5から使えるようになっているので、もう使えるようになってから10年以上経っています。

このpremainメソッドの第2引数にある、Instrumentationはjava.lang.instrumentパッケージで定義されている
インタフェースです。

このInstrumentationに宣言されているメソッドの中にaddTransformerというメソッドがあり、
引数にはClassFileTransformerという、今回の目的にそのものズバリな名前のインタフェースを渡すようになっています。

ClassFileTransformerインタフェースにはただ1つtransformというメソッドが宣言されていて、
今回はこのメソッドの中身をJavassistを使用しながら実装することになります。

では、実装したコード(EnumRewritingクラス)を見てみましょう。

package com.example;

import javassist.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;

public class EnumRewriting {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(
                (loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
            // クラスがcom.example.F1Constructorの場合だけバイトコード書き換えを実行
            if (className.equals("com/example/F1Constructor")) {
                // デフォルトのクラスパスを持つClassPoolオブジェクトを取得
                ClassPool classPool = ClassPool.getDefault();
                // クラスファイルがバイト配列として読み込まれたものをByteArrayInputStreamにする
                ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer);
                try {
                    // クラスを作成、つまりcom.example.F1Constructorを作成
                    CtClass ctClass = classPool.makeClass(stream);
                    // staticイニシャライザのブロックを取得
                    CtConstructor ctConstructor = ctClass.getClassInitializer();
                    // マクラーレンの順序を9に設定
                    ctConstructor.insertAfter("MCLAREN = new com.example.F1Constructor(\"MCLAREN\", 3, \"04\", \"マクラーレン\", 9);");
                    // ザウバーの順序を10に設定
                    ctConstructor.insertAfter("SAUBER = new com.example.F1Constructor(\"SAUBER\", 7, \"08\", \"ザウバー\", 10);");
                    // $VALUESを上書き
                    ctConstructor.insertAfter("$VALUES = new com.example.F1Constructor[] { FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS };");
                    // 書き換えたcom.example.F1Constructorをバイトコードに変換して、Metaspaceに読み込ませる
                    return ctClass.toBytecode();
                } catch (IOException | CannotCompileException e) {
                    // 上記例外発生時にスタックトレースを出力する。これを書いておかないと例外発生時にコンソールに何も出力されない
                    e.printStackTrace();
                    IllegalClassFormatException illegalClassFormatException = new IllegalClassFormatException();
                    illegalClassFormatException.initCause(e);
                    // 例外はIllegalClassFormatExceptionをスローするよう決められているが、挙動はnullをreturnした場合と同じで、コンソールには何も出力されない
                    throw illegalClassFormatException;
                }
            }
            // バイトコードを書き換えない場合はnullをreturnする決まりになっている
            return null;
        });
    }
}

各行がそれぞれどのような処理をしているかは上記のコード内のコメントを見ていただくとして、
どうして、このような実装になるのかをcom.example.F1Constructorを逆コンパイルしたコードで解説します。

バイトコード書き換え前後の姿

まず、書き換え対象となるcom.example.F1Constructorのソースコードがこれです。

package com.example;

/**
 * F1コンストラクター
 */
public enum F1Constructor implements CodeEnum<F1Constructor> {
    FERRARI("01", "フェラーリ", 2),
    FORCE_INDIA("02", "フォース・インディア", 4),
    HAAS("03", "ハース", 7),
    MCLAREN("04", "マクラーレン", 10),
    MERCEDES("05", "メルセデス", 1),
    RED_BULL("06", "レッドブル", 3),
    RENAULT("07", "ルノー", 8),
    SAUBER("08", "ザウバー", 9),
    TORO_ROSSO("09", "トロ・ロッソ", 6),
    WILLIAMS("10", "ウィリアムズ", 5);

    private String code;

    private String name;

    private int order;

    F1Constructor(String code, String name, int order) {
        this.code = code;
        this.name = name;
        this.order = order;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getOrder() {
        return order;
    }
}

最初に説明したとおり、ザウバーのorderが9、マクラーレンのorderには10が設定されています。

上記ソースコードをコンパイルしたら作成されるクラスファイルを逆コンパイルしたのが以下のコードになります。
逆コンパイルにはjadコマンドを利用しました。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   F1Constructor.java

package com.example;

// Referenced classes of package com.example:
//            CodeEnum

public final class F1Constructor extends Enum
    implements CodeEnum
{

    public static F1Constructor[] values()
    {
        return (F1Constructor[])$VALUES.clone();
    }

    public static F1Constructor valueOf(String name)
    {
        return (F1Constructor)Enum.valueOf(com/example/F1Constructor, name);
    }

    private F1Constructor(String s, int i, String code, String name, int order)
    {
        super(s, i);
        this.code = code;
        this.name = name;
        this.order = order;
    }

    public String getCode()
    {
        return code;
    }

    public String getName()
    {
        return name;
    }

    public int getOrder()
    {
        return order;
    }

    public static final F1Constructor FERRARI;
    public static final F1Constructor FORCE_INDIA;
    public static final F1Constructor HAAS;
    public static final F1Constructor MCLAREN;
    public static final F1Constructor MERCEDES;
    public static final F1Constructor RED_BULL;
    public static final F1Constructor RENAULT;
    public static final F1Constructor SAUBER;
    public static final F1Constructor TORO_ROSSO;
    public static final F1Constructor WILLIAMS;
    private String code;
    private String name;
    private int order;
    private static final F1Constructor $VALUES[];

    static
    {
        FERRARI = new F1Constructor("FERRARI", 0, "01", "\u30D5\u30A7\u30E9\u30FC\u30EA", 2);
        FORCE_INDIA = new F1Constructor("FORCE_INDIA", 1, "02", "\u30D5\u30A9\u30FC\u30B9\u30FB\u30A4\u30F3\u30C7\u30A3\u30A2", 4);
        HAAS = new F1Constructor("HAAS", 2, "03", "\u30CF\u30FC\u30B9", 7);
        MCLAREN = new F1Constructor("MCLAREN", 3, "04", "\u30DE\u30AF\u30E9\u30FC\u30EC\u30F3", 10);
        MERCEDES = new F1Constructor("MERCEDES", 4, "05", "\u30E1\u30EB\u30BB\u30C7\u30B9", 1);
        RED_BULL = new F1Constructor("RED_BULL", 5, "06", "\u30EC\u30C3\u30C9\u30D6\u30EB", 3);
        RENAULT = new F1Constructor("RENAULT", 6, "07", "\u30EB\u30CE\u30FC", 8);
        SAUBER = new F1Constructor("SAUBER", 7, "08", "\u30B6\u30A6\u30D0\u30FC", 9);
        TORO_ROSSO = new F1Constructor("TORO_ROSSO", 8, "09", "\u30C8\u30ED\u30FB\u30ED\u30C3\u30BD", 6);
        WILLIAMS = new F1Constructor("WILLIAMS", 9, "10", "\u30A6\u30A3\u30EA\u30A2\u30E0\u30BA", 5);
        $VALUES = (new F1Constructor[] {
            FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS
        });
    }
}

今回の書き換え処理でやっていることをこの逆コンパイルしたコードで表すと以下のようになります。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   F1Constructor.java

package com.example;

// Referenced classes of package com.example:
//            CodeEnum

public final class F1Constructor extends Enum
    implements CodeEnum
{

    public static F1Constructor[] values()
    {
        return (F1Constructor[])$VALUES.clone();
    }

    public static F1Constructor valueOf(String name)
    {
        return (F1Constructor)Enum.valueOf(com/example/F1Constructor, name);
    }

    private F1Constructor(String s, int i, String code, String name, int order)
    {
        super(s, i);
        this.code = code;
        this.name = name;
        this.order = order;
    }

    public String getCode()
    {
        return code;
    }

    public String getName()
    {
        return name;
    }

    public int getOrder()
    {
        return order;
    }

    public static final F1Constructor FERRARI;
    public static final F1Constructor FORCE_INDIA;
    public static final F1Constructor HAAS;
    public static final F1Constructor MCLAREN;
    public static final F1Constructor MERCEDES;
    public static final F1Constructor RED_BULL;
    public static final F1Constructor RENAULT;
    public static final F1Constructor SAUBER;
    public static final F1Constructor TORO_ROSSO;
    public static final F1Constructor WILLIAMS;
    private String code;
    private String name;
    private int order;
    private static final F1Constructor $VALUES[];

    static
    {
        FERRARI = new F1Constructor("FERRARI", 0, "01", "\u30D5\u30A7\u30E9\u30FC\u30EA", 2);
        FORCE_INDIA = new F1Constructor("FORCE_INDIA", 1, "02", "\u30D5\u30A9\u30FC\u30B9\u30FB\u30A4\u30F3\u30C7\u30A3\u30A2", 4);
        HAAS = new F1Constructor("HAAS", 2, "03", "\u30CF\u30FC\u30B9", 7);
        MCLAREN = new F1Constructor("MCLAREN", 3, "04", "\u30DE\u30AF\u30E9\u30FC\u30EC\u30F3", 10);
        MERCEDES = new F1Constructor("MERCEDES", 4, "05", "\u30E1\u30EB\u30BB\u30C7\u30B9", 1);
        RED_BULL = new F1Constructor("RED_BULL", 5, "06", "\u30EC\u30C3\u30C9\u30D6\u30EB", 3);
        RENAULT = new F1Constructor("RENAULT", 6, "07", "\u30EB\u30CE\u30FC", 8);
        SAUBER = new F1Constructor("SAUBER", 7, "08", "\u30B6\u30A6\u30D0\u30FC", 9);
        TORO_ROSSO = new F1Constructor("TORO_ROSSO", 8, "09", "\u30C8\u30ED\u30FB\u30ED\u30C3\u30BD", 6);
        WILLIAMS = new F1Constructor("WILLIAMS", 9, "10", "\u30A6\u30A3\u30EA\u30A2\u30E0\u30BA", 5);
        $VALUES = (new F1Constructor[] {
            FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS
        });
        MCLAREN = new F1Constructor("MCLAREN", 3, "04", "\u30DE\u30AF\u30E9\u30FC\u30EC\u30F3", 9);
        SAUBER = new F1Constructor("SAUBER", 7, "08", "\u30B6\u30A6\u30D0\u30FC", 10);
        $VALUES = new F1Constructor[] { FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS };
    }
}

staticイニシャライザブロックの最後にctConstructor.insertAfterメソッドで追加した3行が入ることで、
定数を上書きされた結果挙動が変わることがご理解いただけると思います。

バイトコードを書き換えるとは言っても、Javassitを使うと人間に簡単に理解できることコードで
あっさり実現できてしまうことに拍子抜けした方もいるのではないでしょうか?

ビルドと実行

バイトコードを書き換えるプログラム(EnumRewritingクラス)はjarファイルとしてビルドする必要があります。
今回はGradleでビルドすることにしたため、build.gradleファイルを作成しました。

group 'javassist-sample'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'javassist', name: 'javassist', version: '3.12.1.GA'
}

jar {
    manifest {
        attributes 'Premain-Class': 'com.example.EnumRewriting', 'Boot-Class-Path': 'javassist-3.12.1.GA.jar', 'Can-Redefine-Classes': 'true'
    }
}

dependenciesには必要なライブラリとしてjavassistを指定しています。

バイトコード書き換えを行う場合、jarファイルの中に以下の3項目を書いたマニフェストファイルが必要で、
上記jar.manifestブロック内はそのための記述です。

  • Premain-Class:この項目のみ設定必須。バイトコード操作を行うクラスを指定するので、
    今回はcom.example.EnumRewritingになる。
  • Boot-Class-Path:バイトコード操作に必要なライブラリやクラスを記述する。
    今回はjavassistのjarファイル名になる。
  • Can-Redefine-Classes:バイトコードを操作してクラスの再定義を許すかどうかを指定するため、
    今回はtrueになる。

ビルドして出来上がるjarファイルは、javassist-sample-1.0-SNAPSHOT.jarになります。

mainメソッドを実装したクラスとして以下のMainクラスを作成しました。
F1Constructorの列挙子を表示順にソートした結果を出力するだけのシンプルな実装です。

package com.example;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<F1Constructor> orderedList = CodeEnum.getOrderedList(F1Constructor.class);
        orderedList.forEach(e -> System.out.println(e.getName()));
    }
}

上記Mainクラスをまずは単純に以下のコマンドで実行してみます。

java com.example.Main

結果は

メルセデス
フェラーリ
レッドブル
フォース・インディア
ウィリアムズ
トロ・ロッソ
ハース
ルノー
ザウバー
マクラーレン

になり、実装どおりマクラーレンよりザウバーが上に来ています。

今度はEnumを書き換えるため、javaコマンドを実行するディレクトリに先ほどビルドした
javassist-sample-1.0-SNAPSHOT.jarと、javassist-3.12.1.GA.jarも置いて、
以下のコマンドを実行します。

バイトコード書き換えはmainメソッドが動く前に動作させる必要があるため、作成したjarファイルは
-javaagentオプションで指定する必要があります。

java -javaagent:javassist-sample-1.0-SNAPSHOT.jar com.example.Main

結果は、

メルセデス
フェラーリ
レッドブル
フォース・インディア
ウィリアムズ
トロ・ロッソ
ハース
ルノー
マクラーレン
ザウバー

となり、無事にマクラーレンとザウバーの順序が入れ替わりました!

まとめ

果たしてこの記事が誰かの役に立つことはあるのか!?と書いている本人が思ってしまうくらい
マニアックな内容になりましたが、Javaってこんなこともできるんだ・・・!と楽しんでいただけたなら幸いです。

再度、念押ししておきますが、このようなバイトコード書き換えはやらずに済むならその方がいいです。
通常のプログラムよりかなりデバッグしづらく、思ったように動かない場合は相当難航します。
バイトコード書き換えは慎重に。