読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

finalなフィールドの値をUnsafeで無理やり書き換える

Java

[2014-03-25追記]
インスタンスフィールドなら、finalであってもリフレクションで変更できました。
以下の話は、static finalなフィールド限定です。



昔はどうだったか知りませんが、少なくともJava7ではリフレクションを駆使してもstatic finalフィールドの値を書き換えることができません。

例えば、

public class Foo {

    static class Sample {
        static final List list = Arrays.asList("foo");
    }

    public static void main(final String... args) throws Exception {
        final Field f = Sample.class.getDeclaredField("list");

        f.set(null, Arrays.asList("bar"));

        System.out.println(f.get(null));
    }
}

のようなコードは、

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final java.util.List field foo.Foo$Sample.list to java.util.Arrays$ArrayList
	at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(
UnsafeFieldAccessorImpl.java:73)
	at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(
UnsafeFieldAccessorImpl.java:77)
	at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(
UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
	at java.lang.reflect.Field.set(Field.java:741)
	at foo.Foo.main(Foo.java:26)

と怒られるだけ。


そもそも何でstatic finalフィールドを書き換えたいかと言うと、
とあるOSSライブラリのパフォーマンスチューニングをしていて、
どうしても書き換えたいprivate static finalなフィールドが出てきたため。

ソースまるごとコピーして書き換えるというのは余りやりたくなかったので、
どうにか他の手段がないか探しました。

結論:Unsafeを使う

出オチ感満点で恐縮ですが、他に良い手段がなかったのでUnsafeを使うことにしました。
「Unsafeって何?」な方はどうぞググって頂ければと思いますが、
要するにヒープメモリを直接いじくり回すネイティブメソッド群です。


もちろん、Javassistバイトコードを動的に書き換える方法でも良いのですが、
アプリケーション起動時に気をつけなければならないことが増える(特に、クラスローディングの挙動)ので止めました。


Unsafeはヒープを直接操作します。従って使い方を間違えると即、VMクラッシュになります。
VMクラッシュ時のリカバリ用スクリプトの試験に向いているかもw

実装

さて、Unsafeを直接使うとEclipse様が激おこプンプン丸なので
ファサード経由で使うようにしました。まあこんな感じ。

/* PUBLIC DOMAIN */
final class FinalFieldSetter {

    private static final FinalFieldSetter INSTANCE;

    static {
        try {
            INSTANCE = new FinalFieldSetter();
        } catch (final ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final Object unsafeObj;

    private final Method putObjectMethod;

    private final Method objectFieldOffsetMethod;

    private final Method staticFieldOffsetMethod;

    private final Method staticFieldBaseMethod;

    private FinalFieldSetter() throws ReflectiveOperationException {

        final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");

        final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);

        this.unsafeObj = unsafeField.get(null);

        this.putObjectMethod = unsafeClass.getMethod("putObject", Object.class,
            long.class, Object.class);
        this.objectFieldOffsetMethod = unsafeClass.getMethod("objectFieldOffset",
            Field.class);
        this.staticFieldOffsetMethod = unsafeClass.getMethod("staticFieldOffset",
            Field.class);
        this.staticFieldBaseMethod = unsafeClass.getMethod("staticFieldBase",
            Field.class);
    }

    public static FinalFieldSetter getInstance() {
        return INSTANCE;
    }

    public void set(final Object o, final Field field, final Object value) throws Exception {

        final Object fieldBase = o;
        final long fieldOffset = (long) this.objectFieldOffsetMethod.invoke(
            this.unsafeObj, field);

        this.putObjectMethod.invoke(this.unsafeObj, fieldBase, fieldOffset, value);
    }

    public void setStatic(final Field field, final Object value) throws Exception {

        final Object fieldBase = this.staticFieldBaseMethod.invoke(this.unsafeObj, field);
        final long fieldOffset = (long) this.staticFieldOffsetMethod.invoke(
            this.unsafeObj, field);

        this.putObjectMethod.invoke(this.unsafeObj, fieldBase, fieldOffset, value);
    }
}


これを使うと、無事

public class Foo {

    static class Sample {
        static final List list = Arrays.asList("foo");
    }

    public static void main(final String... args) throws Exception {
        System.out.println(Sample.list); // 注:staticフィールドを初期化するために必要

        final Field f = Sample.class.getDeclaredField("list");
        FinalFieldSetter.getInstance().setStatic(f, Arrays.asList("bar"));

        System.out.println(Sample.list);
    }
}

がエラー無く動くわけです。めでたしめでたし。

Unsafeクラスの使い方は解説するのが面倒なので、各自Javadocを参照して下さい。手抜き。
まあ思いっきりメモリ操作風なAPIです。ほぼC。

Javadocはどこにある?

Unsafeは非公開・非推奨APIなのでOracleからの公式なJavadocはリリースされていませんが、例えば

http://www.docjar.com/docs/api/sun/misc/Unsafe.html

あたりにブツがあります。OracleJDKではなくてOpenJDKのものですが、まあ同じです。