Spring がリクエストパラメータを処理する流れを理解したい

`@ModelAttribute` を利用したリクエストパラメータをオブジェクトにバインドする処理ってどうなってるんだっけ、と調べることがあったので結果をまとめておく。
あまりドキュメントにも記載がないことが多く、ソースコードを読んだ結果をまとめている。

なお、`@RequestParam` でもリクエストパラメータを取得できるが、今回は対象外。

# 環境

- Spring Boot 2.1.6.RELEASE

# `@ModelAttribute` が利用できる箇所

- メソッド
- Handler メソッドの引数

実は、Handler メソッドの引数には `@ModelAttribute` 付与しなくてもよかったりする。
デフォルトの挙動では、Handler メソッドの引数に `BeanUtils.isSimpleProperty` で `false` と判定されるクラスを指定すると、`@ModelAttribute` が付与された場合と同様の動作となる。
具体的には、プリミティブ型とそのラッパー、`Enum`、`String`、`CharSequence`、`Number`、`Date`、`URI`、`URL`、`Locale`、`Class`、配列、以外のクラス。
ただ、優先度は一番低いので、他の `HandlerMethodArgumentResolver` が引数を解決できるのであれば、そちらで解決される。

## `ModelAttributeMethodProcessor`

Spring では、`HandlerAdapter` が  Handler メソッドを呼び出す。いくつかの実装クラスがあるが、`@RequestMapiing` を利用している場合は、`RequestMappingHandlerAdapter` が Handler メソッドの呼び出しを担う。
このクラスが Handler メソッドの引数に渡す値を解決したり、戻り値をハンドリングしたりしていて、Handler メソッドの引数に `Model` や `RedirectAttributes` を指定するといい感じに処理できるのは、このクラスが頑張っているから。

で、実際に引数に渡す値を解決するのは `HandlerMethodArgumentResolver` の実装クラスの役割で、`@ModelAttribute` が指定された場合には `ModelAttributeMethodProcessor` が処理を行っている。

# メソッドに `@ModelAttribute` を付与した場合の挙動

ここは本題ではなかったが、`@ModelAttribute` を指定したメソッドの動作もまとめておく。

メソッドの戻り値を `Model` に格納する処理が Handler メソッドの実行前に処理が行われる。
（厳密には `ModelAndViewContainer` 内の `ModelMap` だが、ややこしくなりそうなので `Model` ということにしておく。）
例えば以下のような Controller を作ったとする。

```java
@Controller
public class HelloController {}
    @ModelAttribute
    public User setUp() {
        return new User();
    }

    @GetMapping("/")
    public String index() {
        return "hello";
    }
}
```

その際の挙動は以下のようなイメージになる。（あくまでもイメージで厳密には違う。）

```java
@Controller
public class HelloController {
    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute(new User());
        return "hello";
    }
}
```

以下のように`@ModelAttriubte` に属性名を指定した場合は、

```java
@ModelAttribute("hoge")
public User setUp() {
    return new User();
}
```

`model.addAttribute()` の第一引数に追加されるイメージ。

```java
@GetMapping("/")
public String index(Model model) {
    model.addAttribute("hoge", new User());
    return "index";
}
```

ちなみに、`@ModelAttribute` が付与されたメソッドを呼び出しているのは、`ModelFactory` というクラス。

# Handler メソッドの引数に `@ModelAttribute` を付与した場合の挙動

ここからが本題。

## オブジェクトの取得、生成

まず、`Model` から指定されたオブジェクトを取得する。`@ModelAttribute` に `value` or `name` を指定した場合は `Model` から取得する際の key を指定できる。省略した場合は、クラス名から Spring が自動的に決定する。

`Model` から取得できるパターンはいくつかあり、前述の `@ModelAttribute` を付与したメソッドで `Model` に格納していたり、リダイレクト時に Flash スコープを利用していたり、`@SessionAttribute` で Session スコープを利用したりしていると、Handler メソッド実行前（というより引数に渡す値の解決前）に `Model` に格納されていることがある。

`Model` からオブジェクトが取得できなかった場合は、オブジェクトを生成する。
コンストラクタが 1 つの場合はそれを利用し、複数ある場合はデフォルトコンストラクタを利用するが、このときデフォルトコンストラクタが無い場合は死ぬ。
引数ありコンストラクタを利用する場合は、リクエストパラメータをバインドする処理が挟まるが、これは後述。

## リクエストパラメータのバインド

取得 or 生成したオブジェクトに対して、リクエストパラメータの値をバインドする。
リクエストパラメータの名前と一致するオブジェクトのフィールドに対して値がバインドされる。

オブジェクトの値を上書きするが、リクエストパラメータに含まれない値に対しては何も行わない。

```java
@Controller
public class HelloController {
    @ModelAttribute
    public User setUp() {
        User user = new User();
        user.setName("なまえ");
        user.setEmail("メール");
        return user;
    }

    @GetMapping("/")
    public String index(User user) {
        return "hello";
    }
}
```

`User` が `name` と `email` というフィールドを持っているとする。
で、以下のように `name` だけをリクエストパラメータに付与して送信する。

```
curl http://localhost:8080?name=hogehoge
```

`User` オブジェクトの `name` は `hogehoge` に、`email` は `メール` となり、`null` で上書きされることはない。

### パラメータありのコンストラクタでオブジェクトを生成する場合

パラメータありのコンストラクタを用いてオブジェクトを生成する場合は、そのタイミングでデータのバインドを試みる。
この場合、フィールド名ではなくコンストラクタのパラメータ名 or `@ConstructorProperties` で指定された値と一致するリクエストパラメータをバインドする。

```java
public class User {
    private String name;
    private String email;

    public User(String n, String e) {
        this.name = n;
        this.email = e;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}
```

上記のようなコンストラクタの場合、リクエストパラメータ名は `n` や `e` にしないといけない。

```java
public class User {
    private String name;
    private String email;

    @ConstructorProperties({"name", "email"})
    public User(String n, String e) {
        this.name = n;
        this.email = e;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}
```

上記のように `@ConstructorProperties` を使えば、リクエストパラメータ名は `name` や `email` にできる。なお、`@ConstructorProperties` がある場合はそちらが優先されてしまうため、`n` や `e` では動作しなくなる。

ただ、オブジェクトの生成が完了した後に、通常と同様のリクエストパラメータをバインドする処理が実行されるので、setter がある場合や、ダイレクトフィールドアクセスが可能な `DataBinder` を利用している場合は、そちらの結果が優先されてしまう。

例えば、以下のようなクラスを作った場合、

```java
public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name + "hoge";
        this.email = email + "fuga";
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
```

コンストラクタでは、`hoge` や `fuga` を付与してフィールドに保持しているが、その後 setter が実行されてしまうので、最終的にバインドされる値には、`hoge` や `fuga` が付与されていない。

## リクエストパラメータのバインドを抑止する

`@ModelAttribute` の `binding` を `false` にすることで、リクエストパラメータのバインドを抑止することができる。

```java
@Controller
public class HelloController {
    @ModelAttribute
    public User setUp() {
        User user = new User();
        user.setName("なまえ");
        user.setEmail("メール");
        return user;
    }

    @GetMapping("/")
    public String index(@ModelAttribute(binding = false) User user) {
        return "hello";
    }
}
```

上記のような状態で以下のリクエストを送信しても `User` のオブジェクトの `name` は `なまえ`、`email` は `メール` のままとなる。

```
curl http://localhost:8080?name=hogehoge&email=fugafuga
```

## バリデーション

引数に `@Validated` が付与されるなど、バリデーションが必要な場合は実施する。

バリデーションの結果、エラーがある場合でかつ Handler メソッドの直後の引数に `Errors` がない場合、`BindingException` がスローされ、ある場合は `BindingResult` として結果を保持する。（`Errors` は `BindingResult` の親インターフェース）

ちなみに Handler メソッドで `BindingResult` を引数に設定する際に、順番に気を付けなければならないのは、ここの実装によるもの。

話が横道にそれるが Handler メソッドの引数に `BindingResult` を設定した場合、`ErrorsMethodArgumentResolver` というクラスが引数の値を解決する。
このクラスは `Model` に格納された最後の要素が `BindingResult` かどうかをチェックし、そうであれば引数に設定するという処理を行っている。
そのため、バリデーションを行って `BindingResult` を `Model` に格納した直後に実行されないと上手く動作しない可能性がある。
なので、Handler メソッドの引数には `@Validated` を付与した引数の直後に `BindingResult` が無いといけないっぽい。

これを調べて思ったが、以下のようにリクエストパラメータをバインドするオブジェクトを 2 つに分けて、それぞれのバリデーション結果を取得するようなコードも実現できる。
`User` のバリデーション結果は `result1` に、`OtherObj` のバリデーション結果は `result2` に格納される。

```java
@Controller
public class HelloController {
    @GetMapping("/")
    public String index(@Validated User user, BindingResult result1, @Validated OtherObj other, BindingResult result2) {

        return "index";
    }
}
```

### パラメータありのコンストラクタを利用する場合

パラメータありのコンストラクタを用いる場合は、オブジェクトを生成するタイミングで必要に応じて型変換が行われる。
その際、型変換ができないフィールドが含まれていた場合はその結果を `BindingResult` として保持するが、それ以降のバインド処理、バリデーションが実施されない。
つまり、setter によるリクエストパラメータのバインド処理や、Bean Validation によるフィールドチェックが実施されない。

```java
public class User {
    @NotEmpty
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    // getter 省略
}

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(@Validated User user, BindingResult result) {
        return "index";
    }
}
```

上記のようなクラスを作成した場合に、以下のようなリクエストを送る。

```
curl http://localhost:8080?age=hogehoge
```

本来期待しているバリデーション結果は、`name` が空であること、`age` が `Integer` に変換できないことだが、後者しか検出することができない。
パラメータありのコンストラクタを削除し、デフォルトコンストラクタと setter を利用するように変更すると、どちらも検出できる。

## Model へ格納

上記までの処理で生成したオブジェクトと `BindingResult` を `Model` に格納する。
`@ModelAttribute` で属性名が指定されている場合は、その値を key として利用する。

# さいごに

なるほどー。
パラメータありのコンストラクタを利用する場合に、微妙に動作が異なることがあるので注意しないといけない。