Spring Bootのフォームクラスでリクエストパラメータを送信・受け取る方法を新人エンジニア向けに解説

こんにちは。ゆうせいです。

今回は、Spring Bootで「フォームクラスを使ってリクエストパラメータを扱う方法」を、新人エンジニア向けに解説します。

最初に大切なポイントを整理します。

正確に言うと、フォームクラスがリクエストパラメータを送信するわけではありません。

リクエストパラメータを送信するのはHTMLフォームです。

そして、Spring Boot側でそのリクエストパラメータを受け取るための入れ物がフォームクラスです。

たとえるなら、HTMLフォームは「申込用紙」です。

ユーザーが申込用紙に名前や価格を入力して送信します。

フォームクラスは、Spring Boot側でその申込内容を受け取る「専用の箱」です。

Spring MVCでは、@RequestParamを使うとクエリパラメータやフォームデータをControllerの引数にバインドできます。また、@ModelAttributeを使うと、リクエストパラメータをオブジェクトにまとめてバインドできます。Spring公式ドキュメントでも、@RequestParamはクエリパラメータやフォームデータをメソッド引数にバインドできること、@ModelAttributeではデフォルトでコンストラクターバインディングとプロパティデータバインディングが適用されることが説明されています。

フォームクラスとは何か

フォームクラスとは、画面から送信された入力値を受け取るためのJavaクラスです。

たとえば、車を検索する画面があるとします。

検索画面には、次の入力欄があるとします。

入力欄意味
name車名
minPrice最低価格
maxPrice最高価格

この3つの値を1つずつ@RequestParamで受け取ることもできます。

@PostMapping("/cars/search")
public String search(
        @RequestParam String name,
        @RequestParam Integer minPrice,
        @RequestParam Integer maxPrice) {
    // 検索処理
    return "cars/search";
}




ただし、項目が増えるとControllerの引数がどんどん長くなります。

そこで、入力値をまとめて受け取るためにフォームクラスを作ります。

public class CarSearchForm {

    private String name;
    private Integer minPrice;
    private Integer maxPrice;

    public String getName() {
        return name;
    }

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

    public Integer getMinPrice() {
        return minPrice;
    }

    public void setMinPrice(Integer minPrice) {
        this.minPrice = minPrice;
    }

    public Integer getMaxPrice() {
        return maxPrice;
    }

    public void setMaxPrice(Integer maxPrice) {
        this.maxPrice = maxPrice;
    }

    @Override
    public String toString() {
        return "CarSearchForm{" +
                "name='" + name + '\'' +
                ", minPrice=" + minPrice +
                ", maxPrice=" + maxPrice +
                '}';
    }
}




このCarSearchFormが、画面から送られてきた検索条件を受け取る箱になります。

DTOと似ていますが、フォームクラスは「画面入力を受け取るためのクラス」と考えるとわかりやすいです。

フォームクラスを使う全体の流れ

フォームクラスを使った送信処理は、次の流れになります。

順番場所処理
1Controller画面表示用に空のフォームクラスをModelへ入れる
2HTMLフォームクラスのフィールドに対応する入力欄を作る
3ブラウザユーザーが入力して送信する
4Spring Bootリクエストパラメータをフォームクラスに自動でセットする
5Controllerフォームクラスから値を取り出して処理する

たとえるなら、画面がアンケート用紙です。

ユーザーがアンケートに回答します。

Spring Bootは、回答内容をCarSearchFormという箱に自動で入れてくれます。

新人エンジニアがまず覚えるべきポイントは、HTMLのname属性とフォームクラスのフィールド名が対応することです。

Controllerを作る

まず、Controllerを作ります。

今回は、車検索画面を表示し、検索条件を受け取るサンプルにします。

package com.example.demo.controller;

import com.example.demo.form.CarSearchForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class CarController {

    @GetMapping("/cars/search")
    public String showSearchForm(Model model) {

        CarSearchForm form = new CarSearchForm();

        model.addAttribute("carSearchForm", form);

        return "cars/search";
    }

    @PostMapping("/cars/search")
    public String searchCars(
            @ModelAttribute CarSearchForm carSearchForm,
            Model model) {

        System.out.println("受け取った検索条件: " + carSearchForm);

        model.addAttribute("carSearchForm", carSearchForm);
        model.addAttribute("message",
                "車名: " + carSearchForm.getName()
                + "、最低価格: " + carSearchForm.getMinPrice()
                + "、最高価格: " + carSearchForm.getMaxPrice());

        return "cars/search";
    }
}




@Controllerは、画面を返すControllerを作るためのアノテーションです。

@GetMapping("/cars/search")は、検索画面を表示する処理です。

@PostMapping("/cars/search")は、フォーム送信後の処理です。

@ModelAttribute CarSearchForm carSearchFormと書くことで、送信されたリクエストパラメータがCarSearchFormに自動でセットされます。

Spring公式ドキュメントでは、@ModelAttributeを使った引数ではデータバインディングが適用されると説明されています。また、セキュリティ上の理由から、Webバインディング専用にカスタマイズされたオブジェクトを使うことが推奨されています。つまり、DBのEntityを直接フォーム受け取りに使うより、今回のような専用フォームクラスを作るほうが安全な設計になりやすいです。

HTMLフォームを作る

次に、Thymeleafを使ったHTMLフォームを作ります。

ファイルの場所は、次のようなイメージです。

src/main/resources/templates/cars/search.html

コードは次のとおりです。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>車検索</title>
</head>
<body>

<h1>車検索</h1>

<form th:action="@{/cars/search}" th:object="${carSearchForm}" method="post">

    <div>
        <label for="name">車名</label>
        <input type="text" th:field="*{name}" id="name">
    </div>

    <div>
        <label for="minPrice">最低価格</label>
        <input type="number" th:field="*{minPrice}" id="minPrice">
    </div>

    <div>
        <label for="maxPrice">最高価格</label>
        <input type="number" th:field="*{maxPrice}" id="maxPrice">
    </div>

    <button type="submit">検索する</button>

</form>

<p th:text="${message}"></p>

</body>
</html>




ここで大事なのは、次の2つです。

Thymeleafの書き方意味
th:object="${carSearchForm}"このフォームはcarSearchFormを使うという宣言
th:field="*{name}"carSearchFormのnameフィールドと入力欄を結びつける

th:field="*{name}"を書くと、HTML上ではname="name"のような属性が生成されます。

Spring Bootは、送信されたnameというリクエストパラメータを見て、CarSearchFormのsetNameメソッドを呼びます。

つまり、次のように対応します。

HTML側フォームクラス側
name="name"setName(String name)
name="minPrice"setMinPrice(Integer minPrice)
name="maxPrice"setMaxPrice(Integer maxPrice)

Thymeleafを使わない普通のHTMLでも送信できる

フォームクラスを使うために、必ずThymeleafが必要というわけではありません。

普通のHTMLでも、name属性がフォームクラスのフィールド名と一致していれば、Spring Boot側で受け取れます。

<form action="/cars/search" method="post">

    <div>
        <label for="name">車名</label>
        <input type="text" id="name" name="name">
    </div>

    <div>
        <label for="minPrice">最低価格</label>
        <input type="number" id="minPrice" name="minPrice">
    </div>

    <div>
        <label for="maxPrice">最高価格</label>
        <input type="number" id="maxPrice" name="maxPrice">
    </div>

    <button type="submit">検索する</button>

</form>




このHTMLから送信されるリクエストパラメータは、次のような形になります。

name=プリウス
minPrice=1000000
maxPrice=3000000





Spring Bootは、このパラメータ名を見て、CarSearchFormへ値を入れます。

郵便でたとえるなら、name、minPrice、maxPriceという宛名ラベルが貼られた荷物を、Spring Bootが対応する箱に入れてくれるようなものです。

フォームクラスに値が入る仕組み

フォーム送信時、ブラウザは次のようなデータをSpring Bootへ送ります。

name=プリウス&minPrice=1000000&maxPrice=3000000





Spring Bootは、CarSearchFormのフィールド名とリクエストパラメータ名を照合します。

リクエストパラメータ呼ばれるsetterセットされる値
namesetNameプリウス
minPricesetMinPrice1000000
maxPricesetMaxPrice3000000

この「リクエストパラメータをJavaオブジェクトへ入れる処理」をバインドといいます。

バインドとは、ひもづけるという意味です。

スマホとBluetoothイヤホンをペアリングするように、画面の入力値とJavaのフィールドをひもづけるイメージです。

@RequestParamとの違い

フォームクラスを使わずに、@RequestParamで1つずつ受け取ることもできます。

@PostMapping("/cars/search")
public String searchCars(
        @RequestParam String name,
        @RequestParam Integer minPrice,
        @RequestParam Integer maxPrice,
        Model model) {

    return "cars/search";
}




この方法も間違いではありません。

Spring公式ドキュメントでも、@RequestParamはクエリパラメータやフォームデータをControllerのメソッド引数にバインドできると説明されています。

ただし、入力項目が多くなるとControllerが読みにくくなります。

比較@RequestParamフォームクラス
向いている場面項目が少ない項目が複数ある
Controllerの見た目引数が増えやすいフォームクラス1つで受け取れる
入力値の管理バラバラになりやすい1つのクラスにまとまる
バリデーション個別に書きがちフォームクラスにまとめやすい

検索条件が1つだけなら@RequestParamで十分です。

検索条件が3つ、5つ、10個と増えるなら、フォームクラスを使うほうが整理しやすいです。

GETでもフォームクラスは使える

検索画面では、method="get"を使うこともあります。

GET送信では、入力値がURLに付きます。

/cars/search?name=プリウス&minPrice=1000000&maxPrice=3000000





GETでも、フォームクラスで受け取れます。

@GetMapping("/cars/search/result")
public String searchCarsByGet(
        @ModelAttribute CarSearchForm carSearchForm,
        Model model) {

    System.out.println("GETで受け取った検索条件: " + carSearchForm);

    model.addAttribute("carSearchForm", carSearchForm);

    return "cars/search";
}




HTMLは次のようになります。

<form th:action="@{/cars/search/result}" th:object="${carSearchForm}" method="get">

    <input type="text" th:field="*{name}">
    <input type="number" th:field="*{minPrice}">
    <input type="number" th:field="*{maxPrice}">

    <button type="submit">検索する</button>

</form>




GETは検索条件に向いています。

なぜなら、URLをコピーすれば、同じ検索条件を他の人に共有しやすいからです。

一方で、パスワードや個人情報のような見せたくない値をGETで送るのは避けましょう。

POSTとGETの使い分け

比較GETPOST
値の見え方URLに表示されるURLには表示されにくい
向いている処理検索、絞り込み、一覧表示登録、更新、削除
ブラウザの戻る操作比較的扱いやすい再送信確認が出ることがある
車名で検索する車情報を登録する

たとえるなら、GETは「検索メモを見える場所に貼る」イメージです。

POSTは「申込書を封筒に入れて提出する」イメージです。

検索ならGET、登録や更新ならPOSTと覚えるとよいです。

車登録フォームのサンプル

検索だけでなく、登録フォームでもフォームクラスを使えます。

次は、車情報を登録する例です。

CarRegisterFormクラス

package com.example.demo.form;

public class CarRegisterForm {

    private String name;
    private Integer price;

    public String getName() {
        return name;
    }

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

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "CarRegisterForm{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}




Controller

package com.example.demo.controller;

import com.example.demo.form.CarRegisterForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class CarRegisterController {

    @GetMapping("/cars/new")
    public String showRegisterForm(Model model) {

        model.addAttribute("carRegisterForm", new CarRegisterForm());

        return "cars/new";
    }

    @PostMapping("/cars/new")
    public String registerCar(
            @ModelAttribute CarRegisterForm carRegisterForm,
            Model model) {

        System.out.println("登録フォームの内容: " + carRegisterForm);

        model.addAttribute("message",
                carRegisterForm.getName() + "を登録しました。価格は"
                + carRegisterForm.getPrice() + "円です。");

        return "cars/new";
    }
}




HTML

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>車登録</title>
</head>
<body>

<h1>車登録</h1>

<form th:action="@{/cars/new}" th:object="${carRegisterForm}" method="post">

    <div>
        <label for="name">車名</label>
        <input type="text" th:field="*{name}" id="name">
    </div>

    <div>
        <label for="price">価格</label>
        <input type="number" th:field="*{price}" id="price">
    </div>

    <button type="submit">登録する</button>

</form>

<p th:text="${message}"></p>

</body>
</html>




このフォームで「プリウス」「2500000」と入力して送信すると、Spring Boot側では次のようなCarRegisterFormが作られます。

CarRegisterForm{name='プリウス', price=2500000}

このように、フォームクラスを使うと、画面入力をJavaのオブジェクトとして扱えます。

フォームクラスをDAOへ渡すべきか

新人エンジニアが迷いやすい点として、「フォームクラスをそのままDAOへ渡してよいのか?」があります。

学習用の小さなサンプルなら、フォームクラスをDAOへ渡しても動きます。

ただし、実務では次のように役割を分けることが多いです。

クラス役割
Form画面から送られた入力値を受け取る
DTOアプリ内部やDB処理に必要なデータを運ぶ
DAOSQLを実行してDBにアクセスする
Service業務処理の流れを組み立てる

たとえば、登録処理では次のようにFormからDTOへ詰め替えることがあります。

CarDto carDto = new CarDto();
carDto.setName(carRegisterForm.getName());
carDto.setPrice(carRegisterForm.getPrice());

carsDao.insertCar(carDto);




Formは画面用の入れ物です。

DTOは処理用の入れ物です。

お弁当でたとえるなら、Formはお店の注文用紙です。

DTOは厨房に渡す調理指示書です。

似ていますが、使う場所が違います。

Integerを使う理由

フォームクラスでは、価格をintではなくIntegerにしています。

private Integer minPrice;
private Integer maxPrice;




理由は、入力欄が空の可能性があるからです。

intは基本型なので、nullを持てません。

Integerは参照型なので、未入力をnullとして扱えます。

nullを持てるかフォーム入力との相性
int持てない未入力を扱いにくい
Integer持てる未入力をnullで表せる

検索フォームでは、最低価格や最高価格を空にすることがあります。

そのため、Integerを使うと扱いやすいです。

よくあるエラー1:フォームクラスにsetterがない

フォームクラスにsetterがないと、Spring Bootが値をセットできません。

悪い例です。

public class CarSearchForm {

    private String name;

    public String getName() {
        return name;
    }
}




このクラスにはsetNameがありません。

そのため、リクエストパラメータnameの値を入れられません。

良い例です。

public class CarSearchForm {

    private String name;

    public String getName() {
        return name;
    }

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




フォームクラスには、基本的にgetterとsetterを用意しましょう。

よくあるエラー2:HTMLのname属性とフィールド名が違う

フォームクラスのフィールド名がnameなのに、HTML側がcarNameになっていると、自動で入りません。

フォームクラスです。

private String name;




悪いHTML例です。

<input type="text" name="carName">




この場合、Spring BootはCarSearchFormのnameに値を入れられません。

良いHTML例です。

<input type="text" name="name">




Thymeleafを使うなら、次のように書くと対応関係を間違えにくくなります。

<input type="text" th:field="*{name}">




よくあるエラー3:Modelにフォームオブジェクトを入れていない

Thymeleafでth:object="${carSearchForm}"を使う場合、ControllerでcarSearchFormをModelに入れておく必要があります。

悪い例です。

@GetMapping("/cars/search")
public String showSearchForm() {
    return "cars/search";
}




この場合、HTML側でcarSearchFormが見つからない可能性があります。

良い例です。

@GetMapping("/cars/search")
public String showSearchForm(Model model) {

    model.addAttribute("carSearchForm", new CarSearchForm());

    return "cars/search";
}




画面を表示する前に、空のフォームオブジェクトを用意しておくのが基本です。

よくあるエラー4:数値に変換できない値が送られる

フォームクラスでIntegerを使っている場合、Spring Bootは送信された文字列をIntegerに変換しようとします。

たとえば、次のような値なら変換できます。

2500000





しかし、次のような値はIntegerに変換できません。

abc





Spring公式ドキュメントでも、@RequestParamでString以外の型を使う場合には型変換が適用されると説明されています。フォームクラスでも同じように、数値型へ変換できない値が来るとエラーになる可能性があります。

実務では、入力チェックを入れます。

入力チェックは、次の学習テーマであるバリデーションにつながります。

フォームクラスを使うメリット

メリット内容
Controllerが読みやすくなる入力項目を1つのクラスにまとめられる
入力値を管理しやすい画面ごとの入力項目が明確になる
バリデーションを追加しやすいフォームクラスに入力チェックルールをまとめられる
画面との対応がわかりやすいHTMLの入力欄とJavaのフィールドが対応する
Entityを直接使わずに済むWeb入力専用の安全な入れ物を作れる

特に重要なのは、Entityを直接使わないことです。

EntityはDBテーブルに近いクラスです。

画面から送信された値をEntityに直接バインドすると、意図しないフィールドまで書き換えられる危険があります。

Spring公式ドキュメントでも、Webバインディング専用にカスタマイズされたオブジェクトを使うことが推奨されています。

フォームクラスを使うデメリット

デメリット内容
クラスが増える画面ごとにFormクラスを作るためファイル数が増える
DTOへの詰め替えが必要になることがあるFormとDTOを分ける場合、変換処理が必要
命名を間違えると値が入らないHTMLのname属性とフィールド名の対応が重要
型変換エラーに注意が必要数値欄に文字列が来るとエラーになる場合がある

ただし、クラスが増えるデメリットより、入力値を安全に整理できるメリットのほうが大きいです。

学校のプリントを教科ごとに分けると、ファイルは増えます。

でも、数学、英語、理科が混ざったプリントの山より、ずっと探しやすくなりますよね。

フォームクラスも同じです。

新人エンジニア向けの実装手順

フォームクラスを使った送信処理を作るときは、次の順番で進めると迷いにくいです。

順番作業
1画面で入力する項目を決める
2入力項目に対応するフォームクラスを作る
3getterとsetterを作る
4GETメソッドで空のフォームクラスをModelに入れる
5HTMLフォームを作る
6POSTメソッドで@ModelAttributeを使って受け取る
7受け取った値をログや画面で確認する
8必要ならServiceやDAOへ渡す

最初からDB登録まで一気に作ると混乱しやすいです。

まずはControllerで値を受け取れているか確認しましょう。

System.out.println(carRegisterForm);




この確認ができてから、ServiceやDAOに進むのがおすすめです。

まとめ

Spring Bootのフォームクラスとは、HTMLフォームから送信されたリクエストパラメータをまとめて受け取るためのJavaクラスです。

送信するのはHTMLフォームで、受け取るのがフォームクラスです。

ポイント内容
フォームクラスの役割画面入力を受け取る専用の入れ物
受け取り方@ModelAttributeを使ってControllerで受け取る
HTML側の重要点name属性またはth:fieldをフォームクラスのフィールド名に合わせる
@RequestParamとの違い単独項目なら@RequestParam、複数項目ならフォームクラスが便利
注意点getter、setter、Modelへの追加、型変換エラーに注意する

一言でまとめるなら、フォームクラスは「画面から送られてきた入力値を、Spring Boot側でまとめて受け取る箱」です。

新人エンジニアは、まずCarRegisterFormやCarSearchFormのような小さなフォームクラスを作り、HTMLのname属性とJavaのフィールド名が対応する感覚をつかんでください。

今後の学習では、@RequestParam、@ModelAttribute、Thymeleafのth:objectとth:field、GETとPOSTの違い、入力チェックであるバリデーション、ServiceとDAOへの受け渡しを順番に学ぶとよいです。まずは「入力欄2つのフォームを作り、フォームクラスで受け取って画面に表示する」ところから始めてください!

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。