AWS Lambda 与API网关使用
1. 概述
AWS Lambda 是由 Amazon Web Services 提供的无服务器计算服务。
在之前的两篇文章中,我们讨论了如何使用 Java 创建 AWS Lambda 函数 ,以及如何从 Lambda 函数访问 DynamoDB 。
在本教程中,我们将讨论如何使用**AWS Gateway 将 Lambda 函数发布为 REST 端点**。
我们将详细了解以下主题:
- API网关的基本概念和术语
- 使用 Lambda 代理集成将 Lambda 函数与 API Gateway 集成
- API 的创建、其结构以及如何将 API 资源映射到 Lambda 函数
- API的部署和测试
2. 基础和术语
API Gateway 是一项完全托管的服务,使开发人员能够以任何规模创建、发布、维护、监控和保护 API。
我们可以实现一致且可扩展的基于 HTTP 的编程接口(也称为 RESTful 服务)来访问后端服务,如 Lambda 函数、其他 AWS 服务(例如 EC2、S3、DynamoDB)和任何 HTTP 端点。
功能包括但不限于:
- 交通管理
- 授权和访问控制
- 监控
- API版本管理
- 限制请求以防止攻击
与 AWS Lambda 一样,API Gateway 会自动横向扩展并按 API 调用计费。 详细信息可以在官方文档 中找到。
2.1. 条款
API Gateway是一项 AWS 服务,支持创建、部署和管理 RESTful 应用程序编程接口以公开后端 HTTP 端点、AWS Lambda 函数和其他 AWS 服务。
API Gateway API是可以与后端的 Lambda 函数、其他 AWS 服务或 HTTP 端点集成的资源和方法的集合。API 由构成 API 结构的资源组成。每个 API 资源都可以公开一个或多个 API 方法,这些方法必须具有唯一的 HTTP 动词。
要发布 API,我们必须创建API deployment并将其与所谓的阶段相关联。阶段就像 API 的时间快照。如果我们重新部署 API,我们可以更新现有阶段或创建新阶段。通过这种方式,可以同时使用不同版本的 API,例如*dev *阶段、test 阶段,甚至多个生产版本,如v1、v2等。
Lambda 代理集成是 Lambda 函数和 API Gateway 之间集成的简化配置。
API Gateway 将整个请求作为输入发送到后端 Lambda 函数。在响应方面,API Gateway 将 Lambda 函数输出转换回前端 HTTP 响应。
3. 依赖关系
我们需要与 AWS Lambda Using DynamoDB With Java 一 文中相同的依赖项。
最重要的是,我们还需要JSON Simple 库:
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
4. 开发和部署 Lambda 函数
在本节中,我们将使用 Java 开发和构建我们的 Lambda 函数,我们将使用 AWS 控制台对其进行部署,并且我们将运行一个快速测试。
由于我们要演示将 API Gateway 与 Lambda 集成的基本功能,我们将创建两个函数:
- 功能 1: 使用 PUT 方法从 API 接收有效负载
- 功能 2: 演示如何使用来自 API 的 HTTP 路径参数或 HTTP 查询参数
在实现方面,我们将创建一个RequestHandler类,它有两个方法——每个函数一个。
4.1. 模型
在我们实现实际的请求处理程序之前,让我们快速浏览一下我们的数据模型:
public class Person {
private int id;
private String name;
public Person(String json) {
Gson gson = new Gson();
Person request = gson.fromJson(json, Person.class);
this.id = request.getId();
this.name = request.getName();
}
public String toString() {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
return gson.toJson(this);
}
// getters and setters
}
我们的模型由一个简单的Person类组成,它有两个属性。唯一值得注意的部分是*Person(String)*构造函数,它接受 JSON 字符串。
4.2. RequestHandler 类的实现
就像在 AWS Lambda With Java 文章中一样,我们将创建 RequestStreamHandler接口的实现:
public class APIDemoHandler implements RequestStreamHandler {
private static final String DYNAMODB_TABLE_NAME = System.getenv("TABLE_NAME");
@Override
public void handleRequest(
InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
// implementation
}
public void handleGetByParam(
InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
// implementation
}
}
正如我们所见,RequestStreamHander接口只定义了一个方法,handeRequest()。无论如何,我们可以在同一个类中定义更多的函数,就像我们在这里所做的那样。另一种选择是 为每个函数创建一个RequestStreamHander实现。
在我们的具体情况下,为了简单起见,我们选择了前者。但是,必须根据具体情况进行选择,同时考虑性能和代码可维护性等因素。
我们还从TABLE_NAME环境变量中读取了 DynamoDB 表的名称 。我们稍后会在部署期间定义该变量。
4.3. 功能一的实现
在我们的第一个函数中,我们想演示如何从 API Gateway 获取有效负载(例如从 PUT 或 POST 请求):
public void handleRequest(
InputStream inputStream,
OutputStream outputStream,
Context context)
throws IOException {
JSONParser parser = new JSONParser();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
JSONObject responseJson = new JSONObject();
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
DynamoDB dynamoDb = new DynamoDB(client);
try {
JSONObject event = (JSONObject) parser.parse(reader);
if (event.get("body") != null) {
Person person = new Person((String) event.get("body"));
dynamoDb.getTable(DYNAMODB_TABLE_NAME)
.putItem(new PutItemSpec().withItem(new Item().withNumber("id", person.getId())
.withString("name", person.getName())));
}
JSONObject responseBody = new JSONObject();
responseBody.put("message", "New item created");
JSONObject headerJson = new JSONObject();
headerJson.put("x-custom-header", "my custom header value");
responseJson.put("statusCode", 200);
responseJson.put("headers", headerJson);
responseJson.put("body", responseBody.toString());
} catch (ParseException pex) {
responseJson.put("statusCode", 400);
responseJson.put("exception", pex);
}
OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
writer.write(responseJson.toString());
writer.close();
}
如前所述,我们稍后将配置 API 以使用 Lambda 代理集成。我们希望 API Gateway 将完整的请求传递给InputStream参数中的 Lambda 函数。
我们所要做的就是从包含的 JSON 结构中选择相关的属性。
如我们所见,该方法基本上包括三个步骤:
- 从我们的输入流中获取body对象并从中创建一个Person对象
- 将该Person对象存储在 DynamoDB 表中
- 构建一个 JSON 对象,它可以包含多个属性,例如响应的*body *、自定义标头以及 HTTP 状态代码
这里有一点值得一提:API Gateway 期望body 是一个String(对于请求和响应)。
正如我们期望从 API Gateway 获得一个String作为body ,我们将body 转换为String并初始化我们的Person对象:
Person person = new Person((String) event.get("body"));
API Gateway 还期望响应主体为String:
responseJson.put("body", responseBody.toString());
这个话题在官方文档中没有明确提到。但是,如果我们仔细观察,我们可以看到 body 属性在请求和响应 的两个片段中都是一个String。
优点应该很明显:即使 JSON 是 API Gateway 和 Lambda 函数之间的格式,实际正文也可以包含纯文本、JSON、XML 或其他任何内容。然后 Lambda 函数负责正确处理格式。
当我们在 AWS 控制台中测试我们的函数时,我们将看到请求和响应正文的外观。
这同样适用于以下两个功能。
4.4. 功能二的实现
在第二步中,我们要演示如何使用路径参数或查询字符串参数来使用其 ID 从数据库中检索Person项目:
public void handleGetByParam(
InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
JSONParser parser = new JSONParser();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
JSONObject responseJson = new JSONObject();
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
DynamoDB dynamoDb = new DynamoDB(client);
Item result = null;
try {
JSONObject event = (JSONObject) parser.parse(reader);
JSONObject responseBody = new JSONObject();
if (event.get("pathParameters") != null) {
JSONObject pps = (JSONObject) event.get("pathParameters");
if (pps.get("id") != null) {
int id = Integer.parseInt((String) pps.get("id"));
result = dynamoDb.getTable(DYNAMODB_TABLE_NAME).getItem("id", id);
}
} else if (event.get("queryStringParameters") != null) {
JSONObject qps = (JSONObject) event.get("queryStringParameters");
if (qps.get("id") != null) {
int id = Integer.parseInt((String) qps.get("id"));
result = dynamoDb.getTable(DYNAMODB_TABLE_NAME)
.getItem("id", id);
}
}
if (result != null) {
Person person = new Person(result.toJSON());
responseBody.put("Person", person);
responseJson.put("statusCode", 200);
} else {
responseBody.put("message", "No item found");
responseJson.put("statusCode", 404);
}
JSONObject headerJson = new JSONObject();
headerJson.put("x-custom-header", "my custom header value");
responseJson.put("headers", headerJson);
responseJson.put("body", responseBody.toString());
} catch (ParseException pex) {
responseJson.put("statusCode", 400);
responseJson.put("exception", pex);
}
OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
writer.write(responseJson.toString());
writer.close();
}
同样,三个步骤是相关的:
- 我们检查是否存在 带有id属性的pathParameters 或queryStringParameters数组。
- 如果为true,我们使用属于值从数据库中请求具有该 ID的Person项目。
- 我们将接收到的项目的 JSON 表示添加到响应中。
官方文档对 Proxy Integration的输入格式 和输出格式 提供了更详细的说明。
4.5. 规范
同样,我们可以简单地使用 Maven 构建我们的代码:
mvn clean package shade:shade
JAR 文件将在*target *文件夹下创建。
4.6. 创建 DynamoDB 表
我们可以按照 AWS Lambda 使用 DynamoDB 和 Java 中的说明创建表。
让我们选择Person作为表名,id作为主键名,Number作为主键的类型。
4.7. 通过 AWS 控制台部署代码
构建代码并创建表后,我们现在可以创建函数并上传代码。 这可以通过重复AWS Lambda with Java 文章中的步骤 1-5 来完成,对我们的两种方法中的每一种重复一次。
让我们使用以下函数名称:
- 用于handleRequest方法的StorePersonFunction (函数 1)
- 用于handleGetByParam方法的GetPersonByHTTPParamFunction (函数 2)
我们还必须定义一个 值为*“Person”的环境变量TABLE_NAME*。
4.8. 测试功能
在继续实际的 API 网关部分之前,我们可以在 AWS 控制台中运行一个快速测试,以检查我们的 Lambda 函数是否正确运行并且可以处理代理集成格式。
从 AWS 控制台测试 Lambda 函数的工作方式如AWS Lambda with Java 一文中所述。
但是,当我们创建测试事件时,我们必须考虑我们的函数所期望的特殊代理集成格式。我们可以使用 *API Gateway AWS Proxy *模板并根据需要对其进行自定义,也可以复制并粘贴以下事件:
对于 StorePersonFunction,我们应该使用这个:
{
"body": "{\"id\": 1, \"name\": \"John Doe\"}"
}
如前所述,body 必须具有String类型,即使包含 JSON 结构。原因是 API Gateway 将以相同的格式发送其请求。
应返回以下响应:
{
"isBase64Encoded": false,
"headers": {
"x-custom-header": "my custom header value"
},
"body": "{\"message\":\"New item created\"}",
"statusCode": 200
}
在这里,我们可以看到响应的body 是一个String,尽管它包含一个 JSON 结构。
让我们看看 GetPersonByHTTPParamFunction 的输入。
为了测试路径参数功能,输入如下所示:
{
"pathParameters": {
"id": "1"
}
}
发送查询字符串参数的输入是:
{
"queryStringParameters": {
"id": "1"
}
}
作为回应,我们应该得到以下两种情况方法:
{
"headers": {
"x-custom-header": "my custom header value"
},
"body": "{\"Person\":{\n \"id\": 88,\n \"name\": \"John Doe\"\n}}",
"statusCode": 200
}
同样,body 是一个String。
5. 创建和测试 API
在上一节中创建并部署了 Lambda 函数之后,我们现在可以使用 AWS 控制台创建实际的 API。
让我们看一下基本的工作流程:
- 在我们的 AWS 账户中创建一个 API。
- 将资源添加到 API 的资源层次结构。
- 为资源创建一个或多个方法。
- 设置方法与所属 Lambda 函数之间的集成。
我们将在以下部分中为我们的两个函数中的每一个重复步骤 2-4。
5.1. 创建 API
为了创建 API,我们必须:
- 通过apigateway 登录 API Gateway 控制台
- 单击“开始”,然后选择“新 API”
- 输入我们的 API 名称(TestAPI)并点击“Create API”确认
创建 API 后,我们现在可以创建 API 结构并将其链接到我们的 Lambda 函数。
5.2. 函数 1 的 API 结构
我们的StorePersonFunction需要执行以下步骤 :
- 选择“资源”树下的父资源项,然后从“操作”下拉菜单中选择“创建资源”。然后,我们必须在“新建子资源”窗格中执行以下操作:
- 在“资源名称”输入文本字段中键入“人员”作为名称
- 在“资源路径”输入文本字段中保留默认值
- 选择“创建资源”
- 选择刚刚创建的资源,在“Actions”下拉菜单中选择“Create Method”,执行以下步骤:
- 从 HTTP 方法下拉列表中选择 PUT,然后选择复选标记图标以保存选择
- 将“Lambda 函数”保留为集成类型,并选择“使用 Lambda 代理集成”选项
- 从“Lambda Region”中选择区域,我们之前在其中部署了 Lambda 函数
- 在“Lambda 函数”中输入*“StorePersonFunction ”*
- 选择“保存”并在提示“向 Lambda 函数添加权限”时点击“确定”确认
5.3. 函数 2 的 API 结构 – 路径参数
我们检索路径参数的步骤类似:
- 选择“资源”树下的*/persons*资源项,然后从“操作”下拉菜单中选择“创建资源”。然后,我们必须在 New Child Resource 窗格中执行以下操作:
- 在“资源名称”输入文本字段中键入*“人员”*作为名称
- 将“资源路径”输入文本字段更改为*“{id}”*
- 选择“创建资源”
- 选择刚刚创建的资源,在“Actions”下拉菜单中选择“Create Method”,执行以下步骤:
- 从 HTTP 方法下拉列表中选择 GET,然后选择复选标记图标以保存选择
- 将“Lambda 函数”保留为集成类型,并选择“使用 Lambda 代理集成”选项
- 从“Lambda Region”中选择区域,我们之前在其中部署了 Lambda 函数
- 在“Lambda 函数”中输入“ GetPersonByHTTPParamFunction ”
- 选择“保存”并在提示“向 Lambda 函数添加权限”时点击“确定”确认
注意:此处将“Resource Path”参数设置为*“{id}”很重要,因为我们的GetPersonByPathParamFunction*期望此参数完全像这样命名。
5.4. 函数 2 的 API 结构 - 查询字符串参数
接收查询字符串参数的步骤有点不同,因为我们不必创建资源,而是必须为id参数创建查询参数:
- 选择“Resources”树下的*/persons*资源项,从“Actions”下拉菜单中选择“Create Method”,执行以下步骤:
- 从 HTTP 方法下拉列表中选择 GET,然后选择复选标记图标以保存选择
- 将“Lambda 函数”保留为集成类型,并选择“使用 Lambda 代理集成”选项
- 从“Lambda Region”中选择区域,我们之前在其中部署了 Lambda 函数
- 在“Lambda 函数”中键入*“GetPersonByHTTPParamFunction ”*。
- 选择“保存”并在提示“向 Lambda 函数添加权限”时点击“确定”确认
- 选择右侧的“方法请求”并执行以下步骤:
- 展开 URL 查询字符串参数列表
- 点击“添加查询字符串”
- 在名称字段中输入*“id”*,然后选择复选标记图标保存
- 选中“必填”复选框
- 单击面板顶部“请求验证器”旁边的笔符号,选择“验证查询字符串参数和标头”,然后选择复选标记图标 注意:将“Query String”参数设置为*“id”很重要,因为我们的GetPersonByHTTPParamFunction*期望此参数的名称与此完全相同。
5.5. 测试 API
我们的 API 现已准备就绪,但尚未公开。在我们发布它之前,我们想先从控制台运行一个快速测试。
为此,我们可以在“资源”树中选择要测试的相应方法,然后单击“测试”按钮。在接下来的屏幕上,我们可以输入我们的输入,就像我们通过 HTTP 与客户端一起发送它一样。
对于 StorePersonFunction,我们必须在“请求正文”字段中键入以下结构:
{
"id": 2,
"name": "Jane Doe"
}
对于带有路径参数的GetPersonByHTTPParamFunction,我们必须在“路径”下的“{id}”字段中输入2作为值。
对于带有查询字符串参数的GetPersonByHTTPParamFunction,我们必须在“查询字符串”下的“{persons}”字段中输入id=2作为值。
5.6. 部署 API
到目前为止,我们的 API 尚未公开,因此只能从 AWS 控制台获得。
如前所述,当我们部署 API 时,我们必须将其与阶段相关联,这就像 API 的时间快照。如果我们重新部署 API,我们可以更新现有阶段或创建新阶段。
让我们看看我们 API 的 URL 方案的外观:
https://{restapi-id}.execute-api.{region}.amazonaws.com/{stageName}
部署需要以下步骤:
- 在“API”导航窗格中选择特定的 API
- 在资源导航窗格中选择“操作”,然后从“操作”下拉菜单中选择“部署 API”
- 从“部署阶段”下拉列表中选择“[新阶段]”,在“阶段名称”中键入*“测试”*,并可选择提供阶段和部署的描述
- 通过选择“部署”触发部署
在最后一步之后,控制台将提供 API 的根 URL,例如 https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test 。
5.7. 调用端点
由于 API 现在是公开的,我们可以使用任何我们想要的 HTTP 客户端来调用它。
使用cURL,调用将如下所示。
StorePersonFunction:
curl -X PUT 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons' -H 'content-type: application/json' -d '{"id": 3, "name": "Richard Roe"}'
GetPersonByHTTPParamFunction获取路径参数:
curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons/3' -H 'content-type: application/json'
查询字符串参数的GetPersonByHTTPParamFunction :
curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons?id=3' -H 'content-type: application/json'