用API网关和S3创建一个简单的API存根
这篇文章展示了一种使用API Gateway中的映射模板来转换路径并创建一个简单的存根的好方法。
不久前,我的团队想为一个内部基于JSON HTTP的API创建一个存根。要存根的服务很简单。该服务暴露了一个REST API端点,用于列出特定类型的资源。该API支持分页和一些特定的请求/查询参数。
对该服务的GET请求是这样的:
/items?type=sometype&page=2
我们想在我们的AWS账户中为该服务创建一个简单的存根,我们可以用它来测试。我们想要执行的测试之一是测试服务是否被关闭。如果是这样,我们的应用程序将遵循一个特定的代码路径。当然,我们不能轻易地用远程API的真实服务来测试,所以我们尽量使事情简单。
用API网关、Lambda和S3创建API
由于我们领域内的大多数服务都是基于亚马逊API网关和AWS Lambda的,我们一开始就开始研究这个方向。由于我们的存根是只读的,而且我们不需要修改项目,我们选择从远程API创建一个初始的数据集导出为JSON文件,我们可以将其存储在S3中。为了存储这些文件,我们选择使用一个与我们的类型和页面参数相似的文件名模式。
{TYPE}_{PAGE_NUMER}(.json)
这导致了一个像这样的桶:

API存根的简化设计将如下:

我们的代码的一个示例版本看起来类似于这样。
Java
public static final String TYPE_QUERY_PARAM = "type";
public static final String JSON_FILE_EXTENSION = ".json";
public static final String PAGE_PREFIX = "_p";
private S3Client s3Client = S3Client.builder()
.region(Region.EU_WEST_1)
.httpClient(UrlConnectionHttpClient.builder().build())
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.build();
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
Map<String, String> queryStringParameters = input.getQueryStringParameters();
String page = queryStringParameters.getOrDefault("page", "1");
String type;
if(!queryStringParameters.containsKey(TYPE_QUERY_PARAM)){
throw new MissingTypeException("Parameter Type cannot be null");
} else {
type = queryStringParameters.get(TYPE_QUERY_PARAM);
}
String key = type + PAGE_PREFIX + page + JSON_FILE_EXTENSION;
ResponseInputStream<GetObjectResponse> s3ClientObject = s3Client
.getObject(GetObjectRequest
.builder()
.bucket("apigatelambdas3integrationdemo")
.key(key)
.build());
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withHeaders(
Map.of("Content-Type", s3ClientObject.response().contentType(),
"Content-Length", s3ClientObject.response().contentLength().toString()))
.withBody(getFileContentAsString(context, s3ClientObject));
}
private String getFileContentAsString(Context context, ResponseInputStream<GetObjectResponse> s3ClientObject) {
String fileAsString = "";
try (BufferedReader reader = new BufferedReader(new InputStreamReader(s3ClientObject))) {
fileAsString = reader.lines().collect(Collectors.joining(System.lineSeparator()));
} catch (IOException e) {
context.getLogger().log("Oops! Something went wrong while converting the file from S3 to a String");
e.printStackTrace();
}
return fileAsString;
}
正如你在上面的片段中看到的,我们基本上是根据一些请求参数来计算通往S3中的对象的路径。当解决了路径问题后,我们就从S3中获取文件,并将其转换为字符串回复给API Gateway,后者又将文件返回给消费者。我们的lambda函数只是充当了一个简单的代理,我们在想我们是否可以完全摆脱lambda函数。维护代码、依赖关系等是一个负担,所以如果我们不需要它,我们想摆脱它。
只用API Gateway和S3创建API
API Gateway对与其他AWS服务的直接集成有很大的支持,所以我们开始探索我们的选择。我们希望的解决方案是类似于下图的东西:

在浏览API Gateway的文档时,我们发现了一个很好的例子,说明如何使用API Gateway作为S3的代理。所提供的解决方案让API Gateway镜像S3中的文件夹和文件结构:很有用,但它并不包括我们的使用情况。
其他看起来有希望的选项之一是配置映射模板。我们以前曾使用过这种方法,将传入的请求体转换为远程后端的不同格式。如果你不熟悉API Gateway的映射模板,映射模板是一个用Velocity模板语言(VTL)表达的脚本,并使用JSONPath表达式应用于有效载荷。
在挖掘了API Gateway的文档后,我们还发现,映射模板可以用来改变查询字符串、头文件和路径。
| 请求主体映射模板 | 响应主体映射模板 |
|---|---|
| $context.requestOverride.header.header_name | $context.responseOverride.header.header_name |
| $context.requestOverride.path.path_name | $context.responseOverride.status |
| $context.requestOverride.querystring.querystring_name | |
修改路径正是我们想要的,所以我们试了一下,效果非常好。让我们看一下我们的API网关GET请求的结果设置。
正如你在上面的部分所看到的,我们已经在我们的API的根部添加了一个GET方法。这个GET方法有一个方法请求,定义了两个查询参数;页面和类型。
对于集成请求,我们定义了我们要集成的远程服务,并指定了桶和一个动态fileName。

在集成请求的路径覆盖中,有两件重要的事情需要注意:
- 在路径覆盖参数的开头,我们提供了S3桶的名称。
- 作为第二个参数,我们提供一个名为fileName的动态值 。
因此,路径覆盖将是{bucket-name}/{fileName}。
现在在我们的映射模板中,我们将填写fileName参数,这样API Gateway就知道要从S3中获取哪个文件。
让我们来看看这个映射模板。

正如你所看到的,我们已经为内容类型为application/json的请求设置了一个模板。现在,当一个带类型和页面查询参数的GET请求到达时,它将在路径覆盖中集合生成fileName变量。
速度
#set($type = $input.params('type')
#set($page = $input.params('page'))
#if( $page == '')
#set($context.requestOverride.path.fileName = $type + '_p1.json')
#else
#set($context.requestOverride.path.fileName = $type + '_p'+ $page + '.json')
#end
当该特定桶中的文件被找到时,它将返回相应的JSON。当文件没有找到时,它将抛出一个404响应体。我们还可以用一个映射模板来映射响应代码,以产生一些漂亮的错误信息。
最后的一些想法
在研究这个解决方案的时候,我还看到了Tom Vincent的一个帖子。汤姆写了一篇关于他称之为Lambdaless的好文章。我喜欢这个词,它与我们在这篇文章中试图实现的目标很有共鸣。
我认为这篇文章展示了在API网关中使用映射模板来转换路径并创建一个简单的存根的好方法。请记住,我们只是将其用于测试,我们不使用这种设置来运行生产工作负载。我还认为,如果API更复杂,我们就不会采取这种方法。尽管如此,对于我们的存根来说,它的设置和使用都很简单。