
はじめに
アドベントカレンダー 6 日目担当の廣瀬と申します。 私は doda グロース開発部に所属しており、普段は doda サイトの開発に従事しております。 業務では Java、Spring Framework あたりがメイン言語となっています。
Spring Framework といえば、Spring Boot4.0 が 11 月 21 日にリリースされましたね! Java 25 のファーストクラスサポートに加え、API バージョニングや HTTP サービスクライアントのサポートが強化されていました。 本記事では HTTP サービスクライアントで追加された機能について試していきます。 github.com
作るもの
今回は Spring Boot 4 の新機能「HTTP Service Clients」を使って、
任意の国の祝日取得 API
日本 × ベトナム(× 任意の国)の祝日比較 API
を実装します。
背景
弊社では現在、ベトナムオフショアの活用を積極的に進めています。 日頃から BSE(Bridge SE)の方々と連携しながら開発を進めていますが、
「日本は祝日なので明日は連絡が取れません」 「こちらは平日ですが、そちらは祝日でしたか…?」
といった祝日のズレによるすれ違いが発生します。
そこで今回は日本、ベトナム(必要に応じてその他の国も)の祝日を比較できる API を作っていきます!
利用環境
- Spring Boot 4.0
- Java 25
- Docker
- 外部API(Public Holiday API) Public Holiday API - Nager.Date
ディレクトリ構成
spring-boot4-holidays/
├── Dockerfile
├── mvnw
├── .mvn/
├── pom.xml
└── src
└── main
├── java
│ └── com/example/demo
│ ├── DemoApplication.java
│ ├── client
│ │ └── HolidayClient.java
│ ├── service
│ │ └── HolidayService.java
│ └── web
│ └── HolidayController.java
└── resources
└── application.properties
全体の処理の流れ

実装
1. Spring Initializr で雛形作成
まずは、Spring Initializr で Spring プロジェクトの雛形を作成します。
- Spring Boot: 4.0.0
- Java: 25
- Dependencies: Spring Web, HTTP Client

2. application.properties の設定
spring.application.name=demo spring.http.clients.connect-timeout=1s spring.http.serviceclient.holiday.base-url=https://date.nager.at
3. HTTP Service Clientの登録
@ImportHttpServices で com.example.demo.client 配下の @HttpExchange interface を HTTP Service Client として登録。
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.service.registry.ImportHttpServices; @SpringBootApplication @ImportHttpServices( group = "holiday", basePackages = "com.example.demo.client" ) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
4. HTTP Service Client の実装
Public Holidays API を叩く HTTP Service Interface を定義。
package com.example.demo.client; import java.time.LocalDate; import java.util.List; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; @HttpExchange public interface HolidayClient { @GetExchange("/api/v3/PublicHolidays/{year}/{countryCode}") List<PublicHoliday> getPublicHolidays(@PathVariable int year, @PathVariable String countryCode); // Nager.Date のレスポンスに合わせたレコード // https://date.nager.at/api/v3/PublicHolidays/2025/JP 参照 record PublicHoliday( LocalDate date, String localName, String name, String countryCode, boolean global, List<String> types ) {} }
5. HolidayService の実装
祝日を比較するロジックを作成。
package com.example.demo.service; import java.util.*; import java.util.stream.Collectors; import com.example.demo.client.HolidayClient; import com.example.demo.client.HolidayClient.PublicHoliday; import org.springframework.stereotype.Service; @Service public class HolidayService { private final HolidayClient holidayClient; public HolidayService(HolidayClient holidayClient) { this.holidayClient = holidayClient; } public List<PublicHoliday> getHolidays(int year, String countryCode) { return holidayClient.getPublicHolidays(year, countryCode.toUpperCase()); } public HolidayComparison compare(int year, String country1, String country2) { var c1 = country1.toUpperCase(); var c2 = country2.toUpperCase(); List<PublicHoliday> list1 = holidayClient.getPublicHolidays(year, c1); List<PublicHoliday> list2 = holidayClient.getPublicHolidays(year, c2); // date で比較する Map<String, PublicHoliday> byDate1 = list1.stream() .collect(Collectors.toMap(h -> h.date().toString(), h -> h)); Map<String, PublicHoliday> byDate2 = list2.stream() .collect(Collectors.toMap(h -> h.date().toString(), h -> h)); List<PublicHoliday> shared = byDate1.keySet().stream() .filter(byDate2::containsKey) .map(byDate1::get) .collect(Collectors.toList()); List<PublicHoliday> only1 = byDate1.keySet().stream() .filter(k -> !byDate2.containsKey(k)) .map(byDate1::get) .collect(Collectors.toList()); List<PublicHoliday> only2 = byDate2.keySet().stream() .filter(k -> !byDate1.containsKey(k)) .map(byDate2::get) .collect(Collectors.toList()); return new HolidayComparison(c1, c2, only1, only2, shared); } public record HolidayComparison( String country1, String country2, List<PublicHoliday> onlyInCountry1, List<PublicHoliday> onlyInCountry2, List<PublicHoliday> sharedHolidays ) {} }
6. HolidayController の実装
REST API
package com.example.demo.controller; import java.util.List; import com.example.demo.client.HolidayClient.PublicHoliday; import com.example.demo.service.HolidayService; import com.example.demo.service.HolidayService.HolidayComparison; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/holidays") public class HolidayController { private final HolidayService holidayService; public HolidayController(HolidayService holidayService) { this.holidayService = holidayService; } // 汎用:どの国でも取れる // 例: /holidays/2025?country=JP // /holidays/2025?country=VN @GetMapping("/{year}") public List<PublicHoliday> getHolidays( @PathVariable int year, @RequestParam(defaultValue = "JP") String country ) { return holidayService.getHolidays(year, country); } // 比較API:日本×ベトナムとか // 例: /holidays/compare/2025?country1=JP&country2=VN @GetMapping("/compare/{year}") public HolidayComparison compare( @PathVariable int year, @RequestParam(defaultValue = "JP") String country1, @RequestParam(defaultValue = "VN") String country2 ) { return holidayService.compare(year, country1, country2); } }
7. Dockerfile
Docker 上でビルドする構成(Java 25 + Maven)
# ============================ # Build Stage (Java 25 + Maven) # ============================ FROM eclipse-temurin:25-jdk AS build WORKDIR /workspace # Maven wrapper を使えるようにコピー COPY mvnw . COPY .mvn .mvn COPY pom.xml . # 依存関係の事前ダウンロード RUN ./mvnw -q dependency:go-offline # ソースコードコピー COPY src src # Spring Boot アプリをビルド RUN ./mvnw -q package -DskipTests # ============================ # Run Stage (Java 25 JRE) # ============================ FROM eclipse-temurin:25-jre WORKDIR /app # Build Stage で生成された jar をコピー COPY --from=build /workspace/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
起動・動作確認
Dockerビルド
# ビルド docker build -t boot4-holidays . # 起動 docker run --rm -p 8080:8080 boot4-holidays
API 動作確認(日本 × ベトナム比較)
ブラウザで下記の URL にリクエストします。
http://localhost:8080/holidays/compare/2025?country1=JP&country2=VN
下記のレスポンスが返ってきました。

気になる祝日の重複ですが…
元日しか重複していなかったです。(意外と重複していないことが分かりました。)

Spring Boot3.5との比較
HTTP Service Clients の最大の差分は
「クライアント生成の“初期設定コード”を自前で書く必要がなくなったこと」だと思います。
3.5 の場合の実装
HolidayClient.java
@Bean WebClient holidayWebClient(WebClient.Builder builder) { return builder.baseUrl("https://date.nager.at").build(); } @Bean HolidayClient holidayClient(WebClient holidayWebClient) { var adapter = WebClientAdapter.forClient(holidayWebClient); var factory = HttpServiceProxyFactory.builder(adapter).build(); return factory.createClient(HolidayClient.class); }
4.0 の場合の実装
DemoApplication.java
@SpringBootApplication @ImportHttpServices(group = "holiday", basePackages = "com.example.demo.client") class DemoApplication {}
application.properties
spring.http.serviceclient.holiday.base-url: https://date.nager.at
HTTP Service Clientsの新機能によって何が便利になったのか(比較表)
| 項目 | Spring Boot 3.5 | Spring Boot 4.0 |
|---|---|---|
| HTTP Interface | 使えるが“クライアントを動かす準備コード”が必要 | @ImportHttpServices だけで自動登録 |
| base-url | WebClient Builder に定義 | application.properties に集約 |
| 設定方法 | クラスごとにコードで設定 | グループ単位で properties 化 |
| 拡張性 | API が増えるほど Config ファイルも増加 | interface を置くだけで追加可能 |
まとめ
Spring Boot 4 の HTTP Service Clients により 外部 API クライアントの実装が劇的にシンプルになりました
Bean 配線が不要・設定は集約され、実務での保守性が大幅向上しました
日本 × ベトナムの祝日比較 API という題材で 新機能の実用例を確認できました
GitHub リポジトリ

廣瀬 達也 Tatsuya Hirose
プロダクト開発統括部 グロース開発部 dodaサイト開発2グループ リードエンジニア
前職では不動産系のWebアプリ・モバイルアプリの受託開発を5年ほど経験。2023年6月にパーソルキャリアへ中途入社。以降は求人検索や応募系の機能の開発業務に従事。
