タイトルにもあるとおり、SpringBootのJUnitで『BeanNotOfRequiredTypeException』が発生して、?? となったので、考えうる原因と対策をまとめました!
一応エラーが発生しなくなったのですが、実はまだ真因までは特定できておらず・・・
SpringBootのDIにすごく詳しい方、ご教示頂けると嬉しいです!
環境
- Java11
- SpringBoot v2.4.0
- JUNIT 5.7.0
テストコード
業務で、新規ロジッククラスを作ったので、そのテストコードを実装しました。
(以下はあくまでコード例です)
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* DemoLogicImpl実装のテスト.
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
class DemoLogicImplTest {
@Autowired
private DemoLogicImpl target;
@Test
void test_method() {
// テスト内容
}
}
エラー内容
テストコードを実行すると以下のエラーが出ました。
Error creating bean with name 'xxxx.DemoLogicImplTest': Unsatisfied dependency expressed through field 'target'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'DemoLogicImpl is expected to be of type 'xxxx.DemoLogicImpl but was actually of type 'com.sun.proxy.$Proxy134' org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'xxxx.DemoLogicImplTest': Unsatisfied dependency expressed through field 'target'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'DemoLogicImpl is expected to be of type 'xxxx.DemoLogicImpl but was actually of type 'com.sun.proxy.$Proxy134'
・・・
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'DemoLogicImpl is expected to be of type 'xxxx.DemoLogicImpl' but was actually of type 'com.sun.proxy.$Proxy134'
Springのトラブルシューティングでは、『Caused by』の後が根本原因であることが鉄則なので、そこに注目します。
すると、『BeanNotOfRequiredTypeException』が起きていますね。
『DemoLogicImplというBean名は、型が'xxxx.DemoLogicImpl' である必要があるのに、実際は'com.sun.proxy.$Proxy134'という型になっていますよ。』 と書いてあります。
ひとまず、Googleさんで『SpringBoot BeanNotOfRequiredTypeException』で調べてみると 、期待値となる型(ここでいうDemoLogicImpl)に@Autowiredをつけて自動配線してあげる必要があると書いてありました。
次は、'com.sun.proxy.$Proxy134' のほうに着目し、Springプロキシが悪さしているのでは? と調べました。
すると・・・
SpringAOPのプロキシ化の仕組みにはJDK dynamic proxyとCGLIBという二つの仕組みがあるんですが(デフォルトはJDK dynamic proxy)
JDK dynamic proxyでProxy化されたbeanのインスタンスを直接実装クラス指定でAutowired出来ない仕様らしいです。
僕のコードは実装クラスを直接指定しているからこれが原因か!?
そう思い、@Autowiredするクラスを実装クラスではなく、インターフェースに変更しました。
@Autowired
private DemoLogic target;
ようやく解消か! と思いきや、結果変わらず・・・
インターフェースを指定したときに、そのインターフェースを実装しているクラスが複数ある場合は、Beanの紐付けがうまくいかない場合があるそうです。
例えば、以下みたいなケース。
public interface Shape {
String form();
}
@Component
public class Circle implements Shape {
@Override
public String form() {
return "Circle";
}
}
@Component
public class Square implements Shape {
@Override
public String form() {
return "Square";
}
}
でも僕のコードは複数のクラスに紐づいてもいないんだよな・・
謎が深まるばかりです・・・
あれこれと調べた結果、とりあえず以下を追記したらエラーは起きなくなりました。
@ContextConfiguration(classes = DemoLogicImpl.class)
最終的なコード
最終的なコードは以下のようになりました。
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* DemoLogicImpl実装のテスト.
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
@ContextConfiguration(classes = DemoLogicImpl.class)
class DemoLogicImplTest {
@Autowired
private DemoLogicImpl target;
@Test
void test_method() {
// テスト内容
}
}
え、でも@SpringBootTestがあるときは、@ContextConfigurationいらないんじゃなかったっけ?
@SpringBootTest アノテーションはテスト実行時のコンフィグレーションクラスを自動検出する機能を備えています。SpringBootを使わない場合には @ContextConfiguration でコンフィグレーションを明示する必要があるわけですがそれが不要になるわけです。
ですよね・・まあ動いたからひとまずいいか・・
そんなこんなでコードは1ヶ月くらいそのままにしていましたが、後日、@ContextConfigurationを削除してテストクラスを実行してみたら、テストが通るようになっていたんです。
当記事の一番最初に載せたコードでもテストが通るようになっていたんです。
もう何がなんだか訳わからず・・
1on1でチームリーダーにも訊いたのですが、曖昧な感じの回答になってしまい原因がわからず仕舞いでした。
真因が分からないのも気持ち悪いので、SpringBootのDIにすごく詳しい方、ご教示頂けると嬉しいです!
以上です。
参考