はじめに
これまで3回にわたって、GAS(Google Apps Script)でキャンプ予約の自動化をやってきました。GASは本当に手軽で、思いついたその日に動くものができるのが最高です。
ただ、書いているうちに「これJavaで組んだらどうなるんだろう」と思う瞬間が出てきました。せっかく別シリーズでSpring Bootの環境を作ってあるので、そこにAIを足してみます。
Spring Bootのローカル環境構築とDB連携は別記事で書いたので、この記事ではそこを前提にします。
やってみて分かったのは、ハマるのは環境構築ではなくSpring AI側だったということです。ネットの記事をコピペするとまず動きませんでした。理由は後述します。
この記事のゴール
URLに食材を入れると、Geminiがキャンプ飯を提案してくれるAPIです。
GET /camp-meal?ingredients=ベーコン,アボカド
料理名:アボカドのベーコン巻き焼き
手順:
1. アボカドを半分に切って種を抜き、くぼみに醤油を少し垂らす
2. ベーコンで巻いて爪楊枝で留める
3. 網の上で焦げ目がつくまで焼く
ひとこと:トロトロのアボカドとカリカリベーコンの塩気が、そのままつまみになります。
作るJavaファイルは1つだけでした。
環境
| 項目 | バージョン |
|---|---|
| macOS | Sonoma 14.x(M1 Pro) |
| Java | 21.0.7-tem(Temurin) |
| Spring Boot | 4.0.6 |
| Spring AI | 2.0.0 |
| ビルドツール | Maven |
| モデル | gemini-2.5-flash(Gemini Developer API) |
バージョンの組み合わせが一番大事なところです
Spring AI 2.0.x が対応するのは Spring Boot 4.0.x / 4.1.x です。Spring Boot 3系のままなら Spring AI 1.1.x を使う必要があります。ここがズレると、起動時に原因の分かりにくいエラーが出ます。
シリーズ既存記事が Spring Boot 4.0.6 なので、そのまま Spring AI 2.0.0 に乗れました。
1. 依存関係を追加する
既存プロジェクトの pom.xml に、BOMとスターターを追加します。
<!-- <dependencyManagement> に追加 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- <dependencies> に追加 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
スターター側に <version> は書きません。BOMに解決させるのが作法です。
ここが最初の落とし穴でした
検索して出てくるSpring AI + Gemini の記事は、だいたいspring-ai-vertex-ai-gemini(またはspring-ai-starter-model-vertex-ai-gemini)を使っています。
このモジュールは Spring AI 2.0 で削除されました。後継はspring-ai-starter-model-google-genaiで、新しい Google GenAI Java SDK ベースになっています。プロパティの接頭辞もパッケージ名も変わっているので、古い記事は設定ごと丸ごと読み替えが必要です。
| 旧(削除済み) | 現行 | |
|---|---|---|
| スターター | spring-ai-starter-model-vertex-ai-gemini |
spring-ai-starter-model-google-genai |
| プロパティ接頭辞 | spring.ai.vertex.ai.gemini |
spring.ai.google.genai |
| パッケージ | org.springframework.ai.vertexai.gemini |
org.springframework.ai.google.genai |
2. APIキーを取得する(GCPは不要)
Geminiの認証には2つのモードがあります。ここも詰まりやすいポイントなので整理しておきます。
| 認証モード | 必要なもの | 向いている用途 |
|---|---|---|
| Gemini Developer API | APIキー1本(Google AI Studioで発行) | 個人開発・プロトタイプ |
| Vertex AI | GCPプロジェクト+gcloud認証 |
本番・エンタープライズ |
今回はAPIキーだけの方でいきます。Google AI Studio でキーを発行するだけで、GCPプロジェクトの作成も gcloud のインストールも不要です。GASでAPIキーを使っていた感覚とほぼ同じです。
発行したキーは環境変数に置きます。
export GEMINI_API_KEY="発行したキー"
3. 設定を書く
src/main/resources/application.properties に3行だけ追加します。
# --- Gemini(Google GenAI)---
spring.ai.google.genai.api-key=${GEMINI_API_KEY}
spring.ai.google.genai.chat.model=gemini-2.5-flash
spring.ai.google.genai.chat.temperature=0.8
YAML派はこちら。
spring:
ai:
google:
genai:
api-key: ${GEMINI_API_KEY}
chat:
model: gemini-2.5-flash
temperature: 0.8
temperature を少し高めにしているのは、同じ食材でも毎回違う提案が返ってきた方が面白いからです。真面目な用途なら下げてください。
一番ハマったのはここです
project-id や location を書くと、クライアントはVertex AIモードに切り替わります。そしてAPIキーは拒否され、認証エラー(400)になります。
「無料枠を超えたのかな」と疑いたくなるエラーですが、原因は設定の書きすぎです。APIキー運用なら project-id と location は1行も書かない、と覚えておけば大丈夫です。
逆にVertex AIで動かしたい場合は、api-key を消して project-id と location を書きます。
モデル名にも注意が必要です。Gemini 1.x系の識別子はすべて停止済みで404になります。gemini-2.0-flash も2026年6月に停止しました。この記事では gemini-2.5-flash を使っています(spring.ai.google.genai.chat.model の既定値でもあります)。モデル名は記事の賞味期限が一番短い部分なので、動かないときはまずモデル一覧を見てください。
4. コントローラを1つ書く
src/main/java/com/example/demo/camp/CampMealController.java を作ります。
package com.example.demo.camp;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CampMealController {
private final ChatClient chatClient;
// ChatClient.Builder はSpring AIが自動で用意してくれる(DI)
public CampMealController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
あなたはベテランのキャンプコーディネーターです。
焚き火や炭火で作れる、簡単でワイルドなキャンプ飯を提案します。
回答は「料理名」「手順(3行以内)」「ひとこと」の3ブロックで書いてください。
""")
.build();
}
@GetMapping("/camp-meal")
public String suggest(@RequestParam(name = "ingredients") String ingredients) {
return chatClient.prompt()
.user(u -> u.text("次の食材で1品提案してください。\n食材: {ingredients}")
.param("ingredients", ingredients))
.call()
.content();
}
}
ポイントを3つだけ。
ChatClient.Builder はDIで降ってきます。 @Bean 定義も設定クラスも書いていません。前回JPAで TaskRepository がinterfaceだけで動いたのと同じノリです。
役割の指示(system)とその都度の入力(user)を分けています。 GASで書いていたときは1つの長い文字列にまとめていましたが、systemはdefaultSystem()で1回書けば全リクエストに効くので、こちらの方が見通しが良くなりました。
{ingredients} はSpring AIのテンプレート記法です。 .param() で値を差し込みます。Java標準の "...".formatted() でも動きますが、テンプレートに寄せた方が後々プロンプトを外部ファイルに切り出しやすくなります。
org.springframework.ai.chat.ChatClient ではありません
ChatClient の正しいパッケージは org.springframework.ai.chat.client.ChatClient です
(client が入ります)。
5. 動かしてみる
環境変数を読ませた状態で起動します。
export GEMINI_API_KEY="発行したキー"
./mvnw spring-boot:run
叩いてみます。日本語をクエリに入れるので、curlは --get --data-urlencode を使うのが楽です。
curl --get \
--data-urlencode "ingredients=ベーコン,アボカド" \
http://localhost:8080/camp-meal
数秒待つと、冒頭のゴールに書いたようなレシピが返ってきます。ここまでで書いたのは、pom.xmlに5行、propertiesに3行、Javaファイル1つだけです。
GASのときと何が違ったか
同じ「AIを呼ぶだけ」の処理を、GASとSpring AIで比べるとこうなりました。
| GAS | Spring Boot + Spring AI | |
|---|---|---|
| AI呼び出し |
UrlFetchApp でJSONを手組み |
ChatClient のfluent API |
| レスポンス処理 | JSONを自分で掘る |
.content() でテキストが取れる |
| モデルの差し替え | エンドポイントとJSON構造を書き換え | 依存とプロパティを差し替え |
| キー管理 | スクリプトプロパティ | 環境変数・設定ファイル |
| 動かし始めるまで | 数分 | 前回までの環境があれば10分ほど |
一番効いたのはJSONを手で組まなくなったことです。GASのときはリクエストJSONの contents → parts → text という入れ子を毎回書いていて、レスポンスも同じ深さから引っ張っていました。Spring AIはそこを全部隠してくれます。
同時に、前回JPAで感じたのと同じ引っかかりも残りました。楽になった分、裏で何が起きているかは見えなくなっています。 認証モードの切り替えでハマったのも、まさに「隠れている部分」につまづいてしまった形です。抽象化の便利さと、中身を知る必要は別物だな、と改めて思いました。
ハマりポイントメモ
| 症状 | 原因 / 対処 |
|---|---|
spring-ai-vertex-ai-gemini が解決できない |
Spring AI 2.0で削除済み。spring-ai-starter-model-google-genai に置き換える |
| APIキーが拒否される(400) |
project-id / location を書いている。Vertex AIモードに切り替わるので、APIキー運用では書かない |
| モデルが見つからない(404) | Gemini 1.x系は停止済み。gemini-2.5-flash など現行モデルを指定する |
| 起動時にクラスが見つからない系のエラー | スターターに個別の <version> を書いた。BOMに任せる |
ChatClient がimportできない |
org.springframework.ai.chat.client.ChatClient(client が必要) |
| Spring Bootとバージョンが合わない | Spring AI 2.0.xはBoot 4.0.x/4.1.x。Boot 3系ならSpring AI 1.1.x |
| curlで日本語が化ける |
--get --data-urlencode を使う |
入力に { } が入ると落ちる |
テンプレート記法と衝突する。ユーザー入力を直接テンプレートに埋めるなら要サニタイズ |
ここから先に必要そうなこと
動くところまでは来たものの、今のコードは実運用にはまだ足りません。気づいた分だけメモしておきます。
- タイムアウトとリトライ:LLMは数秒かかることも失敗することもあります。今は落ちたらそのまま500です
- レート制限とコスト:無料枠には日次のリクエスト上限があります。公開APIにするなら手前で絞る必要があります
-
出力をそのまま返している:LLMの生テキストを直接レスポンスにしているので、形式が毎回ぶれます。JSONで受け取りたいなら
response-mime-type=application/jsonや構造化出力の仕組みを使う方が確実です - プロンプトがコードに埋まっている:調整のたびに再ビルドが必要です。外部ファイルに出したいところです
まとめ
| やったこと | 内容 |
|---|---|
| 依存追加 | Spring AI BOM + spring-ai-starter-model-google-genai
|
| 認証 | Google AI StudioのAPIキーのみ(GCP不要) |
| 設定 |
spring.ai.google.genai.* を3行 |
| 実装 |
ChatClient をDIしてコントローラ1つ |
| 動作確認 | curlで食材を渡してレシピを取得 |
Javaは環境構築が大変、という印象は前回までのシリーズでだいぶ薄れていましたが、AIを足す部分はさらにあっさりでした。 実質、propertiesを3行とJavaファイル1つです。
一方で、詰まったのは全部「情報の古さ」でした。モジュール名、プロパティ名、モデル名。Spring AIもGeminiも動きが速いので、まず公式ドキュメントのバージョンを確認するのが結局一番の近道でした。この記事も同じ運命なので、動かないときは公式を見てください。
改善するかも?
提案されたレシピが毎回消えていくのが、さすがに惜しくなってきました。前回作ったPostgreSQLがすでにあるので、次はここに提案履歴を溜めて、過去の提案を踏まえた回答を返すところ(いわゆるRAGの入口)に手を出してみようと思います。