はじめに (対象読者・この記事でわかること)

この記事は、Javaでサーバーサイドを開発し、iOSアプリにプッシュ通知を送っている(またはこれから実装を検討している)エンジニアを対象にしています。特に「開発環境ではプッシュ通知が届くのに、本番環境(App Store/TestFlight配布版)だけ届かない」という事象に悩まれている方は必見です。

この記事を読むことで、APNs(Apple Push Notification service)における「開発証明書」「本番証明書」「プロビジョナルプロファイル」「トークン種別(sandbox/production)」の違いが明確になり、本番環境でも確実にプッシュ通知を届けるための設定・実装のポイントを押さえることができます。筆者も商用アプリで同様の問題に3日間ハマり、証明書を作り直したりサーバーコードを修正したりして解決した経験を元に、再発防止策を共有します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Java 11以上の基本的な文法と、Maven/Gradleを使ったライブラリ導入の知識 - iOSアプリのBundle IDや、Apple Developer Programで証明書・プロビジョナルプロファイルを作成した経験 - Pushyやjava-apnsなど、Java用のAPNsライブラリを使ったプッシュ通知送信用の基礎知識

APNsにおける「開発」と「本番」の2つの世界

iOSのプッシュ通知を送る際、APNsには「sandbox(開発)」「production(本番)」の2つの環境が存在します。ほとんどのJavaライブラリは、接続先ホストやトークン生成ロジックでこの2つを使い分ける必要があります。開発ビルド(XcodeのRunやAdHocのDebugビルド)はsandbox経由で、App StoreやTestFlight、AdHocのReleaseビルドはproduction経由で届きます。ここで「開発版では届くのに本番で届かない」という現象が発生します。

原因は大きく3つに分類できます。 1. サーバー側で本番用のAPNsクライアントを生成していない(sandboxのまま接続している) 2. 本番用プロビジョナルプロファイルに「プッシュ通知」機能が入っていない、または証明書が間違っている 3. デバイストークンがsandboxで取得されたものをproductionで使い回している

以降、JavaコードとApple側の設定を交えながら、これらを網羅的に解決します。

本番環境でも確実に届く!証明書・トークン・サーバー設定の見直し

ステップ1:Apple Developer Consoleで本番用証明書・プロビジョナルプロファイルを作り直す

  1. [Certificates, Identifiers & Profiles] → [Certificates] → [+] → 「Apple Push Notification service SSL (Sandbox & Production)」を選択
  2. App IDは本番アプリと完全に一致するBundle IDを選び、CSRをアップロード
  3. 作成した証明書をダブルクリックしてKeychainに登録し、秘密鍵とともに.p12でエクスポート
  4. [Profiles] → [+] → App StoreまたはAd Hocを選び、先ほどのApp IDと対応するDistribution用証明書を紐付け
  5. プロビジョナルプロファイル内の「Push Notifications」がEnabledになっていることを確認

ポイント:「Sandbox & Production」一本でも動きますが、セキュリティ方針によっては「Apple Push Notification service SSL (Sandbox only)」「Apple Push Notification service SSL (Production only)」を分けることもできます。

ステップ2:JavaサーバーでAPNsクライアントを環境別に生成する

Pushy(3.x以降)を例に説明します。Maven依存は以下。

Xml
<dependency> <groupId>com.eatthepath</groupId> <artifactId>pushy</artifactId> <version>0.15.2</version> </dependency>

環境ごとにクライアントを用意します。

Java
public enum ApnsEnv { SANDBOX, PRODUCTION } public final class ApnsClientFactory { private static final ApnsClient SANDBOX_CLIENT; private static final ApnsClient PRODUCTION_CLIENT; static { try { SANDBOX_CLIENT = new ApnsClientBuilder() .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) // 注意:sandboxホスト .setSigningKey(APNsSigningKey.loadFromPkcs8File( Paths.get("sandbox.p8"), "TEAM_ID", "KEY_ID")) .build(); PRODUCTION_CLIENT = new ApnsClientBuilder() .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) // 本番ホスト .setSigningKey(APNsSigningKey.loadFromPkcs8File( Paths.get("production.p8"), "TEAM_ID", "KEY_ID")) .build(); } catch (Exception e) { throw new RuntimeException("APNs client init failed", e); } } public static ApnsClient get(ApnsEnv env) { return env == ApnsEnv.SANDBOX ? SANDBOX_CLIENT : PRODUCTION_CLIENT; } }

トークン認証(.p8)を使っている場合、同じ鍵で両環境に接続できますが、ホスト名は必ず分けること。api.sandbox.push.apple.com:443api.push.apple.com:443 です。

ステップ3:デバイストークンを環境別に管理する

iOSアプリ側で、デバイストークンを取得する際にUIApplication.shared.registerForRemoteNotifications()の結果を判別し、サーバーに「sandbox/production」と送るようにします。

Swift
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() let isSandbox = Bundle.main.object(forInfoDictionaryKey: "APP_STORE_ENV") as? String != "PRODUCTION" api.register(token: token, sandbox: isSandbox) }

サーバー側では、トークン文字列と環境フラグをDBに保存しておき、プッシュ通知送信時に環境に応じたクライアントを選択します。

Java
public void sendPush(String deviceToken, boolean isSandbox, Payload payload) { ApnsClient client = ApnsClientFactory.get(isSandbox ? ApnsEnv.SANDBOX : ApnsEnv.PRODUCTION); client.sendNotification( new SimpleApnsPayloadBuilder() .setAlertBody(payload.getBody()) .build(), deviceToken, new DeliveryPriority IMMEDIATE, new Topic("com.example.app")); }

ハマった点とエラー解決

  1. BadDeviceTokenエラーが大量に出る - 原因:sandboxで取得したトークンをproductionクライアントで送信している - 解決:トークン保存時の環境フラグを見直し、該当レコードを再取得させる

  2. MissingTopicエラー - 原因:topic(Bundle ID)がAPNs側と一致していない - 解決:payloadに明示的にtopicを設定、または証明書作成時のApp IDを再確認

  3. 通知が届くがバッジ・サウンドが反映されない - 原因:payloadのフィールド名が間違っている(badge→countなど) - 解決:最新のAPNsフォーマットに従う。PushyのSimpleApnsPayloadBuilderを使えば自動対応

解決策(再掲)

  1. Apple Developer Consoleで本番用プロビジョナルプロファイルを再作成
  2. Java側でsandbox/production用のApnsClientを切り替える
  3. デバイストークンを環境と紐付けて保存・管理する
  4. 本番ビルドをTestFlightで検証し、プッシュ通知ログを確認

まとめ

本記事では、JavaでAPNs連携アプリを開発する際に「開発環境ではプッシュ通知が届くのに、本番環境(App Store/TestFlight配布版)だけ届かない」という事象を、証明書・プロビジョナルプロファイル・サーバーコードの3つの観点で解決しました。

  • Apple Developer Consoleでの本番用プロビジョナルプロファイル作成手順
  • Java(Pushy)で環境別にAPNsクライアントを生成する実装パターン
  • デバイストークンを環境と紐付けて管理する運用方法

この記事を通して、APNsの「sandbox/production」という2つの世界を正しく使い分けることで、本番環境でも確実にプッシュ通知を届けることができるようになります。今後は、トークン認証(.p8)の自動ローテーションや、APNsからのフィードバックサービスを使った無効トークン削除についても記事にする予定です。

参考資料