23
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Spring Framework 5.0 コア機能の主な変更点

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

シリーズ

動作検証バージョン

  • Spring Framework 5.0.0.RC1
  • JDK 1.8.0_121

コア機能の変更点

Spring Framework 5.0では、アプリケーションの種類に依存しない汎用的な機能(=コア機能)に対して以下のような変更が行われています。

項番 変更内容
1 JDK 8でサポートされた仕組みを利用してメソッドパラメータに効率的にアクセスできるようになります。[詳細へ:arrow_right:]

Note: 内部実装の話なので外部仕様は変わりません。
2 いくつかのインタフェースにて、JDK 8でサポートされたデフォルトメソッドが実装されるようになります。[詳細へ:arrow_right:]

Note: それらのインタフェースの実装クラス(拡張クラス)を作成する際には、必要なメソッドだけ実装すればよくなるので、「空実装を提供するAdapterクラスの作成」や「無駄に空実装を行う」といった対応が不要になります。
3 JDK 7でサポートされたStandardCharsetsを使うようになります。[詳細へ:arrow_right:]

Note: 内部実装の話なので外部仕様は変わりません。
4 JDK 9で非推奨になる予定のClass#newInstance()の代わりにConstructor#newInstance()を呼び出してインスタンスを生成するようになります。[詳細へ:arrow_right:]

Note: 内部実装の話なので外部仕様は変わりません。
5 spring-jclモジュールが追加され、Commons LoggingのAPIを介してLog4j 2.x, SLF4J, JUL(java.util.logging)経由でログを出力できるようになります。[詳細へ:arrow_right:]

Note: Spring Framework 4.3までは必要だったログブリッジ用のライブラリは不要になります。
6 リソースを抽象化するためのインタフェース(Resource)にisFile()メソッドが追加され、リソースがファイルシステム上に存在するか判定できるようになります。[詳細へ:arrow_right:]
7 リソースを抽象化するためのインタフェース(Resource)にreadableChannel()メソッドが追加され、ReadableByteChannel(New I/O)経由でリソースのデータを読み込むことができるようになります。[詳細へ:arrow_right:]

JDK 8で追加されたAPIを介してメソッドパラメータへアクセスできる :thumbsup:

[SPR-14055] : JDK 8より、java.lang.reflect.Methodjava.lang.reflect.ConstructorクラスはJDK 8から追加されたjava.lang.reflect.Executableクラスを継承するようになっており、Spring Framework 5.0ではExecutableクラスのメソッドを利用してメソッドおよびコンストラクタのパラメータ情報にアクセスするようになりました。
今回修正されたクラスは基本的にはフレームワーク内部の処理で使われているクラスなのでアプリケーション開発者が直接使用することはあまりないと思いますが、フレームワーク拡張などのAP基盤部品を作る時に使う可能性はあるかもしれませんね。

たとえば・・・

package com.example;

import org.springframework.beans.factory.annotation.Value;

public class Foo {
    public Foo(String text, int number) {
        // ...
    }
    public String bar(@Value("${text:dummy}") String text) {
        return text;
    }
}

というクラスがあったとして、このクラスのコンストラクタとメソッドの引数にアクセスするコードをみてみましょう。

Spring Framework 4.3では、MethodParameterクラスのforMethodOrConstructorメソッドを使用してパラメータ情報を取得します。(Spring Framework 5.0で非推奨APIになっています)

〜4.3
package com.example;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

class CoreTest {

    @Test
    void constructorParameter() throws Exception {
        Constructor<?> constructor = Foo.class.getConstructor(String.class, int.class);
        MethodParameter parameter0 = MethodParameter.forMethodOrConstructor(constructor, 0);
        MethodParameter parameter1 = MethodParameter.forMethodOrConstructor(constructor, 1);

        Assertions.assertEquals(String.class, parameter0.getParameterType());
        Assertions.assertEquals(int.class, parameter1.getParameterType());
    }

    @Test
    void methodParameter() throws Exception {
        Method method = Foo.class.getMethod("bar", String.class);
        MethodParameter parameter0 = MethodParameter.forMethodOrConstructor(method, 0);

        Assertions.assertEquals(String.class, parameter0.getParameterType());
        Assertions.assertEquals("${test:dummy}", parameter0.getParameterAnnotation(Value.class).value());
    }

}

Spring Framework 5.0では、MethodParameterクラスのforExecutableまたはforParameterメソッドを使用してパラメータ情報を取得します。

5.0〜
package com.example;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

class CoreTest {

    @Test
    void constructorParameter() throws Exception {
        Constructor<?> constructor = Foo.class.getConstructor(String.class, int.class);
        MethodParameter parameter0 = MethodParameter.forExecutable(constructor, 0);
        MethodParameter parameter1 = MethodParameter.forExecutable(constructor, 1);

        Assertions.assertEquals(String.class, parameter0.getParameterType());
        Assertions.assertEquals(int.class, parameter1.getParameterType());
    }

    @Test
    void methodParameter() throws Exception {
        Method method = Foo.class.getMethod("bar", String.class);
        MethodParameter parameter0 = MethodParameter.forParameter(method.getParameters()[0]);

        Assertions.assertEquals(String.class, parameter0.getParameterType());
        Assertions.assertEquals("${text:dummy}", parameter0.getParameterAnnotation(Value.class).value());
    }

}

Note:

ちなみに・・・実際のパラメータ名を取得したい場合は、MethodParameterDefaultParameterNameDiscovererを設定する必要があります。(この仕組みを利用する場合は、コンパイルオプションとして「-parameters」または「-g」の指定が必要です)

MethodParameter parameter0 = MethodParameter.forParameter(method.getParameters()[0]);
parameter0.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
// ...
Assertions.assertEquals("text", parameter0.getParameterName());

インタフェースにデフォルトメソッドが実装される :thumbsup:

[SPR-14432] : Spring Frameworkが提供しているいくつかのインタフェースにて、JDK 8でサポートされたデフォルトメソッドが実装されるようになります。この変更により、それらのインタフェースの実装クラス(拡張クラス)を作成する際には、必要なメソッドだけ実装すればよくなるので、「空実装を提供するAdapterクラスの作成」や「無駄に空実装を行う」といった対応が不要になります。

たとえば、spring-beansモジュールから提供されているBeanPostProcessorインタフェースにはpostProcessBeforeInitialization(Beanの初期処理を実行する前にコールバックされるメソッド)とpostProcessAfterInitialization(Bneaの初期処理を実行した後にコールバックされるメソッド)という2つのメソッドがありますが、そちらか一方だけ実装したいというケースもあります。

Spring Framework 4.3では、以下のように必ず2つのメソッドを実装する必要がありました。

〜4.3
@Configuration
public class AppConfig {
    @Bean
    BeanPostProcessor myBeanPostProcessor() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                // 例)初期化処理が行われる前にbeanの状態を変更するコードを実装する
                return bean;
            }

            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                return bean; // beanをそのまま返却するテンプレ実装が必要・・・
            }
        };
    }
}

Spring Framework 5.0では、以下のように必要なメソッドだけ実装すればよくなります。

5.0〜
@Configuration
public class AppConfig {
    @Bean
    BeanPostProcessor myBeanPostProcessor() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                // 例)初期化処理が行われる前にbeanの状態を変更するコードを実装する
                return bean;
            }
        };
    }
}

フレームワーク内の処理でStandardCharsetsが利用される :thumbsup:

[SPR-14492] : フレームワークの内部処理にて、「UTF-8」などの標準的なCharsetを指定する際に、「Charset#forName(String)」とするのではなくJDK 7でサポートされたStandardCharsetsが使われるようになります。これはフレームワーク利用者にはまったく影響がない話ですが、StandardCharsetsには「US-ASCII」「ISO-8859-1」「UTF-8」「UTF-16BE」「UTF-16LE」「UTF-16」用の定数が用意されているので、みなさんも積極的にこれらの定数を利用しましょう!

Constructor#newInstance()を使用してインスタンスが生成される :thumbsup:

[SPR-14486] : フレームワークの内部処理にて、JDK 9で非推奨になる予定のClass#newInstance()の代わりにConstructor#newInstance()を呼び出してインスタンスを生成するようになります。どうやら・・・Class#newInstance()を使うと、コンストラクタのthrows句に宣言があるチェック例外が発生した際に、チェック例外のハンドリング(catch or throws句の追加)が強制されないため、発生した例外が意図しない形で上位にスローされてしまうみたいです。こちらもフレームワーク利用者にはまったく影響がない話ですが、独自にリフレクションを使用してインスタンス生成を行うような実装を行っているケースはありそうなので、何が問題なのか見てみましょう。

実際にはありえないですが、デフォルトコンストラクタで強制的にIOExceptionを発生するクラスを用意します。

public class Foo {
    public Foo () throws  IOException {
        throw new IOException("error.");
    }
    // ...
}

↑のクラスのインスタンスを生成する際に「new Foor()」とした場合は、コンパイラがIOExceptionのハンドリングを行うように求めてきます。

new
@Test
void newInstance() {
    try {
        new Foo();
        Assertions.fail("does not occurred a IOException.");
    } catch (IOException e) { // ★★★ コンパイラによってIOExceptionのハンドリングが強制される!!
        // NOP
    }
}

次にClass#newInstanceを使ってインスタンスを生成すると、InstantiationExceptionIllegalAccessExceptionのハンドリングを行うように求めてきます。 この状態でテストケースを実行すると・・・

Class.newInstance
@Test
void classNewInstance() {
    try {
        Foo.class.newInstance();
        Assertions.fail("does not occurred a IOException.");
    } catch (InstantiationException | IllegalAccessException e) {
        // NOP
    }
}
java.io.IOException: error.

    at com.example.CoreTest$Foo.<init>(CoreTest.java:97)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at com.example.CoreTest.classNewInstance(CoreTest.java:75)

となり、テストが失敗します。つまり・・・InstantiationExceptionIllegalAccessExceptionをハンドリングしてもコンストラクタで発生したIOExceptionはハンドリングされずに、呼び出し元に意図しない形で例外がスローされてしまいます。

では、Class#newInstanceではなくConstructor#newInstance()を使うとどうなるのか見てみましょう。下記のテストを実行すると、InvocationTargetExceptionが発生し、InvocationTargetExceptionの中にコンストラクタ内で発生した例外(IOException)がラップされています。

Constructor.newInstance
@Test
void constructorNewInstance() {
    try {
        Foo.class.getDeclaredConstructor().newInstance();
        Assertions.fail("does not occurred a IOException.");
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) {
        Assertions.fail("does not occurred a IOException.");
    } catch (InvocationTargetException e) { // ★★★ コンパイラによってコンストラクタ内で発生した:sweat_smile:例外(IOException)をラップする例外のハンドリングが強制される!!
        Assertions.assertEquals(IOException.class, e.getTargetException().getClass());
    }
}

spring-jclモジュールが追加される :thumbsup:

[SPR-14512] : spring-jclモジュールが追加され、「本家のCommons Logging」や「Commons LoggingのAPIを実装したブリッジライブラリ」を使わずにCommons LoggingのAPIを介してLog4j 2.x, SLF4J, JUL(java.util.logging)経由でログを出力できるようになります。
Spring FrameworkはCommons LoggingのAPIを使用してログ出力しており、実際のログ出力を行うためのライブラリは開発者が選択するスタイルになっています。ひと昔前は「Commons Logging + ログライブラリ(Log4jなど)」という組み合わせが一般的でしたが、最近では「SLF4J + SLF4JのAPIを実装したログライグラリ(例えばLogback)」の方が主流になりつつあります(たぶん:sweat_smile:)。
たとえば・・・「SLF4J + Logback」でアプリケーションのログを出力するようなケースでは、Spring Frameworkが出力するログは「Commons LoggingのAPIを実装したブリッジライブラリ(jcl-over-slf4jなど) + SLF4J + Logback」という構成にする必要があります。

上記の構成にするための依存ライブラリの解決方法を見てみましょう。

Spring Framework 4.3では、spring-coreが「本家のCommons Logging」に依存しているため、ブリッジライブラリを使う場合はspring-coreの依存ライブラリから「本家のCommons Logging」を除外する必要がありました。

pom.xml(〜4.3)
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>4.3.8.RELEASE</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId> <!-- ★★★ 本家のCommons Loggingを除外 -->
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</dependencyManagement>
<!-- ... -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId> <!-- ★★★ 「Commons LoggingのAPIを実装したブリッジライブラリ」が必要 -->
  <version>1.7.25</version>
</dependency>

Spring Framework 5.0からは、「Commons LoggingのAPIを実装したブリッジライブラリ」の部分をspring-jclが担うため、jcl-over-slf4jなどのブリッジライブラリを追加することなく、Commons LoggingのAPIを使用して「Log4j 2.x」「SLF4J」「JUL(java.util.logging)」経由でログを出力することができます。Spring Framework 5.0からは、spring-coreは「本家のCommons Logging」ではなくspring-jclに依存しているため、spring-jclがサポートしているログライブラリを依存ライブラリに追加するだけでOKです。

pom.xml(5.0〜)
<!-- ... -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId> <!-- ★★★ SLF4JのAPIを実装しているLogbackだけ追加すればよい -->
  <version>1.2.3</version>
</dependency>

Note:

アプリケーション構築のために使うライブラリが「本家のCommons Logging」や「Commons LoggingのAPIを実装したブリッジライブラリ」に依存している場合は、それらのライブラリの除外設定が必要になります。

ためしにSpringのDIコンテナを生成するコードを実行してみると・・・

@Test
void applicationContext() {
    try (ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class)) {
        // ...
    }
}

以下のようなログがちゃんと出力されました :v:

01:55:50.072 [main] DEBUG org.springframework.core.env.StandardEnvironment - Adding [systemProperties] PropertySource with lowest search precedence
01:55:50.079 [main] DEBUG org.springframework.core.env.StandardEnvironment - Adding [systemEnvironment] PropertySource with lowest search precedence
01:55:50.080 [main] DEBUG org.springframework.core.env.StandardEnvironment - Initialized StandardEnvironment with PropertySources [systemProperties,systemEnvironment]
01:55:50.129 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning
01:55:50.179 [main] INFO org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@46daef40: startup date [Fri May 12 01:55:50 JST 2017]; root of context hierarchy
... (中略)
01:55:50.786 [main] INFO org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@46daef40: startup date [Fri May 12 01:55:50 JST 2017]; root of context hierarchy
01:55:50.786 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'lifecycleProcessor'
01:55:50.787 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@482cd91f: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.event.internalEventListenerProcessor,org.springframework.context.event.internalEventListenerFactory,coreTest.AppConfig,myBeanPostProcessor]; root of factory hierarchy

Resourceがファイルシステム上にあるか判定できる :thumbsup:

[SPR-14484] : リソースを抽象化するためのインタフェース(Resource)にisFile()メソッドが追加され、リソースがファイルシステム上に存在するか判定できるようになります。

@Test
void resourceIsFile() throws IOException {
    Resource fileResource = new FileSystemResource("pom.xml");
    Resource webResource = new UrlResource("http://google.com");

    Assertions.assertTrue(fileResource.isFile());
    Assertions.assertFalse(webResource.isFile());
}

ResourceのデータをReadableByteChannel経由で取得できる :thumbsup:

[SPR-14698] : リソースを抽象化するためのインタフェース(Resource)にreadableChannel()メソッドが追加され、ReadableByteChannel(New I/O)経由でリソースのデータを読み込むことができるようになります。ちなみに・・・RC1でしれっとWritableResourcewritableChannelメソッドが追加され、WritableByteChannel経由でデータを書き込めるようになっていたので、あわせて実装サンプルを紹介しておきます。

@Test
void resourceUsingByteBuffer() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(2048);
    Resource srcResource = new FileSystemResource("pom.xml");
    WritableResource destResource = new FileSystemResource("pom.xml.bk");
    try (ReadableByteChannel readableChannel = srcResource.readableChannel();
            WritableByteChannel writableChannel = destResource.writableChannel()) {
        while (true) {
            buffer.clear();
            if (readableChannel.read(buffer) <= 0) {
                break;
            }
            buffer.flip();
            writableChannel.write(buffer);
        }
    }

    Assertions.assertEquals(srcResource.contentLength(), destResource.contentLength());
}

まとめ

今回は、コア機能の主な変更点を紹介しました。フレームワークを利用したアプリケーションを開発する際に直接使わないようなクラスの変更点の紹介もありましたが、フレームワークの「使用性」や「効率性」を向上するための改善が行われているという印象です。
次回は、「DIコンテナ関連の主な変更点」を紹介する予定です。

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
Sign upLogin
23
Help us understand the problem. What are the problem?