Spring 4.2 vs 4.3 : チェックボックを空でサブミットした時のCollection型へのバインド値の違い

今回は、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](https://jira.spring.io/browse/SPR-13502)）によって、Spring 4.3から動作仕様が変更されました。


# 実際に確認してみると・・・

Thymeleaf(Thymeleaf-Spring)を使用して具体例をみてみましょう。ここでは、簡易的にSpring Bootアプリケーションの中にControllerとフォームクラスを作ってしまいます。

```java
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;
		}

	}

}
```

```html
<!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> <!-- チェックボックスの選択値を２つ用意 -->
            <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](http://localhost:8080/sample)」にアクセスすると、以下のような（しょぼいw）画面が表示されます。

<img width="871" alt="checkbox-form.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/fd9047cb-39ac-53c7-c380-adf4ef15d784.png">


## 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`か否かで行っていた場合は、入力チェックをすり抜けてロジック層に処理が流れてきてしまいます。

```java
static class SampleForm {

	@NotNull // Bad ... 空の要素がバインドされるのでnullチェックだとNG！！
	private List<String> list;

	// ...

}
```

このケースでは、Bean Validation標準の`@Size`がHibernate Validator提供の`@NotEmpty`を使う必要があります。

```java
static class SampleForm {
	@Size(min = 1) // Good
	private List<String> list;
	// ...
}
```

or

```java
static class SampleForm {
	@NotEmpty // Good
	private List<String> list;
	// ...
}
```

# どんな仕組みで初期値（デフォルト値）を設定してるのか？

Springが提供しているView実装をサポートするコンポーネント(JSPタグライブラリ、Freemarkerのマクロなど)や、3rdパーティ製の連携コンポーネント(ThymeleafのSpring連携用のDialectなど)を使用してチェックボックス、セレクトボックスを生成すると、フォームオブジェクトのプロパティを初期化するための特殊なパラメータ(`_`で始まるパラメータ)がhidden項目として埋め込まれます。（本投稿ではこの特殊パラメータのことを以降「リセットパラメータ」と呼びます）

```html:Thymeleafによって生成されたHTMLの中身

<!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. フォーム画面を表示する
2. フォーム画面でチェックボックスを１つチェックをしてサブミットする
3. フォーム画面に再度戻り、チェックボックスの選択状態を解除してサブミットする

を行うと、チェックボックスの選択状態が元の状態（１つチェックしている状態）に戻ってしまいます。
状態が戻ってしまう理由は・・・チェックボックスを全て未選択の状態にするとサブミット時に該当項目のリクエストパラメータが送られないため、セッション上で管理しているフォームオブジェクトの状態を更新する処理が実行されず、結果として前の状態が残ってしまうのです。

Spring MVCは、上記のような状態になることを回避するするために、「リセットパラメータ」という特殊なパラメータを用意しているのです。

> *Note:*
>
> 入力項目を非活性(disabled)にするとサブミット時にリクエストパラメータが送られないので、活性・非活性をユーザ操作によって切り替えるUIになっている場合は、リセットパラメータを一緒に送らないと同様の事象が発生する可能性があります。

# まとめ

今回は、Spring MVCが提供するリセットパラメータ適用時に設定される値がSpring 4.2と4.3で異なるよ〜という話をしました。`null`と空要素を同一視して処理するのは割と一般的？な気もしますが、あなたのアプリケーションはどうなっているでしょうか？
とりあえず、Spring 4.3にアップデートする時は、チェックボックスとセレクトボックス項目の扱いには注意した方がよさそうです。