はじめに
前回までに、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";
}
今回はこの手動チェックをやめて、@Validated と BindingResult を使います。
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 {
以下のように、WeatherForm と BindingResult の間に 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()
+ "¤t_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()
+ "¤t_weather=true"
+ "&timezone=Asia/Tokyo";
この状態で都道府県を選択して送信すると、以下のエラーメッセージが表示されます。
天気情報の取得に失敗しました。時間をおいて再度お試しください。
確認後は、URLを元に戻しました。
String url = "https://api.open-meteo.com/v1/forecast"
+ "?latitude=" + location.getLatitude()
+ "&longitude=" + location.getLongitude()
+ "¤t_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文で未選択チェックを行っていましたが、今回は @NotBlank と BindingResult を使うことで、よりSpring MVCらしい入力チェックに変更できました。
また、外部API取得に失敗した場合でも、アプリ全体がエラー画面にならないように、Service側で例外を投げ、Controller側で受け取って画面にエラーメッセージを表示する流れを確認しました。
正常時だけでなく、入力ミスや外部API失敗時も考慮することで、少し実務に近い形の実装を学ぶことができました。