定义采用 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 并返回 xy 的总和。函数代码如下:

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 库中的类型。

如果您定义了自己的输入类型,它应该是可反序列化的、可变的普通旧 Java 对象 (POJO),对事件中的每个字段具有默认的构造函数和属性。事件中未映射到属性的键以及未包含在事件中的属性将被删除而不显示错误。

输出类型可以是对象或 void。运行时将返回值序列化为文本。如果输出是具有字段的对象,运行时会将其序列化为 JSON 文档。如果它是包装原始值的类型,则运行时返回该值的文本表示形式。

处理程序接口

aws-lambda-java-core 库为处理程序方法定义了两个接口。使用提供的接口简化处理程序配置,并在编译时验证处理程序方法签名。

RequestHandler 接口是一个泛型类型,它采用两个参数:输入类型和输出类型。这两种类型都必须是对象。使用此接口时,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 记录。

HandlerStream.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 函数的行为发生细微变化。要完全控制您的函数所用的依赖项,请使用部署程序包来打包所有依赖项。

  • 将依赖关系的复杂性降至最低。首选在执行环境启动时可以快速加载的更简单的框架。例如,首选更简单的 Java 依赖关系注入 (IoC) 框架,如 DaggerGuice,而不是更复杂的 Spring Framework

  • 将部署程序包大小精简为只包含运行时必要的部分。这样会减少调用前下载和解压缩部署程序包所需的时间。对于用 Java 编写的函数,请勿将整个 AWS SDK 库作为部署包的一部分上传,而是要根据所需的模块有选择地挑选软件开发工具包中的组件(例如 DynamoDB、Simple Storage Service (Amazon S3) 软件开发工具包模块和 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 缓存。要覆盖默认设置,请在初始化 logger 之前将 networkaddress.cache.ttl 设置为 0。例如:

    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); }

    为防止 UnknownHostException 失败,建议将 networkaddress.cache.negative.ttl 设置为 0。您可以使用 AWS_LAMBDA_JAVA_NETWORKADDRESS_CACHE_NEGATIVE_TTL=0 环境变量为 Lambda 函数设置此属性。

    禁用 JVM DNS 缓存并不能禁用 Lambda 的托管式 DNS 缓存。

  • 将依赖关系 .jar 文件置于单独的 /lib 目录中,可减少 Lambda 解压缩部署程序包(用 Java 编写)所需的时间。这样比将函数的所有代码置于具有大量 .class 文件的同一 jar 中要快。有关说明,请参阅使用 .zip 或 JAR 文件归档部署 Java Lambda 函数

示例处理程序代码

本指南的 GitHub 存储库包括演示如何使用各种处理程序类型和接口的示例应用程序。每个示例应用程序都包含用于轻松部署和清理的脚本、一个 AWS SAM 模板和支持资源。

Java 中的 Lambda 应用程序示例
  • java17-examples:这是一种 Java 函数,演示如何使用 Java 记录来表示输入事件数据对象。

  • java-basic – 具有单元测试和变量日志记录配置的最小 Java 函数的集合。

  • java-events – Java 函数的集合,其中包含用于处理来自 Amazon API Gateway、Amazon SQS 和 Amazon Kinesis 等各种服务的事件的框架代码。这些函数使用最新版本的 aws-lambda-events 库(3.0.0 及更新版本)。这些示例不需要 AWS 开发工具包作为依赖项。

  • s3-java – 此 Java 函数可处理来自 Amazon S3 的通知事件,并使用 Java 类库(JCL)从上传的图像文件创建缩略图。

  • 使用 API Gateway 调用 Lambda 函数 – Java 函数,用于扫描包含员工信息的 Amazon DynamoDB 表。然后,该函数使用 Amazon Simple Notification Service 向员工发送短信,祝贺他们工作周年纪念日快乐。此示例使用 API Gateway 调用函数。

java-eventss3-java 应用程序将 AWS 服务事件作为输入并返回字符串。java-basic 应用程序包括几种类型的处理程序:

要测试不同的处理程序类型,只需更改 AWS SAM 模板中的处理程序值即可。有关详细说明,请参阅示例应用程序的自述文件。