精通Spring Boot 3 : 6. 使用 Spring Boot 的 Spring Data NoSQL (3)
Spring 数据 Redis
Spring Data Redis 是一种 NoSQL 内存数据结构存储,支持键值对、列表、集合、排序集合、位图、超日志等多种数据类型。由于其快速的特性,Redis 还可以用于发布/订阅消息。它具备可扩展性和高可用性等多种附加功能,广泛应用于会话管理、实时分析、任务队列、缓存、聊天应用和排行榜等场景。
Spring Data Redis 提供了丰富的低级和高级抽象,方便 Spring 应用程序与 Redis 进行交互。以下是它的一些主要功能:
- Spring 编程模型。
- RedisTemplate 是一个基于模板实现的类,提供了高层次的抽象,便于执行各种 Redis 操作、异常处理和序列化。同时,它还提供了 StringRedisTemplate,支持以字符串为主的操作。
- 在多个 Redis 驱动(如 Lettuce 和 Jedis)之间轻松连接。
- 将异常翻译为 Spring 的可移植数据访问异常体系。
- 发布/订阅支持(例如用于消息驱动的普通 Java 对象的消息监听容器)。
- 支持 Redis 集群和 Redis Sentinel。
- Lettuce 驱动程序的响应式 API 支持。
- JDK、字符串、JSON 以及 Spring 对象/XML 映射的序列化器。
- 基于 Redis 的 JDK 集合实现方式。
- Redis 实现的 Spring 3.1 缓存抽象。
- 包含对使用@EnableRedisRepositories 的自定义查询方法支持的仓库接口。
- 支持流媒体。
基于 Spring Boot 的 Spring Data Redis 使用指南
使用 Spring Data Redis 和 Spring Boot,您可以自动配置 Lettuce 和 Jedis 客户端,并享受 Spring Data Redis 提供的所有抽象功能。如果您希望在 Spring Boot 应用中使用 Spring Data Redis,您需要添加 spring-boot-starter-data-redis 启动依赖。如果您只打算使用 Spring Data Redis,而不使用 Spring Boot,并且使用的是仓库编程模型,则需要在 JavaConfig 类中添加 @EnableRedisRepositories 注解;当然,如果您使用的是 Spring Boot,则不需要这个注解,因为 Spring Boot 会自动处理配置。
让我们来看看在我们的项目中需要做些什么,以便从用户应用程序开始使用 Redis 进行数据持久化。
基于 Spring Boot 的用户应用程序,使用 Spring Data Redis
你可以选择重用之前版本中的一些代码,或者从头开始,访问 Spring Initializr (https://start.spring.io) 生成一个基础项目(不包含任何依赖)。请确保将 Group 字段设置为 com.apress,将 Artifact 和 Name 字段设置为 users。下载项目后,解压并导入到你喜欢的 IDE 中。在本节结束时,你应该能够看到如图 6-3 所示的项目结构。
正如您所见,我们将使用 Docker Compose 和 Redis,这样可以更方便地测试和运行我们的应用。
首先打开 build.gradle 文件,并将其内容替换为清单 6-18 中所示的内容。
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.apress'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Web
implementation 'org.webjars:bootstrap:5.2.3'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
test {
testLogging {
events "passed", "skipped", "failed"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
showStandardStreams = false
}
}
6-18 build.gradle
我们正在添加 spring-boot-starter-data-redis 和 spring-boot-docker-compose 启动依赖,这将使 Spring Boot 能够利用自动配置功能,设置与 Redis 相关的所有内容,包括连接和进行任何交互所需的类。
接下来,创建或打开用户类。请参阅清单 6-19。
package com.apress.users;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.util.Collection;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@RedisHash("USERS")
public class User {
@Id
@NotBlank(message = "Email can not be empty")
private String email;
@NotBlank(message = "Name can not be empty")
private String name;
private String gravatarUrl;
@NotBlank(message = "Password can not be empty")
private String password;
@Singular("role")
private Collection<UserRole> userRole;
private boolean active;
}
6-19 src/main/java/apress/com/users/User.java
用户类现在被标记为 @RedisHashMark 注解,该注解接受一个字符串值,作为与其他领域类区分的前缀。此注解将对象标记为要存储在 Redis 哈希中的聚合根。
接下来,按照清单 6-20 的示例创建或打开 UserRepository 接口。
package com.apress.users;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User,String>{ }
6-20 src/main/java/apress/com/users/UserRepository.java
用户仓库的代码示例 6-20src/main/java/apress/com/users/UserRepository.java
正如您所见,UserRepository 接口与之前的版本相同,继承自 CrudRepository 接口,而 CrudRepository 接口(您已经知道)提供了多个方法,这些方法将由 Spring Data 核心和 Redis 抽象层实现。
接下来,创建或打开 UsersController 类。请参阅第 6-21 号列表。
package com.apress.users;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@AllArgsConstructor
@RestController
@RequestMapping("/users")
public class UsersController {
private UserRepository userRepository;
@GetMapping
public ResponseEntity<Iterable<User>> getAll(){
return ResponseEntity.ok(this.userRepository.findAll());
}
@GetMapping("/{email}")
public ResponseEntity<User> findUserById(@PathVariable String email) throws Throwable {
return ResponseEntity.of(this.userRepository.findById(email));
}
@RequestMapping(method = {RequestMethod.POST,RequestMethod.PUT})
public ResponseEntity<User> save(@RequestBody User user){
if (user.getGravatarUrl()==null)
user.setGravatarUrl(UserGravatar.getGravatarUrlFromEmail(user.getEmail()));
if (user.getUserRole()==null)
user.setUserRole(Collections.singleton(UserRole.INFO));
this.userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{email}")
.buildAndExpand(user.getEmail())
.toUri();
return ResponseEntity.created(location).body(user);
}
@DeleteMapping("/{email}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable String email){
this.userRepository.deleteById(email);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
}
6-21 src/main/java/apress/com/users/UsersController.java
UsersController 类与之前的版本相同。请注意,UserRepository 被使用,并将在应用程序启动时通过构造函数进行注入。
接下来,创建或打开 UserConfiguration 类。请参阅清单 6-22。
package com.apress.users;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class UserConfiguration{
@Bean
ApplicationListener<ApplicationReadyEvent> init(UserRepository userRepository) {
return applicationReadyEvent -> {
userRepository.save(User.builder()
.email("ximena@email.com")
.name("Ximena")
.gravatarUrl("https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar")
.password("aw2s0meR!")
.role(UserRole.USER)
.active(true)
.build());
userRepository.save(User.builder()
.email("norma@email.com")
.name("Norma")
.gravatarUrl("https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar")
.password("aw2s0meR!")
.role(UserRole.USER)
.role(UserRole.ADMIN)
.active(true)
.build());
};
}
}
6-22 src/main/java/apress/com/users/UserConfiguration.java
在 UserConfiguration 类中,我们添加了一些用户,并且在这种情况下,我们还添加了 gravatarUrl 字段的值。在 Spring Data Redis 中,持久化任何对象之前没有回调或事件,因此我们可以在控制器中手动添加 gravatarUrl 字段的值,方法是使用记录类型和自定义构造函数,或者创建一个服务在保存之前处理这个字段。因此,正如你所看到的,我们有多种选择,目前可以在这里的配置中添加它。
UserGravatar 和 UserRole 类与之前的版本保持一致。application.properties 文件为空,没有任何属性。请注意,自动配置将使用默认的连接参数。如果您想要覆盖这些参数,可以将 spring.data.redis.* 属性设置为环境变量,既可以通过命令行设置,也可以在 application.properties 文件中设置。
用户应用测试
要测试用户应用程序,请创建或打开 UsersHttpRequestTests 类。请参见第 6-23 号列表。
package com.apress.users;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UsersHttpRequestTests {
@Value("${local.server.port}")
private int port;
private final String BASE_URL = "http://localhost:";
private final String USERS_PATH = "/users";
@Autowired
private TestRestTemplate restTemplate;
@Test
public void indexPageShouldReturnHeaderOneContent() throws Exception {
assertThat(this.restTemplate.getForObject(BASE_URL + port,
String.class)).contains("Simple Users Rest Application");
}
@Test
public void usersEndPointShouldReturnCollectionWithTwoUsers() throws Exception {
Collection<User> response = this.restTemplate.
getForObject(BASE_URL + port + USERS_PATH, Collection.class);
assertThat(response.size()).isEqualTo(2);
}
@Test
public void userEndPointPostNewUserShouldReturnUser() throws Exception {
User user = User.builder()
.email("dummy@email.com")
.name("Dummy")
.gravatarUrl("https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar")
.password("aw2s0meR!")
.role(UserRole.USER)
.active(true)
.build();
User response = this.restTemplate.postForObject(BASE_URL + port + USERS_PATH,user,User.class);
assertThat(response).isNotNull();
assertThat(response.getEmail()).isEqualTo(user.getEmail());
Collection<User> users = this.restTemplate.
getForObject(BASE_URL + port + USERS_PATH, Collection.class);
assertThat(users.size()).isGreaterThanOrEqualTo(2);
}
@Test
public void userEndPointDeleteUserShouldReturnVoid() throws Exception {
this.restTemplate.delete(BASE_URL + port + USERS_PATH + "/norma@email.com");
Collection<User> users = this.restTemplate.
getForObject(BASE_URL + port + USERS_PATH, Collection.class);
assertThat(users.size()).isLessThanOrEqualTo(2);
}
@Test
public void userEndPointFindUserShouldReturnUser() throws Exception{
User user = this.restTemplate.getForObject(BASE_URL + port + USERS_PATH + "/ximena@email.com",User.class);
assertThat(user).isNotNull();
assertThat(user.getEmail()).isEqualTo("ximena@email.com");
}
}
6-23 src/test/java/apress/com/users/UsersHttpRequestTests.java
正如您所见,UsersHttpRequestTests 类并没有变化。要开始测试,您需要确保 Redis 已经启动并在运行。您可以使用您的 IDE 来运行 docker-compose.yaml 服务,或者在命令行中执行以下命令:
docker compose up -d
一旦 Redis 启动并正常运行,您可以通过 IDE 或以下命令来进行测试:
./gradlew clean test
UsersHttpRequestTests > userEndPointFindUserShouldReturnUser() PASSED
UsersHttpRequestTests > userEndPointDeleteUserShouldReturnVoid() PASSED
UsersHttpRequestTests > indexPageShouldReturnHeaderOneContent() PASSED
UsersHttpRequestTests > userEndPointPostNewUserShouldReturnUser() PASSED
UsersHttpRequestTests > usersEndPointShouldReturnCollectionWithTwoUsers() PASSED
启动用户应用程序
在运行应用程序之前,请确保已停止 Redis(来自 docker compose)。然后,您可以通过您的 IDE 或使用以下命令来启动应用程序:
./gradlew clean bootRun
首先启动 Docker Compose,然后启动应用程序。接下来,您可以通过浏览器访问 http://localhost:8080/users,或者使用以下命令:
curl -s http://localhost:8080/users | jq .
[
{
"email": "ximena@email.com",
"name": "Ximena",
"gravatarUrl": "https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar",
"password": "aw2s0meR!",
"userRole": [
"USER"
],
"active": true
},
{
"email": "norma@email.com",
"name": "Norma",
"gravatarUrl": "https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar",
"password": "aw2s0meR!",
"userRole": [
"USER",
"ADMIN"
],
"active": true
}
]
如果你感兴趣,可以使用 Redis CLI 来查看 Spring Data Redis 是如何保存数据的。你可以执行以下命令:
docker run -it --rm --network users_default redis:alpine redis-cli -h redis
该命令使用了 users_default 网络,并通过 redis-cli 执行 redis:alpine 镜像,带有 -h(主机)参数和 redis(在 docker-compose.yaml 中声明的服务名称)。命令执行后,您将看到 redis:6379> 提示符。接下来,您可以通过执行 KEYS 命令来查看键:
redis:6379> KEYS *
1) "USERS"
2) "USERS:norma@email.com"
3) "USERS:ximena@email.com"
你可以获取类型,比如说:
redis:6379> TYPE "USERS:norma@email.com"
hash
你可以通过以下命令获取哈希表的键:
redis:6379> HKEYS "USERS:norma@email.com"
1) "email"
2) "active"
3) "_class"
4) "userRole.[1]"
5) "password"
6) "userRole.[0]"
7) "gravatarUrl"
8) "name"
你可以从这个哈希中获取值
redis:6379> HVALS "USERS:norma@email.com"
2) "1"
6) "USER"
7) "https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar"
8) "Norma"
你可以获取一个特定的字段
redis:6379> HGET "USERS:norma@email.com" name
"Norma"
在 Redis 客户端中尝试各种命令(您可以在 https://redis.io/commands/查看相关文档)。
接下来,让我们来看看如何在我的复古应用项目中使用 Spring Data Redis。
我使用 Spring Boot 开发的复古应用程序,基于 Spring Data Redis
如果你正在跟着进行,可以重用一些之前版本的代码。或者,你也可以从头开始,访问 Spring Initializr (https://start.spring.io) 生成一个基础项目(不包含任何依赖项);只需确保将 Group 字段设置为 com.apress,将 Artifact 和 Name 字段设置为 myretro。下载项目,解压缩后导入到你喜欢的 IDE 中。在本节结束时,你应该能够看到如图 6-4 所示的结构。
打开 build.gradle 文件,并将其内容替换为清单 6-24 中的内容。
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.apress'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Web
implementation 'org.webjars:bootstrap:5.2.3'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
6-24 build.gradle
列表 6-24 显示我们包含了 spring-boot-starter-data-redis 启动依赖和 Docker Compose。
接下来,创建或打开 board 包以及 RetroBoard 类。请参阅清单 6-25。
package com.apress.myretro.board;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.util.List;
import java.util.UUID;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@RedisHash("RETRO_BOARD")
public class RetroBoard {
@Id
@NotNull
private UUID id;
@NotBlank(message = "A name must be provided")
private String name;
@Singular
private List<Card> cards;
}
6-25 src/main/java/apress/com/myretro/board/RetroBoard.java
列表 6-25 展示了 RetroBoard 类,您可以看到它使用@RedisHash 注解,并使用参数 RETRO_BOARD 来标识 Redis 的哈希。同时,我们使用@Id 注解来标记标识符,这个标识符用于(如在用户应用中)建立该哈希值的键。
接下来,创建或打开持久化包以及 RetroBoardRepository 接口。请参阅清单 6-26。
package com.apress.myretro.persistence;
import com.apress.myretro.board.RetroBoard;
import org.springframework.data.repository.CrudRepository;
import java.util.UUID;
public interface RetroBoardRepository extends CrudRepository<RetroBoard, UUID> { }
6-26 src/main/java/apress/com/myretro/persistence/RetroBoardRepository.java
我们正在使用 CrudRepository,这与其他版本相同,并且我们利用 Spring Data 核心来为我们实现这个接口,执行所有的 Redis 操作。
建议、配置、异常、服务和网络包及其所有类与之前的版本保持一致,在 board 包中,Card 和 CardType 也没有变化。此外,application.properties 文件是空的。因此,总结来说,唯一显著的变化是向 RetroBoard 类添加了@RedisHash 注解。
运行我的复古应用程序
要运行该应用程序,您可以使用 IDE,或者使用以下命令:
./gradlew clean bootRun
然后,请在浏览器中输入 http://localhost:8080/retros,或者执行以下命令:
curl -s http://localhost:8080/retros | jq .
[
{
"id": "9dc9b71b-a07e-418b-b972-40225449aff2",
"name": "Spring Boot Conference",
"cards": [
{
"id": "bb2a80a5-a0f5-4180-a6dc-80c84bc014c9",
"comment": "Spring Boot Rocks!",
"cardType": "HAPPY"
},
{
"id": "e39cd241-4c75-41c8-9750-8b01e8225774",
"comment": "Meet everyone in person",
"cardType": "HAPPY"
},
{
"id": "e8947c01-fd30-4f78-81cf-31acf5cfd46b",
"comment": "When is the next one?",
"cardType": "MEH"
},
{
"id": "ce228247-d414-4cf9-8356-dbccb89f9370",
"comment": "Not enough time to talk to everyone",
"cardType": "SAD"
}
]
}
]
如果你想了解它在 Redis 中的表示方式,可以运行以下 Docker 容器客户端来使用 redis-cli:
docker run -it --rm --network myretro_default redis:alpine redis-cli -h redis
请注意,我们正在使用应用程序运行时生成的 myretro_default 网络。
然后你可以使用 Redis 命令进行查看:
你可以通过以下 Redis 命令来查看键:
redis:6379> keys *
1) "RETRO_BOARD"
2) "RETRO_BOARD:9dc9b71b-a07e-418b-b972-40225449aff2"
你可以审查这些值
redis:6379> hvals "RETRO_BOARD:9dc9b71b-a07e-418b-b972-40225449aff2"
1) "com.apress.myretro.board.RetroBoard"
2) "HAPPY"
3) "Spring Boot Rocks!"
4) "bb2a80a5-a0f5-4180-a6dc-80c84bc014c9"
5) "HAPPY"
6) "Meet everyone in person"
7) "08821ce0-79ed-4dd4-ad8d-c6d0e841738f"
8) "MEH"
9) "When is the next one?"
10) "edf4dcc7-c800-48e2-bf2d-0af41b5c3a23"
11) "SAD"
12) "Not enough time to talk to everyone"
13) "7545e981-4042-4f9f-8986-78ff92242c3c"
14) "9dc9b71b-a07e-418b-b972-40225449aff2"
15) "Spring Boot Conference"
你可以查看这些密钥
redis:6379> Hkeys "RETRO_BOARD:9dc9b71b-a07e-418b-b972-40225449aff2"
1) "_class"
2) "cards.[0].cardType"
3) "cards.[0].comment"
4) "cards.[0].id"
5) "cards.[1].cardType"
6) "cards.[1].comment"
7) "cards.[1].id"
8) "cards.[2].cardType"
9) "cards.[2].comment"
10) "cards.[2].id"
11) "cards.[3].cardType"
12) "cards.[3].comment"
13) "cards.[3].id"
14) "id"
15) "name"
你可以查看字段的值
redis:6379> hget "RETRO_BOARD:9dc9b71b-a07e-418b-b972-40225449aff2" "cards.[3].cardType"
"SAD"
请注意,我们的 Card 域类使用组合键设置,实例名称复数为 cards,采用数组表示法,以及 Card 字段名称。
所以,这就是如何使用 Spring Data Redis 和 Spring Boot 来运行您的应用程序的方法。当然,我们在这里使用的是仓库编程模型,但您也可以直接使用 RedisTemplate、StringRedisTemplate 或其他操作类,例如 HashOperation、ListOperations、ZSetOperations 等。
概要
在本章中,我们讨论了 MongoDB 和 Redis 这两种 NoSQL 数据库,您了解到它们是如何基于 Spring Data 核心项目的,并且它们提供了相同的仓库编程模型,这使得在领域类中只需进行最小的更改就能更轻松地使用。
你了解到 Spring Boot 利用自动配置功能来设置默认的连接参数,并为 NoSQL 数据库启用额外的功能,而你只需添加这些依赖即可。
你还通过这两个项目(用户应用和我的复古应用)学会了如何使用正确的注解(@Document,@RedisHash)来标记类,甚至如何使用核心注解如@Id 和 CrudRepository 接口。如果让 Spring Data 为你处理这些工作,只需进行很少的修改。
在第七章中,我们将深入探讨 Spring Reactor 及其如何用于创建反应式应用。