今回は「Spring Framework 5.0 主な変更点」シリーズの第3回で、DIコンテナ関連の主な変更点(新機能や改善点など)を紹介していきたいと思います。
シリーズ
- 第1回: Spring Framework 5.0 主な変更点の概要
- 第2回: Spring Framework 5.0 コア機能の主な変更点
- 第3回: Spring Framework 5.0 DIコンテナ関連の主な変更点
- 第4回: Spring Framework 5.0 WebMVC関連の主な変更点
- 第5回: Spring Framework 5.0 Test関連の主な変更点
- 第6回: Spring Framework 5.0 WebFlux(Reactive Web Framework) (予定)
- 第7回: Spring Framework 5.0 Kotlinサポート (予定)
動作検証バージョン
- Spring Framework 5.0.0.RC1
- JDK 1.8.0_121
DIコンテナ関連の変更点
Spring Framework 5.0では、DIコンテナに対して以下のような変更が行われています。
項番 | 変更内容 |
---|---|
1 | アプリケーション実行時にクラスパスからコンポーネントをスキャンしてDIコンテナにBean登録する機能(@ComponentScan )の代替(アプリケーション起動時間の短縮と一定化目的)として、コンパイル時にBean登録するコンポーネント候補のインデックスを作成しておく仕組みが追加されます。 [詳細へ] |
2 | オートワイヤリングによるインジェクションが任意(オプション)であることを示す際に、自作または3rdパーティのライブラリから提供されている@Nullable を指定することができるようになります。[詳細へ]Note: アノテーション名が "Nullable" であるか否かで判断しています。 |
3 |
GenericApplicationContext とAnnotationConfigApplicationContext に、関数型インタフェース(Supplier )を利用したBean登録メソッド(registerBean )とBean定義をカスタマイズするためのインタフェース(BeanDefinitionCustomizer )が追加されます。 [詳細へ] |
4 | インタフェースを実装したクラスに対して「CGLIBのProxy」を使用してAOPを適用する際(proxyTargetClass=true 指定した際)に、インタフェースのメソッドに指定したアノテーション(@Transactional , @Cacheable , @Sync など)が読み取られるようになります。 [詳細へ] |
5 | XMLでBean定義する際に指定するxsdファイルの世代管理(過去バージョン用のxsdファイルの提供)が廃止され、該当バージョン向けのxsdファイルのみJARファイルに格納されるようになります。 [詳細へ] Note: XMLファイル内でバージョン付きのxsdファイルの指定は引き続きサポートされますが、XMLファイル解析時には常に同じxsdファイルが利用されます。 |
コンポーネントのインデックススキャンがサポートされる
[SPR-11890] : アプリケーション実行時に指定したパッケージ配下のクラスをスキャンしてDIコンテナにBean登録する機能(@ComponentScan
)がありますが、デフォルトでは実行時にクラスパス上から対象クラスを検索する仕組みになっています。この仕組みだと、クラスをスキャンするために必要になる時間はパッケージ構成やクラス数に左右されることになります。
Spring Framework 5.0からは、クラスパススキャンの代替方法(アプリケーション起動時間の短縮 or 一定化を目的)として、コンパイル時にスキャン候補のクラスを解決しておく仕組みが追加されます。
Note:
JIRAのコメントをみる限りだと劇的に早くなるというわけではなさそうでうし、必ず早くなるというものでもなさそうなので、インデックススキャンを使うか否かは実際に効果をみて決定した方がよいと思います。なお、動作検証用に作成したテストケース(スキャン候補のクラスおよびクラスの総数は全部で10クラス程度)だと、クラスパススキャンの方が若干早かったです。(数十msec程度の違いですけどね )
簡単に仕組みを説明しておくと・・・JDK 1.6で追加された「JSR 269: Pluggable Annotation Processing API」を利用してコンパイル時に「スキャン候補のクラスを取得するためのインデックスファイル(/META-INF/spring.components
)」を作成し、実行時にそのファイルを読み込む仕掛けになっています。(むだに絵にしてみると・・・以下のような感じ)
コンパイル時にインデックスファイルの出力対象になるクラスは、「@Indexed
およびを@Indexed
をメタアノテーションに指定している合成アノテーション(@Component
とか)」「javax.
で始まるアノテーション(@Named
や@ManagedBean
とか)」「package-infoクラス」です。
実行時には、インデックスファイルをもとに以下のようなMap
(インデックス)を生成しておき、コンポーネントスキャン対象のパッケージを実際にスキャンしないでスキャン候補のクラス一覧を取得する作りになっています。
なお、クラスパススキャンと同様に、「スキャン対象のパッケージ外のクラス」「除外フィルタにマッチしたクラス」「包含フィルタにマッチしないクラス」は、DIコンテナに登録されないようになっています。
インデックススキャンを有効にする方法は簡単で、以下のようにspring-context-indexer
モジュールを依存アーティファクトに追加するだけです。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.0.0.RC1</version>
<optional>true</optional> <!-- コンパイルする時だけ必要なのでwarなどにパッケージングされないようにする -->
</dependency>
Note:
クラスパス上にひとつでも有効なインデックスファイル(
/META-INF/spring.components
)」があると、デフォルトではインデックススキャンが使われます。たとえば、自プロジェクトではspring-context-indexer
を使用していないのに、依存ライブラリ(例えば社内の共通ライブラリなど)にインデックスファイルが含まれていると自プロジェクトのコンポーネントがスキャンされません。
この事象を回避するためには、「自プロジェクトでもspring-context-indexer
を使う」 or 「実行時にインデックススキャンしないようにする」の2択です。
「実行時にインデックススキャンしないようにする」ためには、「Springのプロパティファイル(クラスパス直下のspring.properties
)」または「Javaのシステムプロパティ(-D
オプション)」に「spring.index.ignore=true
」を指定してください。
任意のインジェクションポイントに対して@Nullable
が指定できる
[SPR-15028] : オートワイヤリングによるインジェクションが任意(オプション)であることを示す際に、自作または3rdパーティのライブラリ(JSR-305とか)から提供されている@Nullable
を指定することができるようになります。
Note:
内部的にはアノテーション名が
"Nullable"
か否かで判断しているので、自作の@Nullable
も認識してくれます。
たとえば・・・
@Component
public class Foo {
private final Bar bar;
private final Baz baz;
public Foo(Bar bar, @Autowired(required = false) Baz baz) {
this.bar = bar;
this.baz = baz;
}
}
は、以下のようにSpring Frameworkのアノテーションを使わないスタイル(例:JSR-330のアノテーション+JSR-305の@Nullable
)で書き換えることもできるようになります。
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
import javax.annotation.Nullable;
import javax.inject.Named;
//...
@Named
public class Foo {
private final Bar bar;
private final Baz baz;
public Foo(Bar bar, @Nullable Baz baz) {
this.bar = bar;
this.baz = baz;
}
}
ちなみに・・・JSR-330のアノテーション(@Named
や@Inject
)はSpring Framework 4.3でも使えます。また、任意のインジェクションポイントであることを示す方法として、JDK 8で追加されたjava.util.Optional
を使うこともできます。
import java.util.Optional;
// ...
@Named
public class Foo {
private final Bar bar;
private final Optional<Baz> baz;
public Foo(Bar bar, Optional<Baz> baz) {
this.bar = bar;
this.baz = baz;
}
}
関数型インタフェース(Supplier
)を利用してBean登録できる
[SPR-14832] : GenericApplicationContext
とAnnotationConfigApplicationContext
に、関数型インタフェース(Supplier
)を利用したBean登録メソッド(registerBean
)とBean定義をカスタマイズするためのインタフェース(BeanDefinitionCustomizer
)が追加されます。
たとえば、以下のようにすると、ラムダ式で指定したSupplier
から返却したオブジェクトがシングルトンのBeanとしてDIコンテナで管理されます。
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
applicationContext.registerBean(Bar.class);
applicationContext.registerBean(Foo.class, () -> new Foo(applicationContext.getBean(Bar.class))); // ★★★ Supplierを使用したBean登録
applicationContext.refresh();
Foo foo = applicationContext.getBean(Foo.class);
// ...
}
さらに・・・BeanDefinitionCustomizer
インタフェースを利用すると、DIコンテナで管理するBeanのメタ情報(スコープとか)を指定することができます。以下のサンプルでは、Beanのスコープをプロトタイプに変更しています。
@FunctionalInterface
public interface BeanDefinitionCustomizer {
void customize(BeanDefinition bd);
}
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
applicationContext.registerBean(Bar.class);
applicationContext.registerBean(Foo.class, () -> new Foo(applicationContext.getBean(Bar.class)),
bd -> bd.setScope(BeanDefinition.SCOPE_PROTOTYPE)); // ★★★ BeanDefinitionCustomizerを使用してBean定義をカスタマイズ
applicationContext.refresh();
Foo foo = applicationContext.getBean(Foo.class);
Assertions.assertFalse(foo == applicationContext.getBean(Foo.class)); // スコープがプロトタイプなので1回目と2回目は別のインスタンスが返却される
}
Note:
ちなみに・・・Springが提供しているアノテーションなどを一切つかわない(アノテーション駆動開発したくない?な)場合は、
GenericApplicationContext
を使うこともできます。
「CGLIBのProxy」使用時にインタフェースに指定したアノテーションが読み取られる
[SPR-14322など] : インタフェースを実装したクラスに対して「CGLIBのProxy」を使用してAOPを適用する際(proxyTargetClass=true
指定した際)に、インタフェースのメソッドに指定したアノテーション(@Transactional
, @Cacheable
, @Sync
など)が読み取られるようになります。
たとえば、以下のようにSpringのキャッシュ機能を使う際に、インタフェースにキャッシュ制御のアノテーションを付与します。
@CacheConfig(cacheNames = "accounts")
public interface AccountService {
@Cacheable
Account getAccount(int id);
}
public class AccountServiceImpl implements AccountService {
@Override
public Account getAccount(int id) {
return new Account(id);
}
}
コンフィギュレーションクラスでは、Springのキャッシュ機能を適用する際(Proxyオブジェクトを生成する際)に、JDK ProxyではなくCGLIBのProxyを使うように設定します。
@EnableCaching(proxyTargetClass = true) // ★★★ CGLIBのProxyを利用するように設定する
@Configuration
public static class CacheConfiguration {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("accounts");
}
@Bean
AccountService accountService() {
return new AccountServiceImpl();
}
}
この状態で、アプリケーションコンテキストから取得したAccountServiceのメソッドを同じ引数で2回呼び出すと、2回目はキャッシュされたオブジェクトが返却されます。
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(
CacheConfiguration.class)) {
AccountService service = applicationContext.getBean(AccountService.class);
Account account = service.getAccount(1);
Assertions.assertTrue(account == service.getAccount(1)); // Springのキャッシュ機能が適用され、2回目はキャッシュされたオブジェクトが返却される
}
xsdファイルの世代管理の廃止
[SPR-13499] : XMLでBean定義する際に指定するxsdファイルの世代管理(過去バージョン用のxsdファイルの提供)が廃止され、該当バージョン向けのxsdファイルのみJARファイルに格納されるようになります。
XMLファイル内でバージョン付きのxsdファイルの指定は引き続きサポートされますが、XMLファイル解析時には常に同じxsdファイルが利用されます。
Spring Framework 4.3上では、以下のように過去バージョンでサポートされていた属性などを使うことができました。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <!-- ★★★ 3.2用のXSDファイルを指定 -->
<bean class="com.example.di.Foo">
<property name="bar">
<ref local="bar"/> <!-- ★★★ 3.xではlocal属性が存在する -->
</property>
</bean>
<bean id="bar" class="com.example.di.Bar"/>
</beans>
このファイルをSpring Framework 5.0で使うと・・・XMLファイルをパースする際にスキーマ違反になり、以下のようなエラーになります。
org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 9 in XML document from class path resource [com/example/di/applicationContext.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 9; columnNumber: 25; cvc-complex-type.3.2.2: 要素'ref'に属性'local'を含めることはできません。
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:399)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)
このファイルをSpring Framework 5.0上で使うためには、「local
属性のかわりにbean
属性へ変更する」 or 「<property>
要素のref
属性を使う」といった対応が必要になります。なお、「<ref>
要素のlocal
属性」は使えなくなる属性の一例で、他にもいくつか使えなくなっています。
<bean class="com.example.di.Foo">
<property name="bar">
<ref bean="bar"/>
</property>
</bean>
<bean class="com.example.di.Foo">
<property name="bar" ref="bar"/>
</bean>
また、XSDファイル指定時にバージョンを指定してもなんの意味もない(むしろ混乱する)ので、この機会にバージョンを含まないXSDファイルを指定するように変更することをお勧めします。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- ★★★ バージョンなしファイルへ変更 -->
<!-- ... -->
</beans>
まとめ
今回は、DIコンテナ関連の主な変更点を紹介しました。たぶん「インデックススキャンのサポート」が目玉の変更点だと思いますが、劇的に起動が早くなるわけではなさそうなので、今後の改善に期待!!といったところでしょうか
次回は、「WebMVC関連の主な変更点」を紹介する予定です。