完成用户角色域的创建,加载和转换 JSON 数据,并开始构建 Redi2Read API。
在本课程中,你将学习
如果遇到困难
现在我们已经创建了角色,接下来从 src/main/resources/data/users/users.json
中提供的 JSON 数据加载 User
对象。该文件包含一个 JSON 用户对象数组,如下所示:
{
"password": "9yNvIO4GLBdboI",
"name": "Georgia Spencer",
"id": -5035019007718357598,
"email": "[email protected]"
}
JSON 字段与我们的 User POJO 属性的 JavaBean 名称完全对应。
首先,我们将创建 UserRepository
;就像我们创建 RoleRepository
一样,我们将扩展 CrudRepository
。在 src/main/java/com/redislabs/edu/redi2read/repositories
目录下创建 UserRepository
接口,如下所示:
package com.redislabs.edu.redi2read.repositories;
import com.redislabs.edu.redi2read.models.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends CrudRepository<User, String> {
User findFirstByEmail(String email);
}
findFirstByEmail
方法利用了我们之前在 User
模型的 email 字段上创建的索引。Spring Repository 将在运行时提供 finder 方法的实现。当我们处理应用程序的安全性时,我们将使用此 finder。
接下来,在 boot 包下创建另一个 CommandLineRunner
来加载用户。我们将遵循与加载角色类似的步骤,不同之处在于我们将从磁盘加载 JSON 数据并使用 Jackson (https://github.com/FasterXML/jackson),这是一个非常流行的 Java JSON 库。
加载用户的步骤如下
基于上述加载步骤,我们的应用程序目前无法做到但需要做的两件事是
我们的 PasswordEncoder
实现将使用强大的 BCrypt
哈希函数。在 Redi2readApplication
类中添加
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
以及相应的导入
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
正如我们在上一课中了解到的,@Indexed
注解可用于创建二级索引。二级索引支持基于原生 Redis 结构的查找操作。每次保存/更新索引对象时都会维护索引。要在 Role
模型中添加二级索引,只需添加 @Indexed
注解即可
@Data
@Builder
@RedisHash
public class Role {
@Id
private String id;
@Indexed
private String name;
}
不要忘记添加相应的导入
import org.springframework.data.redis.core.index.Indexed;
现在,当创建一个 ID 为 "abc-123"
、角色为 "superuser"
的新 Role
实例时,Spring Data Redis 将执行以下操作:
"by name"
索引:创建为一个 Redis Set,其 key 为 com.redislabs.edu.redi2read.models.Role:name:superuser
,包含一个条目:被索引对象的 id "abc-123"
Role
"superuser" 的索引列表:创建一个 Redis Set,其 key 为 "com.redislabs.edu.redi2read.models.Role:abc-123:idx"
,包含一个条目:索引的 key "com.redislabs.edu.redi2read.models.Role:name:superuser"
遗憾的是,要为已经创建的角色创建索引,我们需要要么检索它们并重新保存,要么重新创建它们。由于我们已经自动化了角色的填充过程,并且还没有创建任何关联对象,我们可以简单地使用 Redis CLI 和 DEL 命令删除它们并重新启动服务器
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"
127.0.0.1:6379> DEL "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464" "com.redislabs.edu.redi2read.models.Role:9d383baf-35a0-4d20-8296-eedc4bea134a" "com.redislabs.edu.redi2read.models.Role"
(integer) 3
127.0.0.1:6379>
DEL 命令接受一个或多个 key。我们将传入角色哈希的三个当前 key 和角色 key set。
创建角色的 name 字段上的二级索引后,我们可以为 RoleRepository
添加一个 finder 方法
@Repository
public interface RoleRepository extends CrudRepository<Role, String> {
Role findFirstByName(String role);
}
在 src/main/java/com/redislabs/edu/redi2read/boot
目录下创建 CreateUsers.java
文件,内容如下
package com.redislabs.edu.redi2read.boot;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.redislabs.edu.redi2read.models.Role;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.RoleRepository;
import com.redislabs.edu.redi2read.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(2)
@Slf4j
public class CreateUsers implements CommandLineRunner {
@Autowired
private RoleRepository roleRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void run(String... args) throws Exception {
if (userRepository.count() == 0) {
// load the roles
Role admin = roleRepository.findFirstByname("admin");
Role customer = roleRepository.findFirstByname("customer");
try {
// create a Jackson object mapper
ObjectMapper mapper = new ObjectMapper();
// create a type definition to convert the array of JSON into a List of Users
TypeReference<List<User>> typeReference = new TypeReference<List<User>>() {
};
// make the JSON data available as an input stream
InputStream inputStream = getClass().getResourceAsStream("/data/users/users.json");
// convert the JSON to objects
List<User> users = mapper.readValue(inputStream, typeReference);
users.stream().forEach((user) -> {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.addRole(customer);
userRepository.save(user);
});
log.info(">>>> " + users.size() + " Users Saved!");
} catch (IOException e) {
log.info(">>>> Unable to import users: " + e.getMessage());
}
User adminUser = new User();
adminUser.setName("Adminus Admistradore");
adminUser.setEmail("[email protected]");
adminUser.setPassword(passwordEncoder.encode("Reindeer Flotilla"));//
adminUser.addRole(admin);
userRepository.save(adminUser);
log.info(">>>> Loaded User Data and Created users...");
}
}
}
让我们分解一下
@Autowired
注解注入 RoleRepository
、UserRepository
和 BCryptPasswordEncoder
。CreateRoles
CommandLineRunner
一样,只有在数据库中没有用户时才执行逻辑。findFirstByName
加载 admin 和 customer 角色。Class
对象的 getResourceAsStream
方法,我们从 resources 目录加载 JSON 文件ObjectMapper
将传入的输入流转换为 List
的 User
对象应用程序重启后,现在应该看到
2021-04-03 10:05:04.222 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.Redi2readApplication : Started Redi2readApplication in 2.192 seconds (JVM running for 2.584)
2021-04-03 10:05:04.539 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.boot.CreateRoles : >>>> Created admin and customer roles...
2021-04-03 10:06:27.292 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.boot.CreateUsers : >>>> 1000 Users Saved!
2021-04-03 10:06:27.373 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.boot.CreateUsers : >>>> Loaded User Data and Created users...
如果你在 MONITOR 模式下观察 Redis CLI,你可能会看到我们刚刚创建的 1001 个用户执行了一连串 Redis 命令。让我们使用 CLI 探索数据
127.0.0.1:6379> KEYS "com.redislabs.edu.redi2read.models.User"
1) "com.redislabs.edu.redi2read.models.User"
127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.User"
set
127.0.0.1:6379> SCARD "com.redislabs.edu.redi2read.models.User"
(integer) 1001
127.0.0.1:6379> SRANDMEMBER "com.redislabs.edu.redi2read.models.User"
"-1848761758049653394"
127.0.0.1:6379> HGETALL "com.redislabs.edu.redi2read.models.User:-1848761758049653394"
1) "id"
2) "-1848761758049653394"
3) "_class"
4) "com.redislabs.edu.redi2read.models.User"
5) "roles.[0]"
6) "com.redislabs.edu.redi2read.models.Role:a9f9609f-c173-4f48-a82d-ca88b0d62d0b"
7) "name"
8) "Janice Garza"
9) "email"
10) "[email protected]"
11) "password"
12) "$2a$10$/UHTESWIqcl6HZmGpWSUHexNymIgM7rzOsWc4tcgqh6W5OVO4O46."
现在我们有一个 Redis Set,其中包含存储用户实例的 Redis Hash 的用户 key 集合。我们使用 SCARD 命令获取 set 的基数(1001,来自 JSON 的 1000 个用户加上 admin 用户)。使用 SRANDMEMBER 命令,我们可以从 Set
中随机抽取一个成员。然后,我们使用该成员和 User
Hashes 前缀来检索随机用户哈希的数据。有几点需要指出:
roles.[0]、roles.[1]
等)存储,其值为给定角色的 key。这是使用 @Reference
注解 Java Set of Role 的结果现在我们已经有了 User
和 Role
,接下来创建一个 UserController
来暴露一些用户管理功能。
package com.redislabs.edu.redi2read.controllers;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping
public Iterable<User> all() {
return userRepository.findAll();
}
}
现在我们可以发出 GET 请求来检索所有用户
$ curl --location --request GET 'http://localhost:8080/api/users/'
输出应该是一个 JSON 对象数组,例如
[
{
"id": "-1180251602608130769",
"name": "Denise Powell",
"email": "[email protected]",
"password": "$2a$10$pMJjQ2bFAUGlBTX9cHsx/uGrbbl3JZmmiR.vG5xaVwQodQyLaj52a",
"passwordConfirm": null,
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
},
...
]
为了成为“优秀的 RESTful 公民”,我们应该在返回时过滤掉 password
和 passwordConfirm
字段。为此,我们利用 Jackson 是 Spring Web 中默认序列化器的事实,这意味着我们可以使用 @JsonIgnoreProperties
注解 User
类,只允许 setters(以便我们可以加载数据),但在序列化期间隐藏 getters,如下所示:
@JsonIgnoreProperties(value = { "password", "passwordConfirm" }, allowSetters = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
@Data
@RedisHash
public class User {
...
}
以及导入语句
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
再次发出请求应该会反映 JSON 响应中的更改
[
{
"id": "-1180251602608130769",
"name": "Denise Powell",
"email": "[email protected]",
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
},
...
]
让我们为 UserController
再添加一个方法。我们将添加按电子邮件地址检索用户的功能,这将利用 User 对象中电子邮件字段上的二级索引。我们将其实现为控制器 GET 根端点上的一个过滤器
@GetMapping
public Iterable<User> all(@RequestParam(defaultValue = "") String email) {
if (email.isEmpty()) {
return userRepository.findAll();
} else {
Optional<User> user = Optional.ofNullable(userRepository.findFirstByEmail(email));
return user.isPresent() ? List.of(user.get()) : Collections.emptyList();
}
}
我们使用一个请求参数来接收 email,如果存在,则调用 findFirstByEmail
finder 方法。我们将结果包装在列表中以匹配方法的返回类型。我们使用 Optional 来处理 finder 返回 null 的情况。别忘了导入所需的包
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.springframework.web.bind.annotation.RequestParam;
使用 curl 调用该端点
curl --location --request GET 'http://localhost:8080/api/users/[email protected]'
返回预期结果
[
{
"id": "-1266125356844480724",
"name": "Donald Gibson",
"email": "[email protected]",
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
}
]