【SpringBoot】SpringBoot 优雅地校验参数

这篇具有很好参考价值的文章主要介绍了【SpringBoot】SpringBoot 优雅地校验参数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、为什么要校验参数?

在日常的开发中,为了防止非法参数对业务造成影响,需要对接口的参数进行校验,以便正确性地入库。
例如:登录时,就需要判断用户名、密码等信息是否为空。虽然前端也有校验,但为了接口的安全性,后端接口还是有必要进行参数校验的。

同时,为了校验参数更加优雅,这里就介绍了 Spring Validation 方式。

Java API 规范(JSR303:JAVA EE 6 中的一项子规范,叫做 Bean Validation)定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解。如:@Email、@Length。

JSR 官网

Hibernate Validator 官网

Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

2、引入依赖

如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于等于 2.3.x,则需要手动引入依赖。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.0.Final</version>
</dependency>

对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  • POST、PUT 请求,使用 @requestBody 接收参数
  • GET 请求,使用 @requestParam、@PathVariable 接收参数

接下来就简单介绍下吧~~~


3、@requestBody 参数校验

对于 POST、PUT 请求,后端一般会使用 @requestBody + 对象 接收参数。此时,只需要给对象添加 @Validated 或 @Valid 注解,即可轻松实现自动校验参数。如果校验失败,会抛出 MethodArgumentNotValidException 异常。

UserVo :添加校验注解

@Data
public class UserVo {

	private Long id;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

UserController :

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid UserVo userVo) {
        return "addUser";
    }
}

或者使用 @Validated 注解:

@PostMapping("/addUser")
public String addUser(@RequestBody @Validated UserVo userVo) {
    return "addUser";
}

4、@requestParam、@PathVariable 参数校验

GET 请求一般会使用 @requestParam、@PathVariable 注解接收参数。如果参数比较多(比如超过 5 个),还是推荐使用对象接收。否则,推荐将一个个参数平铺到方法入参中。

在这种情况下,必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如:@Min )。如果校验失败,会抛出 ConstraintViolationException 异常

@RestController
@RequestMapping("/user")
@Validated
public class UserController {

    @GetMapping("/getUser")
    public String getUser(@Min(1L) Long id) {
        return "getUser";
    }
}

5、统一异常处理

如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示

@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return "参数校验失败" + msg;
    }

    @ExceptionHandler({ConstraintViolationException.class})
    public String handleConstraintViolationException(ConstraintViolationException ex) {
        return "参数校验失败" + ex;
    }

}

6、分组校验

在实际项目中,可能多个方法需要使用同一个类对象来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。

如:保存 User 的时候,userId 是可空的,但是更新 User 的时候,userId 的值必须 >= 1L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

约束注解上声明适用的分组信息 groups

1:定义分组接口

public interface ValidGroup extends Default {
	// 添加操作
    interface Save extends ValidGroup {}
    // 更新操作
    interface Update extends ValidGroup {}
	// ...
}

为什么要继承 Default ?下文有。

2:给需要校验的字段分配分组

@Data
public class UserVo {
	
    @Null(groups = ValidGroup.Save.class, message = "id要为空")
    @NotNull(groups = ValidGroup.Update.class, message = "id不能为空")
    private Long id;

    @NotBlank(groups = ValidGroup.Save.class, message = "用户名不能为空")
    @Length(min = 2, max = 10)
    private String userName;

    @Email
    @NotNull
    private String email;
}

根据校验字段看:

  • id:分配分组:Save、Update。添加时,一定为 null;更新时,一定不为 null
  • userName:分配分组:Save。添加时,一定不能为空
  • email:分配分组:无。即:使用默认的分组

3:给需要校验的参数指定分组

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated(ValidGroup.Save.class) UserVo userVo) {
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @Validated(ValidGroup.Update.class) UserVo userVo) {
        return "updateUser";
    }
}

测试校验。

4:默认分组

如果 ValidGroup 接口 不继承 Default 接口,那么,将无法校验 email 字段(未分配分组);继承后,ValidGroup 就属于 Default 类型,即:默认分组/所以,可以对 email 校验

7、嵌套校验

必须要用 @Valid 注解

@Data
public class UserVo {

	@NotNull(groups = {ValidGroup.Save.class, ValidGroup.Update.class})
    @Valid
    private Address address;
}

8、自定义校验

案例一、自定义校验 加密id

假设我们自定义加密 id(由数字或者 a-f 的字母组成,32-256 长度)校验,主要分为两步:

1.自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class}) // 自定义验证器
public @interface EncryptId {

    // 默认错误消息
    String message() default "加密id格式错误";
    // 分组
    Class<?>[] groups() default {};
    // 负载
    Class<? extends Payload>[] payload() default {};
}

2.编写约束校验器

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

3.使用

@Data
public class UserVo {

    @EncryptId
    private String id;
}

案例二、自定义校验 性别只允许两个值

UserVo 类中的 sex 性别属性,只允许前端传递传 M,F 这2个枚举值,如何实现呢?

1.自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {SexValidator.class})
public @interface SexValid {


    // 默认错误消息
    String message() default "value not in enum values";

    // 分组
    Class<?>[] groups() default {};

    // 负载
    Class<? extends Payload>[] payload() default {};

    String[] value();
}

2.编写约束校验器

public class SexValidator implements ConstraintValidator<SexValid, String> {

    private List<String> sexs;

    @Override
    public void initialize(SexValid constraintAnnotation) {
        sexs = Arrays.asList(constraintAnnotation.value());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (StringUtils.isEmpty(value)) {
            return true;
        }
        return sexs.contains(value);
    }

}

3.使用

@Data
public class UserVo {

    @SexValid(value = {"F", "M"}, message = "性别只允许为F或M")
    private String sex;

}

4.测试

@GetMapping("/get")
private String get(@RequestBody @Validated UserVo userVo) {
    return "get";
}

9、实现校验业务规则

业务规则校验 指 接口需要满足某些特定的业务规则。举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。 这就要求在创建用户时需要校验用户名称、手机号码、邮箱是否被注册;编辑用户时不能将信息修改成已有用户的属性。

最优雅的实现方法应该是参考 Bean Validation 的标准方式,借助自定义校验注解完成业务规则校验。

1.自定义约束注解

首先我们需要创建两个自定义注解,用于业务规则校验:

  • UniqueUser:表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱
  • NotConflictUser:表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.UniqueUserValidator.class)
public @interface UniqueUser {

    String message() default "用户名、手机号码、邮箱不允许与现存用户重复";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.NotConflictUserValidator.class)
public @interface NotConflictUser {

    String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

2.编写约束校验器

想让自定义验证注解生效,需要实现 ConstraintValidator 接口。接口的第一个参数是 自定义注解类型,第二个参数是 被注解字段的类,因为需要校验多个参数,我们直接传入用户对象。 需要提到的一点是 ConstraintValidator 接口的实现类无需添加 @Component 它在启动的时候就已经被加载到容器中了。

public class UserValidator<T extends Annotation> implements ConstraintValidator<T, UserVo> {

    protected Predicate<UserVo> predicate = c -> true;

    @Override
    public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
        return predicate.test(userVo);
    }

    public static class UniqueUserValidator extends UserValidator<UniqueUser>{
        @Override
        public void initialize(UniqueUser uniqueUser) {
            UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
            predicate = c -> !userDao.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
        }
    }

    public static class NotConflictUserValidator extends UserValidator<NotConflictUser>{
        @Override
        public void initialize(NotConflictUser notConflictUser) {
            predicate = c -> {
                UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
                Collection<UserVo> collection = userDao.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
            };
        }
    }

}
@Component
public class ApplicationContextHolder implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
        return context;
    }

    public static Object getBean(String name) {
        return context != null ? context.getBean(name) : null;
    }

    public static <T> T getBean(Class<T> clz) {
        return context != null ? context.getBean(clz) : null;
    }

    public static <T> T getBean(String name, Class<T> clz) {
        return context != null ? context.getBean(name, clz) : null;
    }

    public static void addApplicationListenerBean(String listenerBeanName) {
        if (context != null) {
            ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
            applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
        }
    }

}

3.测试

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/addUser")
    public String addUser(@RequestBody @UniqueUser UserVo userVo) {
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @NotConflictUser UserVo userVo) {
        return "updateUser";
    }
}

10、@Valid 和 @Validated 的区别

区别如下:

【SpringBoot】SpringBoot 优雅地校验参数

11、常用注解

Bean Validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:

注解 详细信息
@Null 任意类型。被注释的元素必须为 null
@NotNull 任意类型。被注释的元素不为 null
@Min(value) 数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
@Max(value) 数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
@DecimalMin(value) 数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
@DecimalMax(value) 数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
@Size(max, min) 字符串、集合、Map、数组类型。被注释的元素的大小(长度)必须在指定的范围内
@Digits (integer, fraction) 数值类型、数值型字符串类型。其值必须在可接受的范围内。 integer:整数精度;fraction:小数精度
@Past 日期类型。被注释的元素必须是一个过去的日期
@Future 日期类型。被注释的元素必须是一个将来的日期
@Pattern(value) 字符串类型。被注释的元素必须符合指定的正则表达式

Hibernate Validator 在原有的基础上也内嵌了几个注解,如下:文章来源地址https://www.toymoban.com/news/detail-451955.html

注解 详细信息
@Email 字符串类型。被注释的元素必须是电子邮箱地址
@Length 字符串类型。被注释的字符串的长度必须在指定的范围内
@NotEmpty 字符串、集合、Map、数组类型。 被注释的元素的长度必须非空
@Range 数值类型、字符串类型。 被注释的元素必须在合适的范围内

到了这里,关于【SpringBoot】SpringBoot 优雅地校验参数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • (一)springboot实战——为什么是springboot?

    为什么是springboot?江湖夜雨,传说依旧,不懂springboot一技之长,如何混迹java圈,本节内容我们介绍一下spring的一些基本特性。尤其是springboot3的基本特性,使得我们更好的理解springboot3。 springboot的特点 -可以快速的创建一个独立的spring项目,取代老式的SSM项目 - 直接嵌入T

    2024年02月09日
    浏览(85)
  • 有了Spring为什么还需要SpringBoot呢

    目录 一、Spring缺点分析 二、什么是Spring Boot 三、Spring Boot的核心功能 3.1 起步依赖 3.2 自动装配 1. 配置文件和依赖太多了!!! spring是一个非常优秀的轻量级框架,以IOC(控制反转)和AOP(面向切面)为思想内核,极大简化了JAVA企业级项目的开发。虽然Spring的组件代码是轻

    2024年02月08日
    浏览(47)
  • 多进程运行含有任意参数的函数、为什么multiprosessing会进行多次初始化

    目录 多进程运行含有任意个参数的函数,以map_async为例 为什么multiprocessing 的了进程会多次初始化?         使用偏函数:偏函数有点像数学中的偏导数,可以让我们只关注其中的某一个变量而不考虑其他变量的影响。 如以下代码中,我们要将set_seq、tokenizer和model作为变量

    2024年02月03日
    浏览(55)
  • java八股文面试[多线程]——为什么要用线程池、线程池参数

     速记7个: 核心、最大 存活2 队列 工厂 拒绝 线程池处理流程: 线程池底层工作原理: 线程复用原理:   知识来源: 【并发与线程】为什么使用线程池,参数解释_哔哩哔哩_bilibili 【并发与线程】线程池处理流程_哔哩哔哩_bilibili 【并发与线程】线程池的底层工作原理_哔哩

    2024年02月11日
    浏览(49)
  • springboot~InvocationHandler中为什么不能使用@Autowired

    @Autowired 是 Spring Framework 中用于自动注入依赖的注解,通常情况下可以正常工作,但有一些情况下可能无法获取到 bean 对象: Bean未定义或未扫描到 :如果要注入的 bean 没有在 Spring 上下文中定义或者没有被正确扫描到, @Autowired 将无法找到要注入的 bean。确保你的 bean 配置正

    2024年02月10日
    浏览(55)
  • 【C++学习】C++入门 | 缺省参数 | 函数重载 | 探究C++为什么能够支持函数重载

    上一篇文章我介绍了C++该怎么学,什么是命名空间,以及C++的输入输出, 这里是传送门:http://t.csdn.cn/Oi6V8 这篇文章我们继续来学习C++的基础知识。 目录 写在前面: 1. 缺省参数 2. 函数重载 3. C++是如何支持函数重载的 写在最后: 在学习C语言的时候,如果一个函数存在参数

    2024年02月13日
    浏览(47)
  • SpringBoot有的时候引入依赖为什么不用加版本号

    有的小伙伴做项目时候,引入新的包时候,会有疑问,为什么有些依赖需要加版本号,有些依赖不需要加版本号?不加版本号的依赖,版本号都写在哪里了呢? 内置的依赖可以不加版本号 这是因为SpringBoot内置了很多依赖,引入这些内置的依赖时不需要加版本号,相反,如果

    2024年01月19日
    浏览(64)
  • 1.JavaEE进阶篇 - 为什么要学习SpringBoot呢?

    大家好,我是晓星航。今天为大家带来的是 JavaEE 进阶导读 相关的讲解!😀 学习框架相当于从“小作坊”到“工厂”的升级,小作坊什么都要自己做,工厂是组件式装配,特点就是高效。 框架更加易用、简单且高效。 框架主要体现在思维方式和编程思想上,与代码语言无关

    2024年04月15日
    浏览(47)
  • SpringBoot复习:(34)@EnableWebMvc注解为什么让@WebMvcAutoconfiguration失效?

    它导入了DelegatingWebMvcConfiguration 它会把容器中的类型为WebMvcConfigurer的bean注入到类型为WebMvcConfigurerComposite的成员变量configurers中。 可以看到它继承了WebMvcConfigurerSupport类 而WebMvcConfigureAutoConfiguration类定义如下 可以看到一个@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)注解。 所

    2024年02月13日
    浏览(45)
  • SpringBoot 日志文件:日志的作用?为什么要写日志?

    日志、日志,日志就是记录发生了什么。为啥要记录发生了什么呢?想象⼀下,如果程序报错了,不让你打开控制台看⽇志,那么你能找到报错的原因吗?因此我们需要记录程序的行为,通过这些行为能让我们更好的发现和定位错误所在位置。 除了发现和定位问题之外,还可

    2024年02月11日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包