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で住所から天気を検索するWebアプリを作ってみた

0
Posted at

はじめに

今回は、Spring Bootで「住所を入力すると現在の天気を表示するWebアプリ」を作成しました。

前回までは都道府県を選択して天気を表示していましたが、今回は一段進めて、住所入力から緯度・経度を取得し、その位置情報をもとに天気情報を取得する流れにしました。

使用したAPIは以下の2つです。

HeartRails Geo API
→ 入力された住所から緯度・経度を取得する

Open-Meteo API
→ 緯度・経度から現在の天気情報を取得する

また、GETとPOSTの違いを復習するため、以下のような画面遷移にしました。

名前入力画面
↓ POST送信
住所入力画面
↓ GET送信
天気結果画面

作成したもの

今回作成した画面は以下の3つです。

1. 名前入力画面
2. 住所入力画面
3. 天気結果画面

名前入力画面では、ユーザー名を入力します。

名前を入力してください
[ 田中 ]
[ 次へ ]

住所入力画面では、天気を調べたい住所を入力します。

住所から天気を検索

田中 さん、住所を入力してください。

[ 東京都新宿区西新宿 ]
[ 天気を検索 ]

天気結果画面では、入力した住所、取得できた住所、緯度・経度、天気情報を表示します。

田中 さんの天気検索結果

入力した住所:東京都新宿区西新宿
取得できた住所:東京都新宿区西新宿
緯度:...
経度:...

天気:曇り
気温:...
風速:...

環境

今回の環境は以下です。

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

プロジェクト構成

新しく作成したプロジェクト名は以下です。

AddressWeatherApp

パッケージ構成は以下のようにしました。

src/main/java/com/example/addressweather
 ├─ AddressWeatherAppApplication.java
 ├─ controller
 │   └─ WeatherController.java
 ├─ form
 │   ├─ NameForm.java
 │   └─ AddressSearchForm.java
 ├─ service
 │   ├─ GeoService.java
 │   └─ WeatherService.java
 └─ dto
     ├─ LocationInfo.java
     └─ WeatherInfo.java

src/main/resources/templates
 ├─ name-form.html
 ├─ address-form.html
 └─ weather.html

今回は少し実務寄りに、役割ごとにパッケージを分けました。

controller
→ 画面遷移を担当する

form
→ 画面から送られてきた入力値を受け取る

service
→ 外部API呼び出しや処理を担当する

dto
→ 画面表示やService間で使うデータを持つ

GETとPOSTの使い分け

今回のアプリでは、GETとPOSTを以下のように使い分けました。

名前入力
→ POST送信

住所検索
→ GET送信

名前入力画面では、以下のようにPOST送信にしています。

ファイル:templates/name-form.html

<form th:action="@{/address}" th:object="${nameForm}" method="post">

POST送信にすると、入力した名前はURLに表示されません。

一方、住所入力画面ではGET送信にしています。

ファイル:templates/address-form.html

<form th:action="@{/weather}" th:object="${addressSearchForm}" method="get">

GET送信にすると、入力した住所がURLに表示されます。

/weather?address=東京都新宿区西新宿

住所検索は「検索処理」なので、検索条件がURLに出るGET送信にしました。


Formクラス

画面から送られてきた入力値を受け取るために、Formクラスを作成しました。

ファイル:form/NameForm.java

package com.example.addressweather.form;

import jakarta.validation.constraints.NotBlank;

import lombok.Getter;
import lombok.Setter;

// ユーザー名入力フォーム
@Getter
@Setter
public class NameForm {

	@NotBlank(message = "名前を入力してください。")
	private String userName;
}

名前が未入力の場合は、@NotBlank によってエラーになります。


ファイル:form/AddressSearchForm.java

package com.example.addressweather.form;

import jakarta.validation.constraints.NotBlank;

import lombok.Getter;
import lombok.Setter;

// 住所検索フォーム
@Getter
@Setter
public class AddressSearchForm {

	@NotBlank(message = "住所を入力してください。")
	private String address;
}

住所も未入力の場合はエラーになるようにしています。


DTOクラス

外部APIから取得した値や、画面に表示する値をまとめるためにDTOクラスを作成しました。

ファイル:dto/LocationInfo.java

package com.example.addressweather.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

// 住所検索APIから取得した位置情報
@Getter
@AllArgsConstructor
public class LocationInfo {

	private String prefecture;

	private String city;

	private String town;

	private String postal;

	private double latitude;

	private double longitude;

	public String getFullAddress() {
		return prefecture + city + town;
	}
}

LocationInfo は、HeartRails Geo APIから取得した住所情報を持つクラスです。

都道府県
市区町村
町域
郵便番号
緯度
経度

を保持します。


ファイル:dto/WeatherInfo.java

package com.example.addressweather.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

// 天気結果画面に表示する情報
@Getter
@AllArgsConstructor
public class WeatherInfo {

	private String userName;

	private String searchedAddress;

	private String resolvedAddress;

	private double latitude;

	private double longitude;

	private String time;

	private double temperature;

	private double windspeed;

	private int winddirection;

	private int weathercode;

	private String weatherName;
}

WeatherInfo は、天気結果画面に表示する情報をまとめるクラスです。

入力した住所だけでなく、APIから取得できた住所や緯度・経度も表示できるようにしました。


Controller

画面遷移を担当するControllerを作成しました。

ファイル:controller/WeatherController.java

package com.example.addressweather.controller;

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;

import com.example.addressweather.dto.LocationInfo;
import com.example.addressweather.dto.WeatherInfo;
import com.example.addressweather.form.AddressSearchForm;
import com.example.addressweather.form.NameForm;
import com.example.addressweather.service.GeoService;
import com.example.addressweather.service.WeatherService;

import jakarta.servlet.http.HttpSession;

// 画面遷移を担当するController
@Controller
public class WeatherController {

	private final GeoService geoService;

	private final WeatherService weatherService;

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

	@GetMapping("/")
	public String showNameForm(Model model) {

		model.addAttribute("nameForm", new NameForm());

		return "name-form";
	}

	@PostMapping("/address")
	public String receiveName(
			@Validated NameForm nameForm,
			BindingResult bindingResult,
			HttpSession session) {

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

		session.setAttribute("userName", nameForm.getUserName());

		return "redirect:/address";
	}

	@GetMapping("/address")
	public String showAddressForm(HttpSession session, Model model) {

		String userName = (String) session.getAttribute("userName");

		if (userName == null) {
			return "redirect:/";
		}

		model.addAttribute("userName", userName);
		model.addAttribute("addressSearchForm", new AddressSearchForm());

		return "address-form";
	}

	@GetMapping("/weather")
	public String showWeather(
			@Validated AddressSearchForm addressSearchForm,
			BindingResult bindingResult,
			HttpSession session,
			Model model) {

		String userName = (String) session.getAttribute("userName");

		if (userName == null) {
			return "redirect:/";
		}

		model.addAttribute("userName", userName);

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

		try {
			String address = addressSearchForm.getAddress();

			LocationInfo locationInfo = geoService.findLocationByAddress(address);

			WeatherInfo weatherInfo = weatherService.getWeather(
					userName,
					address,
					locationInfo
			);

			model.addAttribute("weatherInfo", weatherInfo);

			return "weather";

		} catch (Exception e) {
			model.addAttribute("apiErrorMessage", "住所または天気情報の取得に失敗しました。入力内容を確認してください。");

			return "address-form";
		}
	}
}

POST後にredirectする

名前入力後の処理では、以下のようにしています。

ファイル:controller/WeatherController.java

session.setAttribute("userName", nameForm.getUserName());

return "redirect:/address";

最初は、POST処理の中で直接 address-form.html を表示していました。

しかし、その場合、住所入力画面に遷移した直後に名前が表示されませんでした。

そこで、POST処理ではSessionに名前を保存したあと、redirect:/address でGETの住所入力画面へ遷移させる形にしました。

POST /address
↓
Sessionに名前を保存
↓
redirect:/address
↓
GET /address
↓
Sessionから名前を取得
↓
画面に名前を表示

この流れは、POST-Redirect-GETという考え方に近いものです。


GeoService

住所から緯度・経度を取得する処理は、GeoService に分けました。

ファイル:service/GeoService.java

package com.example.addressweather.service;

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import com.example.addressweather.dto.LocationInfo;

import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

// HeartRails Geo APIを使って、住所から緯度・経度を取得するService
@Service
public class GeoService {

	public LocationInfo findLocationByAddress(String address) throws Exception {

		RestClient restClient = RestClient.create();

		String result = restClient.get()
				.uri(uriBuilder -> uriBuilder
						.scheme("https")
						.host("geoapi.heartrails.com")
						.path("/api/json")
						.queryParam("method", "suggest")
						.queryParam("matching", "like")
						.queryParam("keyword", address)
						.build())
				.retrieve()
				.body(String.class);

		ObjectMapper mapper = new ObjectMapper();
		JsonNode root = mapper.readTree(result);

		JsonNode response = root.get("response");

		if (response == null) {
			throw new Exception("住所検索APIのレスポンスが不正です。");
		}

		JsonNode error = response.get("error");
		if (error != null) {
			throw new Exception(error.asString());
		}

		JsonNode locations = response.get("location");

		if (locations == null) {
			throw new Exception("住所が見つかりませんでした。");
		}

		JsonNode location;

		if (locations.isArray()) {
			if (locations.size() == 0) {
				throw new Exception("住所が見つかりませんでした。");
			}

			location = locations.get(0);

		} else {
			location = locations;
		}

		String prefecture = getText(location, "prefecture");
		String city = getText(location, "city");
		String town = getText(location, "town");
		String postal = getText(location, "postal");

		double longitude = Double.parseDouble(getText(location, "x"));
		double latitude = Double.parseDouble(getText(location, "y"));

		return new LocationInfo(
				prefecture,
				city,
				town,
				postal,
				latitude,
				longitude
		);
	}

	private String getText(JsonNode node, String fieldName) {

		if (node.get(fieldName) == null) {
			return "";
		}

		return node.get(fieldName).asString();
	}
}

URLエンコードで詰まったところ

最初は、住所を以下のように手動でURLエンコードしていました。

ファイル:service/GeoService.java

String encodedAddress = URLEncoder.encode(address, StandardCharsets.UTF_8);

しかし、この状態だとAPI側にうまく住所が渡らず、以下のようなエラーが返ってきました。

City '%E6%96%B0%E5%AE%BF' does not exist.

これは、API側に「新宿」ではなく、エンコード後の文字列がそのまま渡ってしまっていたためです。

そのため、手動で URLEncoder を使うのをやめて、uriBuilder.queryParam() を使う形に変更しました。

ファイル:service/GeoService.java

.queryParam("keyword", address)

これにより、Spring側にURLの組み立てを任せる形になり、正常に住所検索できるようになりました。


WeatherService

緯度・経度から天気情報を取得する処理は、WeatherService に分けました。

ファイル:service/WeatherService.java

package com.example.addressweather.service;

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import com.example.addressweather.dto.LocationInfo;
import com.example.addressweather.dto.WeatherInfo;

import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

// Open-Meteo APIを使って天気情報を取得するService
@Service
public class WeatherService {

	public WeatherInfo getWeather(
			String userName,
			String searchedAddress,
			LocationInfo locationInfo) throws Exception {

		try {
			RestClient restClient = RestClient.create();

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

			String result = restClient.get()
					.uri(url)
					.retrieve()
					.body(String.class);

			ObjectMapper mapper = new ObjectMapper();
			JsonNode root = mapper.readTree(result);

			JsonNode currentWeather = root.get("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);

			return new WeatherInfo(
					userName,
					searchedAddress,
					locationInfo.getFullAddress(),
					locationInfo.getLatitude(),
					locationInfo.getLongitude(),
					time,
					temperature,
					windspeed,
					winddirection,
					weathercode,
					weatherName
			);

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

	private String convertWeatherCode(int weathercode) {

		switch (weathercode) {
		case 0:
			return "快晴";
		case 1:
			return "主に晴れ";
		case 2:
			return "一部曇り";
		case 3:
			return "曇り";
		case 45:
		case 48:
			return "霧";
		case 51:
		case 53:
		case 55:
			return "霧雨";
		case 61:
		case 63:
		case 65:
			return "雨";
		case 71:
		case 73:
		case 75:
			return "雪";
		case 80:
		case 81:
		case 82:
			return "にわか雨";
		case 95:
			return "雷雨";
		default:
			return "不明";
		}
	}
}

画面側の実装

名前入力画面

ファイル:templates/name-form.html

<form th:action="@{/address}" th:object="${nameForm}" method="post">

	<input type="text" th:field="*{userName}" placeholder="例:田中">

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

	<button type="submit">次へ</button>
</form>

名前入力画面ではPOST送信を使っています。


住所入力画面

ファイル:templates/address-form.html

<p>
	<span th:text="${userName}"></span> さん、住所を入力してください。
</p>

<form th:action="@{/weather}" th:object="${addressSearchForm}" method="get">

	<input type="text" th:field="*{address}" placeholder="住所を入力してください">

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

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

	<button type="submit">天気を検索</button>
</form>

住所入力画面ではGET送信を使っています。

検索後は、URLに住所が表示されます。

/weather?address=東京都新宿区西新宿

天気結果画面

ファイル:templates/weather.html

<h1>
	<span th:text="${weatherInfo.userName}"></span> さんの天気検索結果
</h1>

<div class="item">
	<span class="label">入力した住所:</span>
	<span th:text="${weatherInfo.searchedAddress}"></span>
</div>

<div class="item">
	<span class="label">取得できた住所:</span>
	<span th:text="${weatherInfo.resolvedAddress}"></span>
</div>

<div class="item">
	<span class="label">緯度:</span>
	<span th:text="${weatherInfo.latitude}"></span>
</div>

<div class="item">
	<span class="label">経度:</span>
	<span th:text="${weatherInfo.longitude}"></span>
</div>

<div class="item">
	<span class="label">天気:</span>
	<span th:text="${weatherInfo.weatherName}"></span>
</div>

<div class="item">
	<span class="label">気温:</span>
	<span th:text="${weatherInfo.temperature}"></span></div>

結果画面では、入力住所、取得住所、緯度・経度、天気情報を表示します。


動作確認

アプリを起動し、以下にアクセスします。

http://localhost:8080/

まず名前を入力します。

田中

POST送信なので、この時点で名前はURLに表示されません。

次に住所を入力します。

東京都新宿区西新宿

住所検索はGET送信なので、URLに検索条件が表示されます。

/weather?address=東京都新宿区西新宿

天気結果画面で、住所・緯度・経度・天気情報が表示されれば成功です。


今回学んだこと

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

新規Spring Bootプロジェクトの作成
役割ごとのパッケージ分割
Formクラスによる入力値の受け取り
DTOクラスによるデータ保持
Sessionを使った名前の保持
POST送信とGET送信の使い分け
HeartRails Geo APIによる住所検索
Open-Meteo APIによる天気取得
外部APIのJSON解析
Serviceクラスへの処理分離

特に重要だと感じたのは、GETとPOSTの使い分けです。

POST
→ URLに出したくない情報を送る場合に使う

GET
→ 検索条件としてURLに表示してもよい情報を送る場合に使う

また、外部APIを使う場合は、APIから返ってくるJSONの形や、URLの作り方に注意が必要だと分かりました。


まとめ

今回は、HeartRails Geo APIとOpen-Meteo APIを組み合わせて、住所から現在の天気を検索するWebアプリを作成しました。

前回までの都道府県選択型の天気アプリから発展し、今回は住所入力、緯度・経度取得、天気取得という流れを実装しました。

また、名前入力ではPOST送信、住所検索ではGET送信を使い分けることで、HTTPメソッドの違いも確認できました。

処理をController、Form、Service、DTOに分けることで、以前よりも役割が整理された構成にできたと思います。

今後は、住所候補を複数表示して選択できるようにしたり、CSSを外部ファイル化したりして、さらに実用的な形に近づけていきたいです。

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?