大家好👋,今天我想展示给大家如何在Spring Boot里验证请求,以及如何创建最棒的自定义验证器😎!
我们将从上一篇文章中继续,这篇文章是关于Spring boot的,我们将从上文我们留下的地方继续,我们在那里创建了一个简单的CRUD应用,但这并不表示你不能把这些内容应用到其他项目中。
简介 先决条件- Java开发工具包(JDK)
- 对REST API和Java的基本了解
- 拥有MySQL数据库的安装
- 你最喜欢的集成开发工具(推荐使用IntelliJ IDEA)
- 对Spring Boot的基本了解,如果你不了解,可以参考这篇文章文章
验证用于确保我们接收到的数据是正确的,以避免意外行为或错误的发生。这些验证通常在控制器层执行任何操作之前进行。
必要的概念DTO(数据传输对象)是一种用来与其他系统进行通信的设计模式,在接收和发送数据时具有灵活性,从而可以减少请求次数,减少数据传输的量,或避免泄露敏感信息。
原文链接:从https://velog.io/@kkd04250/DTOData-Transfer-Object(数据传输对象)
开发中 安装依赖项我们需要一个新的依赖来处理验证过程,Spring 提供了一个这样的库,可以轻松安装且无需配置!
Gradle
implementation("org.springframework.boot:spring-boot-starter-validation") // 引入 Spring Boot 的验证启动器
Maven 构建工具
<依赖>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</依赖>
有了这个依赖,我们就可以开始实施我们的验证逻辑了。
创建DTO对象在大多数项目里,通常会有一个名为DTOs的文件夹,包含所有的DTO,但我不是很喜欢这样的方式,因为对于某个模型或API,请求和响应有时会不同,因此我会把它们分成两到三个文件夹,例如Request DTOs、Response DTOs等。
- 响应
- 请求
- 数据传输对象(DTO):例如,这里指的是那些在请求和响应中相同的DTO。
说完了,接下来看看我们新的 DTO 请求,用于用户控制器。
public class 用户更新请求 {
@NotNull
@NotEmpty
private String name;
@NotNull
@NotEmpty
private String lastname;
@NotNull
@Min(value = 18)
private Integer 年龄;
}
public class 存储用户请求 继承自 用户更新请求 {
@NotNull
@NotEmpty
private String username;
// 新增的字段,也请在模型中添加
}
正如你所见,它们与我们的模型非常相似,但有两点主要的不同:一是我们不允许插入ID字段,二是我们添加了大量的注解。这些注解告诉Spring需要进行哪些验证。
你可能会问为什么需要两个DTO?这是因为当你创建一个模型的时候,可能插入一些数据,这些数据在之后是无法更新的,例如你在某些平台上注册时插入的用户名,一旦创建了用户,你就无法更改用户名了。DTO通常翻译为“数据传输对象”。
我们现在需要切换到使用新的DTO,我将只展示将要改变的方法。请注意,DTO是指数据传输对象(Data Transfer Object)。
@RestController()
@RequestMapping("/users")
public class UserController {
/**
* 存储用户信息
* @param userRequest 存储用户请求
* @return 用户
*/
@PostMapping
public User store(@RequestBody @Valid StoreUserRequest userRequest) {
return userService.store(userRequest);
}
/**
* 根据ID更新用户信息
* @param id 用户ID
* @param userRequest 更新用户请求
* @return 用户
*/
@PutMapping("/{id}")
public User update(@PathVariable Long id, @RequestBody @Valid UpdateUserRequest userRequest) {
return userService.update(id, userRequest);
}
}
可以看到,我们将 User 更改为 UserRequest,并增加了一个名为 Valid 的新注解,这告诉 Spring 在执行方法前需要先验证这些对象是否有效。
现在我们需要升级我们的服务,来支持这个新的东西。
@Service
public class 用户服务类 {
public 用户 store(StoreUserRequest request) {
var user = 转换请求(request, new 用户());
user.setUsername(request.getUsername());
return userRepository.save(user);
}
private 用户 转换请求(UpdateUserRequest request, 用户 user) {
user.setName(request.getName());
user.setLastname(request.getLastname());
user.setAge(request.getAge());
user.setRole(roleService.获取角色(request.getRoleId()));
return user;
}
public 用户 update(Long id, UpdateUserRequest request) {
var user = show(id);
return userRepository.save(转换请求(request, user));
}
}
如你所见,我们接收 DTO,然后创建一个 User 对象并用所有数据将其填充起来。对于更新方法的过程,找到后,我们更新数据并保存。目前这些操作都非常简单。
自定义验证注释现在,想象一下你需要在创建用户名之前验证它是唯一的,而用当前的验证方法是无法做到的。因此,我们将创建一个自定义验证注解来帮助我们验证用户名是否唯一。
为此目的,我们需要创建一个标注和一个类,我会把这些东西放到 validations 文件夹里。
@文档化
@约束(validatedBy = UniqueValidator.class)
@目标( { ElementType.METHOD, ElementType.FIELD })
@保留(RetentionPolicy.RUNTIME)
public @interface 唯一约束:确保字段或方法的值是唯一的 {
String message() default "该值已存在,请勿重复输入";
String 检查方法();
Class<?> 存储库();
Class<?>[] 组() default {};
Class<? extends Payload>[] 负载() default {};
}
这是我们将使用的注解,名为 Constrain 的注解来自于 Jakarta(或旧版本中的 javax),这个注解允许我们定义哪些类将用于验证我们的新约束。
Target 注解定义了此注解可以在哪些地方被使用,而 Retention 注解则非常重要,因为它指定了需要在运行时保留此注解,并且 RetentionPolicy.RUNTIME 指定了 Java 需要在运行时保留此注解。
在注释中我们会找到 5 个变量,我会列出它们
- message: 这是一个必要的字段,当验证出错时会返回这个消息
- method: 我们用它来获取进行验证所需的方法
- repository: 我们会在这里存放要用的仓库类
- groups, payload: groups 和 payload是我们不会使用的其他必要字段
现在,我们可以看到约束条件了
public class UniqueValidator implements ConstraintValidator<UniqueConstraint, Object> {
// 这是一个Spring类,帮助你获取bean
private final ApplicationContext applicationContext;
private String method;
private Class<?> repository;
// Spring会注入你需要的任何内容到这个类中
public UniqueValidator(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void initialize(UniqueConstraint constraintAnnotation) {
// 我们从注解中的数据获取所需的数据
this.method = constraintAnnotation.method();
this.repository = constraintAnnotation.repository();
}
// 这里你可以放置任何你想要实现的逻辑
// 参数需要是我们将接收的类型,在这里我不限制我可以接收什么
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
// 如果值为null,我们返回true,
// 这是因为这个验证逻辑不需要检查null值
if (value == null) {
return true;
}
try {
// 我们获取repository bean
Object instance = applicationContext.getBean(repository);
// 我们在repository类中查找这个方法
Method callable = ClassUtils.getMethod(repository, method, null);
// 我们调用这个方法
Object result = callable.invoke(instance, value);
if (result instanceof Optional<?> el) {
return el.isEmpty();
}
if (result instanceof Boolean exists) {
return !exists;
}
return result == null;
} catch (Exception e) {
// 视情况而定,我们可以记录异常或抛出自定义异常
throw new RuntimeException("UniqueValidator 中发生了错误", e);
}
}
}
你看, 并不太大, 很简单啦 😎, 我加了一些注释, 这会让大家更好地理解发生的事情和为什么。
经过这一切,我们需要使用这些标注,并为此需要如下编辑StoreUserRequest。
public class StoreUserRequest extends UpdateUserRequest {
@NotNull
@NotEmpty
@UniqueConstraint(method = "findByUsername", repository = UserRepository.class)
private String username; // 用户名字段,不能为空且必须唯一。
}
就像你看到的那样,使用我们新的注解非常简单易懂,但有一个问题出现了,我们在仓库里找不到findByUsername
这个方法,确实如此,我们需要这样来实现它。
public interface 用户Repository extends JpaRepository<User, Long> {
Optional<User> 通过用户名查找用户(String username);
}
就这样,我们已经完成了新的注释 🙌!比如,尝试创建两个具有相同用户名的用户,你会看到一个类似的错误提示,哦oho。
{
"timestamp": "2024-11-06T22:04:54.081+00:00",
"status": 400,
"error": "无效请求",
"path": "路径"
}
我们的验证正在运行,但为什么这么平淡呢?Spring 默认有这样的响应类型,但这不会显示验证信息,我们只能在控制台看到这些信息,我们怎样才能显示这些信息呢?
在此情况下,我们可以通过使用 ControllerAdvice
来改变 Spring 在错误发生时的响应方式,但我们将在另一篇文章中再讨论这个话题!
希望你喜欢这篇文章,也能学到一些新东西。
分享并关注我们,获取更多资讯 🙌!
另外,你可以在仓库里找到其他的自定义验证,去检查一下。
栈学 🎓感谢你一路读到最后,在你离开前:
- 请给作者鼓掌👏 并关注他!
- 关注我们 X | LinkedIn | YouTube | Discord | Newsletter | Podcast
- 在Differ上免费创建一个AI驱动的博客吧。
- 更多内容请看 Stackademic.com
共同学习,写下你的评论
评论加载中...
作者其他优质文章