为了账号安全,请及时绑定邮箱和手机立即绑定

如何在Spring中验证请求及自定义验证器的创建指南

标签:
Java Spring

大家好👋,今天我想展示给大家如何在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 在错误发生时的响应方式,但我们将在另一篇文章中再讨论这个话题!

希望你喜欢这篇文章,也能学到一些新东西。

分享并关注我们,获取更多资讯 🙌!

代码库链接

另外,你可以在仓库里找到其他的自定义验证,去检查一下。

栈学 🎓

感谢你一路读到最后,在你离开前:

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消