SpringでフォームにMapを使うとLinkedHashMapであってもサブミット時に順序が保証されない

SpringでフォームにMapのフィールドを作って使う場合、LinkedHashMapを使って順序を指定したつもりでも、その値をサブミットすると順番が入れ替わってしまった。

# 要素技術

- Java
- Spring
- Thymeleaf

# 発生した事象
![01_初期表示.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513231/2920b44e-1f4a-137c-f3b7-592186c6ab6c.png)
このとき submit ボタンをクリックすると以下のような画面になる。

![02_サブミット後.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513231/6b033893-2124-659a-0987-136de6689928.png)

「spring」と「java」が入れ替わっているだと・・・

# 作成したコード（ダメなコード）

■ コントローラ
ポイントはinitメソッドで
1. LinkedHashMapを使ってMapを作成しており、順序を意識（しようと）している
2. model.addAttributeでフォームをモデルに登録しているところ

```java
@RequestMapping("/contents/convertor/app")
@Controller
public class ConvertorController {

	private ConvertorForm form;

	@Autowired
	public ConvertorController(ConvertorForm form){
		this.form = form;
	}

	/**
	 * 初期表示用
	 */
	@GetMapping
	public void init(Model model) {
		Map<String, String> map = new LinkedHashMap<String, String>(); // LinkedHashMapで順番に登録しようと試みる 
		map.put("spring", "SPRING");
		map.put("java", "JAVA");
		form.setMap(map);
		model.addAttribute("convertorForm", form); // フォームをモデルに登録
	}

	/**
	 * サブミットしたときに動作する。
	 */
	@PostMapping
	public void submit(Model model, ConvertorForm form) {
	}
}
```

■ フォーム（Mapだけを持つSessionScopeのBean）

```java
@Component
@SessionScope
public class ConvertorForm {
	private Map<String, String> map;

	public Map<String, String> getMap() {
		return map;
	}
	public void setMap(Map<String, String> map) {
		this.map = map;
	}
}
```

■ HTML(Thymeleaf)

```html
<form th:action="@{/contents/convertor/app}" method="post" th:object="${convertorForm}">
  <th:block th:each="element : *{map}">
    <span th:text="${element.getKey()}"></span>
    <input type="text" th:field="${convertorForm.map[__${element.getKey()}__]}"/>
    <br />
  </th:block>
  <button>submit</button>
</form>
```

# 解決方法

Controller を以下のように修正する。
修正箇所は model.addAttribute でモデルに map を追加していた箇所を@ModelAttribute で設定するだけ。
@ModelAttributeを使うことで、モデルに属性を追加できることは知っていたのですがmodel.addAttributeとの違いは登録するタイミングだけの問題で大差ないと思っておりました。以下解説が続きますので、是非ご一読ください。

```java
@Controller
public class ConvertorController {

	private ConvertorForm form;

	@Autowired
	public ConvertorController(ConvertorForm form){
		this.form = form;
	}

	/**
	 * 追加箇所
	 */
	@ModelAttribute
	public ConvertorForm setup() {
		return form;
	}

	/**
	 * 初期表示用
	 * @param model
	 */
	@GetMapping
	public void init(Model model) {
		Map<String, String> map = new LinkedHashMapCustom<String, String>();
		map.put("spring", "SPRING");
		map.put("java", "JAVA");
		form.setMap(map);
//		↓を削除して@ModelAttributeにする
//		model.addAttribute("convertorForm", form); // フォームをモデルに登録
	}

	/**
	 * サブミットしたときに動作する。
	 *
	 * @param model
	 * @param form
	 */
	@PostMapping
	public void submit(Model model, ConvertorForm form) {
	}
}
```

# 解説（簡易版）

Springにおいて、リクエストで値が送られてきたときにどのようにフォームにマッピングされるのかが非常に重要になってくる。
submit ボタンをクリックしたときに、送られてきたリクエストからコントローラのsubmitメソッドの引数であるConvertorFormを生成しようとする。この時以下のような動作をする。

## 修正前

1. Formがない！Formを新たに作成しよう！
1. 当然Mapもないから作成しよう！
1. mapに入れる値はput しておこう！どうせ詰めるなら、リクエストで来た値はソートして Map に格納してあげよう！

結果、「spring」と「java」はソートされるので順序が入れ替わっている。

## 修正後

1. @ModelAttribute でモデルに Autowired したFormを詰めよう！
1. Form がある。よし、何もしなくて良いな！
1. Map もある。よし、何もしなくて良いな！
1. map に入れる値は put しておこう！

結果、「spring」と「java」は put されるだけであるため順序が入れ替わらない。  
当たり前かもしれないが、@ModelAttribute は値をバインドする前に動作する。これが肝になっていた。
はまっているときはこういうところに気がつかないんだよなぁ・・・

# 解説（もう少し詳細）

私個人的には、Map への格納はリクエストで来た順。つまりServletRequest に格納されている順序でSpringさんが勝手にput していってくれるものとばかり考えていたた。
そこで、そもそもなぜソートされるのか？なぜリクエストの順序ではないのか？が気になったため調査してみた。

Spring で値のバインドは org.springframework.web.bind.ServletRequestDataBinder.bindで行われている。

```java
public void bind(ServletRequest request) {
    MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); // ← ここ
    MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
    if (multipartRequest != null) {
        bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
    }
    else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {
        HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
        if (httpServletRequest != null) {
            StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());
        }
    }
    addBindValues(mpvs, request);
    doBind(mpvs);
}
```

この bind メソッドの 1 行目 ServletRequestParameterPropertyValues のコンストラクタを少したどるとは以下のような実装になっている。
コメントで記載しているが、request.getParameterしたものを TreeMap に格納していくためソートされているようである。

```java
public static Map<String, Object> getParametersStartingWith(ServletRequest request, @Nullable String prefix) {
    Assert.notNull(request, "Request must not be null");
    Enumeration<String> paramNames = request.getParameterNames();
    Map<String, Object> params = new TreeMap<>(); // ← TreeMapを使ってる！！！！
    if (prefix == null) {
        prefix = "";
    }
    while (paramNames != null && paramNames.hasMoreElements()) {
        String paramName = paramNames.nextElement();
        if (prefix.isEmpty() || paramName.startsWith(prefix)) {
            String unprefixed = paramName.substring(prefix.length());
            String[] values = request.getParameterValues(paramName);
            if (values == null || values.length == 0) {
                // Do nothing, no values found at all.
            }
            else if (values.length > 1) {
                params.put(unprefixed, values);
            }
            else {
                params.put(unprefixed, values[0]);
            }
        }
    }
    return params;
}
```
うーーーーん個人的にはソートされない方が直感的なのですが、どうなのでしょうか？  
どなたかご意見ある方教えてください。。。。

# 総括
- 実行順は当たり前だが、@ModelAttribute⇒DataBinder⇒ コントローラメソッド(model.addAttribute)
- 基本的に@ModelAttribute を使う方が無難な気がする
- @InitBinder とも関連があるため要注意（そのうち記事を書くかも）