Springのproxyとfinalメソッド、それからnull
SpringのRepositoryとComponentアノテ、動きに違いがあるのか、、
Repositoryだとフィールドがnullになってハマった
— こざけ (@s_kozake) 2018年2月17日
ようやく🍥Repositoryでフィールドの値がnullになる原因が分かった、、
メソッドをfinal指定してました
サブクラスベースのProxyではメソッドやクラスにfinalをつけてはいけないとあれほど_:(´ཀ`」 ∠):
— こざけ (@s_kozake) 2018年2月19日
ここら辺、どこか詳しい資料ありますか?
CGLIBを用いてサブクラスベースのProxyを実現している状況で、メソッドをfinalにした場合、内部でどのような状況になってnullのインスタンスを見るようになったかという
まあ、そもそもfinal切るなって話なのですが
— こざけ (@s_kozake) 2018年2月19日
こざけさんがこんな感じでわちゃわちゃしていたので、詳しい資料はどこにあるか分からないけれど、なぜそうなったのか解説しようと思います。
なぜそうなるのか概ね理解していたけれど、理解していなかった点を調べる過程で私自身も学ぶことがあったので、正直こざけさんには調べるきっかけを作ってくれてありがとうございますという感じです!
@Repositoryとproxy
@Repository
をクラスに付けると、そのクラスのproxyが作られます。
proxyてなんやねんって話ですが、これは実行時に生成される該当クラスのサブクラスで、他のコンポーネントにインジェクションされるのはこのproxyのインスタンスです。
実際のクラスのインスタンスへはproxyのインスタンスを経由して委譲される仕組みになっています。
このproxyの仕組みには、コンポーネントの利用者がスコープを意識しなくてよくなるという利点があります。
どういうことか、順を追って見ていきましょう。
proxyという概念が無いDIコンテナ
DIコンテナにはスコープというものがあります。
これはコンポーネントのライフサイクルを表すもので、Webアプリで動くDIコンテナであれば「リクエストスコープ」や「セッションスコープ」を持っています。
「リクエストスコープ」はその名の通りHTTPリクエストの開始から終了までの間に有効となるスコープです。
「セッションスコープ」もその名の通りでセッションを開始してから破棄されるまでの間に有効となるスコープです。
では「セッションスコープのコンポーネント」から「リクエストスコープのコンポーネント」を使用することを考えてみましょう。
コードで書くとこんな感じ。
//※これはSpringではない架空のDIコンテナのコード
@SessionScope
public class Hoge {
@Inject
private Fuga fuga;
public void action() {
fuga.process();
}
}
@RequestScope
public class Fuga {
//フィールドやメソッドの定義は省略
}
この Hoge
と Fuga
を使った処理の流れは、
- HTTPリクエストを受け取る
- セッションを作成する
-
Hoge
のインスタンスを作成する -
Fuga
のインスタンスを作成する -
Hoge
にFuga
をインジェクションする -
Hoge
のaction
メソッドを実行する -
Fuga
のprocess
メソッドを実行する - HTTPリクエストが終了する
-
Fuga
のインスタンスをDIコンテナから破棄する - HTTPレスポンスを返す
という感じ。
あんまり問題なさそうに見えますが、 Fuga
の process
メソッドが実行中に同じユーザーからもう一つHTTPリクエストが来たらどうなるでしょうか?
- HTTPリクエストAを受け取る
- セッションを作成する
-
Hoge
のインスタンスを作成する -
Fuga
のインスタンスを作成する(リクエストAのスコープ) -
Hoge
にFuga
をインジェクションする -
Hoge
のaction
メソッドを実行する -
Fuga
のprocess
メソッドを実行し始める - HTTPリクエストBを受け取る
-
Fuga
のインスタンスを作成する(リクエストBのスコープ) -
Hoge
にFuga
をインジェクションする......!?
とまあ、こんな感じで Hoge
にインジェクションされる Fuga
がバッティングするわけです。
そういうわけでproxyの無いDIコンテナでは、あるスコープのコンポーネントには、それよりも小さいスコープ(ライフサイクルが短いと言い換えても良さそう)のコンポーネントをインジェクションできませんでした。
proxyが無くてこのような制約のあるDIコンテナとしてはSeasar2が挙げられます。
proxyがあるDIコンテナ
先に述べた問題をproxyがどのように解決するのか見ていきましょう。
Fuga
のproxyをコードで書くとこんな感じになります。
//実際には実行時にクラスファイルが生成されるのでソースコードは存在しない
//あくまでもproxyのイメージ
public class FugaProxy extends Fuga {
private Container container;
public void process() {
Fuga component = container.getComponent(Fuga.class);
component.process();
}
}
コード上のコメントにも書きましたが、proxyは実際には実行時にクラスファイル(というかインメモリ上にバイトコードのデータ)として生成されるのが普通なので、ソースコードはありません。
コード例は雰囲気です。
コードを見てみると process
メソッドが呼ばれるとDIコンテナからproxyではない実際の Foo
インスタンスが取り出され、そのインスタンスの process
メソッドが呼ばれています。
先ほどのproxyが無いDIコンテナで Fuga
がバッティングしてしまったシナリオを、proxyがあるDIコンテナではどうなるか見てみましょう。
- HTTPリクエストAを受け取る
- セッションを作成する
-
Hoge
のインスタンスを作成する -
Fuga
のインスタンスを作成する(リクエストAのスコープ) -
Fuga
のproxyを作成する -
Hoge
にFuga
のproxyをインジェクションする -
Hoge
のaction
メソッドを実行する -
Fuga
のproxyのprocess
メソッドを実行し始める -
Fuga
のproxy内でDIコンテナから実際のFuga
インスタンス(リクエストAのスコープ)を取り出してprocess
メソッドを実行し始める - HTTPリクエストBを受け取る
-
Fuga
のインスタンスを作成する(リクエストBのスコープ) -
Hoge
のaction
メソッドを実行する -
Fuga
のproxyのprocess
メソッドを実行し始める -
Fuga
のproxy内でDIコンテナから実際のFuga
インスタンス(リクエストBのスコープ)を取り出してprocess
メソッドを実行し始める
このようにproxyを経由してDIコンテナから異なる Fuga
インスタンスを取り出して process
メソッドを実行できました。
以上のことからproxyがあるDIコンテナではスコープを意識することなくインジェクションを行えることが分かりました。
finalメソッドからフィールドを参照したらnullになった理由
当初の問題に戻りましょう。 「 @Respository
を付けたクラスの final
なメソッドを実行するとインジェクションされたはずのフィールドへアクセスしたときに NullPointerException
が発生した」という問題です。
次のコードを見てください。
@Repository
public class Foo {
@Autowired
private Bar bar;
public String method1() {
return String.format("%s%n%s%n", bar, getClass());
}
public final String method2() {
return String.format("%s%n%s%n", bar, getClass());
}
}
method1
と method2
はどちらもフィールド bar
と自分自身のクラスを文字列にして返しています。 異なる点は method2
は final
であるということだけです。
method1
の実行結果はこちら。
com.example.demo.Bar@1db75e25
class com.example.demo.Foo
method2
の実行結果はこちら。
null
class com.example.demo.Foo$$EnhancerBySpringCGLIB$$a65476ff
当初の問題と同じように final
な方の method2
では bar
が null
になっていますね。
ただ、クラス名にも注目してください。 method1
では Foo
となっていますが method2
は Foo
のproxyになっています。
ここで Foo
のproxyがどうなっているのか、再び雰囲気でコードにしてみましょう。
//雰囲気
public class Foo$$EnhancerBySpringCGLIB$$a65476ff extends Foo {
Container container;
@Override
public String method1() {
Foo component = container.getComponent(Foo.class);
return component.method1();
}
//method2はfinalなのでoverrideできない
}
ご覧の通り method1
はコンテナから実際のインスタンスを取り出して委譲していますが、 method2
は final
なため override
できずに実際のインスタンスに委譲されることなく実行されてしまいます。
proxyは、コンテナから実際のインスタンスを取り出して委譲するものなので、proxyに対してコンポーネントをインジェクションする意味はありません。
実際、元のクラスでインジェクション対象となっているフィールドはproxyでは null
になります。
以上が、proxyが作られるクラスに定義された final
メソッドがインジェクション対象のフィールドを参照している場合に NullPointerException
になる理由です。
コンストラクタインジェクションとproxy
さて、ここからが私も初めて知った事柄になります。
久しぶりにびっくりした。
次のようにコンストラクタインジェクションしているクラスを考えてみましょう。
@Repository
public class Foo {
private final Bar bar;
public Foo(Bar bar) {
this.bar = Objects.requireNonNull(bar);
}
public String method1() {
return String.format("%s%n%s%n", bar, getClass());
}
public final String method2() {
return String.format("%s%n%s%n", bar, getClass());
}
}
proxyの雰囲気コードはこんな感じ。
//雰囲気
public class Foo$$EnhancerBySpringCGLIB$$a65476ff extends Foo {
Container container;
public Foo$$EnhancerBySpringCGLIB$$a65476ff(Bar bar) {
super(bar);
}
@Override
public String method1() {
Foo component = container.getComponent(Foo.class);
return component.method1();
}
//method2はfinalなのでoverrideできない
}
この場合、生成されるproxyのコンストラクタ引数 bar
には何が入るのでしょうか?
先に述べた通り、proxyのインジェクション対象フィールドにはインジェクションする意味はありません。
かと言って null
を渡すとスーパークラスのコンストラクタで Objects.requireNonNull
によって NullPointerException
がスローされます。
それではSpringはproxyをインスタンス化するときに何を渡すのでしょうか?
色々とがんばってソースコードを追っかけた末に分かったのですが、コンストラクタを呼ばずにインスタンス化していました。
何を言っているのか分かりませんね?
proxyのインスタンス化にはObjenesisというライブラリの次のクラスが使われていました。
(実際には org.springframework.objenesis
にrepackageされています)
クラスのJavadocに次の記載があります。
Instantiates an object, WITHOUT calling it's constructor, using
internal sun.reflect.ReflectionFactory
お、おう......!
って感じですが、JDKのinternalなクラスを使ってコンストラクタを呼ばずにインスタンス化していました。
だからフィールドが null
だった、というわけですね。
コンストラクタ呼ばれないってどういうことだよ......と思いながら SunReflectionFactoryInstantiator
を眺めていたら newConstructorForSerialization
というメソッド名が出てきて、そういえばシリアライズされたオブジェクトをデシリアライズする時ってコンストラクタ呼ばれないんだっけ、とか雑な記憶で雑に思いました。
とか書いておくと詳しい人がコメントくれるはずです。 他力本願。
まとめ
- DIコンテナにはproxyを経由してスコープを意識せずに使える機能がある
- proxyは実行時にサブクラスを生成して実現するので
final
メソッドを使うとマズい - SpringではproxyはJDKのinternalなクラスを使用してコンストラクタ呼び出し無しにインスタンス化される(その結果フィールドが
null
になる)
こんな感じで、割と真っ黒な黒魔術に辿り着いた感があって楽しかったです。
こざけさん、ありがとう!
ちなみに
警告みたいなの欲しいですねー
— opengl-8080 (@opengl_8080) 2018年2月20日
INFO
レベルですが、ログは出ているみたいです。
018-02-22 22:22:14.298 INFO 25770 --- [ restartedMain] o.s.aop.framework.CglibAopProxy : Final method [public final java.lang.String com.example.demo.Foo.method2()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.
Discussion