从零开始 Spring Boot 30:数据校验

news2024/11/20 21:29:21

从零开始 Spring Boot 30:数据校验

spring boot

图源:简书 (jianshu.com)

在从零开始 Spring Boot 13:参数校验 - 红茶的个人站点 (icexmoon.cn)一文中,我讨论了一些可以用于参数校验的注解。实际上这些注解都是来自于Jakarta Bean Validation的Java数据验证体系的一部分。关于Bean Validation在Spring中的应用,还可以进行更进一步的探索,这将是本文接下来的内容。

关于Jakarta Bean Validation的更多介绍,可以参考Jakarta Bean Validation - Home。

将验证移入Service层

我们之前讨论的都是怎么在Controller层对入参进行验证,比如下面的例子:

@Data
public class UserDTO {
    @NotBlank
    private String name;
    @NotBlank
    private String password;
    @NotBlank
    private String phone;
    @Min(1)
    private Integer age;
}

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/add")
    public String addUser(@Validated @RequestBody UserDTO user) {
        userService.addUser(user);
        return Result.success().toString();
    }
}

通常来说这样做是最正确的,因为可以将错误输入排除在所有的服务端处理逻辑之外。但是某些时候我们也可能希望将验证行为移动到Service层,这样做的理由可能是并非所有输入数据都来自Web接口的请求,可能有一些其他方式的输入动作。此时为了能充分复用数据验证逻辑,我们可以将数据验证逻辑移动到Service层:

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/add")
    public String addUser(@RequestBody UserDTO user) {
        userService.addUser(user);
        return Result.success().toString();
    }
}

public interface UserService {
    void addUser(UserDTO userDTO);
}

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private Validator validator;

    public void addUser(UserDTO userDTO) {
        Set<ConstraintViolation<UserDTO>> violations = validator.validate(userDTO);
        if (!violations.isEmpty()) {
            //没有通过验证
            StringBuilder sb = new StringBuilder();
            for (ConstraintViolation<UserDTO> constraintViolation : violations) {
                sb.append(constraintViolation.getMessage());
            }
            throw new ConstraintViolationException("没有通过验证:" + sb.toString(), violations);
        }
        //通过验证
        //这里可以添加向持久层添加用户的操作
    }
}

注意,这里在Controller中,addUser方法的user参数前没有注解@Validated,所以不会在Controller层触发验证逻辑。

UserServiceImpl中,注入了一个Validator。这是Spring默认的用于验证的对象。

也可以自己配置一个LocalValidatorFactoryBean类型的bean,具体可以参考核心技术 (springdoc.cn)。

需要注意的是,这里使用的Validatorjakarta.validation.Validator,而非Spring的同名类。

通过Validator.validate方法可以利用相应的校验用注解进行验证,如果没有通过验证,会返回一个Set<ConstraintViolation<?>>类型的对象,其中包含了所有验证出错信息。

当然这里也可以注入Spring的Validator进行验证,具体的验证写法有所不同,相应的示例可以看后边的自定义ValidatorDataBinder一节。

自定义校验注解

可以自定义类似Bean Validation中那样的数据校验注解。

这里同样以上面的User类的验证为例。

先定义一个验证User类的注解:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UserConstraintValidator.class)
public @interface UserConstraint {
    //这里可以根据需要添加一些属性用于丰富验证手段
    Pattern value() default Pattern.CHECK_ALL;
    String message() default "用户信息有错,无法通过验证";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    enum Pattern {
        CHECK_ALL, ONLY_NAME
    }
}

要注意的是,自定义注解中必须包含以下三个属性:

  • message,包含验证出错后的提示信息。
  • groups
  • payload

如果缺少这三个属性,就会报错。

在这个示例中,我增加了一个value属性,用于指定一个检查模式,CHECK_ALL是检查UserDTO的所有属性,ONLY_NAME是仅检查UserDTOname属性。

创建一个类,并实现ConstraintValidator<A extends Annotation, T>接口:

public class UserConstraintValidator implements ConstraintValidator<UserConstraint, UserDTO> {
    private UserConstraint.Pattern pattern;
    @Autowired
    private UserService userService;

    @Override
    public void initialize(UserConstraint constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        pattern = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(UserDTO userDTO, ConstraintValidatorContext constraintValidatorContext) {
        if (pattern == UserConstraint.Pattern.CHECK_ALL) {
            if (userDTO.getAge() == null || userDTO.getAge() <= 0) {
                return false;
            }
            if (userDTO.getName() == null || userDTO.getName().isEmpty()) {
                return false;
            }
            if (userDTO.getPassword() == null || userDTO.getPassword().isEmpty()) {
                return false;
            }
            if (userDTO.getPhone() == null || userDTO.getPhone().isEmpty()) {
                return false;
            }
        }
        if (pattern == UserConstraint.Pattern.ONLY_NAME) {
            if (userDTO.getName() == null || userDTO.getName().isEmpty()) {
                return false;
            }
        } else {
            ;
        }
        return true;
    }
}

因为Spring会使用SpringConstraintValidatorFactory创建ConstraintValidator实例,也就是说自定义的ConstraintValidator也是以bean的方式被注入,因此可以在自定义的ConstraintValidator类中使用@Autowired进行依赖注入。

当然这里注入的userService实际上并没有任何用途,只是为了说明可以进行注入。

现在就可以像其他bean validation注解那样使用自定义注解了,比如:

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/add")
    public String addUser(@UserConstraint @RequestBody UserDTO user) {
        userService.addUser(user);
        return Result.success().toString();
    }
}

这里的@UserConstraint@Min之类的注解用法类似,只不过实现的是自定义的验证逻辑。

自定义Validator

我们也可以像之前介绍的ConverterFormatter那样,简单实现SpringValidator接口,并在Controller中注册以使用:

public class UserValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return UserDTO.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        ValidationUtils.rejectIfEmpty(errors, "password", "password.empty");
        ValidationUtils.rejectIfEmpty(errors, "phone", "phone.empty");
        UserDTO user = (UserDTO) target;
        if (user.getAge() < 0 || user.getAge() > 150) {
            errors.rejectValue("age", "too.low.or.too.big");
        }
    }
}

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/add")
    public String addUser(@Validated @RequestBody UserDTO user) {
//        userService.addUser(user);
        return Result.success().toString();
    }

    @InitBinder
    public void initBinder(WebDataBinder webDataBinder){
        webDataBinder.addValidators(new UserValidator());
    }
}

现在即使注释掉UserDTO中的相应注解,也可以正常进行验证:

@Data
public class UserDTO {
//    @NotBlank
    private String name;
//    @NotBlank
    private String password;
//    @NotBlank
    private String phone;
//    @Min(1)
    private Integer age;
}

DataBinder

Validator结合DataBinder可以在任何地方进行验证,比如之前说的将验证移入Service层,如果要使用自定义的Spring Validator,可以这样写:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private org.springframework.validation.Validator springValidator;
	// ...
    @Override
    public void addUserWithSpringValidator(UserDTO userDTO) {
        DataBinder dataBinder = new DataBinder(userDTO);
        dataBinder.addValidators(springValidator, new UserValidator());
        dataBinder.validate();
        BindingResult bindingResult = dataBinder.getBindingResult();
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        if (!allErrors.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (ObjectError error : allErrors) {
                String objectName = error.getObjectName();
                if (error instanceof FieldError){
                    FieldError fieldError = (FieldError)error;
                    objectName = fieldError.getField();
                }
                String errorMsg = error.getDefaultMessage();
                if (ObjectUtils.isEmpty(errorMsg)) {
                    errorMsg = error.getCode();
                }
                sb.append(objectName).append(" ").append(errorMsg);
                sb.append(",");
            }
            throw new ValidationException(sb.toString());
        }
        //通过验证
        //这里可以添加向持久层添加用户的操作
    }
}

这里通过DataBinder.addValidators()方法将Spring默认的Validator与自定义的Validator都添加了进去,这样,在调用DataBinder.validate()方法时,就可以让UserDTO上使用的bean validation注解和自定义的UserValidator相应的验证逻辑都生效。

Controller层:

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
	// ...
    @PostMapping("/add/springValidator")
    public String addUserWithSpringValidator(@RequestBody UserDTO user){
        userService.addUserWithSpringValidator(user);
        return Result.success().toString();
    }
}

The End,谢谢阅读。

本文所有的示例代码可以通过ch30/validator · 魔芋红茶/learn_spring_boot - 码云 - 开源中国 (gitee.com)获取。

参考资料

  • MethodValidationPostProcessor (Spring Framework 6.0.8-SNAPSHOT API)
  • 自定义校验注解ConstraintValidator - 陈皮的JavaLib - 博客园 (cnblogs.com)
  • Spring Validation in the Service Layer | Baeldung — 服务层中的Spring验证|白洞
  • 从零开始 Spring Boot 13:参数校验 - 红茶的个人站点 (icexmoon.cn)
  • Jakarta Bean Validation - Home
  • 核心技术 (springdoc.cn)
  • 聊聊Spring中的数据绑定 — DataBinder本尊(源码分析)【享学Spring】_YourBatman的博客-CSDN博客
  • SpringMVC常见组件之DataBinder数据绑定器分析_流烟默的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/540991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第7章异常、断言和曰志

Java和C异 在C中&#xff0c;throw说明符在运行时执行。Java在编译时执行。 处理错误 异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器。 如果由于出现错误而使得某些操作没有完成&#xff0c;程序应该&#xff1a;返回到一种安全状态&#…

ChatGLM + PEFT 进行finetune

一、前言 1.1 硬件需求 注&#xff1a;r 为LoRA 维数大小&#xff0c;p 为前缀词表大小&#xff0c;l 为微调层数&#xff0c;ex/s 为每秒训练的样本数。gradient_accumulation_steps 参数设置为 1。上述结果均来自于单个 Tesla V100 GPU&#xff0c;仅供参考。 1.2 微调方法…

开放原子训练营第一季——铜锁探“密” 圆满落幕!

【开放原子训练营第一季结营总结】——铜锁探“密” 开放原子训练营第一季「铜锁探密」由开放原子开源基金会&铜锁社区共同举办&#xff0c;包含 5 次课程&#xff0c;以“抽丝剥茧&#xff0c;循序渐进&#xff0c;一起揭开商用密码的面纱”为主题&#xff0c;让参与者更加…

c++面向对象之类

一、类的定义 class 类名{成员属性构造函数析构函数成员函数 }Person.h #include <string> #include <iostream> using namespace std;class Person {int m_age;string m_name;Person();Person(int age,string name);~Person();int getAge();void setAge(int age…

蓝鲸平台通过标准运维 API 安装 Agent

目录 一、背景 二、目的 三、创建安装agent流程 四、通过标准运维 API 安装 Agent 五、总结 一、背景 蓝鲸平台正常情况纳管主机需要在节点管理手工安装agent&#xff0c;不能达到完成自动化安装agent的效果。想通过脚本一键安装agent&#xff0c;而不需要在蓝鲸平台进行过…

Golang指针的操作以及常用的指针函数

目录 指针的操作 定义指针 获取变量地址 解引用指针 指针作为函数参数 指针的空值 常用的指针函数 new 函数 make 函数 append 函数 copy 函数 指针的操作 在Go语言中&#xff0c;指针是一种非常重要的类型&#xff0c;可以用来传递变量的地址而不是变量本身。 定…

【leetcode刷题总结】——代码随想录(链表总结)

代码随想录按照数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构&#xff0c;再从简单刷起&#xff0c;做了几个类型题目之后&#xff0c;再慢慢做中等题目、困难题目。 以下是个人刷题总结&#xff0c;官…

多个Node.js版本之间切换

本篇文章会讲windows和Mac系统下实现多个node.js版本之间的切换。 1.windows下采用nvm&#xff08;nvm-window&#xff09; 2.Mac下采用nvm和n 注&#xff1a;window和mac下的nvm地址是不一样的 一、windows系统 什么是nvm&#xff1f; nvm是一个简单的bash脚本&#xff…

真题详解(有限自动机)-软件设计(七十七)

确定有限自动机 和 不确定有限自动机 解析&#xff1a; M1的A当0的时候&#xff0c;会变成A&#xff0c;也可能变成B&#xff0c; 所以M1是不确定有限自动机。 M2的A1的时候只会使A&#xff0c;0的时候只会是B B的0只会是B&#xff0c;1的时候只会是C。 C0的时候只会是B&a…

中小型企业需要“数据防泄露”吗?

数据防泄露是指企业采取的各种管理、技术与监督措施&#xff0c;以防止敏感和关键数据在传输、存储与使用过程中被未经授权访问或窃取&#xff0c;从而导致机密性、完整性或可用性受到损害。 数据防泄露工作通常包括数据分类与风险评估、权限管理与访问控制、加密与安全技术、数…

基于Gabor-小波滤波深度图表面法线的特征提取算法【通过正常Gabor-小波的直方图进行2D或3D特征提取】研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Bean的生命周期揭秘:从诞生到消亡,一个对象的壮丽演绎!

大家好&#xff0c;我是你们的小米。今天我要给大家揭秘一下Java开发中重要的概念——Spring Bean的生命周期。作为Java开发者&#xff0c;无论是在面试还是实际工作中&#xff0c;了解Bean的生命周期都是必备的知识点。让我们一起来深入了解吧&#xff01; 什么是Spring Bean …

【zabbix】PostgreSQL表信息查询收集监控

昨天开发那边给了一个需求&#xff0c;每隔一段时间查询一下数据库某表中的数据是否在最近更新&#xff0c;让我这边做一个监控信息收集 一、agent linux侧配置 这边目前就直接在zabbix上实现&#xff0c;首先就是脚本&#xff0c;我用python2实现 脚本名&#xff1a;check_y…

软件测试学什么——如何才能按时上线?

目录 引言 一、提前介入测试&#xff0c;认真做好需求分析。 二、测试计划没必要花太多精力。 三、测试用例编写 四、测试执行的关键点 五、测试环境 【一套系统提升学习的好资料】 阅读书籍文档 总结 引言 上线&#xff0c;永远是软件测试工程师最关注的问题。 上线…

Linux文件系统学习笔记

笔记 修改文件属性与权限数字修改文件权限符号修改文件权限 目录与文件的意义文件的权限目录的权限 路径 修改文件属性与权限 chgrp:修改用户组 chown:修改拥有者 chmod:修改用户权限 其中修改权限:chmod可以用数字修改或者符号修改 数字修改文件权限 一共有三种身份:拥有者…

freeswitch的2833和inband对接方案

概述 freeswitch支持三种模式的DTMF传输方式&#xff0c;分别时inband、INFO、2833。 在传统的PSTN网络中&#xff0c;所有的DTMF码都是inband模式&#xff0c;所以VOIP网络和PSTN网络对接中&#xff0c;需要将DTMF码做格式转换&#xff0c;通常是2833和inband之间的转换。 …

《思考致富》制造借口是一个根深蒂固的习惯

目录 经典摘录 领导方式有两种 拖拉 决心&#xff1a;克服拖拉的坏习惯&#xff08;通往致富之路的第七步&#xff09; 毅力&#xff1a;催生信念的持久努力&#xff08;通往致富之路的第八步&#xff09; 智囊团的力量&#xff1a;致富驱动力&#xff08;通往致富之路的第…

【分享】你用免费的GPT4了嘛?

哈喽&#xff0c;大家好&#xff0c;我是木易巷~ 现在OpenAI的ChatGPT4需要升级为plus会员使用&#xff0c;对于国内的用户来说&#xff0c;不仅需要魔法&#xff0c;还需要有一张外网的信用卡来开通会员&#xff0c;真的麻烦。 最近木易巷发现了一个项目&#xff0c;是GitHu…

基于欧式距离的聚类算法的Kmeans作业

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 基于欧式距离的聚类算法&#xff0c;其认为两个目标的距离越近&#xff0c;相似度越大。 该实验产生的点为二维空间中的点。 环境配置 java环境&#xff0c;使用原生的Java UI组件JPanel和JFrame 算法原理 基于欧式距离的聚…

问卷调查类型全面解析

调查问卷是从人们那里收集反馈和意见的强大工具。通过提出结构化问题&#xff0c;研究人员、组织和企业可以获得大量有关如何改进产品或服务、解决问题或做出明智决策的信息。但在设计调查问卷之前&#xff0c;必须首先了解不同的类型以及何时使用它们。 1、结构化问卷 结构化…