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

  • 42
    いいね
  • 3
    コメント

2016/6/10リリースされたSpring Framework 4.3の主な新機能と改善点を紹介します。
(リリース前の投稿を更新しました!! 差分は、「★6/11追加」でマークしてあります)

  • Core Container Improvements
  • Data Access Improvements
  • Caching Improvements
  • JMS Improvements
  • Web Improvements
  • WebSocket Messaging Improvements
  • Testing Improvements
  • Support for new library and server generations (★6/11 追加)

本投稿は、「New Features and Enhancements in Spring Framework 4.3」で紹介されている内容を、サンプルも交えて具体的に説明したものです。(逆にいうと、「New Features and Enhancements in Spring Framework 4.3」にのっていない変更点は紹介しないので、あしからず・・・ :wink:

なお、2016/6/23リリース予定のSpring Boot 1.4も、Spring 4.3ベースになります!!

シリーズ

動作検証環境

  • Spring Framework 4.3.0.RELEASE
  • Spring Boot 1.4.0.BUILD-SNAPSHOT (2016/6/11時点)

Core Container Improvements

今回は、DIコンテナ関連の主な変更点と新しいライブラリやAPサーバーのサポートの変更点をみていきます。

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

No DIコンテナ関連の主な変更点
1 DI時に発生した例外から、エラー箇所を特定するための情報を取得できるようになります。 (★6/11追加)
2 Java SE 8でサポートされたインタフェースのデフォルトメソッドをBeanのプロパティとして扱うことができるようになります。 (★6/11追加)
3 オートワイヤリングによるコンストラクタインジェクションを行う際に、@Autowiredの指定が省略できるようになります。
4 Java Configクラス(@Configurationを付与したクラス)でコンストラクタインジェクションが利用できるようになります。
5 @EventListenercondition属性に指定するSpEL中で、DIコンテナに登録されているBeanを参照できるようになります。
6 合成アノテーションの非配列属性を使用して、メタアノテーション内の配列属性をオーバーライドできるようになります。
7 @Scheduled@Schedulesをメタアノテーションとして利用できるようになります。
8 @Scheduledを任意のスコープのBeanで利用できるようになります。

ライブラリバージョンとAPサーバーのサポートの変更点

No ライブラリバージョンとAPサーバーバージョンのサポートの変更点
9 新たに「Hibernate ORM 5.2」「Jackson 2.8」「OkHttp 3.x」「Netty 4.1」 がライブラリバージョンとしてサポートされます。(★6/11追加)
10 新たに「Undertow 1.4」「Tomcat 8.5.2」「Tomcat 9.0 M6」がAPサーバーバージョンとしてサポートされます。 (★6/11追加)

DIエラー時のエラー箇所を特定できる情報が取得できる :thumbsup:

Spring 4.3から、DI時に失敗した場合に、エラー箇所(DI箇所)を特定するためのメタ情報(org.springframework.beans.factory.InjectionPoint)が取得できるようになります。具体的には、UnsatisfiedDependencyExceptionをキャッチしてgetInjectionPointメソッドを呼び出してください。が、しかし・・・、あまり使うことはない気がします・・・

public static void main(String[] args) {
    try {
        SpringApplication.run(Spr43DemoApplication.class, args);
    } catch (UnsatisfiedDependencyException e) {
        InjectionPoint injectionPoint = e.getInjectionPoint();
        // 必要に応じてメタ情報へアクセス
    }
}

この対応により、Spring 4.2まではBeanCreationExceptionが発生していたところが、UnsatisfiedDependencyExceptionになります。ただ、UnsatisfiedDependencyExceptionBeanCreationExceptionのサブクラスなので、アプリケーションに与える影響は基本的にはないでしょう。(特殊な例外ハンドリング処理を実装していると、ひょっとしたら影響があるかもしれませんが・・・ :sweat_smile:

デフォルトメソッドをBeanのプロパティとして扱うことができる :thumbsup:

Spring 4.3から、Java SE 8でサポートされたインタフェースのデフォルトメソッドをBeanのプロパティとして扱うことができるようになります。いい感じの使い方がパッと浮かんできませんが、例えば以下のような感じでしょうか!?

public interface TargetDateConsumer {

    default void setTargetDateString(String dateString) { // デフォルトメソッドでsetterを用意
        setTargetDate(LocalDate.parse(dateString));
    }

    void setTargetDate(LocalDate date);

}
public class UnusedFileRemover implements TargetDateConsumer {
    private LocalDate targetDate;
    @Override
    public void setTargetDate(LocalDate targetDate) {
        this.targetDate = targetDate.plusDays(7);
    }
    // ...
}
<bean class="com.example.UnusedFileRemover">
    <property name="targetDateString" value="2016-06-10"/> <!-- デフォルトメソッド(setterメソッド)に値を指定 -->
</bean>

こうすると、インタフェースのデフォルトメソッド(setTargetDateString)を介してsetTargetDateメソッドが呼び出されます。上記の例だと、UnusedFileRemovertargetDateフィールドは、「2016-06-17」になります。なお、Spring 4.2で同じことを行うと、以下のエラーが発生します。

Caused by: org.springframework.beans.NotWritablePropertyException: Invalid property 'targetDateString' of bean class [com.example.UnusedFileRemover]: Bean property 'targetDateString' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?

コンストラクタインジェクションで@Autowiredを省略できる :thumbsup:

Spring 4.2では、オートワイヤリングによるコンストラクタインジェクションを行う際は@Autowiredの指定が必要でしたが、Spring 4.3から省略できるようになります。ただし、この仕組みは、引数をとるコンストラクタが複数ある場合は利用できません。

〜4.2
@Autowired
public MessageService(MessageSource messageSource) {
    this.messageSource = messageSource;
}
4.3〜
// @Autowired ← 省略できる!!
public MessageService(MessageSource messageSource) {
    this.messageSource = messageSource;
}

ぱっとみ大した改善ではないように見えるかもしれませんが、この改善により、Lombokで生成したコンストラクタを使用してふつうにインジェクションができるようになります。4.2でもLombokで生成したコンストラクタを使用してインジェクションすることはできますが、Lombokのアノテーションにやや特殊な指定が必要でした。

〜4.2+Lombok
// 生成するコンストラクタに@Autowiredが付与するためにonConstructor属性の指定が必要
@lombok.RequiredArgsConstructor(onConstructor = @__(@Autowired)) 
@Service
public class MessageService {
    private final MessageSource messageSource;
    // ...    
}

@Configurationを付与したクラスでコンストラクタインジェクションが利用できる :thumbsup:

Spring 4.3から、Java Configクラス(@Configurationを付与したクラス)でコンストラクタインジェクションが利用できるようになります。

4.3〜
package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class JdbcConfig {

    private final DataSource dataSource;

    public JdbcConfig(DataSource dataSource) { // コンストラクタインジェクションが可能
        this.dataSource = dataSource;
    }
    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource);
    }
}

@EventListenercondition属性でBeanを参照ができる :thumbsup:

Spring 4.3から、@EventListenercondition属性に指定するSpEL中で、DIコンテナに登録されているBeanを参照できるようになります。 ここでは、外部リソースとの接続状態を管理するBeanのメソッドを呼び出して、接続先の外部リソースがアクティブな状態の時だけイベントを受け取るようにしてみます。

まず、外部リソースとの接続状態を管理するBeanを作ります。サンプルでは説明を簡略化するために固定値を返却しています。

@Component
public class ExternalResourceStatus {
    // 状態を表現する列挙型
    public enum Status {
        ACTIVE, INACTIVE;
        public boolean active() {
            return ACTIVE == this;
        }
    }
    // データソース(DB)との接続状態を返却するメソッド
    public Status dataSource() {
        return Status.ACTIVE;
    }
    // MQとの接続状態を返却するメソッド
    public Status mq() {
        return Status.INACTIVE;
    }
}

イベントリスナーは以下のような実装にします。

4.3〜
@Service
public class MessageService {
    // ...
    @EventListener(condition = "@externalResourceStatus.dataSource().active()")
    public void onMessageStoreToDb(Message message) {
        System.out.println("onMessageStoreToDb:" + message.getText());
    }

    @EventListener(condition = "@externalResourceStatus.mq().active()")
    public void onMessageSendingToMq(Message message){
        System.out.println("onMessageSendingToMq:" + message.getText());
    }
}

イベントを発行します。

動作確認用のテストケース作成例
@Rule
public OutputCapture capture = new OutputCapture();
@Autowired
ApplicationEventPublisher eventPublisher;
// ...

@Test
public void publishEvent() {
    Message message = new Message();
    message.setText("test");

    eventPublisher.publishEvent(message); // イベントの発行

    assertThat(capture.toString()).contains("onMessageStoreToDb:test");
    assertThat(capture.toString()).doesNotContain("onMessageSendingToMq:test");
}
コンソール
...
10:05:49.603 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'externalResourceStatus'
10:05:49.613 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'messageService'
onMessageStoreToDb:test
10:05:49.614 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'externalResourceStatus'
1
...

このケースだと、MessageService#onMessageStoreToDbメソッドだけが呼び出されます。この改善により、イベントを受け取る条件をダイナミックに切り替えることができます。

メタアノテーション内の配列属性を合成アノテーションの非配列属性で上書きできる :thumbsup:

Spring 4.3から、合成アノテーションの非配列属性を使用して、メタアノテーション内の配列属性をオーバーライドできるようになります。いまいち言葉だと伝えずらいので、サンプルコードを使って説明しましょう。

ここでは、フォーム画面を表示する際に、「GET /xxx?form」という形式のURLでアクセスするという共通的なルールがある前提で話をすすめます。このようなルールがある場合は、「GETメソッドを利用する」「リクエストパラメータにformを含める」という部分が共通ルールになります。この共通ルール部分をメタアノテーションで表現する合成アノテーションを作ってみましょう。

メタアノテーション側の定義例(@RequestMappingの抜粋)
public @interface RequestMapping {
    // ...
    @AliasFor("value")
    String[] path() default {}; // 配列属性
    // ...
}
4.3〜(合成アノテーションの作成例)
package com.example;

import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@RequestMapping(method = RequestMethod.GET, params = "form") // 共通ルールはメタアノテーション側に指定
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetForm {

    @AliasFor(annotation = RequestMapping.class)
    String path(); // 非配列属性で上書き

}

作成した合成アノテーションを利用して、リクエストマッピングを定義してみます。

4.3〜(合成アノテーションの利用例)
@Controller
public class AccountController {
    // GET /account/create?formをハンドリングする
    @GetForm(path = "/account/create")
    public String createForm() {
        return "account/createForm";
    }
    // ...
    // GET /account/update?formをハンドリングする
    @GetForm(path = "/account/update")
    public String updateForm() {
        return "account/updateForm";
    }
    // ...
}

この改善と関係ありませんが、Spring 4.3から@RequestMappingをメタアノテーションに指定した合成アノテーション(@GetMapping, @PostMappingなど)がいくつか追加されています。追加された合成アノテーションについては、後日Web関連の変更点を紹介する時に説明します。

@Scheduled@Schedulesをメタアノテーションとして使用できる :thumbsup:

Spring 4.3から、@Scheduled@Schedulesをメタアノテーションとして利用できるようになります。ここでは、@Scheduledからみてきます。

〜4.2
@Scheduled(fixedRate = 1000)
public void deleteOldMessages() {
    System.out.println("deleteOldMessages:" + LocalDateTime.now());
}

4.3以降では、@Scheduledをメタアノテーションにしていした合成アノテーションを作成し、合成アノテーションを使用して定期実行するメソッドを指定することができます。

4.3〜(合成アノテーションの作成例)
package com.example;

import org.springframework.scheduling.annotation.Scheduled;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Scheduled(fixedRate = 1000) // @Scheduledをメタアノテーションとして使用
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PurgeScheduled {
}
4.3〜(定期実行対象のメソッドの指定例)
@PurgeScheduled // 作成した合成アノテーションを指定 (@Scheduled(fixedRate = 1000)と同義)
public void deleteOldMessages() {
    System.out.println("deleteOldMessages:" + LocalDateTime.now() + " on " + Thread.currentThread().getName());
}
4.3〜(動作確認用のテストケース作成例)
@Rule
public OutputCapture capture = new OutputCapture();
// ...
@Test
public void scheduled() throws InterruptedException {
     TimeUnit.MILLISECONDS.sleep(1500);
    assertThat(capture.toString()).contains("deleteOldMessages:");
}
コンソール
...
10:46:13.863 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'spring.http.multipart-org.springframework.boot.autoconfigure.web.MultipartProperties'
deleteOldMessages:2016-05-27T10:46:14.743 on pool-1-thread-1
10:46:15.367 [main] DEBUG org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate - Retrieved ApplicationContext from cache with key [[MergedContextConfiguration@27a8c74e testClass = CoreContainerTests, locations = '{}', classes = '{class com.example.Spr43DemoApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@343f4d3d, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]
...

なお、@Scheduledを利用する場合は、Java Config(@Configurationが付与されたクラス)に@org.springframework.scheduling.annotation.EnableSchedulingを付与する必要があります。このアノテーションがないと、スケジューリング機能が有効にならず、deleteOldMessagesは定期実行されません。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling // 追加
@SpringBootApplication
public class Spr43DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(Spr43DemoApplication.class, args);
    }
}

最後に@Schedulesについて簡単に説明しておきましょう。
@Schedulesは、@Scheduledを複数指定できるようにするためのアノテーションで、Java SE 8+であれば@Scheduledをメソッドやアノテーションに列挙することができるため、このアノテーションを直接使う必要はありません。

@Scheduled(...)
@Scheduled(...) // Java SE 8+なら@Scheduledを列挙できる
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PurgeScheduled {
}

Java SE 7以前の場合は、@Schedulesを使用して複数の@Scheduledを指定する必要があります。

@Schedules({
        @Scheduled(...),
        @Scheduled(...)
}) // Java SE 7以前なら@Schedulesを使って複数指定
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PurgeScheduled {
}

@Scheduledを任意のスコープのBeanに付与できる :thumbsup:

Spring 4.3から、@Scheduledを任意のスコープのBeanで利用できるようになります。Spring 4.3までは、非LazyなシングルトンのBeanにしか付与できませんでした。(付与しても無視されていました)
とはいえ、シングルトン以外のBeanで@Scheduledを付与するケースって・・・、ほとんどない気もしています :sweat_smile:
ちなみに・・・任意のスコープのBeanで利用できると書きましたが・・・Web環境用のリクエストスコープ(@RequestScope)やセッションスコープ(@SessionScope)のBeanに付与すると実行時にエラーになるので、事実上付与できません。

そもそもこの対応が行われたきっかけは、「LazyなシングルトンのBeanで@Scheduledが有効にならないぜ!」というIssueから派生しています。たしかにLazyなシングルトンのBeanについては、@Scheduledをつけたいと思うことはある気がします。

新たなライブラリバージョンのサポート :thumbsup:

Spring 4.3から、以下のライブラリバージョンがサポートされます。なお、旧バージョンのサポート状況は以下のとおりです。

ライブラリ サポートバージョン 旧バージョンのサポート状況
Hibernate ORM 5.2 4系(4.2, 4.3)と5系(5.0, 5.1)の旧バージョンは引き続きサポートされますが、3系(3.6)は非推奨扱いになります。
Jackson 2.8 2.6以上がサポート対象です。
OkHttp 3.x 2.x系も引き続きサポート対象です。
Netty 4.1 リファレンスに明記はありませんが、4.0系も引き続きサポート対象のようです。

Springに内蔵されているASMとObjenesisのバージョンも更新され、それぞれASM 5.1、Objenesis 2.4が内蔵されます。

新たなAPサーバーバージョンのサポート :thumbsup:

Spring 4.3から、以下のAPサーバーバージョンがサポートされます。なお、旧バージョンのサポート状況は以下のとおりです。

APサーバー サポートバージョン 旧バージョンのサポート状況
Undertow 1.4 リファレンスに明記はありませんが、Spring 4.2とサポート状況は同じ模様です。
Tomcat 8.5.2, 9.0 M6 リファレンスに明記はありませんが、Spring 4.2とサポート状況は同じ模様です。

まとめ

今回は、DIコンテナ関連の主な変更点を紹介しました。Spring 4.3のリリースにより、地味ではありますが、確実に使いやすくなります。次回は、「データアクセス関連の主な変更点」を紹介する予定です。

参考サイト

補足

Spring 4.3 GAに伴い変更点を追加 (2016/6/11)

ついに4.3がGAになり、そのタイミングで主な変更点に追加されたトピックスを反映しました。(「★6/11追加」でマークしてあります)