今回は「Spring Framework 5.0 主な変更点」シリーズの第4回で、WebMVC関連の主な変更点(新機能や改善点など)を紹介していきたいと思います。
シリーズ
- 第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
- Spring Boot 2.0.0.M1
- Apache Tomcat 8.5.15
- JDK 1.8.0_121
- Mac
個別
- Yasson 1.0.0-M2 (JSON-B検証用)
- Glassfish JSR 374 (JSON Processing) Default Provider 1.1.0-M2 (JSON-B検証用)
- Reactor 3.1.0.M1 (Reactive Type検証用)
- Undertow 2.0.0.Alpha2-SNAPSHOT (Servlet 4.0検証用) ※ 2017/5/18時点
今回は動作検証するためにSpring Boot 2.0.0.M1(デフォでSpring Framework 5.0.0.RC1に依存している)を使用しました。以下に検証用プロジェクトを作成した時の手順を簡単に紹介しておきます。
Note:
SPRING INITIALIZRやIDEの機能を使って作ることもできます。
- プロジェクトの作成
$ curl -s https://start.spring.io/starter.tgz\
-d name=spring5-web-demo\
-d artifactId=spring5-web-demo\
-d dependencies=web\
-d baseDir=spring5-web-demo\
-d bootVersion=2.0.0.M1\
| tar -xzvf -
- Spring Bootの起動
$ ./mvnw spring-boot:run
/usr/local/apps/spring5-web-demo
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building spring5-web-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:2.0.0.M1:run (default-cli) > test-compile @ spring5-web-demo >>>
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ spring5-web-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ spring5-web-demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source files to /usr/local/apps/spring5-web-demo/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ spring5-web-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /usr/local/apps/spring5-web-demo/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ spring5-web-demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source files to /usr/local/apps/spring5-web-demo/target/test-classes
[INFO]
[INFO] <<< spring-boot-maven-plugin:2.0.0.M1:run (default-cli) < test-compile @ spring5-web-demo <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:2.0.0.M1:run (default-cli) @ spring5-web-demo ---
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.M1)
2017-05-17 01:19:22.357 INFO 26370 --- [ main] c.e.s.Spring5WebDemoApplication : Starting Spring5WebDemoApplication on xxx with PID 26370 (/usr/local/apps/spring5-web-demo/target/classes started by xxx in /usr/local/apps/spring5-web-demo)
2017-05-17 01:19:22.360 INFO 26370 --- [ main] c.e.s.Spring5WebDemoApplication : No active profile set, falling back to default profiles: default
2017-05-17 01:19:22.411 INFO 26370 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@25c892fe: startup date [Wed May 17 01:19:22 JST 2017]; root of context hierarchy
2017-05-17 01:19:23.717 INFO 26370 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2017-05-17 01:19:23.733 INFO 26370 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2017-05-17 01:19:23.735 INFO 26370 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.15
2017-05-17 01:19:23.822 INFO 26370 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2017-05-17 01:19:23.822 INFO 26370 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1414 ms
2017-05-17 01:19:23.956 INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2017-05-17 01:19:23.961 INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-05-17 01:19:23.961 INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-05-17 01:19:23.961 INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-05-17 01:19:23.961 INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2017-05-17 01:19:24.206 INFO 26370 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@25c892fe: startup date [Wed May 17 01:19:22 JST 2017]; root of context hierarchy
2017-05-17 01:19:24.292 INFO 26370 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-05-17 01:19:24.293 INFO 26370 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-05-17 01:19:24.323 INFO 26370 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-17 01:19:24.323 INFO 26370 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-17 01:19:24.370 INFO 26370 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-17 01:19:24.533 INFO 26370 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-05-17 01:19:24.610 INFO 26370 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http)
2017-05-17 01:19:24.615 INFO 26370 --- [ main] c.e.s.Spring5WebDemoApplication : Started Spring5WebDemoApplication in 2.996 seconds (JVM running for 6.973)
- Spring Bootの停止
ターミナル上で「Ctrl + C」!!
WebMVC関連の変更点
Spring Framework 5.0では、WEB機能に対して以下のような変更が行われています。
項番 | 変更内容 |
---|---|
1 | Spring Framework提供のサーブレットフィルタがServlet 3.1のAPI仕様準拠の実装になります。(実はどう準拠したのかわかってない・・・) [詳細へ] |
2 | Spring MVCのHandlerメソッドの引数として、Java EE 8の構成要素であるServlet 4.0で追加されるPushBuilder (HTTP/2のServer Pushを行うためのAPI)を受け取れるようになります。 [詳細へ] |
3 | Servlet 3.0でサポートされたファイルアップロード機能のサイズ超過エラーが発生した際に、MaxUploadSizeExceededException (MultipartException のサブ例外)がスローされるようになります。 [詳細へ] Note: ただし、エラーメッセージに特定の単語( "size" と"exceed" )が含まれているか否かで判定しているため、アプリケーションサーバの実装によっては従来どおりMultipartException がスローされる可能性があるという点は意識しておいた方がよいでしょう。 |
4 | 統一的なメディアタイプ解決の仕組み(MediaTypeFactory クラス)が追加されます。 [詳細へ] Note: この対応に伴いJAF(JavaBeans Activation framework)依存のコードが排除される。 |
5 | イミュータブルオブジェクトへのデータバインディングがサポートされます。 [詳細へ] |
6 | Jackson 2.9がサポート対象になります。 [詳細へ] |
7 | Java EE 8の構成要素であるJSON Bindingがサポート対象になります。 Note: これはJacksonおよびGSONの代替として使用することができます。 [詳細へ] |
8 | Google Protobuf 3.xがサポート対象になります。 [詳細へ] |
9 | Reactive Programing Model(後述するSpring WebFlux)のサポートに伴い、Reactor 3.1のクラス(Flux , Mono )、RxJava 1.3と2.1のクラス(Observable , Sigle , Flowable など)をHandlerメソッドの返り値として扱うことができるようになります。 [詳細へ] |
10 |
AntPathMatcher の代替として、ParsingPathMatcher が追加されます。 [詳細へ] Note: Spring WebFlux関連のコンポーネントでは、 ParsingPathMatcher がデフォルトで使用されます。 |
11 |
@ExceptionHandler メソッドの引数として、RedirectAttributes を受け取れるようになります。(=RedirectAttributes を介してリダイレクト先とデータを連携することができるようになります) [詳細へ] |
12 |
ResponseStatusException が追加され、任意のステータスコードとリーズンフレーズを指定して例外を生成・スローすることで、HTTPステータスを制御できるようになります。 [詳細へ] Note: ResponseStatusException はSpring WebFlux向けに作成したみたいですが、Spring MVCでも使えるように対応されています。 |
13 |
ScriptTemplateView (JSR-223のスクリプトエンジンを使用したView実装)にて、「メッセージの国際化」および「テンプレートのフラグメント化」を実現するために必要となるオブジェクト(Locale ,MessageSource など)がテンプレートエンジン側に引き渡されるようになります。 [詳細へ] Note: 「国際化」や「テンプレートをフラグメント化」するための実装自体はSpringからは提供されないので、Springのテストケースなどを参考に開発者が実装する必要があります。 |
サーブレットフィルタの実装がServlet 3.1のAPI仕様に準拠される?
[SPR-14467] : どうやら純粋に(サーブレットフィルタ限定ではなく)全体的にServlet 3.1のAPI仕様ベースの実装になるよ(=Servlet 3.0上での動作はサポート外)!ということらしいです。これに関連し、Spring Framework 4.3まではServlet 2.5環境との互換性を保つためのコードが実装されていましたが、Spring Framework 5.0からはServlet 2.5環境のサポートは一切打ち切られます(SPR-13189)。
HandlerメソッドでServlet 4.0のPushBuilderが受け取れる
[SPR-12674] Spring MVCのHandlerメソッドの引数として、Java EE 8の構成要素であるServlet 4.0で追加されるPushBuilder
(HTTP/2のServer Pushを行うためのAPI)を受け取れるようになります。
簡単なサンプルを実装して、実際にHTTP/2のServer Pushを試してみましょう。
まず、cssファイルとcssファイルにlinkをもつHTMLファイルを作ります。
body {
background-color: azure;
}
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css"/> <!-- ★★★ cssファイルへのリンク -->
</head>
<body>
<h1>Hello World !!</h1>
</body>
</html>
つぎに、Controllerを作ります。
package com.example.spring5webdemo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.PushBuilder;
@Controller
public class PushBuilderController {
@GetMapping("/push-builder")
String get(PushBuilder builder) { // ★★★ 引数にPushBuilderを宣言
builder.path("/style.css").push(); // ★★★ cssファイルをpush対象のパスに設定してpush
return "forward:/hello.html"; // HTMLに遷移するためのView名を返却
}
}
「さ〜サーバーを起動して確認しましょう!」と言いたいところですが、そもそもServlet 4.0のAPI(PushBuilder
)がみつからないのでコンパイルできないと思います。なので・・・Servlet 4.0に対応しているアプリケーションサーバを使うようにpom.xml
ファイルを修正したいと思います。最初はTomcat 9.0で試そうと思ったのですが、Undertowの方がHTTP/2化するのが簡単そうだったので、ここではServlet 4.0に対応しているUndertow 2.0(開発中のバージョン)を使用して動作確認を行います。
Note:
Undertow 2.0.0.Alpha1がMaven Centralにリリースされていますが、このバージョンは残念ながら利用できません。理由は、Servlet 4.0のAPI仕様が途中でかわり、その変更内容が取り込まれいないためです。なので・・・本エントリーでは、GitHubからソースコードを取得してUndertow 2.0.0.Alpha2-SNAPSHOTをローカルリポジトリへインストールします。
$ git clone https://github.com/undertow-io/undertow.git $ cd undertow $ mvn -U install -DskipTests=true
pom.xml
ファイルを修正し、Tomcatの代わりにUndertow 2.0.0.Alpha2-SNAPSHOTを使うようにします。
<!-- ... -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<undertow.version>2.0.0.Alpha2-SNAPSHOT</undertow.version> <!-- ★★★ Servlet 4.0サポートのバージョンを指定(ローカルリポジトリにインストールしたバージョン) -->
</properties>
<!-- ... -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- ★★★ Tomcatを除外 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- ... -->
<!-- ★★★ Undertowを追加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- ★★★ alpn-bootを追加 -->
<!--
ALPN (Application Layer Protocol Negotiation)の実装ライブラリをローカルリポジトリへダウンロードするための設定。
ダウンロードしたファイルはアプリケーション実行時の起動オプション(-Xbootclasspath)に指定。
-->
<dependency>
<groupId>org.mortbay.jetty.alpn</groupId>
<artifactId>alpn-boot</artifactId>
<version>8.1.11.v20170118</version>
<scope>provided</scope>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- ★★★ ALPNの実装ライブラリを起動オプション(-Xbootclasspath)に指定 -->
<jvmArguments>
-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/8.1.11.v20170118/alpn-boot-8.1.11.v20170118.jar
</jvmArguments>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
HTTP/2で通信を行うためには、サーバーとブラウザの間の通信をHTTPSによるSSL/TLS通信にする必要があるため、自己署名証明書を格納したキーストアを作成します。
$ keytool -genkey -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore src/main/resources/keystore.p12 -validity 3650
キーストアのパスワードを入力してください:
新規パスワードを再入力してください:
姓名は何ですか。
[Unknown]: Sample
組織単位名は何ですか。
[Unknown]: Sample
組織名は何ですか。
[Unknown]: Sample
都市名または地域名は何ですか。
[Unknown]: Sample
都道府県名または州名は何ですか。
[Unknown]: Sample
この単位に該当する2文字の国コードは何ですか。
[Unknown]: JP
CN=Sample, OU=Sample, O=Sample, L=Sample, ST=Sample, C=JPでよろしいですか。
[いいえ]: Y
作成した自己署名証明書が格納されているキーストアをUndertowに設定し、サーバをHTTPS化します。
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=password
最後に、以下のようなBean定義を追加すればUndertowのHTTP/2対応は完了です。
// ...
import io.undertow.UndertowOptions;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
// ...
@SpringBootApplication
public class Spring5WebDemoApplication {
// ...
@Bean
public WebServerFactoryCustomizer<UndertowServletWebServerFactory> webServerFactoryCustomizer() {
return undertow -> {
undertow.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true));
};
}
}
Spring Bootを起動してUndertowがHTTP/2化されていることを確認しましょう。
$ ./mvnw spring-boot:run
...
2017-05-18 09:02:40.807 INFO 60554 --- [ main] o.s.b.w.e.u.UndertowServletWebServer : Undertow started on port(s) 8080 (https)
2017-05-18 09:02:40.813 INFO 60554 --- [ main] c.e.s.Spring5WebDemoApplication : Started Spring5WebDemoApplication in 2.538 seconds (JVM running for 3.086)
$ curl -s -D - --http2 -k https://localhost:8080/
HTTP/2.0 404
content-type:application/json;charset=UTF-8
date:Thu, 18 May 2017 00:07:16 GMT
{"timestamp":"2017-05-18T00:07:15.967+0000","status":404,"error":"Not Found","message":"Not Found","path":"/"}
Note:
MacのcURLコマンドがHTTP/2対応されていなかったので、以下のコマンドを実行してHTTP/2対応されたcURLをインストールしました。$ brew reinstall curl -- --with-nghttp2 $ brew link curl --force $ hash -r
HTTP/2化できていることが確認できたので、Chromeを使用して実際にHTTP/2のServer Pushを行うパスへアクセスしてみます。
少しみずらいですが・・・ 「/style.css」のInitiatorが「Push /push-builder」になっていることが確認できます。
Warning:
ただ・・・2回目以降はServer Pushが有効になっていないような動きになっている・・・+サーバログに以下のようなログがでている・・・ので、何かが間違っているのかもしれません
サーバーログ2017-05-18 11:02:02.813 ERROR 60871 --- [ XNIO-2 task-9] io.undertow : UT005085: Connection io.undertow.server.protocol.http2.Http2ServerConnection@35f2c3a7 for exchange HttpServerExchange{ GET /style.css request {accept=[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8], accept-language=[en-US,en;q=0.8,ja;q=0.6], cache-control=[max-age=0], :authority=[localhost:8080], accept-encoding=[gzip, deflate, sdch, br], :path=[/style.css], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36], :scheme=[https], :method=[GET], Referer=[https://localhost:8080/push-builder], upgrade-insecure-requests=[1], Host=[localhost:8080]} response {Last-Modified=[Wed, 17 May 2017 19:36:35 GMT], Cache-Control=[no-store], Content-Length=[37], Content-Type=[text/css], Accept-Ranges=[bytes], Date=[Thu, 18 May 2017 02:02:02 GMT], :status=[200]}} was not closed cleanly, forcibly closing connection
Servlet 3.0のファイルアップロード機能でMaxUploadSizeExceededException
がスローされるようになる
[SPR-9294] : Servlet 3.0でサポートされたファイルアップロード機能のサイズ超過エラーが発生した際に、MaxUploadSizeExceededException
(MultipartException
のサブ例外)がスローされるようになります。
ただし、エラーメッセージに特定の単語("size"
と"exceed"
)が含まれているか否かで判定しているため、アプリケーションサーバの実装によっては従来どおりMultipartException
がスローされる可能性があるという点は意識しておいた方がよいと思います。
Note:
Spring FrameworkはCommons FileUploadとの連携部品を提供しており、Commons FileUploadでサイズ超過が発生した時は
MaxUploadSizeExceededException
がスローされていました。
では、実際にサイズ超過エラーを発生させてみましょう。ファイルアップロード画面をつくるのは面倒なので・・・本エントリーではcURLコマンドを使ってファイルをアップロードします。
まず、アップロードファイルを受け取るControllerを作成してSpring Bootを起動します。
package com.example.spring5webdemo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class UploadRestController {
@PostMapping("/upload")
String upload(MultipartFile file) {
return file.getOriginalFilename() + " is uploaded !";
}
}
つぎに、以下のように1MB以内のファイル(最大サイズより小さいファイル)をアップロードしてみます。
$ pwd
/usr/local/apps/spring5-web-demo
$ curl -s -D - -X POST -F file=@pom.xml http://localhost:8080/upload
HTTP/1.1 100
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 21
Date: Tue, 16 May 2017 16:56:48 GMT
pom.xml is uploaded !
こんどは、1MB以上のファイルをアップロードしてみます。 ちなみに・・・1MB以上のファイル(target/spring5-web-demo-0.0.1-SNAPSHOT.jar
)は./mvnw package
で作成できます。
$ ./mvnw package
...
$ curl -s -D - -X POST -F file=@target/spring5-web-demo-0.0.1-SNAPSHOT.jar http://localhost:8080/upload
HTTP/1.1 100
HTTP/1.1 500
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 16:59:21 GMT
Connection: close
{"timestamp":"2017-05-16T16:59:21.255+0000","status":500,"error":"Internal Server Error","message":"Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (14620179) exceeds the configured maximum (10485760)","path":"/upload"}
何やらサイズ超過エラーになりましたが、MaxUploadSizeExceededException
が発生したかわからないので、サーバのログの見てみましょう。
...
2017-05-17 01:59:21.246 ERROR 26570 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (14620179) exceeds the configured maximum (10485760)] with root cause
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (14620179) exceeds the configured maximum (10485760)
at org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:811) ~[tomcat-embed-core-8.5.15.jar:8.5.15]
...
ログメッセージをみるとMaxUploadSizeExceededException
が発生していそうなので、こんどはMaxUploadSizeExceededException
の例外ハンドリンング処理を追加してみます。
package com.example.spring5webdemo;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class UploadRestController {
@PostMapping("/upload")
String upload(MultipartFile file) {
return file.getOriginalFilename() + " is uploaded !";
}
@ExceptionHandler(MaxUploadSizeExceededException.class) // ★★★ 例外ハンドリングの追加
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
String handleMaxUploadSizeExceededException() {
return "Upload file size is too large.";
}
}
さらに・・・マルチパートリクエストの解析をHandlerメソッドの決定後に行う(遅延解析する)ようにします。こうすることで、@ExceptionHandler
メソッドでファイルアップロード関連の例外をハンドリングできるようになります。
spring.servlet.multipart.resolve-lazily=true
Spring Bootを再起動して再度1MB以上のファイルをアップロードしてみてください。
$ curl -s -D - -X POST -F file=@target/spring5-web-demo-0.0.1-SNAPSHOT.jar http://localhost:8080/upload
HTTP/1.1 100
HTTP/1.1 413
Content-Type: text/plain;charset=UTF-8
Content-Length: 30
Date: Tue, 16 May 2017 17:01:38 GMT
Connection: close
Upload file size is too large.
Note:
ちなみに・・・Spring Bootを使うとデフォルトでServlet 3.0のファイルアップロード機能が有効になっており、ファイル単位の最大サイズは1M、リクエスト全体の最大サイズは10Mに制限されています。なお、最大サイズの変更は、
application.properties
で変更することができます。src/main/resources/application.properties# ファイル単位の最大サイズ spring.servlet.multipart.max-file-size=2MB # リクエストの最大サイズ spring.servlet.multipart.max-request-size=20MB
統一的なメディアタイプ解決の仕組みがサポートされる
[SPR-14484][SPR-14908] : 統一的なメディアタイプ解決の仕組み(MediaTypeFactory
クラス)が追加され、JAF(JavaBeans Activation framework)依存のコードが排除されます。新しいクラスの中では、spring-web
の中に格納されている/org/springframework/http/mime.types
(メディアタイプと拡張子のマッピングファイル)の定義にしたがってメディアタイプを解決します。
たとえば、以下のようなHandlerメソッドを作成して、pom.xml
ファイルをアップロードすると・・・
@PostMapping("/upload")
String upload(MultipartFile file) {
MediaTypeFactory.getMediaTypes(file.getOriginalFilename())
.forEach(System.out::println);
return file.getOriginalFilename() + " is uploaded !";
}
コンソールには、以下のメディアタイプが出力されます。
application/xml
さらに、プロジェクト内に/org/springframework/http/mime.types
を作成(コピー)することで、spring-web
提供のマッピング定義をカスタマイズすることもできます。
...(省略)
text/xml xml
再度pom.xml
ファイルをアップロードすると・・・コンソールには、以下のメディアタイプが出力されます。
application/xml
text/xml
イミュータブルオブジェクトへデータバインディングできる
[SPR-15199] : イミュータブルオブジェクトへのリクエストパラメータのデータバインディングができるようになります。コンストラクタ引数にパラメータ名の情報が存在する場合(コンパイルオプションに-d
または-parameters
を付与している場合)は、コンストラクタ引数のパラメータ名とリクエストパラメータ名が一致するリクエストパラメータ値がバインドされます。
Note:
コンストラクタが複数ある場合は、従来通りデフォルトコンストラクタが呼び出されます。また、コンストラクタ引数にパラメータ名の情報を残さない場合は、Java Beansから提供されている
@java.beans.ConstructorProperties
を使用してコンストラクタ引数とリクエストパラメータをマッピングすることができます。
では、イミュータブルなクラスを作成し、そのクラスをHandlerメソッドの引数に指定してみます。
package com.example.spring5webdemo;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
@RestController
public class ImmutableObjectRestController {
@GetMapping("/immutable")
Query search(@Validated Query query) {
return query;
}
public static class Query {
@NotNull
private final String name;
private final String mail;
private final String tel;
private final LocalDate baseDate;
public Query(String name, String mail, String tel,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate baseDate) {
this.name = name;
this.mail = mail;
this.tel = tel;
this.baseDate = baseDate;
}
public String getName() {
return name;
}
public String getMail() {
return mail;
}
public String getTel() {
return tel;
}
public LocalDate getBaseDate() {
return baseDate;
}
}
}
デフォルトの状態だと、LocalDate
をJSONにすると可読性が非常に悪いので・・・それっぽくフォーマットされた値が出力されるようにしておきましょう。
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <!-- JSR-310のクラスのシリアライズ・デシリアライズをサポートするモジュールを追加 -->
</dependency>
spring.jackson.serialization.write-dates-as-timestamps=false
まずは、すべて正常な値を指定してリクエストを送ってみます。
$ curl -s -D - http://localhost:8080/immutable?name=kazuki43zoo\&mail=kazuki43zoo@gmail.com\&tel=09012345678\&baseDate=2017-08-01
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:02:24 GMT
{"name":"kazuki43zoo","mail":"kazuki43zoo@gmail.com","tel":"09012345678","baseDate":"2017-08-01"}
お〜ちゃんとバインドされてますね。
次は、name
を未指定にしてバリデーションが行われるか確認してみます。
$ curl -s -D - http://localhost:8080/immutable?mail=kazuki43zoo@gmail.com\&tel=09012345678\&baseDate=2017-08-01
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:04:25 GMT
Connection: close
{"timestamp":"2017-05-16T17:04:25.515+0000","status":400,"error":"Bad Request","errors":[{"codes":["NotNull.query.name","NotNull.name","NotNull.java.lang.String","NotNull"],"arguments":[{"codes":["query.name","name"],"arguments":null,"defaultMessage":"name","code":"name"}],"defaultMessage":"may not be null","objectName":"query","field":"name","rejectedValue":null,"bindingFailure":false,"code":"NotNull"}],"message":"Validation failed for object='query'. Error count: 1","path":"/immutable"}
こちらもちゃんと動いているようです。
最後に、baseDate
(LocalDate
)に不正な日付を指定して、バインドエラーになるか確認してみます。
$ curl -s -D - http://localhost:8080/immutable?name=kazuki43zoo\&mail=kazuki43zoo@gmail.com\&tel=09012345678\&baseDate=2017-08-32
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:05:10 GMT
Connection: close
{"timestamp":"2017-05-16T17:05:10.161+0000","status":400,"error":"Bad Request","message":"Failed to convert value of type 'java.lang.String[]' to required type 'java.time.LocalDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.format.annotation.DateTimeFormat java.time.LocalDate] for value '2017-08-32'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2017-08-32]","path":"/immutable"}
こちらもちゃんと動いているようですね。
Warning:
以下のようにしてバインドエラーおよびバリデーションエラー時のエラー情報を
BindingResult
として受け取るスタイルに変更してみたところ・・・バリデーションエラーは受け取ることができましたが、バインドエラー時は例外が発生して受け取ることができませんでした。(これ仕様なのかな!?・・・)@GetMapping("/immutable") Query search(@Validated Query query, BindingResult bindingResult) { // ... return query; }
仕様なのかバグなのかよくわからなかったので・・・Spring JIRAにIssue(SPR-15542)をあげておきました。
Jackson 2.9がサポートされる
[SPR-14925] : どうやらJackson 2.9がSpring Framework 5.0上での必須バージョンになる模様です。Jackson 2.9は現時点(2017/5/14)では正式リリースされていませんが、Spring Framework 5.0が正式リリースころにはリリースされる計画のようです。
Spring Frameworkの変更内容をみると・・・Jackson 2.9から、サーバ側の実装(POJOの実装)が問題で発生するエラーの場合はInvalidDefinitionException
がスローされるようになるため、その例外が発生した時にクライアントエラー(400 Bad Request)ではなくサーバエラー(500 Internal Server Error)になるように修正したみたいです。
JSR-367 JSON Bindingがサポートされる
[SPR-14923] : Java EE 8の構成要素であるJSON Bindingがサポートされ、JacksonおよびGSONの代替として使用することができます。
まず、Spring Boot(spring-boot-starter-web
)はJacksonに依存しているので、JSON-Bを使う場合はJacksonを依存ライブラリから取り除いて、JSON-B用のライブラリを追加する必要があります。
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <!-- Jacksonを除外 -->
</exclusion>
</exclusions>
</dependency>
<!-- ... -->
<!-- 本エントリーでは公式の参照実装であるYasson(+JSON-P実装)を追加 -->
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>1.0.0-M2</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.0-M2</version>
</dependency>
</dependencies>
<!-- ... -->
<repositories>
<!-- ... -->
<!-- yassonのリリースリポジトリを追加(マイルストーンバージョンを取得できるようにするため) -->
<repository>
<id>yasson-releases</id>
<name>Yasson Release repository</name>
<url>https://repo.eclipse.org/content/repositories/yasson-releases/</url>
</repository>
</repositories>
次に、Controllerクラスを作成します。Controllerクラスの作成は特別注意する点はありませんが、イミュータブルなクラスへのバインディングはサポートしていない模様です。
package com.example.spring5webdemo;
import java.time.LocalDate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class JsonbRestController {
@PostMapping("/jsonb")
Resource post(@RequestBody Resource resource) {
return resource;
}
public static class Resource {
private String name;
private String mail;
private String tel;
private LocalDate baseDate;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public LocalDate getBaseDate() {
return baseDate;
}
public void setBaseDate(LocalDate baseDate) {
this.baseDate = baseDate;
}
}
}
作成したエンドポイントにアクセスすると、リクエストしたJSONがJavaオブジェクトに変換(デシリアライズ)され、そのオブジェクトがJSONに変換(シリアライズ)されたことが確認できます。
$ curl -s -D - -X POST -H "Content-Type:application/json" -d '{"name":"kazuki43zoo","mail":"kazuki43zoo@gmail.com","tel":"09012345678","baseDate":"2017-08-01"}' http://localhost:8080/jsonb
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 97
Date: Tue, 16 May 2017 17:08:40 GMT
{"baseDate":"2017-08-01","mail":"kazuki43zoo@gmail.com","name":"kazuki43zoo","tel":"09012345678"}
JSON-BはHTTPクライアント用のクラスであるRestTemplate
でも利用できます。また、Spring Bootから提供されているテスト用のHTTPクライアント(TestRestTemplate
)でもJSON-Bを使うことができます。
package com.example.spring5webdemo;
import java.time.LocalDate;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // ★★★ テスト実行時に空きポートを使ってサーバを起動する
public class JsonbTests {
@LocalServerPort private int port; // ★★★ サーバの起動ポートがインジェクションされる
@Autowired TestRestTemplate testRestTemplate; // ★★★ サーバの起動ポートにアクセスするためのHTTPクライアントがインジェクションされる
@Test
public void testUsingRestTemplate() {
LocalDate now = LocalDate.now();
RestOperations restOperations = new RestTemplate(); // ★★★ デフォルトの状態でJSON-Bとの連携コンポーネントが登録される
Resource request = new Resource();
request.setName("kazuki43zoo");
request.setMail("kazuki43zoo@gmail.com");
request.setTel("09012345678");
request.setBaseDate(now);
Resource response = restOperations.postForObject("http://localhost:" + port + "/jsonb", request, Resource.class);
Assertions.assertThat(response.getName()).isEqualTo("kazuki43zoo");
Assertions.assertThat(response.getMail()).isEqualTo("kazuki43zoo@gmail.com");
Assertions.assertThat(response.getTel()).isEqualTo("09012345678");
Assertions.assertThat(response.getBaseDate()).isEqualTo(now);
}
@Test
public void testUsingTestRestTemplate() {
LocalDate now = LocalDate.now();
Resource request = new Resource();
request.setName("kazuki43zoo");
request.setMail("kazuki43zoo@gmail.com");
request.setTel("09012345678");
request.setBaseDate(now);
Resource response = testRestTemplate.postForObject("/jsonb", request, Resource.class); // ★★★ テスト用のHTTPクライアントを使うと、URLの指定はパスからの指定でOK
Assertions.assertThat(response.getName()).isEqualTo("kazuki43zoo");
Assertions.assertThat(response.getMail()).isEqualTo("kazuki43zoo@gmail.com");
Assertions.assertThat(response.getTel()).isEqualTo("09012345678");
Assertions.assertThat(response.getBaseDate()).isEqualTo(now);
}
public static class Resource {
private String name;
private String mail;
private String tel;
private LocalDate baseDate;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public LocalDate getBaseDate() {
return baseDate;
}
public void setBaseDate(LocalDate baseDate) {
this.baseDate = baseDate;
}
}
}
Google Protobuf 3.xがサポートされる
[SPR-13589] : ProtobufHttpMessageConverter
にて、Google Protobuf 3.xがサポート対象になります。どうやらデフォルト(com.google.protobuf:protobuf-java
)でサポートされるフォーマットが「application/x-protobuf
」と「text/plain
」だけになり、その他のフォーマット(「application/json
」「application/xml
」「text/html
」)については別途ライブラリ(「com.google.protobuf:protobuf-java-util
(公式)」や「com.googlecode.protobuf-java-format:protobuf-java-format
(3rdパーティー製)」)の追加が必要になるみたいです。
HandlerメソッドでReactor 3.1とRxJava 1.3/2.1のクラスを返却できる
[SPR-15365] : Reactive Programing Model(後述するSpring WebFlux)のサポートに関連し、Spring MVCでもReactor 3.1のクラス(Flux
, Mono
)、RxJava 1.3と2.1のクラス(Observable
, Sigle
, Flowable
など)をHandlerメソッドの返り値として扱うことができるようになります。Spring MVCは、これらのクラス(以降「Reactiveクラス」とする)が返却された場合は、Spring MVCの非同期処理(Servlet 3.0からサポートされた非同期処理との連携機能)を利用して処理を行う仕掛けになっています。
以前投稿した「Spring MVC(+Spring Boot)上での非同期リクエストを理解する」で紹介した通り、Spring MVCの非同期処理は以下の2つの方式にわかれます。
- 非同期実行が終了してからHTTPレスポンスを開始する方式
- 非同期実行の処理中にHTTPレスポンスを開始する方式
どちらの方式が使われるかは、返却するReactiveクラスの種類とメディアタイプの組み合わせによって変わります。具体的には「Spring Frameworkのリファレンス -Async Requests with Reactive Types- 」をご覧ください。
では、実際にReactiveクラスを返却してみましょう。
本エントリーではSpring WebFluxが利用しているReactorのクラスを使うため、まずReactorを依存ライブラリに追加します。
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.1.0.M1</version>
</dependency>
次に、ReactorのMono
とFlux
を返却するHandlerメソッドを実装します。
package com.example.spring5webdemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.Duration;
import java.util.Random;
@RestController
public class ReactiveRestController {
@GetMapping("/reactor/mono")
Mono<Resource> getMono() {
// subscribeされたらidに乱数を設定したResourceを返却する
// Subscriberの処理が別スレッドになるようにsubscribeOnを呼び出しておく
// -> このサンプルコードの処理だと別スレッドで行う必要はありませんが、
// -> 実際には「重たい処理=CPUバウンドな処理」を行うことを想定している
return Mono.fromSupplier(() -> new Resource(new Random().nextInt()))
.subscribeOn(Schedulers.parallel());
}
@GetMapping("/reactor/flux")
Flux<Resource> getFlux() {
// subscribeされたらidに0〜4を設定したResourceを0.5秒毎に返却する
// intervalを使うとSubscriberの処理は別スレッドで行われる
return Flux.from(Flux.range(0, 5)).zipWith(Flux.interval(Duration.ofMillis(500)))
.map(tuple -> new Resource(tuple.getT1()));
}
static class Resource {
private final int id;
private Resource(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
}
最後に、作成したHandlerメソッドにアクセスしてみましょう。
$ curl -s -D - http://localhost:8080/reactor/mono
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:10:34 GMT
{"id":129024480}
$ curl -s -D - http://localhost:8080/reactor/flux -H Accept:text/event-stream
HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:10:55 GMT
data:{"id":0}
data:{"id":1}
data:{"id":2}
data:{"id":3}
data:{"id":4}
$ curl -s -D - http://localhost:8080/reactor/flux -H Accept:application/stream+json
HTTP/1.1 200
Content-Type: application/stream+json
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:11:22 GMT
{"id":0}
{"id":1}
{"id":2}
{"id":3}
{"id":4}
ちなみに・・・Flux
をapplication/json
で返却すると・・・
$ curl -s -D - http://localhost:8080/reactor/flux -H Accept:application/json
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:11:48 GMT
[{"id":0},{"id":1},{"id":2},{"id":3},{"id":4}]
となり、すべてのデータがSubscriber
に渡された後(約2.5秒後)にJSONがレスポンスされます。
ParsingPathMatcher
が追加される
[SPR-14544] : AntPathMatcher
の代替として、ParsingPathMatcher
が追加されます。Spring MVCではAntPathMatcher
がデフォルトで利用されますが、Spring WebFlux関連のコンポーネントはParsingPathMatcher
がデフォルトで使われる模様です。
ちゃんと見てないですが・・・ AntPathMatcher
で表現できていたことはParsingPathMatcher
でも表現可能のようで、ParsingPathMatcher
の方は新たに「{*変数名}
」という表現がサポートされています。
以下のサンプルを例に説明すると・・・パスマッチング仕様としては「/users/**
」と同じなのですが、「{*変数名}
」を使うと「/**
」の部分を変数として扱うことができます。なお、「/users/{*userPaths}.json
」のように「{*変数名}
」の後に何かしらの文字を指定することはできません。
package com.example.spring5webdemo;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.springframework.util.PathMatcher;
import org.springframework.web.util.ParsingPathMatcher;
public class PathMatcherTests {
@Test
public void parsingPathMatcher() {
PathMatcher pathMatcher = new ParsingPathMatcher();
String pattern = "/users/{*userPaths}";
{
String path = "/users/kazuki43zoo/name";
Assertions.assertThat(pathMatcher.match(pattern, path));
Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
.containsEntry("userPaths", "/kazuki43zoo/name");
}
{
String path = "/users/kazuki43zoo";
Assertions.assertThat(pathMatcher.match(pattern, path));
Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
.containsEntry("userPaths", "/kazuki43zoo");
}
{
String path = "/users/";
Assertions.assertThat(pathMatcher.match(pattern, path));
Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
.containsEntry("userPaths", "/");
}
{
String path = "/users";
Assertions.assertThat(pathMatcher.match(pattern, path));
Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
.containsEntry("userPaths", "");
}
}
}
ParsingPathMatcher
をSpring MVCのリクエストマッチング処理で利用したい場合は、以下のようなBean定義を行うことで実現することができます。
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// ★★★ ParsingPathMatcherを適用
configurer.setPathMatcher(new ParsingPathMatcher());
// ★★★ useSuffixPatternMatchを無効化
// ★★★ useSuffixPatternMatchが有効になっていると、リクエストマッチング処理時にパターンの末尾に「`.*`」が付与されパターン解析時にエラーが発生してしまう
configurer.setUseSuffixPatternMatch(false);
}
};
}
package com.example.spring5webdemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PathMatcherRestController {
@GetMapping("/path-matcher/{*paths}") // ★★★ {*変数名}を使用して、変数値をメソッド引数として受け取る
String get(@PathVariable String paths) {
return paths;
}
}
$ curl -s -D - http://localhost:8080/path-matcher/a/b/c
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 6
Date: Tue, 16 May 2017 19:41:41 GMT
/a/b/c
Note:
ちなみに・・・デフォルトで
ParsingPathMatcher
が適用されるSpring WebFluxで「{*変数名}
」を使ったらリクエストマッピング処理でエラーになりました・・・
「{*変数名}
」使用時に「useSuffixPatternMatch=false
」にしないとダメなのはなんとなくバグ?っぽい気もするので・・・Spring JIRA(SPR-15558)を作成しました。
@ExceptionHandler
メソッドでRedirectAttributes
が受け取れる
[SPR-14651] : @ExceptionHandler
メソッドの引数としてRedirectAttributes
を受け取れるようになり、RedirectAttributes
を介してリダイレクト先とデータを連携することができるようになります。
Note:
IssueをみるとSpring Framework 4.3にもバックポートされてますね・・・。このIssueのことじゃないのかな・・・
本来なら画面を作成して確認した方がよいのですが・・・画面つくるの面倒なのでcURL使って確認します。
package com.example.spring5webdemo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class RedirectAttributesController {
@GetMapping("/redirect/{id}")
@ResponseBody
String getIdWithMessage(@PathVariable int id, @ModelAttribute("message") String message) { // RedirectAttributesに設定した値がメソッドの引数として取得できる
return id + " : " + message;
}
@GetMapping("/redirect")
String get() {
throw new MyException("error.", 100);
}
@ExceptionHandler
String handleMyException(MyException e, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("message", e.getMessage()); // ★★★ Flushスコープにメッセージを設定
redirectAttributes.addAttribute("id", e.getId()); // ★★★ リダイレクトパスのパス変数「{id}」に埋め込む値を設定
return "redirect:/redirect/{id}";
}
private static class MyException extends RuntimeException {
private final int id;
public MyException(String message, int id) {
super(message);
this.id = id;
}
public int getId() {
return id;
}
}
}
$ curl -s -D - -L http://localhost:8080/redirect
HTTP/1.1 302
Set-Cookie: JSESSIONID=13743780747DC2A73F038F2E0DECFDDD; Path=/; HttpOnly
Location: http://localhost:8080/redirect/100;jsessionid=13743780747DC2A73F038F2E0DECFDDD
Content-Language: ja-JP
Content-Length: 0
Date: Tue, 16 May 2017 21:46:50 GMT
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Date: Tue, 16 May 2017 21:46:50 GMT
100 : error.
ResponseStatusException
が追加される
[SPR-14895] : Spring WebFlux向けに作成されたResponseStatusException
がSpring MVCでも使えるようになり、ステータスコードとリーズンフレーズを指定して例外を生成・スローすることで、応答するHTTPステータスを決めることができるようになります。
package com.example.spring5webdemo;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
public class ResponseStatusExceptionRestController {
@GetMapping("/response-status")
void get() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "request invalid."); // ★★★ 任意のステータスコードとリーズンフレーズを指定して例外をスロー
}
}
$ curl -s -D - -L http://localhost:8080/response-status
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 22:34:21 GMT
Connection: close
{"timestamp":"2017-05-16T22:34:21.909+0000","status":400,"error":"Bad Request","message":"request invalid.","path":"/response-status"}
ScriptTemplateView
で「国際化」「テンプレートのフラグメント化」に必要なオブジェクトが連携されるようになる
[SPR-15064] : ScriptTemplateView
(JSR-223のスクリプトエンジンを使用したView実装)にて、「メッセージの国際化」および「テンプレートのフラグメント化」を実現するために必要となるオブジェクト(Locale
,MessageSource
など)がテンプレートエンジン側に引き渡されるようになります。
「国際化」や「テンプレートをフラグメント化」するための実装自体はSpringからは提供されません。
本エントリーでは、スクリプトエンジンとしてJava SE提供の「nashorn」を、テンプレートエンジンとして「mustache.js」を使用して「メッセージの国際化」および「テンプレートのフラグメント化」を実現してみます。
まずは、ScriptTemplateView
を使うためのコンフィギュレーションを行います。
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>mustache</artifactId>
<version>2.3.0</version> <!-- 執筆時点の最新バージョン -->
</dependency>
package com.example.spring5webdemo;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;
import java.util.Optional;
@Configuration
public class ScriptTemplateViewConfig implements WebMvcConfigurer {
private final WebMvcProperties properties;
public ScriptTemplateViewConfig(WebMvcProperties properties) {
this.properties = properties;
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
WebMvcProperties.View view = properties.getView();
registry.scriptTemplate()
.prefix(Optional.ofNullable(view.getPrefix()).orElse("classpath:/templates/"))
.suffix(Optional.ofNullable(view.getSuffix()).orElse(".html"));
}
@Bean
ScriptTemplateConfigurer ScriptTemplateConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("/META-INF/resources/webjars/mustache/2.3.0/mustache.js", "/META-INF/js/render.js");
configurer.setRenderFunction("render");
return configurer;
}
}
Mustache標準のレンダリング処理をカスタマイズするためのJavaScriptファイルを作成します。
カスタマイズという表現にしましたが、実際はカスタム関数をモデルに追加して、Mustacheのrender
関数を呼び出しているだけです。
function render(template, model, renderingContext) {
model["message"] = function () { // ★★★ 「メッセージの国際化」用のカスタム関数を追加
return function (code, render) {
var messageSource = renderingContext.applicationContext.getBean("messageSource");
var locale = renderingContext.locale;
return render(messageSource.getMessage(code.trim(), null, locale));
}
};
model["include"] = function () { // ★★★ 「テンプレートのフラグメント化」用のカスタム関数を追加
return function (viewName, render) {
return render(renderingContext.templateLoader.apply("templates/" + viewName.trim() + ".html"));
}
};
return Mustache.render(template, model, renderingContext);
}
レンダリングメソッドの第3引数にRenderingContext
のインスタンスが渡されるので、そこから必要なオブジェクトを取得して処理を行います。
では、テンプレートファイルとControllerを作って実際に動かしてみます。
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
<p>{{#message}}welcome{{/message}}</p> <!-- ★★★ カスタム関数を利用してメッセージを出力 -->
{{#include}}script-template/footer{{/include}} <!-- ★★★ カスタム関数を利用してフラグメントをインクルード -->
</body>
</html>
<footer>
Copyright (c) 2017 kazuki43zoo@com
</footer>
package com.example.spring5webdemo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletResponse;
@Controller
public class ScriptTemplateController {
@GetMapping("/script-template")
String home(Model model, HttpServletResponse response) {
model.addAttribute("title", "Sample title")
.addAttribute("body", "Sample body");
return "script-template/home";
}
}
あと、メッセージ定義ファイルの作成も必要です。
welcome=こんにちは!
welcome=Hello!
Spring Bootを起動して、http://localhost:8080/script-template にアクセスすると・・・・
$ curl -s -D - http://localhost:8080/script-template
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Language: ja-JP
Content-Length: 183
Date: Sat, 20 May 2017 01:51:53 GMT
<html>
<head>
<title>Sample title</title>
</head>
<body>
<p>Sample body</p>
<p>こんにちは!</p>
<footer>
Copyright (c) 2017 kazuki43zoo.com
</footer>
</body>
</html>
Accept-Language
をen
(英語)にして再度アクセスすると・・・
$ curl -s -D - -H Accept-Language:en http://localhost:8080/script-template
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Language: en
Content-Length: 171
Date: Sat, 20 May 2017 01:53:46 GMT
<html>
<head>
<title>Sample title</title>
</head>
<body>
<p>Sample body</p>
<p>Hello!</p>
<footer>
Copyright (c) 2017 kazuki43zoo.com
</footer>
</body>
</html>
といった感じでレスポンスされ、「メッセージの国際化」「フラグメントのインクルード」が行われたことが確認できます。
まとめ
今回は、WebMVC関連の主な変更点を紹介しました。Spring Framework 5.0からReactive Programing ModelのWebフレームワーク(Spring WebFlux)が追加され、そちらに注目が集まってますが・・・Spring MVCもまだまだ現役です(とりあえず・・・お仕事ではSpring MVCを使うことの方が圧倒的に多いだろうな〜と思うし )。
次回は、「Test関連の主な変更点」を紹介する予定です。