1. 概述
本教程将探讨如何使用 Serverless Application Model (SAM) 框架将 Spring Boot 应用部署到 AWS Lambda。
这种方法在将现有 API 服务器迁移到无服务器架构时特别实用。通过此方案,我们可以充分利用 AWS Lambda 的弹性扩展能力和按执行付费模式,实现高效且经济的应用运行。
2. 理解 Lambda
AWS Lambda 是亚马逊云服务提供的无服务器计算平台。它允许我们运行代码而无需预置或管理服务器。
Lambda 函数与传统服务器的核心差异在于:Lambda 函数是事件驱动的,生命周期极短。
不同于持续运行的服务器,Lambda 函数仅在响应特定事件时触发,例如 API 请求、队列消息或 S3 文件上传。
需要注意:Lambda 首次处理请求时存在启动延迟,称为“冷启动”。如果后续请求在短时间内到达,可能复用同一运行时实例(称为“热启动”);并发请求则会启动多个运行时实例。
由于 Spring Boot 启动时间相对较长(相比 Lambda 理想的毫秒级启动),我们将讨论这对性能的影响。
3. 项目设置
下面通过修改 pom.xml 并添加配置,将现有 Spring Boot 项目迁移到 Lambda。
支持的 Spring Boot 版本包括:2.2.x、2.3.x、2.4.x、2.5.x、2.6.x 和 2.7.x。
3.1. 示例 Spring Boot API
我们的应用包含一个简单 API,处理对 api/v1/users 接口的 GET 请求:
@RestController
@RequestMapping("/api/v1/")
public class ProfileController {
@GetMapping(value = "users", produces = MediaType.APPLICATION_JSON_VALUE)
public List<User> getUser() {
return List.of(new User("John", "Doe", "john.doe@example.com"),
new User("Jane", "Doe", "jane.doe@example.com"));
}
}
返回 User 对象列表:
public class User {
private String name;
private String surname;
private String emailAddress;
// 标准构造器、getter 和 setter
}
启动应用并调用 API:
$ java -jar app.jar
$ curl -X GET http://localhost:8080/api/v1/users -H "Content-Type: application/json"
API 响应:
[
{
"name":"John",
"surname":"Doe",
"email":"john.doe@example.com"
},
{
"name":"Jane",
"surname":"Doe",
"email":"jane.doe@example.com"
}
]
3.2. 通过 Maven 将 Spring Boot 应用转换为 Lambda
在 pom.xml 中添加 aws-serverless-java-container-springboot2 依赖:
<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-springboot2</artifactId>
<version>${springboot2.aws.version}</version>
</dependency>
然后添加 maven-shade-plugin 并移除 spring-boot-maven-plugin。
Maven Shade Plugin 用于创建 shaded(或 uber)JAR 文件。Shaded JAR 是自包含的可执行文件,将所有依赖打包进 JAR 内部,可独立运行:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>org.apache.tomcat.embed:*</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
此配置会在 Maven 构建的 package 阶段生成 shaded JAR。该 JAR 包含 Spring Boot 通常打包的所有类和资源,但排除了 Tomcat 相关内容——因为 AWS Lambda 不需要嵌入式 Web 容器。
4. Lambda 处理器
下一步是创建实现 RequestHandler 的类。
RequestHandler 是定义单个方法 handleRequest 的接口。根据构建的 Lambda 类型,有多种请求处理方式。
这里我们处理来自 API Gateway 的请求,因此使用 RequestHandler<AwsProxyRequest, AwsProxyResponse> 版本,输入为 API Gateway 请求,输出为 API Gateway 响应。
AWS 提供的 Spring Boot 无服务器库包含特殊的 SpringBootLambdaContainerHandler 类,用于通过 Spring 处理 API 调用,使 Spring Boot API 服务器代码库能作为 Lambda 运行。
4.1. 启动时机
在 AWS Lambda 中,初始化阶段限时 10 秒。如果应用启动超过此限制,Lambda 将超时并尝试启动新运行时实例。
根据 Spring Boot 应用的启动速度,可选择两种初始化方式:
- 同步初始化:启动时间远低于限制时
- 异步初始化:启动时间可能接近或超过限制时
4.2. 同步初始化
在 Spring Boot 项目中定义新处理器:
public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); }
catch (ContainerInitializationException ex){
throw new RuntimeException("Unable to load spring boot application",ex); }
}
@Override
public AwsProxyResponse handleRequest(AwsProxyRequest input, Context context) {
return handler.proxy(input, context);
}
}
使用 SpringBootLambdaContainerHandler 处理 API Gateway 请求并传递给应用上下文。在 LambaHandler 类的静态构造器中初始化处理器,并在 handleRequest 函数中调用。
处理器对象随后调用 Spring Boot 应用中的相应方法处理请求并生成响应,最终将响应返回给 Lambda 运行时,再传递回 API Gateway。
通过 Lambda 处理器调用 API:
@Test
void whenTheUsersPathIsInvokedViaLambda_thenShouldReturnAList() throws IOException {
LambdaHandler lambdaHandler = new LambdaHandler();
AwsProxyRequest req = new AwsProxyRequestBuilder("/api/v1/users", "GET").build();
AwsProxyResponse resp = lambdaHandler.handleRequest(req, lambdaContext);
Assertions.assertNotNull(resp.getBody());
Assertions.assertEquals(200, resp.getStatusCode());
}
4.3. 异步初始化
Spring Boot 应用启动可能较慢,因为启动阶段 Spring 引擎会构建上下文,扫描并初始化所有 Bean。此过程可能影响启动时间,在无服务器场景下引发问题。
定义新处理器解决此问题:
public class AsynchronousLambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {
private SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
public AsynchronousLambdaHandler() throws ContainerInitializationException {
handler = (SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse>)
new SpringBootProxyHandlerBuilder()
.springBootApplication(Application.class)
.asyncInit()
.buildAndInitialize();
}
@Override
public AwsProxyResponse handleRequest(AwsProxyRequest input, Context context) {
return handler.proxy(input, context);
}
}
此方法与前一种类似,但 SpringBootLambdaContainerHandler 在请求处理器的对象构造器中创建(而非静态构造器),因此它在 Lambda 启动的不同阶段执行。
5. 部署应用
AWS SAM(Serverless Application Model)是用于在 AWS 上构建无服务器应用的开源框架。定义 Lambda 处理器后,需准备使用 SAM 部署的所有组件。
5.1. SAM 模板
SAM 模板(SAM YAML)是定义部署无服务器应用所需 AWS 资源的 YAML 格式文件。它提供声明式方式指定无服务器应用的配置。
定义 template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
Resources:
ProfileApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: com.baeldung.aws.handler.LambdaHandler::handleRequest
Runtime: java11
AutoPublishAlias: production
SnapStart:
ApplyOn: PublishedVersions
Architectures:
- x86_64
MemorySize: 2048
Environment:
Variables:
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Events:
HelloWorld:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
关键配置字段说明:
- Type – 标识此资源是使用 AWS::Serverless::Function 类型定义的 Lambda 函数
- CodeUri – 指定函数代码位置
- AutoPublishAlias – 指定自动发布新版本时使用的别名
- Handler – 指定 Lambda 处理器类
- Events – 指定触发 Lambda 函数的事件
- Type(Events下)– 标识这是 Api 事件源
- Properties(Events下)– 定义 API Gateway 响应的 HTTP 方法和路径
5.2. SAM 部署
将应用部署为 AWS Lambda:
- 下载并安装 AWS CLI 和 AWS SAM CLI
- 在 template.yaml 所在目录执行:
$ sam build
此命令将 Lambda 函数的源码和依赖打包为部署用的 ZIP 文件。
本地部署测试:
$ sam local start-api
通过 sam local 触发 Spring Boot 服务:
$ curl localhost:3000/api/v1/users
API 响应与之前一致:
[
{
"name":"John",
"surname":"Doe",
"email":"john.doe@example.com"
},
{
"name":"Jane",
"surname":"Doe",
"email":"jane.doe@example.com"
}
]
部署到 AWS:
$ sam deploy
6. 在 Lambda 中使用 Spring 的限制
虽然 Spring 是构建复杂可扩展应用的强大框架,但在 Lambda 场景下并非总是最佳选择。主要原因是:Lambda 设计为小型、单用途函数,需快速高效执行。
6.1. 冷启动
Lambda 冷启动时间指处理事件前初始化函数环境所需时间。影响冷启动性能的因素包括:
- ✅ 包大小:较大包导致初始化时间延长
- ✅ 初始化时间:Spring 框架初始化应用上下文的时间(包括数据库连接、HTTP 客户端等依赖初始化)
- ✅ 自定义初始化逻辑:需最小化并优化冷启动的自定义逻辑
可通过 Lambda SnapStart 改善启动时间。
6.2. 数据库连接池
在 AWS Lambda 等无服务器环境中,函数按需执行,维护连接池具有挑战性。
事件触发 Lambda 时,AWS Lambda 引擎可能创建新应用实例。请求之间运行时可能被暂停或终止。
许多连接池保持打开连接,热启动后复用连接池时可能导致混乱或错误,甚至造成某些数据库引擎的资源泄漏。简言之,标准连接池依赖服务器持续运行维护连接。
AWS 提供的解决方案是 RDS Proxy,它为 Lambda 函数提供连接池服务。使用 RDS Proxy 后,Lambda 函数无需维护自己的连接池即可连接数据库。
7. 总结
本文学习了如何将现有 Spring Boot API 应用转换为 AWS Lambda:
- 使用 AWS 提供的库实现转换
- 分析 Spring Boot 较慢启动时间对配置的影响
- 通过 SAM CLI 部署和测试 Lambda
完整示例代码可在 GitHub 获取。