亚马逊AWS官方博客

OpenSource | 在AWS lambda上运行用Java编写的APLs

从 Spring 和 Spring Boot 到 Jersey 到 Spark,Java 开发人员可以随心选择各种开放源来构建服务器端 API。这些框架通常都在编译包内嵌入了用来运行服务器的 Servlet 容器引擎,例如 Tomcat。AWS LambdaAmazon API Gateway 在无服务器环境中作为 HTTP 前端和计算平台。今天,我们发布了 aws-serverless-java-container 框架 1.0 版。使用无服务器 Java 容器,可以方便地在 Java 中使用 Spring、Spring Boot、Jersey 或 Spark 等框架编写应用程序,并且只需极少的代码修改即可在 AWS Lambda 内部运行。

无服务器 Java 容器库在 Lambda 运行时和您选择的框架之间扮演代理的角色,好比一个 Servlet 引擎,将传入的事件翻译为框架可以理解的请求对象,然后将来自应用程序的应答转换为 API Gateway 理解的格式。无服务器 Java 容器库现已在 Maven 上推出。我们针对不同的框架提供不同风格的库:SpringSpring BootJerseySpark。在本博文中,我们将构建一个 Jersey 应用程序。该库的其他实现也具有类似的结构,请参见 GitHub 上的快速入门指南

使用 Maven 原型

我们为所有支持的框架发布了基本的 Maven 原型。如要运行此教程,您需要在本地计算机上安装 Apache Maven。使用任何终端打开工作区目录,然后运行 Maven 命令以利用原型生成新的项目。请使用您需要的设置代替 groupId 和 artifactId 设置:

$ cd myworkspace
$ mvn archetype:generate -DgroupId=my.service -DartifactId=jersey-sample -Dversion=1.0-SNAPSHOT \
      -DarchetypeGroupId=com.amazonaws.serverless.archetypes \
	  -DarchetypeArtifactId=aws-serverless-jersey-archetype \
	  -DarchetypeVersion=1.0.1 -Dinteractive=false

mvn 客户段将要求您确认参数,然后生成项目结构。在此例中,我们使用 aws-serverless-jersey-archetype – 我们为 spring、springboot 和 spark 准备了类似的工件。下面我们详细介绍生成的代码结构。如果您仅仅需要执行命令并测试您的基本应用程序,请直接跳转至本地测试部分。

Jersey 应用程序

使用您选择的 IDE 打开原型项目。Jersey 原型中包含的简单应用程序定义了一个 /ping 路径,将会返回一条 JSON hello world 消息。在代码包中,以我的例子为例,在 my.service下,有一个 resource 包,它采用 PingResource 类。Ping 类采用 JAX-RS’ @Path 注解,它定义了单一的 @GET 方法。

@Path("/ping")
public class PingResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.WILDCARD)
    public Response createPet() {
		// return a basic map. This will be marshalled automatically into a 
		// json object with a single property
        Map<String, String> pong = new HashMap<>();
        pong.put("pong", "Hello, World!");
        return Response.status(200).entity(pong).build();
    }
}

Lambda 句柄

在我们应用程序的主包中,原型还会生成一个 StreamLambdaHandler 类。下面我们介绍该类中的代码:

public class StreamLambdaHandler implements RequestStreamHandler

我们的类会实现 Lambda 的 RequestStreamHandler 接口。该类是 AWS Lambda 在我们应用程序中的主要入口:Lambda 行话里的“句柄”。我们使用流句柄而不是基于 POJO 的句柄,因为我们的事件模型需要利用注解来编组和解组,但 Lambda 的内嵌序列化器部支持注解。


private static final ResourceConfig jerseyApplication = new ResourceConfig()
                                                            .packages("com.sapessi.jersey.resource")
                                                            .register(JacksonFeature.class);

首先,我们会声明一个静态的 ResourceConfig对象,这是 Jersey Application 实现对象。我们配置此对象以扫描有注解的类的 resource 包,然后加载 JacksonFeature 类以处理 JSON 内容类型。

private static final JerseyLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler
            = JerseyLambdaContainerHandler.getAwsProxyHandler(jerseyApplication);

然后,我们声明 JerseyLambdaContainerHandler 对象的第二个静态实例。我们将使用 getAwsProxyHandler 静态方法和 ResourceConfig 对象,将此对象初始化。 getAwsProxyHandler 方法会自动创建一个配置处理 API Gateway 代理集成事件的库实例。您可以创建 RequestReader 以及 ResponseWriter 对象的自定义实现,以支持自定义事件类型。您将注意到这两个变量都被声明为静态类成员。它们之所以是类成员,是因为我们只需要这些对象的单一实例。AWS Lambda 尝试在不同的调用之间重复使用容器。我们句柄类由运行时作为单例模式持有,每次都会调用 handleRequest 方法。我们可以重复使用 ResourceConfigJerseyLambdaContainerHandler。静态变量将在 Lambda 启动它时由 Java 运行时实例化;从而提高了繁重内省操作的性能。

public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
    handler.proxyStream(inputStream, outputStream, context);

句柄方法的实现时 Lambda 的主要切入点。在句柄方法内部,我们会单次调用容器句柄的 proxyStream 方法。 proxyStream 方法将负责读取我们的输入流并利用其数据创建 HttpServletRequest 。应用程序生成的 HttpServletResponse 将按照 Amazon API Gateway 要求的格式自动写入输入流。

项目根

在项目根中,原型会生成三个文件: pom.xmlsam.yamlREADME.md。pom 文件声明项目并定义 Maven 依存关系。它包含 serverless-java-container 库。

<dependency>
    <groupId>com.amazonaws.serverless</groupId>
   <artifactId>aws-serverless-java-container-jersey</artifactId>
   <version>1.0</version>
</dependency>

pom 文件还使用 Maven Shade 插件来生成一个可以加载到 AWS Lambda 的“uber-jar”。请参阅 <build> 部分。 sam.yaml 文件是一种无服务器应用程序模型 (SAM) 模板,我们可以用它将应用程序部署到 AWS 或在本地使用 SAM Local 进行测试。SAM 是对 CloudFormation 的进一步抽象,从而可以更方便地定义代码中的无服务器堆栈。SAM 文件定义了我们单一资源 AWS::Serverless::Function。该函数配置使用编译进程生成的“uber-jar”并指向我们的句柄类。API 前端在函数资源中的 Events 部分定义。在部署模板时将会隐含创建 API Gateway RestApiStageDeploymentREADME.md 文件包含了所生成的应用程序构建、部署和测试指令集。

在本地测试应用程序

您可以使用 AWS SAM Local 在本地计算机上启动服务。如要启动 SAM Local,您需要安装并运行 Docker (社区版或企业版)。首先安装 SAM Local (如果尚未安装):

$ npm install -g aws-sam-local

然后使用一台终端,打开项目根文件夹并构建 jar 文件。

$ cd myworkspace/jersey-sample
$ mvn clean package

仍在 sam.yaml 文件所在的项目根文件夹中 — 使用 SAM Local CLI 启动 API。

$ sam local start-api --template sam.yaml

...
Mounting com.sapessi.jersey.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH]
...

现在我们已经启动并运行了 API Gateway 和 Lambda 的本地模拟器。使用新的壳,您可以向 API 发送测试 Ping 请求:

$ curl -s http://127.0.0.1:3000/ping | python -m json.tool

{
    "pong": "Hello, World!"
}

部署到 AWS

您可以使用 AWS CLI 快速将应用程序部署到 AWS Lambda 和 Amazon API Gateway。您将需要 S3 存储桶来存储需要部署的工件。创建了 S3 存储桶后,从文件 sam.yaml 所在的项目根文件夹运行如下命令:

$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket <YOUR S3 BUCKET NAME>
Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx  6464692 / 6464692.0  (100.00%)
Successfully packaged artifacts and wrote output template to file output-sam.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name <YOUR STACK NAME>

如命令的输出所显示,您现在可以使用 CLI 来部署应用程序。选择堆栈名称并从包命令的输出中运行 aws cloudformation deploy 命令。

$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessJerseyApi --capabilities CAPABILITY_IAM

应用程序部署完成后,您可以描述堆栈以显示 API 终端节点已经创建。终端节点应当是 OutputsServerlessJerseyApikey 属性:

$ aws cloudformation describe-stacks --stack-name ServerlessJerseyApi --query 'Stacks[0].Outputs[*].{Service:OutputKey,Endpoint:OutputValue}'
[
    {
		"Service": "JerseySampleApi",
		"Endpoint": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping"
    }
]

OutputValue 复制到浏览器中,或者使用 curl 来测试您的第一个请求:

$ curl -s https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping | python -m json.tool

{
    "pong": "Hello, World!"
}

冷启动说明

Java 是一种大型运行时;因此有必要在此包含有关冷启动的内容。冷启动是在 AWS Lambda 需要启动基础设施、启动运行时以及启动代码时第一次调用您的 Lambda 函数。多个因素可能会影响函数首次启动的速度:

  • 内存和 CPU 分配:AWS Lambda 将一定比例的 CPU 周期分配给您分配到函数的内存。生成的 SAM 模板默认使用 512MB 内存。如果您的代码为 CPU 密集型代码,请增加此参数以提高性能。
  • 代码包大小:每当 Lambda 函数首次启动时,它需要下载并解压代码包。“uber-jar”的大小十分重要。在导入依存关系时要特别注意,请使用 Maven Shade 插件来剔除不必要的临时性依存关系。例如,在此库的 Spark 实现中,我们删除了嵌入的 Jetty 容器
  • 代码生命周期:在第一次启动函数时,AWS Lambda 会创建一个句柄对象实例,并且将在未来调用时以单例模式重复使用,直接针对您的 handleRequest 方法。这意味着您可以使用类成员来缓存您希望在不同的调用中重复使用的元数据、对象以及连接。不要缓存保密数据:无法保证 Lambda 实例将在有一天被任何人使用,并且有人在某一天会不可避免地忘记清除调用之间缓存的数据。
  • 框架有不同的功能和性能特征。Spring 和 Spring Boot 在注入依存关系以及自动连接应用程序方面的功能极其强大。但这会让您丧失冷启动时间的灵活性 — 反射十分缓慢。Jersey 仅执行少量的反射以查找其提供商和资源。Spark 中的一切都是“静态链接”的,是目前启动最快的框架。

由于所有这些参数的原因,我们要如何选择正确的框架?如果您没有严格的延迟要求,请选择自己最满意的框架。在实践中利用生产流量,您将发现冷启动仅影响 1% (如果不是 0.1%) 的指标。

结论

利用无服务器 Java 容器,可以方便地使用任意 Java 框架创建可扩展的 API。在本博中,我使用 Jersey,该库还提供 SpringSpring BootSpark 原型。使用原型创建的项目预封装了一个运行的 Lambda 句柄、一个示例 /ping 资源以及一个便于您快速在本地测试应用程序并将其部署到 AWS 的 SAM 模板。如果您遇到有关无服务器 Java 容器库的问题,请在我们的 GitHub 存储库报告。有关 AWS LambdaAmazon API Gateway 的更多信息,请使用 AWS 论坛。