前回は、「Controllerの役割」について学びました。Controllerは、ブラウザからのリクエストを受け取り、適切な処理を実行した後、レスポンスを返す重要な役割を担います。

今回は、実際に入力フォームからデータを送信し、Controllerで受け取る方法を学びます。リクエストとレスポンスの仕組みを理解することで、ユーザーが入力したデータを処理し、画面に反映させるWebアプリケーションを作れるようになります。

今回の学習の重点をオレンジの枠線で図示します。

ブラウザのフォームからコントローラーにデータを渡すところを重点的に学びます

1. フォームの復習

1.1 プロジェクト構成イメージ

src/main/resources/templates 配下に、Thymeleafのテンプレートファイル(index.htmlans.htmlなど)を配置

src/main/java/com.example.demo.controller などのパッケージにControllerクラスを配置

以下のような構成を例示します。

my-demo-form
 ┣ src
 ┃ ┣ main
 ┃ ┃ ┣ java
 ┃ ┃ ┃ ┣ com.example.demo
 ┃ ┃ ┃ ┃ ┗ DemoApplication.java     (Spring Boot起動クラス)
 ┃ ┃ ┃ ┃ ┗ controller
 ┃ ┃ ┃ ┃    ┣ AddController.java    (Controller)
 ┃ ┃ ┗ resources
 ┃ ┃    ┗ templates
 ┃ ┃       ┣ index.html            (入力フォーム)
 ┃ ┃       ┗ ans.html              (結果表示用)
 ┗ pom.xml

1.2 フォーム画面の例

Thymeleafを使ったindex.html を用意します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>index.html</title>
</head>
<body>
  <form th:action="@{/add}" method="get">
    <input type="text" name="num1"><br>
    <input type="text" name="num2"><br>
    <button type="submit">送信</button>
  </form>
</body>
</html>

Thymeleafのフォームでは、th:action="@{/add}" のように書くと、Spring Bootで @GetMapping("/add") を指定したControllerメソッドが呼ばれます。

フォームは「何を、どのメソッドで、どのプログラムに送るのか」を書くものだと理解しましょう。上記の例でいえば、「num1,num2という名前のつけられた1行テキストに入力された文字列を、getメソッドで、 @GetMapping("/add") を指定したControllerに送る」ということが書かれているのですね。

2箇所合わせる必要がある

質問(HTMLフォームに関して)

  • ①このフォームのhttpメソッドは何ですか?
あなたの答え:
  • ②このhttpメソッドの指定は省略できますか?
あなたの答え:
  • ③フォームの中の部品はいくつありますか?
あなたの答え:
  1. ④それぞれどのような部品でしょうか?
あなたの答え:
  • ⑤「name="○○"」で付けられた名前はControllerでどのように使われますか?
あなたの答え:

このname属性に設定された名前はリクエストパラメータとして、文字列形式でコントローラーに送られます。

解説:

  • フォーム送信で渡されるデータは「name属性の値」と「ユーザーの入力内容」がセットになって送られます。
  • Spring BootのController側では @RequestParam("num1") String num1 のように受け取れます。
  • すべて文字列として渡されるため、数値として使う場合は Integer.parseInt() などで変換が必要です。

2. Controllerの役割

2.1 データの受取方法

フォームから送ったデータを受け取るには以下のような Controller を用意します。

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class AddController {

    @GetMapping("/add")
    public String doGetAdd(
         @RequestParam(name="num1") String num1Str,
         @RequestParam(name="num2") String num2Str,
         Model model
    ) {
        // nullチェックや例外処理は適宜追加
        int num1 = Integer.parseInt(num1Str);
        int num2 = Integer.parseInt(num2Str);

        model.addAttribute("ans", num1 + num2);

        return "ans";
    }
}

質問(Controllerに関して)

  • ①このControllerはどのようなURLパスで呼び出されますか?
あなたの答え:
  • ②リクエストパラメータにはどの様な値がどの様な名前で格納されていますか?
あなたの答え:
  • ③Modelにはどのような値を、どのような名前で格納していますか?
あなたの答え:
  • ④返却しているHTMLテンプレートのファイル名は何ですか?
あなたの答え:
  • ⑤ブラウザのアドレスバー表記は、フォームに「1」と「2」をそれぞれ入力して送信した場合どうなりますか?
あなたの答え:http://localhost:8080/

「@RequestParam(name="num1") String num1Str,」と複雑な記述をする理由

2章の数当てゲームでは、「@RequestParam(name="num1") String num1Str,」を「String num1」のように@RequestParam を省略してシンプルに書いていました。

@RequestParam を省略する場合、リクエストのクエリパラメータのキー(例: num1)と、コントローラーのメソッド引数名 (num1) が完全に一致している必要があります。
例えば、クエリパラメータとして ?num1=10 のように送信される場合、メソッド引数も num1 であれば、自動的に値が結びつけられます。

また、「int num1」のようにSpring Bootに型変換を任せる方法も存在します。その場合にはリクエストパラメータが常に適切な数値として送信される保証があるかが重要になります。しかし、実際のWebアプリケーションでは、数値以外の文字列が送信されたり、パラメータが欠落したりする可能性があるため、int num1 を使うと例外処理が複雑化する問題が発生します。

したがって以降は「@RequestParam(name="num1") String num1Str,」のような複雑な記述を採用します。

例題

上記、index.htmlで「name="num1"」となっているところを「name="number1"」と書き換えたとします。そうすると他にどこをどのように書き換える必要がありますか?

あなたの答え:

例題

上記、index.htmlからコントローラーに対して確実にデータが送られていることを①printデバッグ、②デバッガを使ったデバッグで確認しなさい。

確かめた結果:

また、デバッガで"ans"の値は見られないことも確認して下さい。

2.2 結果表示用のThymeleafテンプレート

先ほどmodel.addAttribute("ans", ~)で渡した値をThymeleafで取り出すには、${ans} と書きます。

開タグと閉じタグの間の「要素の内容:以下の"ここに計算結果が表示されます"」は値と置き換わります。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>ans.html</title>
</head>
<body>
  <p th:text="${ans}">ここに計算結果が表示されます</p>
</body>
</html>

これで、index.html で入力 → /add にGET送信 → AddController で計算 → ans.html に結果表示、という流れをSpring Bootで実現できました。

3. HTTPリクエストの読み方

ブラウザのアドレスバーに表示されるURL例:

  1. http:// ・・・通信プロトコル
  2. localhost ・・・サーバー名(ホスト名)
  3. :8080 ・・・ポート番号
  4. /add ・・・コントローラーのURLパス
  5. ?num1=1&num2=2 ・・・GETリクエストで送るパラメータ。複数ある場合は & で連結

4. @RequestParam と 文字列の変換

今回の注目点はこの部分です。

@RequestParam("num1")

アノテーションを使っています。

このアノテーションはフォームから送られたリクエストに入っている文字列を取り出すことができます。ここでフォームから送られるデータはすべて文字列として送られることを思い出してください。

また、フォームから送られるデータは1つとは限りません。そのため“num1”などのテキストフィールドに付けた名前で区別して受け取る必要があるのです。

今回の処理の概要を下図にまとめておきます。

それぞれの①~④が対応していることに着目

@RequestParamは フォームから送られた文字列を受け取ります。

後で数値として利用するなら、Integer.parseInt()などで変換が必要です。

int num1 = Integer.parseInt(num1Str);

なお、NumberFormatException が発生する可能性があるため、エラー処理は必要です。

調べてみましょう @RequestParamの書き方

@RequestParam の基本

Spring Boot の @RequestParam は、クエリパラメータやフォームデータを取得するために使います。

例:

@GetMapping("/hello")
public String hello(@RequestParam String name) {
    return "Hello, " + name;
}

GET /hello?name=John とリクエストすると "Hello, John" というレスポンスが返ります。

必須 (required=true / false)

デフォルトでは @RequestParam必須 ですが、required=false にすると省略可能になります。

例:

@GetMapping("/hello")
public String hello(@RequestParam(required = false) String name) {
    return name != null ? "Hello, " + name : "Hello, Guest";
}

GET /hello?name=John"Hello, John"

GET /hello"Hello, Guest"(エラーにならない)

デフォルト値 (defaultValue)

値が渡されなかったときにデフォルト値を指定できます。

例:

@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "Guest") String name) {
    return "Hello, " + name;
}

GET /hello?name=John"Hello, John"

GET /hello"Hello, Guest"(エラーにならない)

型変換

基本的な型(intdoubleboolean など)は Spring が自動的に変換します。

例:

@GetMapping("/sum")
public int sum(@RequestParam int a, @RequestParam int b) {
    return a + b;
}

GET /sum?a=5&b=1015

GET /sum?a=abc&b=10 → 400エラー(型変換エラー)

複数の値 (List など)

パラメータに配列やリストを渡したい場合は List<String> を使えます。

例:

@GetMapping("/items")
public String getItems(@RequestParam List<String> items) {
    return "Items: " + String.join(", ", items);
}

GET /items?items=apple&items=banana&items=cherry
"Items: apple, banana, cherry"

@RequestParam を省略できる場合

@RequestParam を省略しても動く場合があります。例えば、シンプルなケースでは次のように書くこともできます。

例:

@GetMapping("/hello")
public String hello(String name) {  // @RequestParam なし
    return "Hello, " + name;
}

この場合も name はクエリパラメータから取得できます。ただし、必須ではなくなり、型変換エラーも発生しない ため、慎重に使う必要があります。

どの書き方を選ぶべきか?

次のように考えると分かりやすいです。

パターン使うケース
@RequestParam 省略クエリパラメータがあれば使う、なくてもエラーにしたくないpublic String hello(String name)
必須 (required=true)絶対に値が必要(省略不可)@RequestParam String name
省略可 (required=false)なくてもよいが、null で処理する@RequestParam(required = false) String name
デフォルト値 (defaultValue)省略時に既定の値を使いたい@RequestParam(defaultValue = "Guest") String name
リスト (List<String>)複数の値を渡したい@RequestParam List<String> items

5. エラー入力を適切に処理する

入力が想定外のとき

ビジネスロジック(Modelクラス)やControllerで検証処理を行います。

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class AddController {

    @GetMapping("/add")
    public String doGetAdd(
         @RequestParam(name="num1") String num1Str,
         @RequestParam(name="num2") String num2Str,
         Model model
    ) {
        try {
            int num1 = Integer.parseInt(num1Str);
            int num2 = Integer.parseInt(num2Str);
            model.addAttribute("ans", num1 + num2);
        } catch (NumberFormatException e) {
            model.addAttribute("error", "数値以外が入力されました。正しい整数を入力してください。");
            return "error";
        }

        return "ans";
    }
}

このように、異常な入力を受け取ったら適切なメッセージをセットし、エラー画面を返します。

6. ハイパーリンクやボタンを使ってGETメソッドでデータ送信

ここでは、天気予報プログラムのプロトタイプを考えてみましょう。

全国のユーザーが現在の天気を送るとリアルタイムで全国の天気が分かり、また、予測も可能になるというシステムのプロトタイプです。

天気予報サイトのイメージ

6.1 Controller

以下のようなコントローラーを用意します。

@Controller
public class WeatherController {

	@GetMapping("/link")
	public String link() {
		return "link"; // link.html
	}

	@GetMapping("/button")
	public String button() {
		return "button"; // button.html
	}

	@GetMapping("/weather")
	public String weather2(@RequestParam("weather") String weather, Model model) {
		model.addAttribute("weather", weather);
		return "weather"; // weather.html
	}
}

6.2 リンクでパラメータを送る

リンクを使ってデータを送ることができます。以下のように書けば、

@{/URL(リクエストパラメータ名='値')}

実際に生成されるHTMLは以下のようになります。

"/URL?リクエストパラメータ名=値"

    <a th:href="@{/weather(weather='晴れ')}">晴れ</a>
    <a th:href="@{/weather(weather='曇り')}">曇り</a>
    <a th:href="@{/weather(weather='雨')}">雨</a>

  • @{} を使うと、 URL を自動生成できる。
  • クエリパラメータ(weather=〇〇)は (key='value') の形で記述する。valueがシングルクォーテーション(')で囲まれているのはダブルクオーテーション(")で囲うとHTMLの href 属性のダブルクォーテーションとぶつかり、構文エラーになるからです。

aタグを使ったリンクは常にGETメソッドになることを覚えておいてください。

元来、HTTPのGETメソッドは主にWebページや他のリソース(例えば、画像やCSSファイル)を取得するために使用されてきました。

ブラウザがリンクをクリックするか、URLを直接入力すると、ブラウザはそのURLに対応するリソースを取得するためにGETリクエストをサーバーに送信するのです。

GET=ゲットする、POST=投稿するという元々の意味を考えれば2つのhttpメソッドを記憶するのは容易になると思います。

6.3 weather.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>weather.html</title>
</head>
<body>
  今は <span th:text="${weather}"></span> なんですね。
</body>
</html>

例題

上記link.jspに雪の場合を加えなさい。

例題

これまで作成してきた数当てゲームをリンクを使って実現しなさい。ただし、再利用できるファイルはできるだけ再利用すること。

画面イメージ

6.4 ボタンを使う

テキストボックスからリンクに入力方法を変えたことでユーザーの使いやすさが向上しました。さらにフォーム側での入力チェック(バリデーション)も不要になりました。

ここでは、さらにユーザー入力がしやすいようにボタンを使ってデータを送ります。ボタンはスマートフォンでも押しやすいですからね。

    <form th:action="@{/weather}" method="get">
        <button type="submit" name="weather" value="晴れ">晴れ</button>
        <button type="submit" name="weather" value="曇り">曇り</button>
        <button type="submit" name="weather" value="雨">雨</button>
    </form>

例題

上記button.htmlに雪の場合を加えなさい。

例題

数当てゲームをボタンを使って実現しなさい。

画面イメージ

サーバー側のバリデーションが省略できない理由

リンクやボタンを使ったデータ送信は、ユーザーがブラウザの開発者ツールなどを利用して容易にデータを改ざんできるため、クライアント側のバリデーションだけでは安全性が保証されません。悪意のある攻撃や誤ったデータ送信を防ぐためには、サーバー側でも必ずバリデーションを行い、データの整合性やシステムの安全性を保つ必要があります。なお、研修中はそこまでの配慮は不要とします。

穴埋め問題

空欄①〜⑨に適切なコードを記入してください。

【問題1:Controllerの穴埋め】

次のControllerクラスは、フォームから送信された数値を受け取り、その合計を計算して結果を画面に渡す処理を行います。

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.①(     );
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class AddController {

    @GetMapping("/add")
    public String add(
        @RequestParam("num1") String num1Str,
        @RequestParam("num2") String num2Str,
        ②(     ) model
    ) {
        int num1 = Integer.③(      )(num1Str);
        int num2 = Integer.parseInt(num2Str);
        int result = num1 + num2;

        model.④(     )("ans", result);

        return "⑤(     )";//ans.htmlを表示
    }
}

Thymeleafテンプレート穴埋め問題

以下のThymeleafテンプレート(ans.html)の空欄を埋めて、Controllerで渡した結果を表示できるようにしてください。

<!DOCTYPE html>
<html xmlns:th="⑥(     )">
<head>
    <meta charset="UTF-8">
    <title>計算結果</title>
</head>
<body>
  <p th:⑦(     )="${ans}">ここに計算結果が表示されます</p>
</body>
</html>

リクエストパラメータとフォームの穴埋め問題

次のHTMLフォームの穴を埋めて、/addへ2つの値を送信できるようにしてください。

<form th:⑧(     )="@{/add}" method="⑨(     )">
    <input type="text" name="num1">
    <input type="text" name="num2">
    <button type="submit">送信</button>
</form>


<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□パッケージ名やクラス名とは無関係に、@GetMapping("/xxx") のURLパスでコントローラークラス内に定義された対応するメソッドを呼び出す

□ フォームから送られるデータは基本的に文字列として送られるため、数値にするにはInteger.parseInt()などが必要

□ @RequestParam("num1") はフォームからname="num1"で送られた値を受け取る

@RequestParamは省略可能だが、明示する方が安全・明確

□ 例外処理をつかって入力エラーを適切に処理する

□ aタグを使ったリンクは常にGETメソッドになる

□ ボタンでもGETでパラメータ送信できる

第4章の今回は、フォームやリンクを使ってControllerにデータを渡し、Thymeleafで表示する方法について学びました。

今回紹介していないフォーム部品(ラジオボタンやセレクトボックスなど)も同様に @RequestParam で受け取れますので各自調べて下さい。

第5章は「セッション管理を学ぶ 〜 ログイン機能と買い物かごを作る」です。

セッション管理を学ぶことで複数のページをまたがってデータを保持することができるようになります。

セッション管理を学ぶことで複数のページをまたがってデータを保持できる