完成用户角色域的创建,加载和转换 JSON 数据,并开始构建 Redi2Read API。
在本课中,您将学习
如果你卡住了
现在我们已经创建了角色,让我们从提供的 JSON 数据中加载 用户
,这些数据位于 src/main/resources/data/users/users.json
中。该文件包含如下所示的 JSON 用户对象的数组
{
"password": "9yNvIO4GLBdboI",
"name": "Georgia Spencer",
"id": -5035019007718357598,
"email": "georgia.spencer@example.com"
}
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
模型的电子邮件字段上创建的索引。Spring 存储库将在运行时提供查找方法的实现。当我们处理应用程序的安全问题时,我们将使用此查找器。
让我们在 boot 包中创建另一个 CommandLineRunner
来加载用户。我们将遵循与 Roles 相似的配方,但我们将从磁盘加载 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;
现在,当创建一个新的 Role
实例时,其 ID 为 "abc-123"
,角色为 "superuser"
,Spring Data Redis 将执行以下操作
"按名称"
索引:创建为一个 Redis 集合,其键为 com.redislabs.edu.redi2read.models.Role:name:superuser
,包含一个条目;已索引对象的 id 为 "abc-123"
Role
"superuser" 的索引列表:创建一个 Redis 集合,其键为 "com.redislabs.edu.redi2read.models.Role:abc-123:idx"
,包含一个条目;索引的键为 "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 命令接受一个或多个键。我们将传递 Role 哈希的三个当前键和 Role 键集。
在角色名称上创建辅助索引后,我们可以向 RoleRepository
添加查找方法
@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("admin@example.com");
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
加载管理员和客户角色。ObjectMapper
和一个 TypeReference
,它们将充当将 JSON 数据序列化为 Java 对象的配方。getResourceAsStream
方法(来自 Class
对象),我们从资源目录加载 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) "janice.garza@example.com"
11) "password"
12) "$2a$10$/UHTESWIqcl6HZmGpWSUHexNymIgM7rzOsWc4tcgqh6W5OVO4O46."
现在,我们有一个 Redis 集合,它保存了包含用户实例的 Redis Hashes 的用户键集合。我们使用 SCARD 命令来获取集合的基数(1001,来自 JSON 的 1000 个用户加上管理员用户)。使用 SRANDMEMBER 命令,我们可以从 Set
中提取一个随机成员。然后,我们使用它和 User
Hashes 前缀来检索随机 User hash 的数据。需要注意以下几点:
roles.[0], roles.[1]
等)进行存储,值是给定角色的键。这是使用 @Reference
注解 Java 的 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 'https://localhost:8080/api/users/'
输出应该是一个 JSON 对象数组,如下所示:
[
{
"id": "-1180251602608130769",
"name": "Denise Powell",
"email": "denise.powell@example.com",
"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
类,只允许设置器(这样我们就可以加载数据),但在序列化期间隐藏 getter,如下所示:
@JsonIgnoreProperties(value = { "password", "passwordConfirm" }, allowSetters = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
@Data
@RedisHash
public class User {
...
}
使用 import 语句:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
再次发出请求应该反映 JSON 响应中的更改。
[
{
"id": "-1180251602608130769",
"name": "Denise Powell",
"email": "denise.powell@example.com",
"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();
}
}
我们使用请求参数来获取电子邮件,如果它存在,我们将调用 findFirstByEmail
查找器。我们将结果包装在一个列表中以匹配方法的结果类型。我们使用 Optional 来处理查找器返回的 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 'https://localhost:8080/api/users/?email=donald.gibson@example.com'
返回预期的结果。
[
{
"id": "-1266125356844480724",
"name": "Donald Gibson",
"email": "donald.gibson@example.com",
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
}
]