Help us understand the problem. What is going on with this article?

Spring Framework 5.0 DIコンテナ関連の主な変更点

More than 3 years have passed since last update.

今回は「Spring Framework 5.0 主な変更点」シリーズの第3回で、DIコンテナ関連の主な変更点(新機能や改善点など)を紹介していきたいと思います。

シリーズ

動作検証バージョン

  • Spring Framework 5.0.0.RC1
  • JDK 1.8.0_121

DIコンテナ関連の変更点

Spring Framework 5.0では、DIコンテナに対して以下のような変更が行われています。

項番 変更内容
1 アプリケーション実行時にクラスパスからコンポーネントをスキャンしてDIコンテナにBean登録する機能(@ComponentScan)の代替(アプリケーション起動時間の短縮と一定化目的)として、コンパイル時にBean登録するコンポーネント候補のインデックスを作成しておく仕組みが追加されます。 [詳細へ:arrow_right:]
2 オートワイヤリングによるインジェクションが任意(オプション)であることを示す際に、自作または3rdパーティのライブラリから提供されている@Nullableを指定することができるようになります。[詳細へ:arrow_right:]

Note: アノテーション名が"Nullable"であるか否かで判断しています。
3 GenericApplicationContextAnnotationConfigApplicationContextに、関数型インタフェース(Supplier)を利用したBean登録メソッド(registerBean)とBean定義をカスタマイズするためのインタフェース(BeanDefinitionCustomizer)が追加されます。 [詳細へ:arrow_right:]
4 インタフェースを実装したクラスに対して「CGLIBのProxy」を使用してAOPを適用する際(proxyTargetClass=true指定した際)に、インタフェースのメソッドに指定したアノテーション(@Transactional, @Cacheable, @Syncなど)が読み取られるようになります。 [詳細へ:arrow_right:]
5 XMLでBean定義する際に指定するxsdファイルの世代管理(過去バージョン用のxsdファイルの提供)が廃止され、該当バージョン向けのxsdファイルのみJARファイルに格納されるようになります。 [詳細へ:arrow_right:]

Note: XMLファイル内でバージョン付きのxsdファイルの指定は引き続きサポートされますが、XMLファイル解析時には常に同じxsdファイルが利用されます。

コンポーネントのインデックススキャンがサポートされる :thumbsup:

[SPR-11890] : アプリケーション実行時に指定したパッケージ配下のクラスをスキャンしてDIコンテナにBean登録する機能(@ComponentScan)がありますが、デフォルトでは実行時にクラスパス上から対象クラスを検索する仕組みになっています。この仕組みだと、クラスをスキャンするために必要になる時間はパッケージ構成やクラス数に左右されることになります。
Spring Framework 5.0からは、クラスパススキャンの代替方法(アプリケーション起動時間の短縮 or 一定化を目的)として、コンパイル時にスキャン候補のクラスを解決しておく仕組みが追加されます。

Note:

JIRAのコメントをみる限りだと劇的に早くなるというわけではなさそうでうし、必ず早くなるというものでもなさそうなので、インデックススキャンを使うか否かは実際に効果をみて決定した方がよいと思います。なお、動作検証用に作成したテストケース(スキャン候補のクラスおよびクラスの総数は全部で10クラス程度)だと、クラスパススキャンの方が若干早かったです。(数十msec程度の違いですけどね :sweat_smile:

簡単に仕組みを説明しておくと・・・JDK 1.6で追加された「JSR 269: Pluggable Annotation Processing API」を利用してコンパイル時に「スキャン候補のクラスを取得するためのインデックスファイル(/META-INF/spring.components)」を作成し、実行時にそのファイルを読み込む仕掛けになっています。(むだに絵にしてみると・・・以下のような感じ)

spring50-indexer-overview.png

コンパイル時にインデックスファイルの出力対象になるクラスは、「@Indexedおよびを@Indexedをメタアノテーションに指定している合成アノテーション(@Componentとか)」「javax.で始まるアノテーション(@Named@ManagedBeanとか)」「package-infoクラス」です。
実行時には、インデックスファイルをもとに以下のようなMap(インデックス)を生成しておき、コンポーネントスキャン対象のパッケージを実際にスキャンしないでスキャン候補のクラス一覧を取得する作りになっています。

spring50-indexer-indexmap.png

なお、クラスパススキャンと同様に、「スキャン対象のパッケージ外のクラス」「除外フィルタにマッチしたクラス」「包含フィルタにマッチしないクラス」は、DIコンテナに登録されないようになっています。

インデックススキャンを有効にする方法は簡単で、以下のようにspring-context-indexerモジュールを依存アーティファクトに追加するだけです。

pom.xml
<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が指定できる :thumbsup:

[SPR-15028] : オートワイヤリングによるインジェクションが任意(オプション)であることを示す際に、自作または3rdパーティのライブラリ(JSR-305とか)から提供されている@Nullableを指定することができるようになります。

Note:

内部的にはアノテーション名が"Nullable"か否かで判断しているので、自作の@Nullableも認識してくれます。

たとえば・・・

〜4.3
@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)で書き換えることもできるようになります。

pom.xml
<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>
@Nullableの利用(5.0〜)
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を使うこともできます。

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登録できる :thumbsup:

[SPR-14832] : GenericApplicationContextAnnotationConfigApplicationContextに、関数型インタフェース(Supplier)を利用したBean登録メソッド(registerBean)とBean定義をカスタマイズするためのインタフェース(BeanDefinitionCustomizer)が追加されます。

たとえば、以下のようにすると、ラムダ式で指定したSupplierから返却したオブジェクトがシングルトンのBeanとしてDIコンテナで管理されます。

Supplierを使用したBean生成と登録
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のスコープをプロトタイプに変更しています。

org.springframework.beans.factory.config.BeanDefinitionCustomizer
@FunctionalInterface
public interface BeanDefinitionCustomizer {
    void customize(BeanDefinition bd);
}
BeanDefinitionCustomizerを使用したBean定義のカスタマイズ例
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」使用時にインタフェースに指定したアノテーションが読み取られる :thumbsup:

[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上では、以下のように過去バージョンでサポートされていた属性などを使うことができました。

Bean定義XMLファイル
<?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属性に変更する例
<bean class="com.example.di.Foo">
  <property name="bar">
    <ref bean="bar"/>
  </property>
</bean>
要素のref属性を使う例
<bean class="com.example.di.Foo">
  <property name="bar" ref="bar"/>
</bean>

また、XSDファイル指定時にバージョンを指定してもなんの意味もない(むしろ混乱する)ので、この機会にバージョンを含まない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コンテナ関連の主な変更点を紹介しました。たぶん「インデックススキャンのサポート」が目玉の変更点だと思いますが、劇的に起動が早くなるわけではなさそうなので、今後の改善に期待!!といったところでしょうか :wink:
次回は、「WebMVC関連の主な変更点」を紹介する予定です。

kazuki43zoo
Javaエンジニアで、SpringやMyBatisらへんにそれなりに詳しいです。お仕事のつながりで「Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発」を共著させてもらいました!
https://kazuki43zoo.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした