0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring Bootで@NotBlankとBindingResultを使った入力チェックとAPIエラー処理の実装

0
Posted at

はじめに

前回までに、Spring Bootで都道府県を選択し、選択した地域の天気情報を表示する画面を作成しました。

今回はその続きとして、以下の2点を学習しました。

1. @NotBlank と BindingResult を使った入力チェック
2. 外部API取得に失敗した場合のエラー表示

前回は、都道府県が未選択の場合にController内のif文でチェックしていました。

今回はその処理を、Spring MVCのバリデーション機能を使った形に変更しました。

また、Open-Meteo APIの取得に失敗した場合でも、アプリがエラー画面で落ちないように、画面上にエラーメッセージを表示する処理も追加しました。


環境

今回の環境は以下です。

OS:Windows11
IDE:Eclipse 2026
Java:Java SE 21
Spring Boot:4.1.0
ビルドツール:Maven
テンプレートエンジン:Thymeleaf
JSON処理:Jackson

今回やること

今回の目的は、フォーム送信時と外部API連携時のエラー処理を追加することです。

具体的には、以下のような動きを目指します。

都道府県を選択して送信した場合
→ 天気情報を表示する

都道府県を未選択で送信した場合
→ 「都道府県を選択してください。」と表示する

外部APIの取得に失敗した場合
→ 「天気情報の取得に失敗しました。」と表示する

前回までで、フォーム送信と画面表示はできていたので、今回はエラー時の処理を中心に実装します。


バリデーション用の依存関係を追加する

@NotBlank を使うために、pom.xml にバリデーション用の依存関係を追加します。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

追加後、Eclipseで以下を実行します。

プロジェクトを右クリック
↓
Maven
↓
プロジェクトの更新

これを行わないと、以下のようなエラーが出ることがあります。

インポートされた jakarta.validation は見つかりません
NotBlank を型に解決できません

今回も、pom.xml の変更が反映されていなかったため、最初は @NotBlank が認識されませんでした。


WeatherForm.javaに@NotBlankを追加する

まず、Formクラスに入力チェックのルールを書きます。

package com.example.demo;

import jakarta.validation.constraints.NotBlank;

import lombok.Getter;
import lombok.Setter;

// 画面から送られてきた入力値を受け取るFormクラス
@Getter
@Setter
public class WeatherForm {

	// 都道府県が未選択の場合はエラーにする
	@NotBlank(message = "都道府県を選択してください。")
	private String prefecture;
}

今回追加した重要な部分は以下です。

@NotBlank(message = "都道府県を選択してください。")

これは、prefecture が空の場合にエラーにするための指定です。

HTML側では、未選択用の選択肢を以下のようにしています。

<option value="">選択してください</option>

この状態で送信すると、prefecture には空文字が入ります。

その空文字を @NotBlank がチェックしてくれます。


Controllerで@ValidatedとBindingResultを使う

次に、Controller側を修正します。

前回までは、Controller内で以下のように手動チェックしていました。

if (prefecture == null || prefecture.isEmpty()) {
	model.addAttribute("errorMessage", "都道府県を選択してください。");
	return "weather-form";
}

今回はこの手動チェックをやめて、@ValidatedBindingResult を使います。

package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

// 画面表示を担当するController
@Controller
public class WeatherController {

	private final WeatherService weatherService;

	public WeatherController(WeatherService weatherService) {
		this.weatherService = weatherService;
	}

	// 都道府県選択画面を表示する
	@GetMapping("/weather-form")
	public String showWeatherForm(Model model) {

		model.addAttribute("weatherForm", new WeatherForm());

		return "weather-form";
	}

	// 都道府県が選択されたあと、天気情報を表示する
	@PostMapping("/weather-view")
	public String showWeather(
			@Validated WeatherForm weatherForm,
			BindingResult bindingResult,
			Model model) throws Exception {

		// 入力チェックでエラーがあった場合
		if (bindingResult.hasErrors()) {
			return "weather-form";
		}

		String prefecture = weatherForm.getPrefecture();

		try {
			WeatherInfo weatherInfo = weatherService.getWeather(prefecture);

			model.addAttribute("weatherInfo", weatherInfo);

			return "weather";

		} catch (Exception e) {
			model.addAttribute("apiErrorMessage", "天気情報の取得に失敗しました。時間をおいて再度お試しください。");

			return "weather-form";
		}
	}
}

@Validatedの役割

以下の部分で、WeatherForm に書いたバリデーションを実行しています。

@Validated WeatherForm weatherForm

今回の場合、WeatherForm には以下のチェックがあります。

@NotBlank(message = "都道府県を選択してください。")
private String prefecture;

そのため、フォーム送信時に prefecture が空であればエラーになります。


BindingResultの役割

BindingResult は、入力チェックの結果を受け取るためのものです。

BindingResult bindingResult

エラーがあるかどうかは、以下で確認できます。

if (bindingResult.hasErrors()) {
	return "weather-form";
}

この処理の意味は以下です。

入力エラーがある
↓
天気APIを呼ばない
↓
都道府県選択画面に戻る

未選択のまま送信された場合は、ここで処理を止めるため、Serviceは呼び出されません。


BindingResultの位置に注意する

BindingResult は、チェック対象のFormクラスの直後に書く必要があります。

正しい例です。

public String showWeather(
		@Validated WeatherForm weatherForm,
		BindingResult bindingResult,
		Model model) throws Exception {

以下のように、WeatherFormBindingResult の間に Model を挟むのは避けます。

public String showWeather(
		@Validated WeatherForm weatherForm,
		Model model,
		BindingResult bindingResult) throws Exception {

BindingResult の位置が離れると、バリデーションエラーを正しく受け取れないことがあります。


Thymeleafでバリデーションエラーを表示する

次に、weather-form.html 側でエラーメッセージを表示します。

<form th:action="@{/weather-view}" th:object="${weatherForm}" method="post">

	<select th:field="*{prefecture}">
		<option value="">選択してください</option>
		<option value="tokyo">東京都</option>
		<option value="osaka">大阪府</option>
		<option value="hokkaido">北海道</option>
		<option value="fukuoka">福岡県</option>
		<option value="okinawa">沖縄県</option>
	</select>

	<p class="error"
	   th:if="${#fields.hasErrors('prefecture')}"
	   th:errors="*{prefecture}">
	</p>

	<p class="error"
	   th:if="${apiErrorMessage}"
	   th:text="${apiErrorMessage}">
	</p>

	<button type="submit">天気を表示</button>
</form>

バリデーションエラーを表示しているのは以下です。

<p class="error"
   th:if="${#fields.hasErrors('prefecture')}"
   th:errors="*{prefecture}">
</p>

th:if は、エラーがある場合だけ表示するための指定です。

th:if="${#fields.hasErrors('prefecture')}"

th:errors は、prefecture に設定されたエラーメッセージを表示するための指定です。

th:errors="*{prefecture}"

今回の場合、以下のメッセージが表示されます。

都道府県を選択してください。

エラーメッセージに色を付ける

エラーメッセージを赤く表示するために、weather-form.html<style> 内に以下を追加しました。

.error {
	color: red;
	font-weight: bold;
	margin-top: 8px;
	margin-bottom: 16px;
}

HTML側で以下のように class="error" を付けているため、このCSSが適用されます。

<p class="error"
   th:if="${#fields.hasErrors('prefecture')}"
   th:errors="*{prefecture}">
</p>

CSSでは、タグ名に対して指定する場合はそのまま書きます。

body {
	margin: 40px;
}

class名に対して指定する場合は、先頭に . を付けます。

.error {
	color: red;
}

つまり、以下の関係です。

class="error"
↓
CSSでは .error

外部API失敗時のエラー処理を追加する

次に、Open-Meteo APIの取得に失敗した場合の処理を追加します。

外部API連携では、常に正常にデータが返ってくるとは限りません。

例えば、以下のようなケースがあります。

APIのURLが間違っている
通信に失敗する
API側でエラーが発生する
想定外のJSONが返ってくる

そのため、Service側でAPI取得処理を try-catch で囲みます。


WeatherService.javaの修正

getWeather() メソッドを以下のように修正しました。

public WeatherInfo getWeather(String prefecture) throws Exception {

	try {
		// 選択された都道府県に対応する緯度・経度を取得する
		PrefectureLocation location = getPrefectureLocation(prefecture);

		// 外部APIを呼び出すためのRestClientを作成する
		RestClient restClient = RestClient.create();

		// 選択された都道府県の緯度・経度を使ってURLを作成する
		String url = "https://api.open-meteo.com/v1/forecast"
				+ "?latitude=" + location.getLatitude()
				+ "&longitude=" + location.getLongitude()
				+ "&current_weather=true"
				+ "&timezone=Asia/Tokyo";

		// Open-MeteoのAPIを呼び出し、結果をJSON文字列として取得する
		String result = restClient.get()
				.uri(url)
				.retrieve()
				.body(String.class);

		// JSON文字列をJavaで扱いやすい形に変換する
		ObjectMapper mapper = new ObjectMapper();
		JsonNode root = mapper.readTree(result);

		// JSONの中から current_weather の部分だけ取り出す
		JsonNode currentWeather = root.get("current_weather");

		// current_weather の中から必要な値を取り出す
		String time = currentWeather.get("time").asString();
		double temperature = currentWeather.get("temperature").asDouble();
		double windspeed = currentWeather.get("windspeed").asDouble();
		int winddirection = currentWeather.get("winddirection").asInt();
		int weathercode = currentWeather.get("weathercode").asInt();

		// 天気コードを日本語の天気名に変換する
		String weatherName = convertWeatherCode(weathercode);

		// 取得した値をWeatherInfoに詰めて返す
		return new WeatherInfo(
				location.getName(),
				time,
				temperature,
				windspeed,
				winddirection,
				weathercode,
				weatherName
		);

	} catch (Exception e) {
		throw new Exception("天気情報の取得に失敗しました。", e);
	}
}

try-catchの役割

try の中には、通常の処理を書きます。

try {
	// API呼び出し
	// JSON解析
	// WeatherInfoの作成
}

catch の中には、失敗した場合の処理を書きます。

catch (Exception e) {
	throw new Exception("天気情報の取得に失敗しました。", e);
}

今回の場合、Service側ではエラーを画面に表示せず、Controllerへ例外として伝えています。


ControllerでAPIエラーを受け取る

Controller側では、Service呼び出し部分を try-catch で囲みます。

try {
	WeatherInfo weatherInfo = weatherService.getWeather(prefecture);

	model.addAttribute("weatherInfo", weatherInfo);

	return "weather";

} catch (Exception e) {
	model.addAttribute("apiErrorMessage", "天気情報の取得に失敗しました。時間をおいて再度お試しください。");

	return "weather-form";
}

正常に取得できた場合は、結果画面を表示します。

return "weather";

取得に失敗した場合は、選択画面に戻します。

return "weather-form";

その際、画面に表示するメッセージをModelに入れています。

model.addAttribute("apiErrorMessage", "天気情報の取得に失敗しました。時間をおいて再度お試しください。");

APIエラー表示をHTMLに追加する

weather-form.html 側には、API取得失敗時のメッセージ表示を追加しました。

<p class="error"
   th:if="${apiErrorMessage}"
   th:text="${apiErrorMessage}">
</p>

これにより、Controllerから apiErrorMessage が渡された場合だけ、エラーメッセージが表示されます。


動作確認

以下の3パターンで確認しました。

1. 都道府県を選択して送信する
2. 未選択のまま送信する
3. APIのURLを一時的に壊して送信する

正常時

都道府県を選択して送信すると、天気情報が表示されます。

東京都の現在の天気

この場合は、バリデーションエラーもAPIエラーも発生していません。


未選択時

「選択してください」のまま送信すると、以下のメッセージが表示されます。

都道府県を選択してください。

この場合、@NotBlank による入力チェックに引っかかり、Controllerでは以下の処理に入ります。

if (bindingResult.hasErrors()) {
	return "weather-form";
}

そのため、天気APIは呼び出されません。


API取得失敗時

APIエラーの確認では、一時的にURLを壊して動作確認しました。

String url = "https://api.open-meteo.com/v1/forecast-error"
		+ "?latitude=" + location.getLatitude()
		+ "&longitude=" + location.getLongitude()
		+ "&current_weather=true"
		+ "&timezone=Asia/Tokyo";

この状態で都道府県を選択して送信すると、以下のエラーメッセージが表示されます。

天気情報の取得に失敗しました。時間をおいて再度お試しください。

確認後は、URLを元に戻しました。

String url = "https://api.open-meteo.com/v1/forecast"
		+ "?latitude=" + location.getLatitude()
		+ "&longitude=" + location.getLongitude()
		+ "&current_weather=true"
		+ "&timezone=Asia/Tokyo";

途中で発生したエラー

実装中に、以下のようなエラーも発生しました。

Error resolving template [weathe-form], template might not exist

原因は、Controllerの戻り値でテンプレート名をタイプミスしていたことでした。

誤りです。

return "weathe-form";

正しくは以下です。

return "weather-form";

Spring Bootでは、Controllerで以下のように書くと、

return "weather-form";

Thymeleafは以下のファイルを探しに行きます。

src/main/resources/templates/weather-form.html

そのため、戻り値の文字列とテンプレートファイル名は一致している必要があります。


今回学んだこと

今回学んだことは以下です。

@NotBlank を使った未入力チェック
@Validated の使い方
BindingResult でバリデーションエラーを受け取る方法
Thymeleafでバリデーションエラーを表示する方法
try-catch による例外処理
外部API取得失敗時のエラーメッセージ表示
ControllerとServiceの役割分担

今回の実装で、エラーには大きく2種類あることを意識しました。

入力ミス
→ @NotBlank + BindingResult で対応する

外部APIや通信の失敗
→ try-catch で対応する

このように分けて考えることで、処理の役割が整理しやすくなりました。


まとめ

今回は、都道府県選択の天気APIアプリに、Spring MVCのバリデーションと外部APIエラー時の例外処理を追加しました。

前回までは、Controller内のif文で未選択チェックを行っていましたが、今回は @NotBlankBindingResult を使うことで、よりSpring MVCらしい入力チェックに変更できました。

また、外部API取得に失敗した場合でも、アプリ全体がエラー画面にならないように、Service側で例外を投げ、Controller側で受け取って画面にエラーメッセージを表示する流れを確認しました。

正常時だけでなく、入力ミスや外部API失敗時も考慮することで、少し実務に近い形の実装を学ぶことができました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?