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对象, 而且要校验的属性可能不止username
和password
这两个属性,如果每个方法里面都采用上面这种校验方式的话,代码就会很臃肿,而且不好维护,当改变了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;
}
当校验失败后,会出现如下错误。并且会给出默认的提示信息。
自定义错误信息
那这个错误信息是怎么来的呢。
进入@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
的文件,而且支持国际化。
打开validationMessages_zh.properties
,可以看到里面定义了这么多的提示。而错误提示就是从这文件中获取的。
如果我们不想用默认的校验提示信息的话,可以自己指定。
指定message的值即可。
@NotBlank(message = "用户名不能为空")
private String username;
错误信息的获取与响应
当校验出错时,会默认返回一个错误界面,或者返回错误提示的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 { };
........
}
首先定义两个接口AddGroup
和UpdateGroup
,不需要做任何实现
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
属性没有指定校验的分组,所以在校验的时候,都不会对它进行合法性检查。
自定义校验
当提供的注解不能满足我们需求的时候,可以自定义注解。
例如我们现在给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;
}
常用注解汇总
注解 | 功能 |
---|---|
@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)长度必须在给定范围 |
字符串必须是合法邮件地址 | |
@Past | Date和Calendar对象必须在当前时间之前 |
@Future | Date和Calendar对象必须在当前时间之后 |
@Pattern(regexp=“正则”) | 字符串满足正则表达式的值 |
来源:https://blog.csdn.net/qq_43058685/article/details/114381502


猜你喜欢
- 本文以实例阐述了C++中形参与实参的区别,有助于读者加深对于C++形参与实参的认识。形参出现在函数定义中,在整个函数体内都可以使用, 离开该
- 上一篇文章讲的是Java实现两人五子棋游戏(二) 画出棋盘,已经画好棋盘,接下来要实现控制功能,主要功能:1)选择棋子2)画棋子3)判断胜负
- 测试代码如下: package swt_jface.demo; import org.eclipse.jface.window.Applic
- 目录1、表达式目录树2、构建表达式目录树3、使用Expression来进行不同对象的相同名字的属性映射4、表达式目录树构建SQL删选&nbs
- 本文实例讲述了Java实现数组转字符串及字符串转数组的方法。分享给大家供大家参考,具体如下:字符串转数组使用Java split() 方法s
- SpringBoot @NotBlank错误java 验证出现如下错误:javax.validation.UnexpectedTypeExc
- 这篇文章主要介绍了java获取当前时间并格式化代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋
- 昨天弄了一天的Android Studio svn,感觉没有eclipse的svn好装,中间遇到很多的麻烦问题。这里来记录下吧下载下来的时候
- 加密配置文件的SQL账号密码一般项目的配置文件里的信息都是明文的,导致有时候比较敏感的信息也直接暴露得超级明显,比如SQL的链接 账号 密码
- 本人一直喜欢左手使用鼠标,但有时候同事会临时进行操作,还得在控制面板里进行更改,比较不便,何不编写一个控制台程序,双击一下即可切换左右键 代
- 本文实例为大家分享了C#实现简化QQ聊天窗口的具体代码,供大家参考,具体内容如下如图样式,详细步骤如下整个窗体设置private void
- 本文实例讲述了android动态布局之动态加入TextView和ListView的方法。分享给大家供大家参考。具体实现方法如下:packag
- 本文实例讲述了C#实现鼠标移动到曲线图上显示值的方法。分享给大家供大家参考。具体实现方法如下:一、问题:完成折线图报表后,产品经理要求把折线
- 使用java基础类写的一个简单的zip压缩解压工具类package sun.net.helper;import java.io.*;impo
- 新建两个工程,一个客户端,一个服务端,先启动服务端再启动客户端两个工程的读写操作线程类基本上完全相同服务端:import java.io.B
- 实例如下所示:/** * 创建多级目录文件 * * @param path 文件路径 * @throws IOException */pri
- 这个列表总结了10个Java开发人员最常犯的错误。Array转ArrayList当需要把Array转成ArrayList的时候,开发人员经常
- Android 中倒计时验证两种常用方式实例详解短信验证码功能,这里总结了两种常用的方式,可以直接拿来使用。看图:说明:这里的及时从10开始
- 〇、正则表达式的基本语法符号若只简单匹配固定字符串,则无需任何修饰符,例如:需要匹配字符串 77,则可直接写:new Regex(
- 实例如下:public String stripHtml(String content) { // <p>段落替换为换行 con