配置 RedisTemplate
并学习如何从 Spring REST 控制器访问不同的操作捆绑包以读写 Redis 数据。
在本课程中,您将学习
@RedisHash
注解映射领域对象。@Id
为映射对象提供主键。@Reference
创建对象之间的引用关联。如果您遇到困难
在许多应用中,无论是 Web 应用还是 RESTful Web 服务,首先要实现的领域部分之一就是用户/角色子领域。在 Redi2Read 中,用户拥有一或多个角色,这是一个典型的一对多关联。
在本节中,我们将开始创建 Redi2Read 领域模型。我们将使用 Lombok 简化 POJO,并使用 Spring Data Redis 的 @RedisHash
和其他 Spring Data 注解来配置模型,使其可以持久化到 Redis 中。
Role 模型
让我们从创建领域中最简单的类 Role 类开始,该类位于目录 src/main/java/com/redislabs/edu/redi2read/models
下。我们将文件命名为 Role.java
,内容如下
package com.redislabs.edu.redi2read.models;
import lombok.Data;
@Data
public class Role {
private String id;
private String name;
}
我们首先创建一个使用 Lombok 的 @Data
注解进行标注的类,该注解添加了 @ToString
、@EqualsAndHashCode
、@Getter
/@Setter
以及一个 @RequiredArgsContructor
,从而得到一个完整的 Java POJO。为了让 Spring Data 仓库知道如何将 Role 实例映射到 Redis Hash,我们需要使用 @RedisHash 注解标注该类。
package com.redislabs.edu.redi2read.models;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import lombok.Builder;
import lombok.Data;
@Data
@RedisHash
public class Role {
@Id
private String id;
private String name;
}
@RedisHash 可以接受 Long 类型的 timeToLive 参数和一个 String 值,后者将覆盖默认的 Redis 键前缀(默认情况下,键前缀是类的完全限定名加上一个冒号)。
在类中,支持大多数常用的 Spring Data 注解。对于 Role,我们使用 @Id 注解标注 id 字段。Spring Data Redis 将为被标注的类型自动生成一个合适的 id。
User 模型
User 模型将用于注册/signup 方法。为了进行服务器端验证,我们需要在 Maven POM 中添加 spring-boot-starter-validation 库的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
现在我们可以创建 User 类,内容如下:package com.redislabs.edu.redi2read.models;
import java.util.HashSet;
import java.util.Set;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Transient;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
@Data
@RedisHash
public class User {
@Id
@ToString.Include
private String id;
@NotNull
@Size(min = 2, max = 48)
@ToString.Include
private String name;
@NotNull
@Email
@EqualsAndHashCode.Include
@ToString.Include
@Indexed
private String email;
@NotNull
private String password;
@Transient
private String passwordConfirm;
@Reference
private Set<Role> roles = new HashSet<Role>();
public void addRole(Role role) {
roles.add(role);
}
}
这个类稍微复杂一些,所以我们来分解一下
@Data
),其实例可以作为 Redis Hashes 持久化 (@RedisHash
)@EqualsHashCode.Include
和 @ToString.Include
显式标注的字段添加到 equals/hashcode/toString 方法中@Id
生成了一个自动生成的 String 类型的 Redis Hash 键@Indexed
标注 email 属性来创建(二级)索引。我们将在下一课程中学习更多关于二级索引的知识。javax.validation.constraints
注解来表示字段的类型为 @Email
,可为 @NotNull
,并限制其 @Size
@Transient
,以便 @RedisHash
不会尝试将其序列化到数据库中User
的角色,我们有一个 Role
对象的 Set
集合,这些对象被标记为 @References
,这将导致它们作为给定角色的 id 存储在支持 User
实例的 Redis Hash 中。Role
添加到 User
的 Role Set 中。现在我们已经正确标注了两个模型,我们需要关联的仓库来对这些模型进行数据操作。Spring Data 仓库类似于 DAO (数据访问对象) 模式,因为它们都抽象了对底层数据存储的操作。区别在于,仓库通过专注于对象集合的管理来进一步抽象底层存储机制,而 DAO 更以 SQL/表为中心。
在目录 src/main/java/com/redislabs/edu/redi2read/repositories
下,我们创建 RoleRepository 接口,如下所示
package com.redislabs.edu.redi2read.repositories;
import com.redislabs.edu.redi2read.models.Role;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleRepository extends CrudRepository<Role, String> {
}
我们的仓库扩展了 CrudRepository
,用于 Role
类,其键类型为 String
,它提供了基本的 CRUD 和查找操作。Spring Data Redis 仓库性能很高;它们避免使用反射和字节码生成,而是使用 Spring 的 ProxyFactory API 创建编程性的 JDK 代理实例。参见 https://bit.ly/2PshxEI
让我们通过使用 CommandLineRunner
实现来测试 RoleRepository
。CommandLineRunner
是一个接口,它告诉 Spring 容器需要在启动时执行 run 方法。一个 Spring Boot 应用可以有多个 CommandLineRunner
;为了控制它们的执行顺序,我们可以使用 @Order
注解进一步标注它们。创建目录 src/main/java/com/redislabs/edu/redi2read/boot
,然后添加 CreateRoles
类
package com.redislabs.edu.redi2read.boot;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class CreateRoles implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println(">>> Hello from the CreateRoles CommandLineRunner...");
}
}
我们的 CreateRoles
类将在每次服务器启动或每次热重载时运行(因为我们正在使用 Spring DevTools)。@Order
注解接受一个数值,指示执行顺序。为了测试命令行运行器,我们在 run 方法中有一个 System.out.println
,我们可以在控制台上看到它飞过。
2021-04-02 14:32:58.374 INFO 41500 --- [ restartedMain] c.r.edu.redi2read.Redi2readApplication : Started Redi2readApplication in 0.474 seconds (JVM running for 74714.143)
>>> Hello from the CreateRoles CommandLineRunner...
2021-04-02 14:32:58.375 INFO 41500 --- [ restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged
既然我们知道 CreateRoles
组件可以运行,接下来我们完善它,使其与 RoleRepository
一起工作。
package com.redislabs.edu.redi2read.boot;
import com.redislabs.edu.redi2read.models.Role;
import com.redislabs.edu.redi2read.repositories.RoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(1)
@Slf4j
public class CreateRoles implements CommandLineRunner {
@Autowired
private RoleRepository roleRepository;
@Override
public void run(String... args) throws Exception {
if (roleRepository.count() == 0) {
Role adminRole = Role.builder().name("admin").build();
Role customerRole = Role.builder().name("customer").build();
roleRepository.save(adminRole);
roleRepository.save(customerRole);
log.info(">>>> Created admin and customer roles...");
}
}
}
我们首先使用 @Autowired
注解注入 RoleRepository
实例。因为我们不希望在每次服务器重启时都创建角色,所以我们的逻辑只会在 RoleRepository
中没有 Role
时执行。如果没有 Role
,那么我们就使用 Lombok 的 builder 创建“admin”和“customer”角色。然后使用 RoleRepository
的 save 方法将它们保存到 Redis。为了正确记录消息,我们将使用 Lombok 提供的 @Slf4j
(Simple Logging Facade for Java) 注解,它会创建一个名为 log 的 logger 实例,包含常用的日志级别记录方法。服务器启动时,我们应该会看到一次数据库填充的输出。
2021-04-02 19:28:25.367 INFO 94971 --- [ restartedMain] c.r.edu.redi2read.Redi2readApplication : Started Redi2readApplication in 2.146 seconds (JVM running for 2.544)
2021-04-02 19:28:25.654 INFO 94971 --- [ restartedMain] c.r.edu.redi2read.boot.CreateRoles : >>>> Created admin and customer roles...
让我们使用 Redis CLI 探索 Role
是如何存储的,我们使用 KEYS 命令并传入 Role 的完全限定类名和通配符。结果如下
127.0.0.1:6379> KEYS com.redislabs.edu.redi2read.models.Role*
1) "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
2) "com.redislabs.edu.redi2read.models.Role:9d383baf-35a0-4d20-8296-eedc4bea134a"
3) "com.redislabs.edu.redi2read.models.Role"
前两个值是 Hash
,是 Role
类的实际实例。跟在 :
后面的字符串是单个 Role 的主键。让我们检查其中一个 Hash
127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
hash
127.0.0.1:6379> HGETALL "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
1) "_class"
2) "com.redislabs.edu.redi2read.models.Role"
3) "id"
4) "c4219654-0b79-4ee6-b928-cb75909c4464"
5) "name"
6) "admin"
使用 TYPE 命令返回,正如预期的那样,键下的值是一个 Redis Hash。我们使用 HGETALL 命令来获取 Hash 中的所有值。 _class
是一个元数据字段,用于标记存储在 Hash 中的对象的类。现在让我们检查 KEYS 列表中的第三个值
127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.Role"
set
127.0.0.1:6379> SMEMBERS "com.redislabs.edu.redi2read.models.Role"
1) "9d383baf-35a0-4d20-8296-eedc4bea134a"
2) "c4219654-0b79-4ee6-b928-cb75909c4464"
映射类名下的 Redis Set 用于维护给定类的主键。