使用 Spring Cloud AWS 在 Spring Boot 中集成 Amazon DynamoDB

更新于 2025-12-30

Hardik Singh Behl 2025-05-12

1. 概述

NoSQL 数据库已成为构建应用程序持久层的热门选择。

Amazon DynamoDB 是由亚马逊云服务(AWS)提供的一个无服务器、完全托管的 NoSQL 数据库。近十年来,DynamoDB 凭借其可扩展性、灵活性和高性能,已成为云中最受欢迎和广泛使用的 NoSQL 数据库之一。

在使用 DynamoDB 时,我们主要与表(tables)、项(items)和属性(attributes)这三个核心组件交互。表是一组项的集合,而每个项又是一组属性的集合。

在本教程中,我们将探索如何将 Amazon DynamoDB 集成到 Spring Boot 应用程序中。

2. 项目设置

在开始与 Amazon DynamoDB 服务交互之前,我们需要添加必要的依赖项并正确配置应用程序。

2.1. 依赖项

我们将使用 Spring Cloud AWS 来建立连接并与 DynamoDB 服务交互,而不是直接使用 AWS 提供的 DynamoDB SDK。Spring Cloud AWS 是对官方 AWS SDK 的封装,它极大地简化了配置,并提供了更简洁的方法来与 AWS 服务交互。

首先,在项目的 pom.xml 文件中添加来自 Spring Cloud AWS 的 DynamoDB Starter 依赖:

<dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-starter-dynamodb</artifactId>
    <version>3.3.0</version>
</dependency>

接下来,也在 pom.xml 中引入 Spring Cloud AWS 的 BOM(Bill of Materials)以管理 DynamoDB Starter 的版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-aws</artifactId>
            <version>3.3.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

通过引入 BOM,我们可以从 starter 依赖中移除 <version> 标签。
BOM 能确保所声明依赖之间的版本兼容性,避免冲突,并使未来更新依赖版本更加容易。

2.2. 定义 AWS 配置属性

为了与 DynamoDB 服务交互,我们需要配置用于身份验证的 AWS 凭据以及我们创建表所在的 AWS 区域。

我们在 application.yaml 文件中配置这些属性:

spring:
  cloud:
    aws:
      dynamodb:
        region: ${AWS_REGION}
      credentials:
        access-key: ${AWS_ACCESS_KEY}
        secret-key: ${AWS_SECRET_KEY}

这里我们使用 ${} 属性占位符从环境变量中加载属性值。

2.3. 领域实体

现在,我们定义一个简单的 User 实体类,用于表示 DynamoDB 表的数据模型:

@DynamoDbBean
public class User {

    private UUID id;
    private String name;
    private String email;

    @DynamoDbPartitionKey
    public UUID getId() {
        return id;
    }

    // 标准的 setter 和 getter 方法
}

在这里,我们使用 @DynamoDbBean 注解标记 User 类为可映射到 DynamoDB 表的实体。需要注意的是,Spring Cloud AWS 无法映射使用包私有(package-private)修饰符的字段,因此 User 实体及其对应的 getter 和 setter 方法必须声明为 public

此外,我们通过在 getId() 方法上添加 @DynamoDbPartitionKey 注解,将 id 字段配置为表的分区键(即主键)。请注意,此注解应加在 getter 方法上,而不是字段本身。

DynamoDB 还支持通过可选的排序键(sort key)组成复合主键。我们可以通过在额外字段的 getter 方法上添加 @DynamoDbSortKey 注解来定义复合主键。虽然本教程中不会使用排序键,但了解它的存在是有益的。

3. 定义自定义 DynamoDbTableNameResolver Bean

默认情况下,Spring Cloud AWS 会将实体类名转换为蛇形命名(snake_case)格式,以此确定对应的 DynamoDB 表名。然而,这种约定可能并不总是符合我们的命名规范或需求。

我们可以通过实现 DynamoDbTableNameResolver 接口来定义一个自定义 Bean,从而覆盖这一默认行为。

首先,创建一个自定义注解,用于直接在实体类上指定表名:

@Target(TYPE)
@Retention(RUNTIME)
@interface TableName {
    String name();
}

我们创建了一个简单的 @TableName 注解,它接受一个 name 属性,用于指定所需的 DynamoDB 表名。

接下来,创建自定义的 DynamoDbTableNameResolver 实现:

@Component
class CustomTableNameResolver implements DynamoDbTableNameResolver {

    @Override
    public <T> String resolve(Class<T> clazz) {
        return clazz.getAnnotation(TableName.class).name();
    }
}

在此类中,我们重写了 resolve() 方法,获取应用在实体类上的 @TableName 注解中的 name 属性值,并将其作为表名使用。

最后,将新的 @TableName 注解添加到 User 类上:

@DynamoDbBean
@TableName(name = "users")
class User {
    // ...
}

通过此配置,Spring Cloud AWS 将使用我们的 CustomTableNameResolver 类来确定 User 实体映射到名为 users 的 DynamoDB 表。

4. 使用 LocalStack 设置本地测试环境

在开发过程中,通常需要在本地测试应用程序。LocalStack 是一个流行的工具,允许我们在本地机器上运行模拟的 AWS 环境。我们将使用 Testcontainers 来在应用程序中启动 LocalStack 服务。

通过 Testcontainers 运行 LocalStack 服务的前提是本地已安装并运行 Docker。在本地或 CI/CD 流水线中运行测试套件时,请确保满足此前提条件。

4.1. 测试依赖项

首先,在 pom.xml 中添加必要的测试依赖:

<dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>localstack</artifactId>
    <scope>test</scope>
</dependency>

我们引入了 Spring Cloud AWS Testcontainers 依赖和 Testcontainers 的 LocalStack 模块。这些依赖为我们提供了启动 LocalStack 服务的临时 Docker 实例所需的类。

4.2. 使用初始化钩子(Init Hooks)创建 DynamoDB 表

接下来,我们需要创建一个 DynamoDB 表供应用程序交互。LocalStack 支持在容器启动时通过 Initialization Hooks 自动创建所需的 AWS 资源。

src/test/resources 目录下创建一个名为 init-dynamodb-table.sh 的 Bash 脚本:

#!/bin/bash
table_name="users"
partition_key="id"

awslocal dynamodb create-table \
  --table-name "$table_name" \
  --key-schema AttributeName="$partition_key",KeyType=HASH \
  --attribute-definitions AttributeName="$partition_key",AttributeType=S \
  --billing-mode PAY_PER_REQUEST

echo "DynamoDB table '$table_name' created successfully with partition key '$partition_key'"
echo "Executed init-dynamodb-table.sh"

上述脚本会创建一个名为 users 的 DynamoDB 表。我们在脚本中使用 awslocal 命令(它是 AWS CLI 的包装器,指向 LocalStack 服务)。脚本末尾通过 echo 语句确认脚本成功执行。

在下一节中,我们会将此脚本复制到 LocalStack 容器内的 /etc/localstack/init/ready.d 路径下以供执行。

4.3. 定义 LocalStackContainer Bean

接下来,创建一个带有 @TestConfiguration 注解的类,用于定义我们的 Testcontainers Bean:

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {

    @Bean
    @ServiceConnection
    LocalStackContainer localStackContainer() {
        return new LocalStackContainer(DockerImageName.parse("localstack/localstack:4.3.0"))
          .withServices(LocalStackContainer.Service.DYNAMODB)
          .withCopyFileToContainer(
            MountableFile.forClasspathResource("init-dynamodb-table.sh", 0744),
            "/etc/localstack/init/ready.d/init-dynamodb-table.sh"
          )
          .waitingFor(Wait.forLogMessage(".*Executed init-dynamodb-table.sh.*", 1));
    }
}

我们指定了 LocalStack Docker 镜像的最新稳定版本。
然后启用 DynamoDB 服务,并将我们的 Bash 脚本复制到容器中以确保表被创建。此外,我们还配置了等待策略,直到日志中出现 Executed init-dynamodb-table.sh 字样(如初始化脚本中所定义)。

我们还在 Bean 方法上添加了 @ServiceConnection 注解,该注解会动态注册连接已启动的 LocalStack 容器所需的所有属性。现在,我们只需在测试类上添加 @Import(TestcontainersConfiguration.class) 注解即可使用此配置。

5. 与 DynamoDB 表交互

现在本地环境已设置完毕,我们可以使用 Spring Cloud AWS 自动创建的 DynamoDbTemplate Bean 与我们创建的 users 表进行交互。

5.1. 执行基本的 CRUD 操作

首先,向已创建的 DynamoDB 表中插入一个新的 User 项:

User user = Instancio.create(User.class);

dynamoDbTemplate.save(user);

Key partitionKey = Key.builder().partitionValue(user.getId().toString()).build();
User retrievedUser = dynamoDbTemplate.load(partitionKey, User.class);
assertThat(retrievedUser)
  .isNotNull()
  .usingRecursiveComparison()
  .isEqualTo(user);

我们使用 Instancio 创建一个包含随机测试数据的 User 对象,然后通过 DynamoDbTemplatesave() 方法将其持久化到表中。

为验证此操作,我们使用用户的分区键值创建一个 Key 对象,并将其传递给 load() 方法。然后断言检索到的 retrievedUser 与最初保存的 user 相等。

接下来,更新一个已存在的 User 项:

String updatedName = RandomString.make();
String updatedEmail = RandomString.make();
user.setName(updatedName);
user.setEmail(updatedEmail);
dynamoDbTemplate.update(user);

Key partitionKey = Key.builder().partitionValue(user.getId().toString()).build();
User updatedUser = dynamoDbTemplate.load(partitionKey, User.class);
assertThat(updatedUser.getName())
  .isEqualTo(updatedName);
assertThat(updatedUser.getEmail())
  .isEqualTo(updatedEmail);

这里我们更新了已持久化 User 对象的 nameemail 属性,并调用 update() 方法保存更改。通过重新检索用户并断言其属性是否正确更新来验证操作。

最后,从表中删除一个 User 项:

dynamoDbTemplate.delete(user);

Key partitionKey = Key.builder().partitionValue(user.getId().toString()).build();
User deletedUser = dynamoDbTemplate.load(partitionKey, User.class);
assertThat(deletedUser)
  .isNull();

我们调用 delete() 方法并传入要删除的 User 对象。然后尝试使用相同的分区键检索该对象,并断言它已不存在于表中。

5.2. 执行扫描(Scan)操作

我们已经看到可以使用 DynamoDbTemplateload() 方法通过分区键从表中获取单个项。

但有时我们也需要检索多个项,或根据非键属性进行筛选。DynamoDB 提供了 scan 操作来实现这一点。

首先,看看如何从表中检索所有 User 项:

int numberOfUsers = 10;
for (int i = 0; i < numberOfUsers; i++) {
    User user = Instancio.create(User.class);
    dynamoDbTemplate.save(user);
}

List<User> retrievedUsers = dynamoDbTemplate
  .scanAll(User.class)
  .items()
  .stream()
  .toList();

assertThat(retrievedUsers.size())
  .isEqualTo(numberOfUsers);

我们向表中保存多个 User 对象,然后使用 scanAll() 方法检索所有项,并断言检索到的用户数量与最初保存的数量一致。

默认情况下,如果查询结果超过 1 MB,DynamoDB 会对响应进行分页并返回一个令牌以获取下一页。但 Spring Cloud AWS 在后台自动处理分页,返回表中所有项。

此外,我们还可以使用过滤表达式执行带条件的扫描操作:

Expression expression = Expression.builder()
  .expression("#email = :email")
  .putExpressionName("#email", "email")
  .putExpressionValue(":email", AttributeValue.builder().s(user.getEmail()).build())
  .build();
ScanEnhancedRequest scanRequest = ScanEnhancedRequest
  .builder()
  .filterExpression(expression)
  .build();
User retrievedUser = dynamoDbTemplate.scan(scanRequest, User.class)
  .items()
  .stream()
  .findFirst()
  .get();

assertThat(retrievedUser)
  .isNotNull()
  .usingRecursiveComparison()
  .isEqualTo(user);

这里我们创建了一个 Expression 对象,其中包含匹配特定 Useremail 属性的过滤条件。我们将该表达式包装在 ScanEnhancedRequest 对象中,并传递给 scan() 方法。最后断言检索到的 retrievedUser 与最初保存的 user 相等。

6. IAM 权限

我们在演示中使用了 LocalStack 模拟器。但在对接真实的 DynamoDB 服务时,需要为应用程序中配置的 IAM 用户分配以下 IAM 策略:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:GetItem",
                "dynamodb:UpdateItem",
                "dynamodb:DeleteItem",
                "dynamodb:Scan"
            ],
            "Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/users"
        }
    ]
}

这里我们为 users 表授予了应用程序执行特定操作所需的权限。该 IAM 策略遵循最小权限原则,仅授予应用程序正常运行所必需的权限。

请记得将资源 ARN 中的 REGIONACCOUNT_ID 占位符替换为实际值。

7. 结论

在本文中,我们探讨了如何使用 Spring Cloud AWS 将 Amazon DynamoDB 集成到 Spring Boot 应用程序中。

我们完成了必要的配置,定义了数据模型以及自定义表名解析器。接着,利用 Testcontainers 启动了 LocalStack 服务的临时 Docker 容器,搭建了本地测试环境。

最后,我们使用 DynamoDbTemplate 对已创建的 DynamoDB 表执行了基本的 CRUD 操作和扫描操作,并讨论了所需的 IAM 权限。