きどたかのブログ

いつか誰かがこのブログからトラブルを解決しますように。

jmockitのDeencapsulationでstatic finalを書き換えできるの?それは使ってるバージョンによる!

今、迷っている。
書き換えられるっぽい過去の経験、書き換えられない最近の経験が混ざっている。


よし、確かめよう。
ダラダラ書く。

Deencapsulationのコードをgithubで読んだ。(もちろんコール先も読んだ)
https://github.com/jmockit/jmockit1/blob/master/main/src/mockit/Deencapsulation.java
static finalを書き換える特殊なコードは書かれていないようだった。
実際に、自宅の環境では書き換えに失敗している。


自分は、こういうことを予想していた。
modifiersの書き換えをしているんじゃないだろうか。
一旦finalを外して書き込むとか。
ところがそういうのをやっていないようだった。
過去の経験はまやかしか?


実際に、modifiers書き換えによるstatic finalフィールドへの書き込み例はネットでも見受けられる。
final修飾子を除去するコードを書いて試している。
だけども、この方法は完璧ではないと感じたことがあるので書いてみる。

①そもそも正攻法ではない。
Fieldオブジェクトのmodifiersのフィールド名称は実装に依存するから、正式な方法とはいえない。
誰でもそう感じるだろう。

②書き換えた内容が残らない(Fieldオブジェクトのmodifiersの話)
Fieldオブジェクトのmodifiersを変更したとしても、
ClassオブジェクトからgetDeclaredFieldし直すと、
そのmodifiersは本来の値に戻っている。
これはFieldオブジェクトが別物なんだから多少納得がいくだろう。
ちなみにFieldクラスのequalsメソッドはmodifiersを評価していないようだ。

③modifiersの一貫性が損なわれる(FieldオブジェクトのoverrideFieldAccessorの話)
次の例ではまた別のことを考える必要がある。
getDeclaredFieldを叩いたのちに、setAcessible(true)して、setに失敗しているコードがまず存在していて、
その後、取り直した別のFieldオブジェクトに対してmodifiers書き換え版のコードでsetを試みるとしよう。
この場合、書き換えは失敗する。

これの怖くて面白いところは、Fieldオブジェクトそのものが返すmodifiersと、
内部のaccessorが持つmodifiers関連変数の一貫性がなくなることだ。

逆にmodifierを書き換えるコードを先に成功させてしまえば、accessorは書き換え可能な状態で残る。
accessorについては、2度目のgetDeclareFieldでも同じインスタンスが設定されているようだ。

ここで言っているaccessorというのはsun.reflect.UnsafeFieldAccessorImpl系の話をしている。
内部で型に応じたUnsafeFieldAccessorImplのサブクラスが作られている。
setするときは、overrideFieldAccessorフィールドに置かれるっぽい。
final付きのFieldの場合とそうではない場合で、setされるaccessorは異なる。
isFinal変数を持つクラスと、isReadOnlyを持つクラスがいて、
isReadOnlyをfalseに書き換えるとfinal修飾子が付いたまま書き換えることができるようになる。

  1. isFinal : abstract class sun.reflect.UnsafeFieldAccessorImpl extends sun.reflect.FieldAccessorImpl
  2. abstract class sun.reflect.UnsafeStaticFieldAccessorImpl extends sun.reflect.UnsafeFieldAccessorImpl
  3. isReadOnly : abstract class sun.reflect.UnsafeQualifiedStaticFieldAccessorImpl extends sun.reflect.UnsafeStaticFieldAccessorImpl
  4. : class sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl extends sun.reflect.UnsafeQualifiedStaticFieldAccessorImpl

ちなみに、先にmodifiersを書き換えてfinalがついてない場合は、
isReadOnlyを持つクラスは使われていないようだ。(Qualifiedが付かないクラスが使われている)




うーん、Javaの実装に依存するなら、成功したのは非OracleJavaだったからかな。
OpenJDK7の実装もOracleと同じっぽい。
自宅の環境はOracleJavaのjdk1.8.0_121(最新)にあげて試した。
IBMJavaでも試したいのでWASDを用意した。8.5.5.11。
fixpack11から標準でJava8っぽいな。
IBM Java8およびJava7で試したがやっぱりできないよ。
やっぱり過去の経験はまやかしか?



ここからgradleでテストするように変更を加えた。(build.gradleを用意した)
すると、どうだろう、テストが成功し始めるではないか。
はぁぁぁぁぁぁあ!?
落ち着けオレ。
ちゃんと調べた。
凡ミス、modifierの書き換えが一番初めに動いたせいだ。


ふむ、一般的な解決策は、static finalのインスタンスを置き換えるのではなく、
static finalのインスタンスをDeencapsulationでgetして、それをMockUp(インスタンス)して期待する動作を書くとかだ。
インスタンスを引数に渡すとmockitoのspy相当になる。

jmockitのMockUpの場合、インスタンスを書き換えてるからクラスも同じ、setし直しは不要。
mockitoでspyした場合、別クラスの別インスタンスができてるのでsetし直しが必要で、static finalにsetできず困る。


static finalを書き換える方法については、他にsun.misc.Unsafeを使う方法がネットで調べられる。
まあ確かに一番強力な方法であるが、これも正攻法ではない。説明しないでおく。
正攻法でstatic finalを書き換える方法はない。


まやかしがまだ信じられないので追加調査。
やっぱりまやかしではなかったぞ。
jmockitの古いバージョンではmodifiersを書き換えとるやないか。
Dropped the support for setting static final fields through the Deenc… · jmockit/jmockit1@3819065 · GitHub
えっと、どのバージョンまで書き換え可能なんだ・・・。
1.24まで書き換え可能、1.25から書き換えできない。
1.25は2016年6月26日リリース。issues#281でdropしている。

jmockitのバージョンをあげるときの悲鳴は不可避や。