AWS Lambda
AWS 适配器接收一个 Spring Cloud Function 应用程序,并将其转换为可在 AWS Lambda 中运行的形式。
通常,在 AWS Lambda 上运行 Spring 应用程序有两种方式
-
通过 Spring Cloud Function 使用 AWS Lambda 适配器实现下面概述的功能性方法。这非常适合单一职责 API 和基于事件和消息传递的系统,例如处理来自 Amazon SQS 或 Amazon MQ 队列、Apache Kafka 流的消息,或对 Amazon S3 中的文件上传做出反应。
-
通过 Serverless Java container 项目在 AWS Lambda 上运行 Spring Boot Web 应用程序。这非常适合将现有 Spring 应用程序迁移到 AWS Lambda,或者如果您构建具有多个 API 端点的复杂 API 并希望保持熟悉的
RestController方法。这种方法在 用于 Spring Boot Web 的 Serverless Java 容器中进行了更详细的概述。
以下指南假定您对 AWS 和 AWS Lambda 有基本的了解,并着重于 Spring 提供的附加价值。有关如何开始使用 AWS Lambda 的详细信息不在本文档的范围之内。如果您想了解更多信息,可以导航到 基本的 AWS Lambda 概念或完整的 Java on AWS 概述。
入门
Spring Cloud Function 框架的目标之一是提供必要的基础设施元素,以使简单的功能应用程序与特定环境(如 AWS Lambda)兼容。
在 Spring 的上下文中,一个简单的功能应用程序包含 Supplier、Function 或 Consumer 类型的 bean。
让我们来看一个例子
@SpringBootApplication
public class FunctionConfiguration {
public static void main(String[] args) {
SpringApplication.run(FunctionConfiguration.class, args);
}
@Bean
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
}
}
您可以看到一个完整的 Spring Boot 应用程序,其中定义了一个函数 bean。从表面上看,这只是另一个 Spring Boot 应用程序。但是,当将 Spring Cloud Function AWS 适配器添加到项目中时,它将成为一个完全有效的 AWS Lambda 应用程序
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
</dependencies>
不需要其他代码或配置。我们提供了一个准备好构建和部署的示例项目。您可以在官方 Spring Cloud function 示例存储库中访问它。
您只需执行 mvn clean package 即可生成 JAR 文件。所有必要的 maven 插件都已设置好,以生成适当的 AWS 可部署 JAR 文件。(您可以在JAR 布局注意事项中阅读有关 JAR 布局的更多详细信息)。
AWS Lambda 函数处理器
与通过给定 HTTP 端口(80、443)上的侦听器暴露其功能的传统 Web 应用程序不同,AWS Lambda 函数在预定义的入口点(称为 Lambda 函数处理器)处调用。
部署
构建应用程序后,您可以通过 AWS 控制台、AWS 命令行界面 (CLI) 或基础设施即代码 (IaC) 工具(例如 AWS 无服务器应用程序模型 (AWS SAM)、AWS 云开发工具包 (AWS CDK)、AWS CloudFormation 或 Terraform)手动部署 JAR 文件。
使用 AWS 控制台创建 Hello world Lambda 函数
-
打开 Lambda 控制台的函数页面。
-
选择创建函数。
-
选择从头开始创作。
-
对于函数名称,输入
MySpringLambdaFunction。 -
对于运行时,选择Java 21。
-
选择创建函数。
上传您的代码并测试函数
-
上传之前创建的 JAR 文件,例如
target/function-sample-aws-0.0.1-SNAPSHOT-aws.jar。 -
提供入口处理方法
org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest。 -
导航到“测试”选项卡,然后单击“测试”按钮。函数应以大写形式返回提供的 JSON 有效负载。
要使用基础设施即代码 (IaC) 工具自动化部署,请参阅官方 AWS 文档。
AWS 请求处理器
如入门部分所述,AWS Lambda 函数在预定义的入口点(称为 Lambda 函数处理器)处调用。最简单的形式可以是 Java 方法引用。在上面的示例中,这将是 com.my.package.FunctionConfiguration::uppercase。此配置用于告知 AWS Lambda 在提供的 JAR 中调用哪个 Java 方法。
当调用 Lambda 函数时,它会将附加的请求有效负载和上下文对象传递给此处理器方法。请求有效负载因触发函数的 AWS 服务(Amazon API Gateway、Amazon S3、Amazon SQS、Apache Kafka 等)而异。上下文对象提供有关 Lambda 函数、调用和环境的附加信息,例如唯一的请求 ID(另请参阅官方文档中的 Java 上下文)。
AWS 提供预定义的处理器接口(称为 RequestHandler 或 RequestStreamHandler),通过 aws-lambda-java-events 和 aws-lambda-java-core 库处理有效负载和上下文对象。
Spring Cloud Function 已实现这些接口,并提供 org.springframework.cloud.function.adapter.aws.FunctionInvoker 以完全抽象您的函数代码与 AWS Lambda 的细节。这允许您根据运行函数所在的平台切换入口点。
但是,对于某些用例,您希望与 AWS 环境深度集成。例如,当您的函数由 Amazon S3 文件上传触发时,您可能希望访问特定的 Amazon S3 属性。或者,如果您想在处理来自 Amazon SQS 队列的项目时返回部分批处理响应。在这种情况下,您仍然可以利用通用的 org.springframework.cloud.function.adapter.aws.FunctionInvoker,但您将在函数代码中处理专用的 AWS 对象
@Bean
public Function<S3Event, String> processS3Event() {}
@Bean
public Function<SQSEvent, SQSBatchResponse> processSQSEvent() {}
AWS 函数路由
Spring Cloud Function 的核心功能之一是路由。此功能允许您有一个特殊的 Java 方法(作为 Lambda 函数处理器)来委托给其他内部方法。您已经在 入门部分中看到了这一点,当时通用 FunctionInvoker 自动将请求路由到您的 uppercase 函数。
默认情况下,如果您的应用程序有多个 Function 等类型的 @Bean,它们将从 Spring Cloud FunctionCatalog 中提取,框架将尝试按照搜索顺序查找默认值,首先搜索 Function,然后是 Consumer,最后是 Supplier。这些默认路由功能是必需的,因为 FunctionInvoker 无法确定要绑定哪个函数,因此它内部默认为 RoutingFunction。建议使用多种机制提供额外的路由指令(有关详细信息,请参阅示例)。
正确的路由机制取决于您是将 Spring Cloud Function 项目部署为单个 Lambda 函数还是多个 Lambda 函数。
单个函数 vs. 多个函数
如果您在同一个 Spring Cloud Function 项目中实现多个 Java 方法,例如 uppercase 和 lowercase,您可以部署两个具有静态路由信息的单独 Lambda 函数,或者提供一个动态路由方法,在运行时决定调用哪个方法。让我们看看这两种方法。
-
如果您对每个函数有不同的扩展、配置或权限要求,部署两个单独的 AWS Lambda 函数是合理的。例如,如果您在同一个 Spring Cloud Function 项目中创建两个 Java 方法
readObjectFromAmazonS3和writeToAmazonDynamoDB,您可能希望创建两个单独的 Lambda 函数。这是因为它们需要不同的权限才能与 S3 或 DynamoDB 通信,或者它们的负载模式和内存配置差异很大。通常,对于基于消息传递的应用程序(您从流或队列中读取数据),也建议采用这种方法,因为每个 Lambda 事件源映射都有一个专门的配置。 -
当多个 Java 方法共享相同的权限集或提供内聚的业务功能时,单个 Lambda 函数是一种有效的方法。例如,一个基于 CRUD 的 Spring Cloud Function 项目,包含
createPet、updatePet、readPet和deletePet方法,它们都与同一个 DynamoDB 表通信,并且具有相似的使用模式。使用单个 Lambda 函数将提高部署的简便性、内聚性和共享类 (PetEntity) 的代码重用。此外,它可以减少连续调用之间的冷启动,因为readPet后跟writePet很可能会命中已在运行的 Lambda 执行环境。但是,当您构建更复杂的 API,或者希望利用@RestController方法时,您可能还需要评估 用于 Spring Boot Web 的无服务器 Java 容器选项。
如果您喜欢第一种方法,您还可以创建两个独立的 Spring Cloud Function 项目并单独部署它们。如果不同的团队负责维护和部署这些函数,这可能很有益。但是,在这种情况下,您需要处理它们之间共享的交叉关注点,例如帮助方法或实体类。通常,我们建议对您的功能项目应用与传统基于 Web 的应用程序相同的软件模块化原则。有关如何选择正确方法的更多信息,您可以参考比较无服务器微服务的设计方法。
做出决定后,您可以从以下路由机制中受益。
多个 Lambda 函数的路由
如果您已决定将单个 Spring Cloud Function 项目 (JAR) 部署到多个 Lambda 函数,则需要提供调用哪个特定方法(例如 uppercase 或 lowercase)的提示。您可以使用 AWS Lambda 环境变量来提供路由指令。
请注意,AWS 不允许在环境变量名称中使用点 . 和/或连字符 -。您可以利用 Spring Boot 支持,只需将点替换为下划线,将连字符替换为驼峰式命名。例如,spring.cloud.function.definition 变为 spring_cloud_function_definition,spring.cloud.function.routing-expression 变为 spring_cloud_function_routingExpression。
因此,一个具有两个方法并部署到单独的 AWS Lambda 函数的单个 Spring Cloud 项目的配置可能如下所示
@SpringBootApplication
public class FunctionConfiguration {
public static void main(String[] args) {
SpringApplication.run(FunctionConfiguration.class, args);
}
@Bean
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
}
@Bean
public Function<String, String> lowercase() {
return value -> value.toLowerCase();
}
}
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
MyUpperCaseLambda:
Type: AWS::Serverless::Function
Properties:
Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker
Runtime: java21
MemorySize: 512
CodeUri: target/function-sample-aws-0.0.1-SNAPSHOT-aws.jar
Environment:
Variables:
spring_cloud_function_definition: uppercase
MyLowerCaseLambda:
Type: AWS::Serverless::Function
Properties:
Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker
Runtime: java21
MemorySize: 512
CodeUri: target/function-sample-aws-0.0.1-SNAPSHOT-aws.jar
Environment:
Variables:
spring_cloud_function_definition: lowercase
您可能会问——为什么不使用 Lambda 函数处理器并直接将入口方法指向 uppercase 和 lowercase 呢?在 Spring Cloud Function 项目中,建议使用 AWS 请求处理器中概述的内置 FunctionInvoker。因此,我们通过环境变量提供路由定义。
单个 Lambda 函数中的路由
如果您已决定将具有多个方法(uppercase 或 lowercase)的 Spring Cloud Function 项目部署到单个 Lambda 函数,则需要一种更动态的路由方法。由于 application.properties 和环境变量是在构建或部署时定义的,因此您不能将它们用于单个函数场景。在这种情况下,您可以利用 MessagingRoutingCallback 或 Message Headers,如 Spring Cloud Function 路由部分中所述。
更多详细信息可在提供的示例中找到。
性能考量
无服务器函数的一个核心特性是能够扩展到零并处理突发流量高峰。为了处理请求,AWS Lambda 会启动 新的执行环境。这些环境需要初始化,您的代码需要下载,并且 JVM + 您的应用程序需要启动。这也称为冷启动。为了减少冷启动时间,您可以依靠以下机制来优化性能。
-
利用 AWS Lambda SnapStart 从预初始化快照启动您的 Lambda 函数。
-
通过 AWS Lambda Power Tuning 调整内存配置,以在性能和成本之间找到最佳折衷。
-
遵循 AWS SDK 最佳实践,例如在处理程序代码之外定义 SDK 客户端或利用更高级的预热技术。
-
实现额外的 Spring 机制以减少 Spring 启动和初始化时间,例如功能性 Bean 注册。
有关更多信息,请参阅官方指南。
GraalVM 本机映像
Spring Cloud Function 为在 AWS Lambda 上运行的函数提供 GraalVM 本机映像支持。由于 GraalVM 本机映像不在传统的 Java 虚拟机 (JVM) 上运行,因此您必须将本机 Spring Cloud Function 部署到 AWS Lambda 自定义运行时。最显著的区别是您不再提供 JAR 文件,而是提供一个捆绑在 zip 包中的本机映像和带有启动指令的引导文件
lambda-custom-runtime.zip
|-- bootstrap
|-- function-sample-aws-native
引导文件
#!/bin/sh
cd ${LAMBDA_TASK_ROOT:-.}
./function-sample-aws-native
自定义运行时
Lambda 专注于提供稳定的长期支持 (LTS) Java 运行时版本。官方 Lambda 运行时是围绕操作系统、编程语言和软件库的组合构建的,这些组合会进行维护和安全更新。例如,Java 的 Lambda 运行时支持 LTS 版本,例如 Java 17 Corretto 和 Java 21 Corretto。您可以在此处找到完整列表。没有为非 LTS 版本(如 Java 22、Java 23 或 Java 24)提供运行时。
要使用其他语言版本、JVM 或 GraalVM 本机映像,Lambda 允许您创建自定义运行时。自定义运行时允许您提供和配置自己的运行时以运行其应用程序代码。Spring Cloud Function 提供了所有必要的组件,使其变得简单。
从代码角度来看,应用程序应与其他 Spring Cloud Function 应用程序没有什么不同。您唯一需要做的就是在 ZIP/JAR 的根目录中提供一个运行 Spring Boot 应用程序的 bootstrap 脚本,并在 AWS 中创建函数时选择“自定义运行时”。这是一个“bootstrap”文件示例
#!/bin/sh
cd ${LAMBDA_TASK_ROOT:-.}
java -Dspring.main.web-application-type=none -Dspring.jmx.enabled=false \
-noverify -XX:TieredStopAtLevel=1 -Xss256K -XX:MaxMetaspaceSize=128M \
-Djava.security.egd=file:/dev/./urandom \
-cp .:`echo lib/*.jar | tr ' ' :` com.example.LambdaApplication
com.example.LambdaApplication 代表您的应用程序,其中包含函数 bean。
在 AWS 中将处理程序名称设置为您的函数名称。您也可以在此处使用函数组合(例如,uppercase|reverse)。一旦您将 ZIP/JAR 上传到 AWS,您的函数将在自定义运行时中运行。我们提供了一个示例项目,您还可以在其中查看如何配置您的 POM 以正确生成 ZIP 文件。
函数 Bean 定义样式也适用于自定义运行时,并且比 @Bean 样式更快。自定义运行时甚至比 Java Lambda 的函数 Bean 实现启动更快——这主要取决于您需要在运行时加载的类数量。Spring 在这方面做得不多,因此您可以通过仅在函数中使用基本类型来减少冷启动时间,例如,并且不在自定义 @PostConstruct 初始化器中执行任何工作。
带有自定义运行时的 AWS 函数路由
使用自定义运行时时,函数路由的工作方式相同。您只需将 functionRouter 指定为 AWS 处理程序,就像您将函数名称用作处理程序一样。
将 Lambda 函数部署为容器镜像
与基于 JAR 或 ZIP 的部署不同,您还可以通过镜像注册表将 Lambda 函数部署为容器镜像。有关其他详细信息,请参阅官方 AWS Lambda 文档。
以类似于此处描述的方式部署容器镜像时,请务必记住设置环境变量 DEFAULT_HANDLER,其值为函数名称。
例如,对于下面显示的函数 bean,DEFAULT_HANDLER 的值将是 readMessageFromSQS。
@Bean
public Consumer<Message<SQSMessageEvent>> readMessageFromSQS() {
return incomingMessage -> {..}
}
此外,请务必记住确保 spring_cloud_function_web_export_enabled 也设置为 false。它默认为 true。
JAR 布局注意事项
在 Lambda 中运行时,您不需要 Spring Cloud Function Web 或 Stream 适配器,因此您可能需要在创建发送到 AWS 的 JAR 之前排除它们。Lambda 应用程序必须进行 shading,而 Spring Boot 独立应用程序则不需要,因此您可以使用 2 个单独的 JAR 文件运行同一个应用程序(如示例所示)。示例应用程序创建 2 个 JAR 文件,一个带有用于 Lambda 部署的 aws 分类器,另一个是 可执行(薄)JAR,在运行时包含 spring-cloud-function-web。Spring Cloud Function 将尝试从 JAR 文件清单中查找“主类”,使用 Start-Class 属性(如果您使用 starter parent,Spring Boot 工具将为您添加该属性)。如果清单中没有 Start-Class,您可以在将函数部署到 AWS 时使用环境变量或系统属性 MAIN_CLASS。
如果您不使用功能性 Bean 定义,而是依赖 Spring Boot 的自动配置,并且不依赖于 spring-boot-starter-parent,则必须将额外的转换器配置为 maven-shade-plugin 执行的一部分。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.4.2</version>
</dependency>
</dependencies>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>aws</shadedClassifierName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
<resource>META-INF/spring.factories</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.components</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
构建文件设置
为了在 AWS Lambda 上运行 Spring Cloud Function 应用程序,您可以利用 Maven 或 Gradle 插件。
Maven
为了在 Maven 中使用适配器插件,请将插件依赖项添加到您的 pom.xml 文件中
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
</dependencies>
如JAR 布局注意事项中所述,您需要一个 shaded jar 才能将其上传到 AWS Lambda。您可以使用 Maven Shade Plugin 来完成此操作。有关设置的示例可在上方找到。
您可以使用 Spring Boot Maven Plugin 生成瘦 JAR。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-layout</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
您可以在此处找到用于使用 Maven 将 Spring Cloud Function 应用程序部署到 AWS Lambda 的完整示例 pom.xml 文件。
Gradle
为了在 Gradle 中使用适配器插件,请将依赖项添加到您的 build.gradle 文件中
dependencies {
compile("org.springframework.cloud:spring-cloud-function-adapter-aws:${version}")
}
如JAR 布局注意事项中所述,您需要一个 shaded jar 才能将其上传到 AWS Lambda。您可以使用 Gradle Shadow Plugin 来完成此操作
您可以使用 Spring Boot Gradle Plugin 和 Spring Boot Thin Gradle Plugin 生成瘦 JAR。
下面是一个完整的 gradle 文件
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.2'
id 'io.spring.dependency-management' version '1.1.3'
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'maven-publish'
id 'org.springframework.boot.experimental.thin-launcher' version "1.0.31.RELEASE"
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
mavenLocal()
maven { url 'https://repo.spring.io/milestone' }
}
ext {
set('springCloudVersion', "2024.0.0")
}
assemble.dependsOn = [thinJar, shadowJar]
publishing {
publications {
maven(MavenPublication) {
from components.java
versionMapping {
usage('java-api') {
fromResolutionOf('runtimeClasspath')
}
usage('java-runtime') {
fromResolutionResult()
}
}
}
}
}
shadowJar.mustRunAfter thinJar
import com.github.jengelman.gradle.plugins.shadow.transformers.*
shadowJar {
archiveClassifier = 'aws'
manifest {
inheritFrom(project.tasks.thinJar.manifest)
}
// Required for Spring
mergeServiceFiles()
append 'META-INF/spring.handlers'
append 'META-INF/spring.schemas'
append 'META-INF/spring.tooling'
append 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports'
append 'META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports'
transform(PropertiesFileTransformer) {
paths = ['META-INF/spring.factories']
mergeStrategy = "append"
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws'
implementation 'org.springframework.cloud:spring-cloud-function-context'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
您可以在此处找到用于使用 Gradle 将 Spring Cloud Function 应用程序部署到 AWS Lambda 的完整示例 build.gradle 文件。
用于 Spring Boot Web 的无服务器 Java 容器
您可以使用 aws-serverless-java-container 库在 AWS Lambda 中运行 Spring Boot 3 应用程序。这非常适合将现有 Spring 应用程序迁移到 AWS Lambda,或者如果您构建具有多个 API 端点的复杂 API 并希望保持熟悉的 RestController 方法。以下部分提供了该过程的高级概述。请参阅 官方示例代码以获取更多信息。
-
将无服务器 Java 容器库导入您现有的 Spring Boot 3 Web 应用程序
<dependency> <groupId>com.amazonaws.serverless</groupId> <artifactId>aws-serverless-java-container-springboot3</artifactId> <version>2.1.2</version> </dependency> -
使用内置的 Lambda 函数处理器作为入口点
com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler -
配置名为
MAIN_CLASS的环境变量,以便通用处理程序知道在哪里可以找到您的原始应用程序主类。通常,这是使用 @SpringBootApplication 注解的类。
MAIN_CLAS = com.my.package.MySpringBootApplication
下面是部署配置示例
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
MySpringBootLambdaFunction:
Type: AWS::Serverless::Function
Properties:
Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler
Runtime: java21
MemorySize: 1024
CodeUri: target/lambda-spring-boot-app-0.0.1-SNAPSHOT.jar #Must be a shaded Jar
Environment:
Variables:
MAIN_CLASS: com.amazonaws.serverless.sample.springboot3.Application #Class annotated with @SpringBootApplication
请在此处找到包括 GraalVM 本机映像在内的所有示例。