1. 概述

AWS Lambda 是亚马逊提供的一项无服务器计算 (Serverless computing) 服务。它是一个强大的工具,能帮助我们构建可扩展的事件驱动应用程序。

AWS Lambda 的无服务器特性让我们能专注于业务逻辑,而 AWS 则负责服务器的动态分配和配置。同时,它也是一种经济高效的解决方案,因为我们只需为代码的实际执行时间和内存消耗付费。

在本教程中,我们将探讨如何使用 Java 创建一个基础的 AWS Lambda 函数。我们将涵盖所需的依赖项、创建 Lambda 函数的不同方式、构建部署文件,以及如何使用 LocalStack 在本地测试我们的 Lambda 函数。

要学习本教程,您需要一个有效的 AWS 账户。

2. Maven 依赖

为了启用 AWS Lambda,我们需要在项目中添加以下依赖:

<dependency> 
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.2.3</version>
</dependency>

接下来,我们需要添加 Maven Shade 插件:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
        <goals>
                <goal>shade</goal>
            </goals>
        </execution>
    </executions>
</plugin>

在使用 Java 构建 AWS Lambda 函数时,Maven Shade 插件至关重要。它允许我们将应用程序及其所有依赖项打包成一个独立的、自包含的 JAR 文件,也称为 “fat” JAR(或称 “uber” JAR)

该插件会提取所有依赖项的内容,并将它们与我们的项目类放在一起,这正是 AWS Lambda 所期望的代码部署方式。

我们可以通过执行以下命令,在项目的 target 目录中创建我们的 fat JAR:

mvn clean package

当使用 Gradle 时,我们可以利用 Gradle Shadow 插件来创建 fat JAR

3. 创建处理器

任何 AWS Lambda 函数的入口点都是一个处理器方法。它负责处理传入的请求并返回响应。

创建 Lambda 函数时,我们必须指定其处理器。我们使用 包名.类名 的格式来指定这一点。在后续的测试和部署章节中,我们将介绍如何配置此项

在定义处理器方法时,我们有几种不同的选择,本节将对此进行探讨。

3.1. 实现 RequestHandler 接口

定义处理器最常见且推荐的方式是实现 RequestHandler 接口并重写其 handleRequest() 方法

class LambdaHandler implements RequestHandler<Request, Response> {

    @Override
    public Response handleRequest(Request request, Context context) {
        LambdaLogger logger = context.getLogger();
        logger.log("Processing question from " + request.name(), LogLevel.INFO);
        return new Response("Subscribe to Baeldung Pro: baeldung.com/members");
    }
}

record Request(String name, String question) {}

record Response(String answer) {}

RequestResponse 是简单的 record 类型,代表了我们 Lambda 函数的输入和输出。我们还在 RequestHandler 接口中将这些类型指定为泛型参数。

handleRequest() 方法接收一个 Request record 和一个 Context 对象作为参数。Context 参数提供了关于 Lambda 执行环境的实用信息,**其中包括一个可用于日志记录 (logging) 的 LambdaLogger**。

3.2. 实现 RequestStreamHandler 接口

定义处理器的另一种方法是实现 RequestStreamHandler 接口:

class LambdaStreamHandler implements RequestStreamHandler {

    @Override
    public void handleRequest(InputStream input, OutputStream output, Context context) {
        ObjectMapper mapper = new ObjectMapper();
        Request request = mapper.readValue(input, Request.class);

        try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output))) {
            writer.write("Hello " + request.name() + ", Baeldung has great Java content for you!");
            writer.flush();
        }
    }

    record Request(String name) {}
}

在这里,我们实现 RequestStreamHandler 接口并重写 handleRequest() 方法。**我们使用 ObjectMapperInputStream 中的原始请求数据进行反序列化 (deserialize),并将我们的响应写入 OutputStream**。

当需要处理原始输入和输出流时,这个接口非常有用。

3.3. 自定义处理器方法

最后,我们可以定义一个自定义的处理器方法:

class CustomLambdaHandler {

    public Response handlingRequestFreely(Request request, Context context) {
        LambdaLogger logger = context.getLogger();
        logger.log(request.name() + " has invoked the lambda function", LogLevel.INFO);
        return new Response("Subscribe to Baeldung Pro: baeldung.com/members");
    }

    record Request(String name) {}

    record Response(String answer) {}
}

在这种方法中,我们创建一个 CustomLambdaHandler 类,其中包含一个 handlingRequestFreely() 方法,该方法接收 Request record 和 Context 对象作为参数,这与 RequestHandler 的示例类似。唯一的不同在于我们没有实现任何特定的接口。

与前两种方法不同,创建自定义处理器方法时,我们需要使用 包名.类名::方法名 的格式来配置我们的处理器。例如,如果我们的 CustomLambdaHandler 类位于 com.baeldung.lambda 包中,那么在创建 Lambda 函数时,我们将处理器指定为 com.baeldung.lambda.CustomLambdaHandler::handlingRequestFreely

4. 使用 LocalStack 在本地测试 Lambda 函数

在开发期间,在将函数部署到 AWS 之前先在本地进行测试通常非常方便。LocalStack 是一个流行的工具,它允许我们在本地机器上运行一个模拟的 AWS 环境。

我们将测试之前通过实现 RequestHandler 接口创建的 LambdaHandler 类。

首先,让我们使用 Docker 启动一个 LocalStack 容器:

docker run \
    --rm -it \
    -p 127.0.0.1:4566:4566 \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v ./target:/opt/code/localstack/target \
    localstack/localstack

我们映射了所需的端口,并将包含我们 fat JAR 的项目 target 目录挂载到容器中。需要注意的是,LocalStack 还有其他安装方式。

接下来,我们进入容器的 shell 并创建我们的 Lambda 函数:

awslocal lambda create-function \
    --function-name baeldung-lambda-function \
    --runtime java21 \
    --handler com.baeldung.lambda.LambdaHandler\
    --role arn:aws:iam::000000000000:role/lambda-role \
    --zip-file fileb:///opt/code/localstack/target/java-lambda-function-0.0.1.jar

我们使用 zip-file 参数指定 Java 21 作为运行时、我们的处理器以及 JAR 文件在容器中的位置。

函数创建好后,现在我们来调用它:

awslocal lambda invoke \
    --function-name baeldung-lambda-function \
    --payload '{ "name": "John Doe", "question": "How do I view articles ad-free and in dark mode on Baeldung?" }' output.txt

我们传递函数名和 JSON 请求负载。Lambda 函数的响应会保存在指定的 output.txt 文件中,其内容如下:

{
    "answer": "Subscribe to Baeldung Pro: baeldung.com/members"
}

如果出现任何错误,错误详情也会记录在我们的 output.txt 文件中。

使用 LocalStack 在本地运行我们的 Lambda 函数,可以在开发过程的早期就发现问题

5. 部署 Lambda 函数

现在我们已经创建了 Lambda 函数并在本地进行了测试,让我们看看如何将其部署到我们的 AWS 环境中

我们将使用 AWS CloudFormation,它允许我们定义和管理我们的基础设施即代码。我们将部署上一节中测试过的同一个 Lambda 函数。

5.1. 创建 AWS CloudFormation 模板

首先,我们需要将我们的 fat JAR 文件存储到一个 Amazon S3 存储桶中。这是必需的,因为在部署过程中,CloudFormation 会从指定的 S3 存储桶中引用该 JAR 文件。

接下来,让我们为我们的 Lambda 函数创建一个通用的 CloudFormation 模板:

AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda function deployment with Java 21 runtime

Parameters:
    LambdaHandler:
        Type: String
        Description: The handler for the Lambda function
    S3BucketName:
        Type: String
        Description: The name of the S3 bucket containing the Lambda function JAR file
    S3Key:
        Type: String
        Description: The S3 key (file name) of the Lambda function JAR file

Resources:
    BaeldungLambdaFunction:
        Type: AWS::Lambda::Function
        Properties:
            FunctionName: baeldung-lambda-function
            Handler: !Ref LambdaHandler
            Role: !GetAtt LambdaExecutionRole.Arn
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: !Ref S3Key
            Runtime: java21
            Timeout: 10
            MemorySize: 512
    LambdaExecutionRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Version: 2012-10-17
                Statement:
                    - Effect: Allow
                      Principal:
                          Service: lambda.amazonaws.com
                      Action: sts:AssumeRole
            ManagedPolicyArns:
                - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

在我们的 CloudFormation 模板中,我们定义了一个名为 baeldung-lambda-function 的 Lambda 函数。我们通过名为 LambdaExecutionRole 的 IAM 角色,将托管策略 AWSLambdaBasicExecutionRole 附加到我们的 Lambda 函数上,从而授予其执行并将日志写入 Amazon CloudWatch 的必要权限。

我们 CloudFormation 模板中的 Parameters 允许我们定义在创建堆栈时可以传递给模板的输入值。我们在模板中使用 !Ref 函数来动态引用这些参数值,以定义我们的 Lambda 属性。

我们在模板中定义了一个非常基础的 Timeout(超时)为 10 秒和 MemorySize(内存大小)为 512 MB,但可以根据需要进行更新。

5.2. 创建 CloudFormation 堆栈

既然我们已经定义好了模板,就需要使用 AWS CLI 来创建我们的 CloudFormation 堆栈

aws cloudformation create-stack \
    --stack-name baeldung-cloudformation-java-21-lambda-function \
    --template-body file://java-21-lambda-function-template.yaml \
    --capabilities CAPABILITY_IAM \
    --parameters \
        ParameterKey=LambdaHandler,ParameterValue=com.baeldung.lambda.LambdaHandler\
        ParameterKey=S3BucketName,ParameterValue=baeldung-lambda-tutorials-bucket \
        ParameterKey=S3Key,ParameterValue=java-lambda-function-0.0.1.jar

在我们的 create-stack 命令中,我们提供了三个参数:

  • stack-name:指定我们的 CloudFormation 堆栈名称
  • template-body:指定我们的 CloudFormation 模板文件路径
  • parameters:指定我们 Lambda 函数的参数值

我们还在 capabilities 参数中提供了值 CAPABILITY_IAM。当我们的模板创建新的 IAM 资源时(例如,我们示例中 Lambda 函数的 IAM 角色),这是必需的。

5.3. 触发 Lambda 函数

一旦我们的 Lambda 函数成功部署,我们就可以通过 AWS CLI 来调用它:

aws lambda invoke --function-name my-lambda-function \
    --cli-binary-format raw-in-base64-out \
    --payload '{ "name": "John Doe", "question": "How do I view articles ad-free and in dark mode on Baeldung?" }' output.txt

上述命令与我们之前在 LocalStack 容器中执行的命令略有不同。但是,它的行为将是相同的,并将响应输出到 output.txt 文件中。

在真实场景中,我们的 Lambda 函数通常不是通过 CLI 直接调用的,而是由来自各种 AWS 服务(如 API Gateway 和 S3)的事件所触发

例如,我们可以配置 API Gateway,使其在调用特定的 API 端点时调用我们的 Lambda 函数,从而构建无服务器 API

另一个用例是使用 S3 事件来触发 Lambda 函数。每当在特定的 S3 存储桶中创建、修改或删除对象时,我们都可以执行业务逻辑。这对于图像处理等场景非常有用,因为我们希望对上传的新图像执行操作。

通过利用 AWS Lambda 这种事件驱动的特性,我们可以运行代码来自动响应 AWS 环境中的变化。

6. 使用 Java 作为 Lambda 运行时的注意事项

在结束本教程之前,在选择 Java 作为 AWS Lambda 函数的运行时之前,有几点需要注意。

**对于对时间敏感的应用,使用 Java 作为 Lambda 运行时的一个主要问题是冷启动时间 (cold start time)**。当 Lambda 函数在一段非活动时间后被调用时,JVM 的启动和必要类的加载会有延迟。这种开销增加了我们的 Lambda 函数完成操作所需的时间。

另一个需要考虑的是 Java 应用的内存使用情况。与 Node.js 或 Python 等其他语言相比,Java 往往会消耗更多内存。如果未经优化,这种较高的内存消耗会导致成本增加。微调我们 Lambda 函数的内存设置,在性能和成本之间取得平衡非常重要

最近,GraalVM 已作为一种潜在的解决方案出现。它将我们的 Java 应用编译成本地可执行文件,从而显著减少启动时间并优化内存使用。

7. 总结

在本文中,我们探讨了如何使用 Java 创建 AWS Lambda 函数。

我们讨论了创建可执行 Lambda 函数所需的依赖项和插件。接着,我们研究了定义处理器方法的不同方式。

最后,我们介绍了如何使用 LocalStack 在本地测试我们的 Lambda 函数。一旦我们验证了函数能正确执行,就使用 AWS CloudFormation 将其部署到真实的 AWS 环境中。

一如既往,本文中使用的所有代码示例都可以在 GitHub 上找到。


原始标题:A Basic AWS Lambda Example With Java |Baeldung

« 上一篇: Java周报,158