SpringBoot后端进行数据校验JSR303的使用详解

作者:jiangxiaoju 时间:2022-02-19 01:15:31 

如果只想查看注解,请跳到文章末尾部分

简介

在前后端进行数据交互中,在前端把数据传送到后端前,一般会先进行校验一次,校验成功之后,才把数据发送到后端。但是我们在服务端还得在对数据进行一次校验。因为请求数据发送的链接很容易获取,可以不经过前端界面,使用postman等工具直接向后台发送数据,这就可能造成发送的数据是不合法的情况。

项目创建

首先创建一个springboot项目

使用的springboot版本为:(本文代码以该版本为准,不同版本springboot,在下面内容会出现一些差异


<parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>2.3.9.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
</parent>

引入如下依赖


<dependencies>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
 </dependency>
 <dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
 </dependency>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
   <exclusion>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
   </exclusion>
  </exclusions>
 </dependency>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>
</dependencies>

这个作标在新一点的springboot版本中,需要单独引入。在老版本是默认引入的。这个是用来引入对jsr303注解的支持。


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

接着创建一个Java Bean


package cn.jxj4869.demo.entity;

import lombok.Data;

import javax.validation.constraints.NotNull;

@Data
public class User {
@NotNull
private Integer id;
private String username;
private String password;
private String email;
}

返回类型的JavaBean


package cn.jxj4869.demo.entity;

import java.util.HashMap;

public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;

public R() {
put("code", 0);
put("msg", "success");
}

public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}

public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}

public R put(String key, Object value) {
super.put(key, value);
return this;
}
}

创建一个controller。

index方法用来跳转到首页。


package cn.jxj4869.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class UserController {

@RequestMapping("/")
public String index(){
 return "index";
}
}

首页代码放到resources/templates目录下


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
 div{
  margin-top: 50px;
 }
</style>
</head>
<body>

<div>
新增表单
<br><br>
<form method="post">
 <label>用户名</label>
 <input type="text" name="username"/>
 <br>
 <label>密码</label>
 <input type="text" name="password"/>
 <br>
 <label>邮箱</label>
 <input type="email" name="email"/>
 <br>
 <input type="submit" value="提交"/>
</form>
</div>
<div>
更新表单
<br><br>
<form method="post">
 <input type="hidden" name="id" value="1">
 <label>用户名</label>
 <input type="text" name="username"/>
 <br>
 <label>密码</label>
 <input type="text" name="password"/>
 <br>
 <label>邮箱</label>
 <input type="email" name="email"/>
 <br>
 <input type="submit" value="提交"/>
</form>
</div>

</body>
</html>

传统的检验方式

要在后端进行数据校验,传统的校验方式在controller层接受数据后,按照要求对数据进行校验

比如要接收一个user bean对象。

现在要对user对象中的username属性进行非空校验,password属性进行非空校验和长度校验。


@PostMapping("/user")
@ResponseBody
public R user1(User user) throws Exception {
 if(StringUtils.isEmpty(user.getUsername())) {
  return R.error(400,"username不能为空");
 }
 if(StringUtils.isEmpty(user.getPassword())||user.getPassword().length()>8||user.getPassword().length() <4) {
  return R.error(400,"password无效");
 }
 return null;
}

如果有多个方法都需要接受user对象, 而且要校验的属性可能不止usernamepassword这两个属性,如果每个方法里面都采用上面这种校验方式的话,代码就会很臃肿,而且不好维护,当改变了userbean的属性,或者对校验规则进行修改后,就得对所有的校验代码进行更新。 这是一件工程量很大的事。

使用JSR303

为了解决上述问题,我们可以使用JSR303提供的注解进行校验。

JSR是Java Specification Requests的缩写,意思是Java 规范提案。JSR303也就是第303号提案。

使用JSR303的方法很简单,例如上面的需求,我们只需要在user的属性上加上注解即可。

步骤如下:

1、给Bean添加校验注解,一般是在 javax.validation.constraints这个包下,也还有一些是hibernate提供的。

2、开启校验功能@Valid。

3、当校验失败的时候,会抛出org.springframework.validation.BindException异常。

常用的校验注解在文末


package cn.jxj4869.demo.entity;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotNull;

@Data
public class User {

private Integer id;
@NotBlank
private String username;
@NotBlank
@Length(min = 4,max = 8)
private String password;
private String email;
}

然后在controller里面的方法上,加上@Valid注解即可


@PostMapping("/user2")
@ResponseBody
public R user2(@Valid User user) throws Exception {
 System.out.println(user);
 return null;
}

当校验失败后,会出现如下错误。并且会给出默认的提示信息。

SpringBoot后端进行数据校验JSR303的使用详解

自定义错误信息

那这个错误信息是怎么来的呢。

进入@NotNULL注解的代码里面


@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotBlank {

String message() default "{javax.validation.constraints.NotNull.message}";
............
............
............
}

会有一个message属性。显然就是指定错误的提示内容。而这些错误提示是在一个叫validationMessages.properties的文件中,用idea的搜索工具可以找到,双击shift键打开搜索。

发现有这么多validationMessages.properties的文件,而且支持国际化。

SpringBoot后端进行数据校验JSR303的使用详解

打开validationMessages_zh.properties,可以看到里面定义了这么多的提示。而错误提示就是从这文件中获取的。

SpringBoot后端进行数据校验JSR303的使用详解

如果我们不想用默认的校验提示信息的话,可以自己指定。

指定message的值即可。


@NotBlank(message = "用户名不能为空")
private String username;

SpringBoot后端进行数据校验JSR303的使用详解

错误信息的获取与响应

当校验出错时,会默认返回一个错误界面,或者返回错误提示的json数据。但默认提供的显然不是我们想要的,如果可以拿到错误信息,那我们就能自定义相应数据了。

拿到错误信息的方式也很简单,只要在方法中加上BindingResult result这个参数,错误信息就会封装这里面。


@PostMapping("/user2")
@ResponseBody
public R user2(@Valid User user, BindingResult result) throws Exception {
 System.out.println(user);
 if(result.hasErrors()) { //判断是否有错误
  Map<String,String> map = new HashMap<>();
  //1、获取校验的错误结果
  result.getFieldErrors().forEach((item)->{
   //FieldError 获取到错误提示
   String message = item.getDefaultMessage();
   //获取错误的属性的名字
   String field = item.getField();
   map.put(field,message);
  });
  return R.error(400,"提交的数据不合法").put("data",map);
 }
 // 若没有错误,则进行接下去的业务操作。
 return null;
}

不过不推荐上面这种方式,理由同上,当校验的地方多了,每个方法里面都加上这么个异常处理,会让代码很臃肿。

不知道你们是否还记得,springmvc里面有个全局的异常处理,我们可以自定义一个异常处理,在这里面统一处理异常。

统一处理BinException。这样就可以不用在controller中去处理错误信息了。


package cn.jxj4869.demo.execption;

import cn.jxj4869.demo.entity.R;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice(basePackages = "cn.jxj4869.demo.controller")
public class MyExceptionControllerAdvice {

@ExceptionHandler(value = BindException.class)
public R handleVaildException(BindException e) {

Map<String,String> map = new HashMap<>();
  //1、获取校验的错误结果
  e.getFieldErrors().forEach((item)->{
   //FieldError 获取到错误提示
   String message = item.getDefaultMessage();
   //获取错误的属性的名字
   String field = item.getField();
   map.put(field,message);
  });
  return R.error(400,"提交的数据不合法").put("data",map);

}
}

错误异常类型补充

校验出错的时候,会抛出两种异常

org.springframework.validation.BindException

使用@Valid注解进行校验的时候抛出的

org.springframework.web.bind.MethodArgumentNotValidException

使用@validated校验的时候抛出的

在异常捕获中加入下面这个


@ExceptionHandler(value= MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){

BindingResult bindingResult = e.getBindingResult();

Map<String,String> map = new HashMap<>();
 bindingResult.getFieldErrors().forEach((fieldError)->{
  map.put(fieldError.getField(),fieldError.getDefaultMessage());
 });
 return R.error(400,"提交的数据不合法").put("data",map);
}

分组校验

在不同业务场景下,校验规则是不一样的,比如user对象中id这个属性,在新增的时候,这个属性是不用填的,要为null,但是在修改的时候,id属性是不能为null的。

可以用注解中的groups属性来指定,在什么场合下使用改注解


@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {

Class<?>[] groups() default { };
........
}

首先定义两个接口AddGroupUpdateGroup,不需要做任何实现


package cn.jxj4869.demo.valid;

public interface UpdateGroup {
}

package cn.jxj4869.demo.valid;

public interface AddGroup {
}

在user中指定group。

  • id属性在AddGroup的时候,要为null,在UpdateGroup的时候不能为null

  • username属性在AddGroup和Update的时候,都要进行校验,不能为空。

  • password属性,当校验的时候指定分组的话,会不起作用,因为没有给它指定校验的分组


package cn.jxj4869.demo.entity;

import cn.jxj4869.demo.valid.AddGroup;
import cn.jxj4869.demo.valid.UpdateGroup;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;

@Data
public class User {

@Null(groups = {AddGroup.class})
@NotNull(groups = {UpdateGroup.class})
private Integer id;

@NotBlank(message = "用户名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String username;
@NotEmpty
private String password;
private String email;
}

在controller中用@Validated注解,指定校验的分组


@PostMapping("/user3")
@ResponseBody
public R user3(@Validated(UpdateGroup.class) User user) {
 System.out.println(user);

return null;
}

结果如下图所示,因为password属性没有指定校验的分组,所以在校验的时候,都不会对它进行合法性检查。

SpringBoot后端进行数据校验JSR303的使用详解

SpringBoot后端进行数据校验JSR303的使用详解

自定义校验

当提供的注解不能满足我们需求的时候,可以自定义注解。

例如我们现在给user新加一个属性status,并要求这个属性的值只能是0或者1。

新建一个@StatusValue注解。

根据jsr303的规范,校验注解得有三个属性。

  • message:用来获取错误提示的

  • groups:指定校验分组的。

  • payload:可以自定义一些负载信息

使用@Constraint注解指定该注解的校验器


package cn.jxj4869.demo.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = { StatusValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@interface StatusValue {
String message() default "{cn.jxj4869.valid.StatusValue.message}";

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

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

int[] value() default { };
}

自定义校验器

需要实现ConstraintValidator这个接口,第一个泛型是表示要校验哪个注解,第二个泛型是要校验的数据的类型。

  • initialize是初始化方法

  • isValid校验方法,判断是否校验成功


package cn.jxj4869.demo.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

public class StatusValueConstraintValidator implements ConstraintValidator<StatusValue,Integer> {

private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(StatusValue constraintAnnotation) {

int[] value = constraintAnnotation.value();
 for (int val : value) {
  set.add(val);
 }

}
/**
 * 判断是否校验成功
 * @param value
 * @param context
 * @return
 */
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {

return set.contains(value);
}
}

最后在resources目录下添加一个ValidationMessages.properties文件

用来指定错误信息。


cn.jxj4869.valid.StatusValue.message=必须提交指定的值

UserBean


@Data
public class User {

@Null(groups = {AddGroup.class})
@NotNull(groups = {UpdateGroup.class})
private Integer id;

@NotBlank(message = "用户名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String username;
@NotEmpty
private String password;
private String email;

@StatusValue(value = {0,1},groups = {AddGroup.class,UpdateGroup.class})
private Integer status;
}

SpringBoot后端进行数据校验JSR303的使用详解

SpringBoot后端进行数据校验JSR303的使用详解

常用注解汇总

注解功能
@Null对象必须为null
@NotNull对象必须不为null,无法检查长度为0的字符串
@NotBlank字符串必须不为Null,且去掉前后空格长度必须大于0
@NotEmpty字符串必须非空
@Length(min = 1,max = 50)字符串必须在指定长度内
@Range(min = 0,max = 100)必须在指定范围内
@AssertTrue对象必须为true
@AssertFalse对象必须为false
@Max(Value)必须为数字,且小于或等于Value
@Min(Value)必须为数字,且大于或等于Value
@DecimalMax(Value)必须为数字( BigDecimal ),且小于或等于Value。小数存在精度
@DecimalMin(Value)必须为数字( BigDecimal ),且大于或等于Value。小数存在精度
@Digits(integer,fraction)必须为数字( BigDecimal ),integer整数精度,fraction小数精度
@Size(min,max)对象(Array、Collection、Map、String)长度必须在给定范围
@Email字符串必须是合法邮件地址
@PastDate和Calendar对象必须在当前时间之前
@FutureDate和Calendar对象必须在当前时间之后
@Pattern(regexp=“正则”)字符串满足正则表达式的值

来源:https://blog.csdn.net/qq_43058685/article/details/114381502

标签:SpringBoot,数据校验,JSR303,使用
0
投稿

猜你喜欢

  • Java jar打包工具使用方法步骤解析

    2023-07-01 12:26:47
  • 优化spring boot应用后6s内启动内存减半

    2021-09-13 02:47:59
  • java 垃圾回收机制以及经典垃圾回收器详解

    2022-07-06 05:16:08
  • 基于Java8实现提高Excel读写效率

    2023-11-25 10:01:37
  • 详解Spring Boot Admin监控服务上下线邮件通知

    2023-06-16 21:53:31
  • JavaWeb Servlet实现文件上传与下载功能实例

    2023-06-16 16:41:27
  • Javacsv实现Java读写csv文件

    2022-02-16 01:10:15
  • android调试工具DDMS的使用详解

    2023-06-21 09:06:22
  • 使用Spring Data JDBC实现DDD聚合的示例代码

    2022-05-04 05:11:23
  • Java线程创建的四种方式总结

    2023-10-29 19:36:03
  • springboot开启声明式事务的方法

    2021-06-18 23:50:48
  • SpringBoot项目从搭建到发布一条龙

    2023-11-21 09:28:44
  • js 交互在Flutter 中使用 webview_flutter

    2023-07-20 22:40:14
  • Java 守护线程_动力节点Java学院整理

    2023-11-28 07:51:14
  • Java实现简易界面通讯录

    2023-02-09 20:12:59
  • C# Winform实现进度条显示

    2023-09-14 15:47:51
  • Java设计模式编程之解释器模式的简单讲解

    2022-01-24 16:03:32
  • Java实现的校验银行卡功能示例

    2022-01-28 19:33:53
  • 使用PageHelper插件实现Service层分页

    2023-03-06 00:40:39
  • C++实现LeetCode(159.最多有两个不同字符的最长子串)

    2023-06-20 22:39:46
  • asp之家 软件编程 m.aspxhome.com