【Spring Boot】フォームクラスでリクエストパラメータの受取
初めに
Spring Bootでのリクエストパラメータの受け取り方としてフォームクラスが使われる場合があります。いろんな記事を見ていると、使い方について混乱したので、自分なりにまとめたいと思います。
フォームクラスとは
以下のようなコードでリクエストパラメータを受け取るクラスがフォームクラスです。フォームクラスと呼んでいますが、フォームに関する何かしらの親クラスを継承するわけでもなく、クラス名にFormという単語を入れなくても問題ありません。
import lombok.Data;
import java.io.Serializable;
@Data
public class RequestStudyForm implements Serializable {
private String food;
private String trip;
}
フォームクラスはHTMLフォーム内の入力フィールドの構造をJava Beans(参考:Java Beansとは)として表現したクラスだと本で読んだので、Java Beansの以下3つの条件を満たしていれば、フォームクラスとして機能すると思っています。
- getter/setterメソッドを用意する
- 引数なしのpublicコンストラクタを用意する
- java.io.Serializableを実装する
今回はlombokの@Dataを用いてゲッター、セッターを生成してもらっています。Serializableの実装はフォームクラスをセッションスコープで管理する場合に必要とのことなので、普通の使い方をする場合は省略してもOKです。簡単に説明すると、セッションスコープというのは複数のリクエストを跨いで情報を保持したい場合に用いるものです。
フォームクラスの使い方
以下のようなコントローラのハンドラメソッドとビューとなるhtmlのformタグ部分を作成します(htmlはthymeleafとbootstrapを使用)。
@Controller
@RequestMapping("/request")
public class RequestStudyController {
public RequestStudyController() {
}
@PostMapping("/action3")
public ModelAndView action3(@ModelAttribute RequestStudyForm form,
ModelAndView mav) {
mav.setViewName("request_study/check3");
mav.addObject("food", form.getFood());
mav.addObject("trip", form.getTrip());
return mav;
}
}
<form method="POST" th:action="@{/request/action3}" class="bg-warning p-3">
<h3>Formクラスを使用</h3>
<div class="vstack gap-3">
<div>
<label for="food" class="form-label">食べ物</label>
<input class="form-control" id="food" name="food" value="りんご">
</div>
<div>
<label for="trip" class="form-label">旅行先</label>
<input class="form-control" id="trip" name="trip" value="イタリア">
</div>
<div>
<input type="submit" value="送信">
</div>
</div>
</form>
難しいことは何もありません。htmlのformタグ内で送るパラメータのname属性に先ほどのRequestStudyFormクラスのフィールド名と同じ名前を書けばいいだけです。そして、コントローラのハンドラメソッドの引数にRequestStudyFormクラスを記載します。そうすることで、foodとtripの値を格納したRequestStudyForm型のオブジェクトを受け取ることができます。
より視覚的にイメージしやすくすると以下の図のような感じですね。
フォームクラスのバインディングについて
まずSpringにおけるフォームクラスのバインディングとは、HTMLのフォーム等から送られたリクエストのデータをJavaオブジェクトに変換して、いい感じに受け取れるものだと思ってください。
バインディングの命名ルール
先ほど説明したようにinputタグのname属性とフィールド名が完全一致していれば、データを取得することができます。このとき、コントローラのハンドラメソッドの引数となるformという変数名はなんでも構いません。
ちなみに、以下のように@RequestParamでinputタグのname属性がfoodの値を受け取ることができます。このとき、RequestStudyForm formにもfoodというフィールドが存在するので、formのfoodフィールドと変数foodに同じ値が格納されることになります。
@PostMapping("/action3")
public ModelAndView action3(@ModelAttribute RequestStudyForm form, @RequestParam String food,
ModelAndView mav) {
イメージとしては下図のような感じです。つまり、送られてくるパラメータ1つに対して、複数の変数に値が入る可能性があるということになります。
フォームクラスの前には@ModelAttributeをつけていますが、実はつけなくてもリクエストパラメータを受け取ってくれます。私は一応つけた方がいいのかなと思っています。
バインディングの型変換
先ほどの例ではfoodやtripなどの値をString型で受け取るようなコードを記載しました。リクエストパラメータは物理的には文字列として送られてきますが、Spring MVCでは送られてきた文字列をString以外の型に変換する仕組みがあります。
デフォルトで利用できる型は以下の通りです。
- プリミティブ型(int、booleanなど)およびそれらのラッパー型(Integer、Booleanなど)
- 値を表現する型(String、Dateなど)
- MultipartFile(画像などのファイルのこと)
また、リクエストデータはコレクション(Listなど)や配列として取得することもできます。
デフォルトでさまざまな型への変換がサポートされていますが、サポートされていない型への変換処理の追加やSpring MVCのデフォルトの動作をカスタマイズしたい場合はWebDataBinderクラスを用いてカスタマイズも可能なようです。
シンプル型とのバインディングの例
型変換する上で意識することはあまりありません。先ほどのフォームクラスのフィールドでStringを指定していたところを他の型にするだけです。フォームクラスの例を以下に記載します。
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class BindCheckForm1 {
private int id;
private Integer money;
private String content;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date date;
private String email;
private Boolean bool;
}
型をStringから他の型に変えようと、どのリクエストパラメータがどのフィールドにバインディングされるかはinputタグのname属性とフィールド名によって決まるということは変わりません。しかし型を指定しているので、「money=こんにちは」のようなリクエストパラメータが送られた場合、moneyはInteger型(整数)なので、当然エラーになります。
Date型については例えば<input type="date">を用いた場合、送られる文字列は"2023-09-18"のようになります。この文字列をDate型に変換するために@DateTimeFormatを使用してあげるなどの一工夫が必要になったりするので注意です。
シンプル型のコレクションとのバインディングの例
Listでデータを受け取る場合は、以下コードのようにinputタグのname属性を同じにしてデータを送り、フォームクラスでname属性と同じ名前のList型フィールドを作成するだけです。
<form method="POST" th:action="@{/bind/check2}" class="bg-primary p-3">
<h3>Listでの複数パラメータ受け取り</h3>
<input type="checkbox" name="roles" value="1">利用者
<input type="checkbox" name="roles" value="2">管理者
<input type="checkbox" name="roles" value="3">システム管理者
<input type="submit" value="送信">
</form>
@Data
public class BindCheckForm2 {
List<String> roles;
}
Mapでデータを受け取る例は以下のようになります。inputタグのname属性にはフォームクラスのフィールド名[キー名]が入るような感じですね。このformをそのまま送信すると、drink={1=コーラ, 2=ソーダ, 3=オレンジジュース}みたいな感じでデータを受け取ることができます。
<form method="POST" th:action="@{/bind/check4}" class="bg-info p-3">
<h3>Mapでのパラメータ受け取り</h3>
<input type="text" name="drink['1']" value="コーラ">
<input type="text" name="drink['2']" value="ソーダ">
<input type="text" name="drink['3']" value="オレンジジュース">
<input type="submit" value="送信">
</form>
@Data
public class BindCheckForm4 {
Map<String, String> drink;
}
ネストしたJavaBeansとのバインディングの例
フォームクラスを使用するとリクエストパラメータをネストしたJavaBeansのプロパティへバインドすることができます。イメージ的には下図のような感じです。
このときのコードも参考として記載しておきます。
注意点としては、FoodDtoやTripDtoにも@Dataをつけるなどして、ゲッター・セッターを作成してあげないとバインドしてくれないということですね。
<form method="POST" th:action="@{/bind/check3}" class="bg-success p-3">
<h3>ネストしたJavaBeansとのバインディング</h3>
食べ物名<input type="text" name="food.foodName" value="みかん">
食べ物の値段<input type="text" name="food.price" value="100">
<br>
旅行先<input type="text" name="trip.destination" value="イタリア">
料金<input type="text" name="trip.price" value="100000">
出発日<input type="date" name="trip.departureDate" value="2023-10-01">
<br>
<input type="submit" value="送信">
</form>
@Data
public class BindCheckForm3 {
private FoodDto food;
private TripDto trip;
}
FoodDtoとTripDtoのコード
@Data
public class FoodDto {
private String foodName;
private Integer price;
}
@Data
public class TripDto {
private String destination;
private Integer price;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date departureDate;
}
個人的にややこしかったポイント
formタグのth:object(Thymeleaf)
ネットの記事を見るとformタグに以下のようにth:objectでフォームクラスを指定していたりする場合があります。
<form th:action="@{/binding}" method="post" th:object="${textForm}">
th:objectはデータを送信してTextFormというフォームクラスで受け取る上ではなくても問題ありません。なぜなら、これまで説明した通りinputタグのname属性とフォームクラスのフィールド名が一致すればいいだけで、th:objectでフォームクラスを指定しなければいけないというルールは存在しないからです。th:objectはバリデーションに引っかかったりで送信できない時に画面上に打ち込んだ内容を表示する際に便利だから書いてるって感じですね。
th:objectについて使い方など、詳しくは公式のページを見てみてください。
フォームクラスは勝手にModelにセットされる
以下のようなコントローラのハンドラメソッドがあったとします。
@GetMapping("/")
public ModelAndView index(TextForm form, ModelAndView mav) {
mav.setViewName("board/index");
return mav;
}
このときハンドラメソッドの引数に書いたフォームオブジェクトは自分でModelに格納しなくても、勝手にModelに追加される仕組みになっているようです。
htmlではフォームオブジェクトの値を以下のように受け取れます。この時変数名はTextFormクラスの頭文字が小文字になったtextFormになることに注意です。
<p th:text="${textForm.name}"></p>
上のコントローラで言うと以下のModelに値を格納する処理を自動でやってくれてるよってことなんですね。以下一文を書かなくて良くなるので便利だなって思いました!
mav.addObject("textForm", form);
まとめ
フォームクラスでリクエストパラメータを受け取りたい時のまとめが以下になります!
- フォームクラスの作成(JavaBeans)
- コントローラのハンドラメソッドの引数にフォームクラスを記載
- HTMLのinputタグのname属性の値とフォームクラスのフィールド名を一致させる
- フォームクラスのフィールドの型になるようにリクエストパラメータの文字列を変換してくれる
あと補足するなら、GETメソッドでもPOSTメソッドでもフォームクラスを用いたパラメータの受け取り方は変わりません。
以上参考になれば幸いです。
参考
記事の内容は大体以下の本を参考にしました。
@ModelAttributeについて以下の記事が参考になりました。
Discussion