Java の Lambda 関数ハンドラーの定義 - AWS Lambda

Java の Lambda 関数ハンドラーの定義

Lambda 関数ハンドラーは、イベントを処理する関数コード内のメソッドです。関数が呼び出されると、Lambda はハンドラーメソッドを実行します。関数は、ハンドラーが応答を返すか、終了するか、タイムアウトするまで実行されます。

このガイドの GitHub リポジトリには、さまざまなハンドラータイプを示す、簡単にデプロイできるサンプルアプリケーションが用意されています。詳細については、このトピックの最後を参照してください。

ハンドラーの例: Java 17 ランタイム

以下の Java 17 の例では、HandlerIntegerJava17 という名前のクラスで handleRequest という名前のハンドラーメソッドを定義しています。ハンドラーメソッドは以下の入力を受け取ります。

  • IntegerRecord 。これは、イベントデータを表すカスタム Java レコードです。この例では、IntegerRecord を次のように定義します。

    record IntegerRecord(int x, int y, String message) { }
  • コンテキストオブジェクト。このオブジェクトは、呼び出し、関数、および実行環境に関する情報を示すメソッドおよびプロパティを提供します。

入力 IntegerRecord から message をログに記録し、x と y の合計を返す関数を記述したいとします。関数コードは次のとおりです。

HandlerIntegerJava17.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; // Handler value: example.HandlerInteger public class HandlerIntegerJava17 implements RequestHandler<IntegerRecord, Integer>{ @Override /* * Takes in an InputRecord, which contains two integers and a String. * Logs the String, then returns the sum of the two Integers. */ public Integer handleRequest(IntegerRecord event, Context context) { LambdaLogger logger = context.getLogger(); logger.log("String found: " + event.message()); return event.x() + event.y(); } } record IntegerRecord(int x, int y, String message) { }

関数の設定でハンドラーパラメータを指定することにより、Lambda が呼び出すメソッドを指示します。ハンドラーは次の形式で表現できます。

  • package.Class::method - 完全形式。例: example.Handler::handleRequest

  • package.Classハンドラーインターフェイスを実装するクラスの省略形式。例: example.Handler

Lambda がハンドラーを呼び出すと、Lambda ランタイムはイベントを JSON 形式の文字列として受け取り、オブジェクトに変換します。前の例では、サンプルイベントは次のようになります。

event.json
{ "x": 1, "y": 20, "message": "Hello World!" }

このファイルを保存し、次の AWS Command Line Interface (CLI) コマンドを使用して関数をローカルでテストできます。

aws lambda invoke --function-name function_name --payload file://event.json out.json

ハンドラーの例: Java 11 以下のランタイム

Lambda は Java 17 以降のランタイムのレコードをサポートします。すべての Java ランタイムで、クラスを使用してイベントデータを表すことができます。次の例では、整数のリストとコンテキストオブジェクトを入力として受け取り、リスト内のすべての整数の合計を返します。

以下の例では、Handler という名前のクラスで handleRequest という名前のハンドラーメソッドを定義しています。ハンドラーメソッドは、イベントとコンテキストオブジェクトを入力として受け取り、文字列を返します。

HandlerList.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; import java.util.List; // Handler value: example.HandlerList public class HandlerList implements RequestHandler<List<Integer>, Integer>{ @Override /* * Takes a list of Integers and returns its sum. */ public Integer handleRequest(List<Integer> event, Context context) { LambdaLogger logger = context.getLogger(); logger.log("EVENT TYPE: " + event.getClass().toString()); return event.stream().mapToInt(Integer::intValue).sum(); } }

その他の例については、「サンプルハンドラーコード」を参照してください。

初期化コード

Lambda は、関数を初めて呼び出す前の初期化フェーズで静的コードとクラスコンストラクターを実行します。初期化中に作成されたリソースは、呼び出しと呼び出しの間でメモリ内に保持され、ハンドラーによって何千回も再利用できます。したがって、メインハンドラーメソッドの外側に初期化コードを追加することで、計算時間を節約し、複数の呼び出し間でリソースを再利用することができます。

次の例では、クライアント初期化コードはメインハンドラーメソッドの外にあります。ランタイムは、関数が最初のイベントを処理する前にクライアントを初期化します。Lambda がクライアントを再度初期化する必要がないため、後続のイベントはずっと速くなります。

Handler.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; import java.util.Map; import software.amazon.awssdk.services.lambda.LambdaClient; import software.amazon.awssdk.services.lambda.model.GetAccountSettingsResponse; import software.amazon.awssdk.services.lambda.model.LambdaException; // Handler value: example.Handler public class Handler implements RequestHandler<Map<String,String>, String> { private static final LambdaClient lambdaClient = LambdaClient.builder().build(); @Override public String handleRequest(Map<String,String> event, Context context) { LambdaLogger logger = context.getLogger(); logger.log("Handler invoked"); GetAccountSettingsResponse response = null; try { response = lambdaClient.getAccountSettings(); } catch(LambdaException e) { logger.log(e.getMessage()); } return response != null ? "Total code size for your account is " + response.accountLimit().totalCodeSize() + " bytes" : "Error"; } }

入力タイプと出力タイプの選択

イベントがマッピングするオブジェクトのタイプは、ハンドラーメソッドの署名で指定します。上記の例では、Java ランタイムはイベントを、Map<String,String> インターフェイスを実装するタイプに逆シリアル化します。文字列から文字列へのマッピングは、以下のような階層なしのイベントで機能します。

Event.json - 気象データ
{ "temperatureK": 281, "windKmh": -3, "humidityPct": 0.55, "pressureHPa": 1020 }

ただし、各フィールドの値は文字列または数値であることが必要です。イベント内のフィールドにオブジェクトが値として含まれている場合、ランタイムはそれを逆シリアル化できず、エラーを返します。

関数が処理するイベントデータに使用する入力タイプを選択します。基本タイプ、汎用タイプ、または良定義タイプを使用できます。

入力タイプ
  • Integer LongDouble、など - イベントは、追加の形式のない数値です (3.5 など)。ランタイムは、値を指定されたタイプのオブジェクトに変換します。

  • String - イベントは、引用符を含む JSON 文字列です ("My string." など)。ランタイムは、引用符なしの値を String オブジェクトに変換します。

  • TypeMap<String,Type> など - イベントは JSON オブジェクトです。ランタイムは、それを指定されたタイプまたはインターフェイスのオブジェクトに逆シリアル化します。

  • List<Integer> List<String>List<Object>、など - イベントは JSON 配列です。ランタイムは、それを指定されたタイプまたはインターフェイスのオブジェクトに逆シリアル化します。

  • InputStream - イベントは任意の JSON タイプです。ランタイムは、ドキュメントのバイトストリームを変更せずにハンドラーに渡します。入力を逆シリアル化し、出力を出力ストリームに書き込みます。

  • ライブラリタイプ - AWS サービスによって送信されるイベントの場合、aws-lambda-java-events ライブラリのタイプを使用します。

独自の入力タイプを定義する場合、そのタイプは、逆シリアライズかつ変更可能な Plain Old Java Object (POJO) であり、デフォルトのコンストラクタと、イベントの各フィールドのプロパティが必要です。プロパティにマッピングされないイベントのキー、およびイベントに含まれていないプロパティは、エラーなしでドロップされます。

出力タイプはオブジェクトまたは void です。ランタイムは、戻り値をテキストにシリアル化します。出力が、フィールドを含むオブジェクトの場合、ランタイムは、それを JSON ドキュメントにシリアル化します。出力が、プリミティブ値をラップするタイプの場合、ランタイムは、その値のテキスト表現を返します。

ハンドラーのインターフェイス

aws-lambda-java-core ライブラリは、ハンドラーメソッドの 2 つのインターフェイスを定義します。用意されているインターフェイスを使用して、ハンドラー設定をシンプルにし、コンパイル時にハンドラーメソッドの署名を検証します。

RequestHandler インターフェイスは、入力タイプと出力タイプの 2 つのパラメータを受け取る汎用タイプです。どちらのタイプもオブジェクトであることが必要です。このインターフェイスを使用すると、Java ランタイムはイベントを入力タイプのオブジェクトに逆シリアル化し、出力をテキストにシリアル化します。組み込みのシリアル化が入力タイプと出力タイプで機能する場合は、このインターフェイスを使用します。

Handler.java - ハンドラーインターフェイス
// Handler value: example.Handler public class Handler implements RequestHandler<Map<String,String>, String>{ @Override public String handleRequest(Map<String,String> event, Context context)

独自のシリアル化を使用するには、RequestStreamHandler インターフェイスを実装します。このインターフェイスでは、Lambda はハンドラーに入力ストリームと出力ストリームを渡します。ハンドラーは、入力ストリームからバイトを読み取り、出力ストリームに書き込み、void を返します。

次の Java 21 の例は、Lambda 関数を使用して注文を処理する方法を示しています。この例では、バッファー付きの読み込みクラスと書き込みクラスを使用して入力ストリームと出力ストリームを処理し、関数内でカスタム Java レコードを定義する方法を示しています。

ハンドラーストリーム
import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; public class HandlerStream implements RequestStreamHandler { private static final ObjectMapper objectMapper = new ObjectMapper(); @Override public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { Order order = objectMapper.readValue(input, Order.class); processOrder(order); OrderAccepted orderAccepted = new OrderAccepted(order.orderId); objectMapper.writeValue(output, orderAccepted); } private void processOrder(Order order) { // business logic } public record Order(@JsonProperty("orderId") String orderId, @JsonProperty("items") List<Item> items) { } public record Item(@JsonProperty("name") String name, @JsonProperty("quantity") Integer quantity) { } public record OrderAccepted(@JsonProperty("orderId") String orderId) { } }

Java Lambda 関数のコードのベストプラクティス

Lambda 関数をビルドするときは、次のリストのガイドラインに従って、コーディングのベストプラクティスを実行してください。

  • Lambda ハンドラーをコアロジックから分離します。これにより、関数の単体テストが実行しやすくなります。

  • 関数のデプロイパッケージ内で依存関係を制御します。AWS Lambda 実行環境には多数のライブラリが含まれています。最新の機能やセキュリティ更新プログラムを有効にするために、Lambda はこれらのライブラリを定期的に更新します。この更新により、Lambda 関数の動作が微妙に変化する場合があります。関数で使用する依存関係を完全に制御するには、すべての依存関係をデプロイパッケージでパッケージングします。

  • 依存関係の複雑さを最小限に抑えます。フレームワークを単純化して、実行環境起動時のロードを高速化します。たとえば、Spring Framework などの複雑なフレームワークよりも、DaggerGuice などの単純な Java 依存関係インジェクション (IoC) フレームワークを使用します。

  • デプロイパッケージをランタイムに必要な最小限のサイズにします。これにより、呼び出しに先立ってデプロイパッケージをダウンロードして解凍する所要時間が短縮されます。Java で記述された関数の場合は、デプロイパッケージの一部として AWS SDK ライブラリ全体をアップロードしないようにしてください。代わりに、SDK のコンポーネントを必要に応じて選別するモジュール (DynamoDB、Amazon S3 SDK モジュール、Lambda コアライブラリなど) を使用します。

  • 実行環境の再利用を活用して関数のパフォーマンスを向上させます。関数ハンドラー外で SDK クライアントとデータベース接続を初期化し、静的なアセットを /tmp ディレクトリにローカルにキャッシュします。関数の同じインスタンスで処理された後続の呼び出しは、これらのリソースを再利用できます。これにより、関数の実行時間が短縮され、コストが節約されます。

    呼び出し間でデータが漏れるのを防ぐため、実行環境を使用してセキュリティ上の懸念があるユーザーデータ、イベント、またはその他の情報を保存しないでください。関数がハンドラー内のメモリに保存できない変更可能な状態に依存している場合は、ユーザーごとに個別の関数または個別のバージョンの関数を作成することを検討してください。

  • keep-alive ディレクティブを使用して永続的な接続を維持します。Lambda は、時間の経過とともにアイドル状態の接続を消去します。関数を呼び出すときにアイドル状態の接続を再利用しようとすると、接続エラーが発生します。永続的な接続を維持するには、ランタイムに関連付けられている keep-alive ディレクティブを使用します。例については、「Node.js で Keep-alive を使用して接続を再利用する」を参照してください。

  • 環境変数を使用して、オペレーショナルパラメータを関数に渡します。たとえば、Amazon S3 バケットに書き込む場合、書き込み先のバケット名はハードコーディングせずに、環境変数として設定します。

  • Lambda 関数では、再帰呼び出しを使用しないでください。関数が自身を呼び出すこともあれば、新たに開始されたプロセスで関数が再度呼び出される可能性もあります。これを行うと意図しないボリュームで関数が呼び出され、料金が急増する可能性があります。意図しない呼び出しがいくつも見つかった場合は、すぐに関数の予約済同時実行数を 0 に設定して、コードを更新している間のすべての関数の呼び出しをスロットリングします。

  • Lambda 関数コードで文書化されていない非公開の API を使用しないでください。AWS Lambda マネージドランタイムでは、Lambda が Lambda の内部 API にセキュリティと機能面の更新を定期的に適用します。これらの内部 API 更新には後方互換性がないことがあり、関数にこれらの非公開 API に対する依存関係がある場合、呼び出しの失敗などの意図しない結果につながります。公開されている API のリストについては、「API リファレンス」を参照してください。

  • 冪等性コードを記述します。関数の記述に冪等性コードを使用すると、重複するイベントが同じ方法で処理されるようになります。コードでは、イベントを適切に検証し、重複するイベントを適切に処理する必要があります。詳細については、「Lambda 関数を冪等にするにはどうすればよいですか?」を参照してください。

  • Java DNS キャッシュを使用しない Lambda 関数は、既に DNS レスポンスをキャッシュしています。別の DNS キャッシュを使用すると、接続タイムアウトが発生する可能性があります。

    java.util.logging.Logger クラスは JVM DNS キャッシュを間接的に有効にできます。デフォルト設定を上書きするには、 を初期化する前に networkaddress.cache.ttl を 0 に設定しますlogger。例:

    public class MyHandler { // first set TTL property static{ java.security.Security.setProperty("networkaddress.cache.ttl" , "0"); } // then instantiate logger var logger = org.apache.logging.log4j.LogManager.getLogger(MyHandler.class); }
  • Java で記述されたデプロイパッケージを Lambda で解凍する所要時間を短縮します。そのために、依存する .jar ファイルを別個の /lib ディレクトリにファイルします。これで関数のすべてのコードを多数の .class ファイルと一緒に単一の Jar に収納するよりも高速化されます。手順については、「.zip または JAR ファイルアーカイブで Java Lambda 関数をデプロイする」を参照してください。

サンプルハンドラーコード

このガイドの GitHub リポジトリには、さまざまなハンドラータイプとインターフェイスの使用方法を示すサンプルアプリケーションが含まれています。各サンプルアプリケーションには、簡易のデプロイとクリーンアップ用のスクリプト、AWS SAM テンプレート、サポートリソースが含まれています。

Java のサンプル Lambda アプリケーション
  • [java17-examples] — Java レコードを使用して入力イベントデータオブジェクトを表現する方法を示す Java 関数。

  • java-basic - 単位テストと変数ログ記録設定を使用する、最小限の Java 関数のコレクション。

  • java-events - Amazon API Gateway、Amazon SQS、Amazon Kinesis などのさまざまなサービスからのイベントを処理する方法のスケルトンコードを含む Java 関数のコレクション。これらの関数は、最新バージョンの aws-lambda-java-events ライブラリ (3.0.0 以降) を使用します。これらの例では、依存関係としての AWS SDK が不要です。

  • s3-java - Amazon S3 からの通知イベントを処理し、Java Class Library (JCL) を使用して、アップロードされたイメージファイルからサムネイルを作成する Java 関数。

  • custom-serialization – fastJson 、Gson、Moshi、jackson-jr などの一般的なライブラリを使用してカスタムシリアル化を実装する方法例。

  • API Gateway を使用して Lambda 関数を呼び出す - 従業員情報を含む Amazon DynamoDB テーブルをスキャンする Java 関数。次に、Amazon Simple Notification Service を使用して、仕事の記念日を祝うテキストメッセージを従業員に送信します。この例では、API ゲートウェイを使用して関数を呼び出します。

java-events および s3-java アプリケーションは、AWS サービスのイベントを入力として受け取り、文字列を返します。java-basic アプリケーションには数タイプのハンドラーが含まれています。

さまざまなハンドラータイプをテストするには、AWS SAM テンプレートのハンドラー値を変更するだけです。詳細な手順については、サンプルアプリケーションの readme ファイルを参照してください。