Springの代表的な特徴といえば、それは色々ありますが、一つをあげるとしたらやはり@Autowired
によるDI1ではないかと思います。初めてSpringに接したときはオブジェクトがnew
なしでも動くと言うことが何ともいえない不思議なことに見えました。これがデザインパターンの一つであるということを知ってからはますますすごいなぁと思いましたね。やはり良いコードを書くためには、様々な方面での工夫が必要なものですね。
とにかく、こうも重要で便利なDIですが、最近Spring Bootを触りながら気になったことがありました。今までは当たり前のように、@Autowired
はフィールドに宣言していましたが、今回の案件ではコンストラクターにつけている場合がありました。なぜ一部はフィールドにつけ、一部はコンストラクターにつけるんだろう?と思いましたね。結果的には全てのアノテーションをコンストラクターにつけることになりましたが、それが特にフィールドにつける場合との違いを理解させたわけではないので、少し調べてみました。
結論からいうと、大概の場合に@Autowired
はフィールドよりはコンストラクターにつけた方がいいらしいです。そしてこれを(フィールドやコンストラクターに@Autowired
をつけることを)、それぞれ「フィールドインジェクション」と「コンストラクターインジェクション」と呼ぶらしいです。では、これらをコードを持って説明していきましょう。
Field Injection
まずインジェクションのために以下のようなコンフィギュレーションクラス2を定義したとしましょう。
@Configuration
public class Mapper extends ModelMapper {
@Bean
public Mapper mapper {
return new ModelMapper();
}
}
ここではModelMapperを使ってみます。ModelMapperに関しては、以前のポストにも書きましたが、互いにマッチングするGetter/SetterのあるBean同士のマッピングを自動で行ってくれる便利なライブラリーです。
こうしてSpringでBeanを登録し、Autowiredアノテーションをフィールドにつけるサービスクラスの例が以下です。これをフィールドインジェクションと呼びます。
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private Mapper mapper;
}
自分が最初にSpring Frameworkについて学んだ時はこのようなフィールドインジェクトションが一般的でした。しかし、フィールドインジェクションでは致命的な問題があリます。ここでフィールドがNullだった場合もプログラムは動作するということです。クラスの動作に必要な要件が整ってないのにもかかわらず、プログラムが動作してしまうのはバグを呼ぶこととなりますね。なのでフィールドインジェクションはよくないです。
Setter Injection
実は、インジェクションはSetterを通じても可能らしいです。あまり一般的な方式ではありませんが、これをセッターインジェクションと呼び、コードで表現すると以下のようになります。
@Service
public class ItemServiceImpl implements ItemService {
private Mapper mapper;
@Autowired
public void setMapper(Mapper mapper) {
this.mapper = mapper;
}
}
Setterによるインジェクションの問題は、フィールドインジェクションと同じです。Setterで必要なオブジェクトが注入されたかどうかと関係なくプログラムは動く可能性がありますね。この問題を解決できるのが、次に紹介するコンストラクターインジェクションです。
Constructor Injection
コンストラクターによるインジェクションはコンストラクターインジェクションと呼び、コードは以下のようになります。
@Service
public class ItemServiceImpl implements ItemService {
private Mapper mapper;
@Autowired
public ItemServiceImpl(Mapper mapper) {
this.mapper = mapper;
}
}
コンストラクターによるインジェクションの良い点は、先に述べたような問題が発生する可能性をブロックできるということです。これはSpringというよりJavaの言語仕様の話ですが、コンストラクターで引数の要件が満たされてないクラスはインスタンスを生成できませんね。そしてNullを注入しない限り、NullPointerExceptionは発生しなくなります。
また、コンストラクターインジェクションだと、循環参照3の問題を事前に防ぐことができるというメリットがあります。フィールドインジェクションやセッターインジェクションでは実際のコードが呼ばれるまでは問題を発見することができませんが、コンストラクターインジェクションで循環参照が発生する場合はSpringアプリケーションを起動する時に警告が出力されます。
また、フィールドインジェクションの場合はそのクラスの単体テストができないという問題もあります。Autowiredアノテーションがついているフィールドに対してオブジェクトを注入できる方法がないですので。Setterを使うと一旦注入はできるようになりますが、あえてSetterを使う理由はないですね。
コンストラクターインジェクションが良いもう一つの理由は、フィールドをfinal宣言できるということです。フィールドにfinalをつけることでクラス内でオブジェクトが変更されることを防止できるので、より安全になります。
最後に
今までは自分も当たり前のことのようにフィールドインジェクションを使っていましたが、フィールドインジェクションの問題を知ってからはなるべくコンストラクターインジェクションとしてコードを書くようにしています。あえてそうしなくても、IntelliJでは常にコンストラクターインジェクションを使うことと警告まで出すみたいで、Springの公式のドキュメントでもそういう言及がありました。これは今までの認識を変えざるを得ません。
SpringだけでなくJavaコーディングの話をすると、コンストラクターは基本的に書かなくても暗黙的に引数なしのものが生成されるのがJavaの仕様ですね。Singletonクラスや引数の初期化なしで動くと問題になるクラスではこれを防ぐためにわざとコンストラクターを書くこととなっています。なので常にコンストラクターは明示的に書いておく習慣も大事ですね。こういうことも含めて考えると、コンストラクターを記述することの重要性がわかるような気もします。やはり良いコードを書くには、様々な方面での工夫は必要なものですね!
Dependency Injection(依存性の注入)。ネット上に詳しい説明が多いので深くは入りませんが、簡単に概念を説明するとオブジェクトを外部から生成してコードに入れることでオブジェクトの依存性をコードから独立させることを意味します。注入されたオブジェクトはコードに依存してないので、どこで呼ばれても同じものとして機能することができます。 ↩︎
Configurationアノテーションをつけると自動的にSpring内で設定クラスとして認識されます。ここでオブジェクトをBeanとして定義すると、DIができるようになります。以前はxmlファイルに記入しておく場合もありました。 ↩︎
循環参照とは、複数のオブジェクトが互いを参照していることを意味します。例えばAクラスのインスタンスを生成する時にBを参照することとなっていて、BクラスもAクラスを参照することとなっていると、どちらかのインスタンスを作成する時に互いの参照を繰り返す無限ループに落ちてしまいます。この無限ループの果てはStackOverflowですね。 ↩︎