Claude Java SDKでアノテーションからツールを定義する — 非同期ストリーミングとメッセージバッチの実装
Claude Java SDKはJavaクラスのアノテーションからツールのJSONスキーマを自動生成します。非同期ストリーミング・ファイルアップロード・バッチ処理まで実装例で解説します。
Java SDKを導入してクライアントを設定する
Claude API用のJava SDKは、GradleかMavenで導入します。
// Gradle
implementation("com.anthropic:anthropic-java:2.60.0")<!-- Maven -->
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
<version>2.60.0</version>
</dependency>必要なJavaのバージョンは8以降です。ドキュメントのコード例自体はJDK 25のコンパクトソースファイル形式(void main() と IO.println())で書かれていますが、API呼び出しの内容はどのJDKでも同じで、古いJDKでは IO.println を System.out.println に、main メソッドをクラス内の public static void main(String[] args) に置き換えるだけで動きます。
クライアントは AnthropicOkHttpClient.fromEnv() で環境変数(ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL、対応するシステムプロパティは anthropic.apiKey 等)から自動設定するか、.builder().apiKey(...) で明示的に設定します。両方を組み合わせて fromEnv() のあとに .apiKey(...) で上書きすることもでき、システムプロパティは環境変数より優先されます。
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.maxTokens(1024L)
.addUserMessage("Hello, Claude")
.model(Model.CLAUDE_OPUS_5)
.build();
Message message = client.messages().create(params);アプリケーション内でクライアントを複数生成しないことが公式の推奨です。クライアントごとにコネクションプールとスレッドプールを持つため、リクエスト間で使い回したほうが効率的です。設定を一時的に変えたい場合は新しいクライアントを作らず、client.withOptions(...) で元のクライアントの接続・スレッドプールを共有したまま設定だけ変えたコピーを作ります。
同期と非同期を切り替える
デフォルトのクライアントは同期です。client.async() を呼ぶと非同期実行に切り替わり、CompletableFuture を返すメソッド群が使えるようになります。最初から非同期専用のクライアントとして AnthropicOkHttpClientAsync.fromEnv() を使うこともできます。
ストリーミングでは、同期クライアントは StreamResponse を、非同期クライアントは AsyncStreamResponse を返します。
try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(params)) {
streamResponse.stream().forEach(chunk -> IO.println(chunk));
}非同期ストリーミングは、現在のスレッドをブロックしないよう、クライアントごとに専用のキャッシュドスレッドプール Executor を使います。subscribe() の第2引数に別の Executor を渡すか、クライアント作成時に streamHandlerExecutor(...) を指定すれば、この既定の挙動を上書きできます。
チャンク単位のイベントを、非ストリーミングAPIが返すのと同じ Message オブジェクトへ組み立て直したいときは MessageAccumulator を使います。同期では Stream.peek(messageAccumulator::accumulate) をパイプラインに挟み、非同期では subscribe() のコールバック内で accumulate() を呼びます。ベータ版のメッセージには対になる BetaMessageAccumulator があります。
Javaクラスのアノテーションだけでツールを定義する
他言語のSDKでは、ツールのJSONスキーマを自分で書いてClaudeに渡すのが基本です。Java SDKはこの手間を省き、任意のJavaクラスの構造からツール定義を自動で導出します。クラス名(スネークケースに変換)がツール名、フィールドがツールのパラメータになります。
@JsonClassDescription("Get the weather in a given location")
static class GetWeather {
@JsonPropertyDescription("The city and state, e.g. San Francisco, CA")
public String location;
@JsonPropertyDescription("The unit of temperature")
public Unit unit;
public Weather execute() {
// 実処理
}
}定義したクラスは MessageCreateParams.Builder.addTool(GetWeather.class) でメッセージパラメータへ追加します。Claudeがツール呼び出しを要求してきたら、BetaToolUseBlock.input(GetWeather.class) でJSON形式の引数をクラスのインスタンスへ復元して実行し、結果は BetaToolResultBlockParam.Builder.contentAsJson(Object) でそのままモデルへ返せます。
クラスに付けられる主なアノテーションは5つです。
| アノテーション | 役割 |
|---|---|
@JsonClassDescription | 役割ツールの説明(いつ・どう使うか) |
@JsonPropertyDescription | 役割パラメータごとの詳細な説明 |
@JsonTypeName | 役割ツール名をスネークケース変換以外の名前に上書き |
@JsonIgnore | 役割公開フィールド・getterをスキーマから除外 |
@JsonProperty | 役割非公開フィールド・getterをスキーマに含める |
Claudeへ渡すJSONスキーマがAnthropicの制約を満たしているかは、既定で自動ローカル検証されます。この検証を無効にしたい場合は addTool(GetWeather.class, JsonSchemaLocalValidation.NO) のように第2引数を渡します。
ツール利用の機能にはさらに「strict」モードがあり、モデルの出力が渡したJSONスキーマに必ず一致することを保証します。アノテーションから自動生成したスキーマに対してもstrictモードは適用でき、何が保証されどこまでが保証の範囲外かはClaudeのstrict tool useが保証すること・しないことに切り出して扱っています。
ツール利用の基本的な流れ(モデルへツール一覧を渡し、tool_use を受け取って実行し、tool_result を返す)自体は言語を問わず共通です。手書きのJSON Schemaでこの流れを組む場合との違いは、スキーマの記述とツール名の導出をJacksonが肩代わりする点にあります。
ツール名はキャメルケースからどう変換されるか
クラス名からツール名への変換規則は、単なる小文字化ではありません。単語の境界は「現在の文字が先頭でなく、大文字で、かつ直前が小文字か直後が小文字」という条件で決まります。GetWeather は get_weather に、MyJSONParser は my_json_parser に、ParseJSON は parse_json になります。連続する大文字の塊は、直後に小文字が続く箇所だけで区切られる仕組みです。
この自動変換に頼らず名前を明示したいときは @JsonTypeName アノテーションで上書きします。API連携先やログの規約でツール名のフォーマットが決まっている場合は、変換結果を毎回目視確認するより、最初から @JsonTypeName で固定したほうが安全です。
ここで導出されるのは自作ツールの名前で、Anthropicが提供する公式ツール(code_execution 等)に付く _20250522 のようなバージョン接尾辞とは別の仕組みです。両者を混同すると、公式ツールのアップグレード時に自作ツール名まで変える必要があると誤解しがちなので、Claude APIツールのバージョン管理で切り分けを確認しておくと安全です。
ファイルアップロードとメッセージバッチを扱う
ファイルは MultipartField 経由で送ります。Path から読み込んだ InputStream、URLから開いたストリーム、メモリー上のバイト列のいずれも同じ形で FileUploadParams に渡せます。
FileUploadParams params = FileUploadParams.builder()
.file(
MultipartField.<InputStream>builder()
.value(Files.newInputStream(Paths.get("/path/to/file.pdf")))
.contentType("application/pdf")
.build()
)
.build();
FileMetadata fileMetadata = client.files().upload(params);ダウンロード側は client.files().download(...) が HttpResponse を返し、response.body() を Files.copy(...) や任意の OutputStream へ転送します。この HttpResponse は接続を保持したままのストリームなので、try-with-resources で確実にクローズします。
大量のリクエストをまとめて処理したい場合は、client.messages().batches() 名前空間でメッセージバッチを扱います。一覧・ページングの操作は他のリスト系エンドポイントと共通で、autoPager() を使えば全ページを自動的にたどりながら1件ずつ処理できます。手動でページを進めたい場合は page.hasNextPage() と page.nextPage() を使います。
ベータ機能を有効にしてリクエストIDで失敗を追跡する
正式リリース前の機能は client.beta() の名前空間から使います。個別のベータ機能を有効にするには、リクエストに anthropic-beta HTTPヘッダーを付ける必要があり、Java SDKではメッセージパラメータ構築時に .addBeta(...) を呼ぶとこのヘッダーが自動で送信されます。ヘッダーの値は機能ごとに決まった文字列(例: コンテキスト管理機能なら context-management-2025-06-27)で、AnthropicBeta の対応する定数を渡します。
BetaMessage message = client.beta().messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5)
.maxTokens(1024L)
.addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27)
.addUserMessage("Hello, Claude")
.build());リクエストが失敗してAnthropicへ調査を依頼するときは、request-id レスポンスヘッダーが手がかりになります。withRawResponse() を任意のHTTPメソッド呼び出しの前に付けるとステータスコード・ヘッダー・生のレスポンスボディへアクセスでき、requestId() でこのヘッダーだけを取り出せます。
HttpResponseFor<Message> message = client.messages().withRawResponse().create(params);
Optional<String> requestId = message.requestId();失敗時にこのIDをログへ残しておく運用にしておくと、サポートへの問い合わせ時に再現条件を伝える手間が減ります。
Model や AnthropicBeta はJavaの閉じた enum ではなく、of(String) ファクトリメソッドを持つ拡張可能なクラスです。新しいモデルやベータヘッダーがSDKのリリースより先にAPI側で使えるようになっても、Model.of("some-new-model") のように文字列を直接渡せば動きます。.model("some-new-model") のような文字列オーバーロードも用意されているので、SDKの更新を待たずに新モデルを試せます。ただし補完や非推奨警告が効くのは Model.CLAUDE_OPUS_5 のような型付き定数だけなので、SDKが対応済みならそちらを優先します。
リトライ・タイムアウト・エラー型を理解する
SDKは接続エラー・408・409・429・5xx系のエラーに限り、既定で2回まで指数バックオフ付きの自動リトライを行います。回数は client.builder().maxRetries(4) のように変更できます。
例外は用途別に分かれていて、HTTPエラー全般の基底が AnthropicServiceException、ネットワークI/Oエラーが AnthropicIoException、リトライ可能な失敗全般が AnthropicRetryableException です。ステータスコードは個別の例外型にもマッピングされます。
| ステータス | 例外 |
|---|---|
| 400 | 例外BadRequestException |
| 401 | 例外UnauthorizedException |
| 403 | 例外PermissionDeniedException |
| 404 | 例外NotFoundException |
| 429 | 例外RateLimitException |
| 5xx | 例外InternalServerException |
タイムアウトは既定で10分です。ストリーミングかつ maxTokens を指定している場合は、60 * 60 * maxTokens / 128_000 秒を基準に、最短10分・最長1時間の範囲で動的に計算されます。非ストリーミングでは最短30秒・最長10分の範囲で同様にスケールします。非ストリーミングのリクエストが10分を超えると見込まれる場合、SDKはエラーを送出してリクエストを止めます。ストリーミングへ切り替えるか、クライアントかリクエスト単位でタイムアウトを明示的に上書きすれば、このエラーは出なくなります。
実装でつまずきやすい4つのポイント
- ツールクラスをトップレベルかstaticネストクラスにし忘れる: 非staticの内部クラスはJacksonがインスタンス化できず、実行時エラーになります
- Jacksonのバージョン競合: SDKはJackson 2.13.4以降と互換ですが既定では2.19.4に依存しており、MavenやGradleの設定で別バージョンに上書きされていると非互換を検知して例外を投げます。バージョンが互換だと確信できる場合のみ
checkJacksonVersionCompatibilityで検知を無効化します - 大きな
maxTokensを非ストリーミングで指定する: ネットワークによってはアイドル接続が切られ、応答が返る前にタイムアウトすることがあります。長時間かかりうるリクエストはストリーミングに切り替えます - クライアントを乱立させる: リクエストのたびに新しいクライアントを作ると、コネクションプールとスレッドプールが使い回せず非効率です。設定を一時的に変えたいだけなら
withOptions()を使います
まとめ
Claude Java SDKは、GradleかMavenで導入したあと fromEnv() で認証し、.async() で同期・非同期を切り替えて使います。最大の特徴は、Javaクラスのアノテーションからツールのスキーマとツール名を自動導出する点で、C# SDK実装ガイドやPHP SDK実装ガイドのように手書きのJSON Schemaを管理する他言語の実装より手数が少なくなります。static修飾の要否・Jacksonのバージョン互換・非ストリーミング時のタイムアウトという3点を先に押さえておけば、実装時のつまずきの大半は避けられます。