Spring MVC で Kotlin モデルに POST しようとしてハマった

かなりハマったのでメモ。ハマった状況としては以下。

- 構造的なデータを POST 送信するフォームをもった HTML
- 送信は `application/x-www-form-urlencoded` を使う
- 受けるコントローラは、通常の `@Controller`
- コントローラメソッドでは、モデルクラスにマッピングさせて受ける
- モデルクラスの構造は以下のようなもの

```json
{
  "headerId": 0,
  "shopName": "",
  "items": [ {
    "itemId": 0,
    "itemName": "",
    "price": 0,
    "quantity": 0,
  } ]
}
```

# 環境

- macOS 10.12.4
- Apache Maven 3.3.9 (Maven Wrapper)
- Oracle JDK 1.8.0_121
- Kotlin 1.1.2-2
- Spring Boot 2.0.0.BUILD-SNAPSHOT

```
$ ./mvnw -version
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T01:41:47+09:00)
Maven home: /Users/yo1000/.m2/wrapper/dists/apache-maven-3.3.9-bin/2609u9g41na2l7ogackmif6fj2/apache-maven-3.3.9
Java version: 1.8.0_121, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.12.4", arch: "x86_64", family: "mac"

$ cat pom.xml | grep '<kotlin.version>'
                <kotlin.version>1.1.2-2</kotlin.version>
$ cat pom.xml | grep 'spring-boot-starter-parent' -A 1 | grep version
        <version>2.0.0.BUILD-SNAPSHOT</version>
```

# 検証コード

```kotlin:ShoppingController.kt
@Controller
@RequestMapping("demo/shopping")
class ShoppingController(
        val shoppingService: ShoppingService
) {
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun postIndex(
            @LoginUserPrincipal loginUser: LoginUser,
            shopping: Shopping) {
        shoppingService.create(shopping.copy(created = Date()));
    }
}
```

```html:index.html&nbsp;抜粋
<form method="post" th:action="@{/demo/shopping}">
    <fieldset class="form-group form-inline row jsShoppingHeader">
        <h2>
            <input type="text"  class="form-control form-control-lg"    name="headerId" placeholder="headerId"
            > : <input type="text"  class="form-control form-control-lg"    name="shopName" placeholder="shopName">
            <button type="button" class="btn btn-primary btn-lg jsShoppingSend">Send</button>
        </h2>
    </fieldset>
    <fieldset class="form-group form-inline row">
    <table class="table">
        <thead>
        <tr>
            <th>itemId</th>
            <th>itemName</th>
            <th>price</th>
            <th>quantity</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        <tr class="jsShoppingItem" th:each="i : ${#numbers.sequence(1, 10)}">
            <td><input type="text"  class="form-control form-control-sm"    name="itemId"   placeholder="itemId"></td>
            <td><input type="text"  class="form-control form-control-sm"    name="itemName" placeholder="itemName"></td>
            <td><input type="text"  class="form-control form-control-sm"    name="price"    placeholder="price"></td>
            <td><input type="text"  class="form-control form-control-sm"    name="quantity" placeholder="quantity"></td>
            <td><em th:text="'#item' + ${i}">1</em></td>
        </tr>
        </tbody>
    </table>
    </fieldset>
</form>
<script th:inline="javascript">
(function() {
    $('.jsShoppingSend').on('click', function() {
        var url = /*[[ @{/demo/shopping} ]]*/''
        var _csrf = $('input[name=_csrf]').val()
        var data = {
            headerId : $('.jsShoppingHeader input[name=headerId]').val(),
            shopName : $('.jsShoppingHeader input[name=shopName]').val(),
        }

        var cnt = 0

        $('.jsShoppingItem').each(function(index, element){
            var itemId      = $(element).find('input[name=itemId]').val()
            var itemName    = $(element).find('input[name=itemName]').val()
            var price       = $(element).find('input[name=price]').val()
            var quantity    = $(element).find('input[name=quantity]').val()

            if (!itemId || !itemName || !price || !quantity) {
                return
            }

            data['items[' + cnt + '].itemId']     = itemId;
            data['items[' + cnt + '].itemName']   = itemName;
            data['items[' + cnt + '].price']      = price;
            data['items[' + cnt + '].quantity']   = quantity;

            cnt++
        })

        var token = $("meta[name='_csrf']").attr("content")
        var header = $("meta[name='_csrf_header']").attr("content")

        var headers = {}
        headers[header] = token

        $.post({
            headers     : headers,
            url         : url,
            data        : $.param(data),
            processData : false
        }).done(function (data, textStatus, jqXhr) {
            if (jqXhr.status === 201) {
                location.href = url
            }
        })
    })
})()
</script>
```

# 現象

POST 送信すると以下のようなエラーが吐かれる。コンストラクタが見つからないとのこと。

```
2017-05-11 19:58:26.331 ERROR 5831 --- [io-60080-exec-8] 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.beans.BeanInstantiationException: Failed to instantiate [com.yo1000.demo.model.Shopping]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.yo1000.demo.model.Shopping.<init>()] with root cause

java.lang.NoSuchMethodException: com.yo1000.demo.demo.model.Shopping.<init>()
        at java.lang.Class.getConstructor0(Class.java:3082) ~[na:1.8.0_121]
        at java.lang.Class.getDeclaredConstructor(Class.java:2178) ~[na:1.8.0_121]
        at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:105) ~[spring-beans-5.0.0.RC1.jar:5.0.0.RC1]
```

このとき、使用していたモデルクラスは以下。

```kotlin:Shopping.kt
data class Shopping(
        val headerId: Long,
        val shopName: String,
        val created: Date,
        val items: MutableList<ShoppingItem>
)
```

```kotlin:ShoppingItem.kt
data class ShoppingItem(
        val itemId: Long,
        val itemName: String,
        val price: Int,
        val quantity: Int,
        val subTotal: Int
)
```

全てのプロパティが必須指定になっていたため、デフォルトコンストラクタが見つからない、というエラーを吐いたようだった。

また、デフォルトコンストラクタを呼び出すということからも分かるように、各フィールドへの値の設定は、インスタンス作成後に実施される。つまり、`val` は使えないということになる。

# 解決方法

以上を踏まえて、改めてモデルクラスを作成し直したところ、問題なく動作した。

```kotlin:Shopping.kt
data class Shopping(
        var headerId: Long = 0L,
        var shopName: String = "",
        var created: Date = Date(),
        var items: MutableList<ShoppingItem> = mutableListOf()
)
```

```kotlin:Shopping.kt
data class Shopping(
        var itemId: Long = 0L,
        var itemName: String = "",
        var price: Int = 0,
        var quantity: Int = 0,
        var subTotal: Int = 0
)
```

せっかく Kotlin を使っているというのに、`val` が使えないというのが、なんとも釈然としないが、`application/x-www-form-urlencoded` でフォームデータを送信し、モデルクラスにマッピングして受け取ろうとした場合には、現状このようにするしかないようだ。

Spring MVC、Spring Boot などのアップデートによって、このあたりが解消されていくことを期待。

また、構造的なデータを扱う場合には、`application/json` で送信すれば、Jackson Module Kotlin という Kotlin 対応モジュールがすでにリリースされており、問題なく対応できるようなので、こちらの利用も検討されたし。

- https://github.com/FasterXML/jackson-module-kotlin
- https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin

# 参考

- https://www.bountysource.com/issues/33221033-using-an-object-to-deserialize-form-data
- http://siosio.hatenablog.com/entry/2016/06/10/080446