今回は、Springが提供しているView実装をサポートするコンポーネント(JSPタグライブラリ、Freemarkerのマクロなど)や、3rdパーティ製の連携コンポーネント(ThymeleafのSpring連携用のDialectなど)を使用して生成したチェックボックスを空(=未選択)の状態でフォームをサブミットした際に、フォームオブジェクトに定義したCollection
型(List
やSet
など)に設定される値がSpring 4.2とSpring 4.3で変わるよ〜という話をします。
Note:
ちなみに・・・
Collection
型ではなく配列を使っている場合は、動作は変わりません。(空要素の配列が設定されます)
Spring 4.2とSpring 4.3の違いは・・・
いきなり結論から言うと・・・
Spring 4.2ではnull
がバインドされるのですが、Spring 4.3からは空要素のCollectionオブジェクトがバインドされます。
Note:
Spring利用者からの改善要望(SPR-13502)によって、Spring 4.3から動作仕様が変更されました。
実際に確認してみると・・・
Thymeleaf(Thymeleaf-Spring)を使用して具体例をみてみましょう。ここでは、簡易的にSpring Bootアプリケーションの中にControllerとフォームクラスを作ってしまいます。
package com.example;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@SpringBootApplication
public class CheckboxDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CheckboxDemoApplication.class, args);
}
@RequestMapping("/sample")
@Controller
static class SampleController {
@ModelAttribute // フォームをモデルに格納するためのメソッド
SampleForm setUpForm() {
return new SampleForm();
}
@RequestMapping // フォーム画面を表示するためのHandlerメソッド
String form() {
return "sample/form";
}
@RequestMapping(method = RequestMethod.POST) // フォーム画面からのサブミットを受けるためのHandlerメソッド
String submit(SampleForm form) {
System.out.println("-----------------------");
System.out.println("list :" + form.getList());
return "sample/form";
}
}
static class SampleForm {
private List<String> list; // チェックボックスの値を保持するためのプロパティ
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Sample</title>
</head>
<body>
<div class="container">
<form method="post" th:object="${sampleForm}">
<div class="form-group">
<label>List</label> <!-- チェックボックスの選択値を2つ用意 -->
<ul>
<li>
<input type="checkbox" th:field="*{list}" value="1"/>
<label th:for="${#ids.prev('list')}">A</label>
</li>
<li>
<input type="checkbox" th:field="*{list}" value="2"/>
<label th:for="${#ids.prev('list')}">B</label>
</li>
</ul>
</div>
<button class="btn btn-default">Submit</button>
</form>
</div>
</body>
</html>
Spring Bootアプリケーションを起動して「http://localhost:8080/sample」にアクセスすると、以下のような(しょぼいw)画面が表示されます。
Spring 4.2では?
Spring Boot 1.3.8.RELEASE(Spring 4.2系向けの最新リリース版)で「Submit」ボタンを押下すると、コンソール上に・・・
...
2016-12-23 19:40:42.727 INFO 38839 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 13 ms
-----------------------
list :null
と表示されます。つまり・・・null
がバインドされます。
Spring 4.3では?
一方・・・Spring Boot 1.4.3.RELEASE(Spring 4.3系向けの最新リリース版)で「Submit」ボタンを押下すると、コンソール上に・・・
...
2016-12-23 19:40:42.727 INFO 38839 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 13 ms
-----------------------
list :[]
と表示されます。これは、null
ではなく空のリストがバインドされていることを意味します。
なんか困る or 影響あるんだっけ?
null
が空要素のCollectionになって困ること or 影響があるか否かはあなたのアプリケーションの作りに依存します。たとえば・・・ もともとnull
と空要素は同じものとして扱っているアプリケーションでは困らない(=影響はない)と思います。
しかし、null
と空要素を別物として扱っているアプリケーションでは、なんらかの対応が必要になる可能性があります。
たとえば・・・・以下のようにチェックボックス項目に対する必須チェックをnull
か否かで行っていた場合は、入力チェックをすり抜けてロジック層に処理が流れてきてしまいます。
static class SampleForm {
@NotNull // Bad ... 空の要素がバインドされるのでnullチェックだとNG!!
private List<String> list;
// ...
}
このケースでは、Bean Validation標準の@Size
がHibernate Validator提供の@NotEmpty
を使う必要があります。
static class SampleForm {
@Size(min = 1) // Good
private List<String> list;
// ...
}
or
static class SampleForm {
@NotEmpty // Good
private List<String> list;
// ...
}
どんな仕組みで初期値(デフォルト値)を設定してるのか?
Springが提供しているView実装をサポートするコンポーネント(JSPタグライブラリ、Freemarkerのマクロなど)や、3rdパーティ製の連携コンポーネント(ThymeleafのSpring連携用のDialectなど)を使用してチェックボックス、セレクトボックスを生成すると、フォームオブジェクトのプロパティを初期化するための特殊なパラメータ(_
で始まるパラメータ)がhidden項目として埋め込まれます。(本投稿ではこの特殊パラメータのことを以降「リセットパラメータ」と呼びます)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Sample</title>
</head>
<body>
<div class="container">
<form method="post">
<div class="form-group">
<label>List</label>
<ul>
<li>
<input type="checkbox" value="1" id="list1" name="list" /><input type="hidden" name="_list" value="on" />
<label for="list1">A</label>
</li>
<li>
<input type="checkbox" value="2" id="list2" name="list" /><input type="hidden" name="_list" value="on" />
<label for="list2">B</label>
</li>
</ul>
</div>
<button class="btn btn-default">Submit</button>
</form>
</div>
</body>
</html>
今回の例だと、_list
がリセットパラメータになります。値が「on
」となっていますが、このリセットパラメータはマーカーパラメータなので値に意味はありません。
では、リセットパラメータを送るとSpring MVCはどのような動きをするのでしょうか?
リセットパラメータを送ると・・・通常のリクエストパラメータ(上の例だとlist
)がリクエストパラメータとして送られなかった(=チェックボックスを空にすると送られない)場合に、フォームオブジェクトのプロパティ値を初期化してくれます。
この初期化時に使われる値が、Spring 4.2まではnull
でしたが、Spring 4.3から空要素のCollectionになったというわけです。
Note:
具体的には・・・
org.springframework.web.bind.WebDataBinder
のgetEmptyValue
メソッドの中で初期化時に設定する値を決めています。Spring 4.3からは、java.util.Collection
の実装クラスとjava.util.Map
の実装クラスに対する初期値がかわっています。
なぜリセットパラメータが必要なのか?
リセットパラメータって必要なん???と思った方もいるのではないでしょうか?
結論からいうと・・・必要なのです。
具体的には・・・フォームオブジェクトを@SessionAttributes
を使ってセッションスコープで管理している場合で、以下のような操作・・・・
- フォーム画面を表示する
- フォーム画面でチェックボックスを1つチェックをしてサブミットする
- フォーム画面に再度戻り、チェックボックスの選択状態を解除してサブミットする
を行うと、チェックボックスの選択状態が元の状態(1つチェックしている状態)に戻ってしまいます。
状態が戻ってしまう理由は・・・チェックボックスを全て未選択の状態にするとサブミット時に該当項目のリクエストパラメータが送られないため、セッション上で管理しているフォームオブジェクトの状態を更新する処理が実行されず、結果として前の状態が残ってしまうのです。
Spring MVCは、上記のような状態になることを回避するするために、「リセットパラメータ」という特殊なパラメータを用意しているのです。
Note:
入力項目を非活性(disabled)にするとサブミット時にリクエストパラメータが送られないので、活性・非活性をユーザ操作によって切り替えるUIになっている場合は、リセットパラメータを一緒に送らないと同様の事象が発生する可能性があります。
まとめ
今回は、Spring MVCが提供するリセットパラメータ適用時に設定される値がSpring 4.2と4.3で異なるよ〜という話をしました。null
と空要素を同一視して処理するのは割と一般的?な気もしますが、あなたのアプリケーションはどうなっているでしょうか?
とりあえず、Spring 4.3にアップデートする時は、チェックボックスとセレクトボックス項目の扱いには注意した方がよさそうです。