文章目录
- 前言
- 一、认识JSR303注解
- 二、优雅入参校验
- 引入校验器依赖
- 2.1、实现基本的入参校验异常处理(思路+代码)
- 2.2、实现自定义参数校验注解
- 2.3、实现分组校验(多场景的复杂校验,分析+代码)
- 三、全局异常捕捉(完美针对优雅入参异常响应)
- 3.1、场景介绍及思路分析
- 3.2、实现全局异常捕捉
- 参考资料
前言
对于请求参数校验一直都是一个比较麻烦的问题,因为一旦请求中的参数有多个时,我们如果仅仅通过一个个进行判断就会造成代码冗余的问题,很不优雅,对此本篇博客来介绍JSR303实现对参数的一个优雅校验。
本章博客内容包含有认识JSR303的常用注解、实现基本的入参校验异常、自定义参数校验注解、实现分组校验以及全局异常捕捉优雅返回异常参数。
配套代码:springboot-jsr—Gitee、springboot-jsr—Github
一、认识JSR303注解
相关的注解有如下这些:
# 1、空检查
@Null:元素必须为null。
@NotEmpty:不能为null,而且长度必须大于0,一般用在集合类上面。
@NotNull:不能为null,一般用在基本数据类型的非空校验上,而且被其标注的字段可以使用,@size/@Max/@Min对字段数值进行大小的控制。
@NotBlank只能作用在接收的String类型上,注意是只能,不能为null,而且调用trim()后,长度必须大于0
# 2、Boolean检查
@AssertTrue:验证Boolean对象是否为true
@AssertFalse:验证Boolean对象是否为false
# 3、长度检查
@Size(min = , max = ):验证对象(Array,Collection,Map,String)长度是否在指定的范围内
@Length(min = , max ):验证对象String对象的长度是否在指定的范围内
# 4、日期检查
@Past:验证Date和Calendar对象是都在当前日期之前,验证成立的话备注是的元素一定是一个过去的日期。
@Future:验证Date和Calendar对象是否在当前时间之后,验证成立的话被注释的元素一定是一个将来的日期。
@Pattern:验证String对象是否符合正则表达式的规则,备注是的元素符合指定的正则表达式,regexp:正则表达式 flags指定Patter.Flag的数组,表示正则表达式的相关选项。
# 5、数值检查:建议使用在String,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转化为String为“”,Integer为null
@Min 验证number 和String 对象是否大等于指定的值
@Max 验证number 和String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于指定的值
@DecimalMin 被标注的值必须不小于指定的值
@Digits验证Number和String的构成是否合法
@Digits(integert = , fraction=)验证字符串是否是符合指定格式的数字, integer指定整数精度,fraction指定小数精度
@Range(min=,max=)被指定的元素必须在合适的范围内
# 6、其他常用正则验证
@CreditCardNumber:信用卡验证
@Email:验证是否是邮件地址,如果为null,不进行验证,算通过验证
@ScriptAssert(lang =,script=,alias=)
@URL(protocol=,host=,port=,regexp=,flags=)
二、优雅入参校验
引入校验器依赖
方式一:手动引入两个依赖
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- 若是不引入下面依赖,校验就会无效,因为上面仅仅只是接口并不是实现 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.2.Final</version>
</dependency>
方式二:直接引入一个对应springboot配置好的校验器依赖(推荐)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.1、实现基本的入参校验异常处理(思路+代码)
目标要求:对于某个接口需要对其请求体中的参数来进行校验,具体参数的校验内容如下。
name:品牌名不能为空
logo:品牌名不能为空
descript:描述不能为空
showStatus:状态不能为空,只能传入0或者1
firstLetter:首字母必须是在a-z或者A-Z之间
sort:排序必须是一个大于等于0的整数
思路:
1、在对应的实体类上进行标注JSR303给我们提供的注解。
2、在对应controller中的接口方法中的@RequestBody
前加上@Valid
来开启参数校验。
额外:若是后面参数没有跟上@BindingResult
,那么一旦参数有误就会返回400错误码(由框架发出);若是绑定了的话,我们可以来进行自定义参数异常响应处理。
BrandEntity.java
:在集成JSR303之后,我们只需要简单的在对应的属性上标注注解即可让框架来为我们完成参数校验
package com.changlu.springbootjsr.entity;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
/**
* 品牌
*
* @author changlu
* @date 2022-11-05 16:20:08
*/
@Data
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id:
*/
@NotNull(message = "品牌id不能为空")
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名称不能为空")
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "品牌logo地址不能为空")
@URL(message = "logo必须是一个合法的URL地址")
private String logo;
/**
* 介绍
*/
@NotBlank(message = "描述不能为空")
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "品牌logo地址不能为空")
private Integer showStatus;
/**
* 检索首字母
*/
@NotNull
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@Min(value = 0, message = "排序必须要大于等于0")
private Integer sort;
}
application.yml
:配置下服务端口
server:
port: 8678
R.java
:封装的响应类
package com.changlu.springbootjsr.utils;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author chenshun
* @date 2016年10月27日 下午9:59:27
*/
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() {
return error(500, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
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 static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
BrandController.java
:品牌控制器,若是想要实现参数校验,就只需要在对应的@RequestBody前加上校验注解@Valid
package com.changlu.springbootjsr.controller;
import com.changlu.springbootjsr.entity.BrandEntity;
import com.changlu.springbootjsr.utils.R;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* @Description: 商品控制器
* @Author: changlu
* @Date: 10:30 AM
*/
@RestController
@RequestMapping("/brand")
public class BrandController {
@PostMapping
public R updateBrand(@Valid @RequestBody BrandEntity brandEntity) {
return R.ok().put("data", brandEntity);
}
}
测试一下:
首先准备一下请求参数:
由于参数有误,此时框架就会给我们来进行响应对应的错误信息,这个响应信息并没有带上错误的提示:
这个错误的异常,我们只能够在窗口console中才能够查看:
如何才能够来进行自定义接口的参数异常响应呢?那么我们就需要去在@Valid
的请求参数后面跟上BindingResult result
,此时我们来对result实体来进行校验判断,若是有异常我们来进行统一异常返回:
快速改写下对应的controller
中的updateBrand
方法:
package com.changlu.springbootjsr.controller;
import com.changlu.springbootjsr.entity.BrandEntity;
import com.changlu.springbootjsr.utils.R;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 商品控制器
* @Author: changlu
* @Date: 10:30 AM
*/
@RestController
@RequestMapping("/brand")
public class BrandController {
@PostMapping
public R updateBrand(@Valid @RequestBody BrandEntity brandEntity, BindingResult result) {
//若是实体类校验出现错误
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
//获取校验的所有错误结果
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 R.ok().put("data", brandEntity);
}
}
再次测试一下,此时我们就能够对参数校验不通过的请求来进行自定义异常返回了:
2.2、实现自定义参数校验注解
需求:对于上方的一个属性的状态字段校验需求,我们需要对其进行自定义注解编写
showStatus:状态不能为空,只能传入0或者1
实现思路如下:
ListValue.java
:
package com.changlu.springbootjsr.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.RetentionPolicy.RUNTIME;
/**
* @Description: 指定限定值
* @Author: changlu
* @Date: 1:29 PM
*/
@Documented
//设置自定义的注解校验器
@Constraint(validatedBy = {ListValueConstraintValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
//默认的message值为resources资源目录下的ValidationMessages.properties文件中对应的key值
String message() default "{com.changlu.springbootjsr.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
//自定义传入属性
int[] vals() default {};
}
ListValueConstraintValidator.java
:
package com.changlu.springbootjsr.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* @Description: @ListValue注解限制校验器
* @Author: changlu
* @Date: 1:36 PM
*/
//第一个泛型指的是注解,第二个泛型指的是校验什么类型的数据(一般指的是我们标注在某个类型的数据上)
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
//初始化操作:constraintAnnotation是我们真实标注在某个属性上的完整注解含设置的属性内容
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
//判断校验是否成功
//属性一:Integer value,这个值就是我们标注的属性也就是请求体传来的值,实际上我们会判断这个value是否为0或者1
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
ValidationMessages.properties
:
com.changlu.springbootjsr.valid.ListValue.message=只能传入0或1
接着我们来启动服务,测试一下:
我们去构造一个请求体,其中将showStatus故意写为非0非1:
可以看到校验器能够进行成功校验,但是这里却返回了中文乱码:
解决方案:
方案1:打开settings,将其中的FileEncoding编码设置为UTF-8,设置完之后重启项目
方案2:若是方案1不行,我们就需要去自己实现WebMvcConfigurationSupport,来指定其中的配置文件为UTF-8
package com.changlu.springbootjsr.config;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Configuration
public class WebMvnConfig extends WebMvcConfigurationSupport {
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 解决controller返回字符串中文乱码问题
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
} else if (converter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
}
}
}
@Override
protected Validator getValidator() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setDefaultEncoding("utf-8");// 读取配置文件的编码格式
messageSource.setCacheMillis(-1);// 缓存时间,-1表示不过期
messageSource.setBasename("ValidationMessages");// 配置文件前缀名,设置为Messages,那你的配置文件必须以Messages.properties/Message_en.properties...
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
factoryBean.setValidationMessageSource(messageSource);
return factoryBean;
}
}
重新测试一下:
2.3、实现分组校验(多场景的复杂校验,分析+代码)
分析
场景描述:若是多个请求,对于不同的字段有不同的入参要求,对于这种场景若是我们只是像之前那样标注那么就会出现问题。
需求:
新增:
brandId:为null
name:不为空
logo:不为空
showStatus:不为空 + 只能为0或者1
firstLetter:不能为空 + 检索首字母必须是一个字母
sort:不能为空 + 数字>=0
修改:
brandId:不为null
name:不为空
logo:不为空
showStatus:不为空 + 只能为0或者1
firstLetter:不能为空 + 检索首字母必须是一个字母
sort:不能为空 + 数字>=0
其中的唯一区别就是brandId在不同请求入参的状态,在新增时为null,修改时必须不为null。
解决方案:使用JSR303给我们提供的分组校验。
思路:
实体类属性注解中指定group分组:@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
控制器参数校验注解:将@Valid替换为@Validated({AddGroup.class})
默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效;
实现
首先自定义分组注解:
AddGroup.java
:
package com.changlu.springbootjsr.valid;
/**
* @Description: 新增组
* @Author: changlu
* @Date: 2:37 PM
*/
public interface AddGroup {
}
Update.java
:
package com.changlu.springbootjsr.valid;
/**
* @Description: 更新组
* @Author: changlu
* @Date: 2:37 PM
*/
public interface UpdateGroup {
}
BrandEntity.java
:为每个属性上的校验注解添加groups
,对于brandId是进行区别对待的
package com.changlu.springbootjsr.entity;
import com.changlu.springbootjsr.valid.AddGroup;
import com.changlu.springbootjsr.valid.ListValue;
import com.changlu.springbootjsr.valid.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
import java.io.Serializable;
/**
* 品牌
*
* @author changlu
* @date 2022-11-05 16:20:08
*/
@Data
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id:
*/
@Null(message = "品牌id必须为空", groups = {AddGroup.class})
@NotNull(message = "品牌id不能为空", groups = {UpdateGroup.class})
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名称不能为空", groups = {AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "品牌logo地址不能为空", groups = {AddGroup.class, UpdateGroup.class})
@URL(message = "logo必须是一个合法的URL地址", groups = {AddGroup.class, UpdateGroup.class})
private String logo;
/**
* 介绍
*/
@NotBlank(message = "描述不能为空", groups = {AddGroup.class, UpdateGroup.class})
private String descript;
/**
* 显示状态[0-不显示;1-显示] , message = "只能传入0或者1"
*/
@NotNull(message = "展示状态不能为空", groups = {AddGroup.class, UpdateGroup.class})
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotNull
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序字段不能为空", groups = {AddGroup.class, UpdateGroup.class})
@Min(value = 0, message = "排序必须要大于等于0", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;
}
BrandController.java
:品牌控制器,编写添加、更新接口,将原本的@Valid替换为@Validated,并指定分组
package com.changlu.springbootjsr.controller;
import com.changlu.springbootjsr.entity.BrandEntity;
import com.changlu.springbootjsr.utils.R;
import com.changlu.springbootjsr.valid.AddGroup;
import com.changlu.springbootjsr.valid.UpdateGroup;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 商品控制器
* @Author: changlu
* @Date: 10:30 AM
*/
@RestController
@RequestMapping("/brand")
public class BrandController {
@PostMapping("/add")
public R addeBrand(@Validated({AddGroup.class}) @RequestBody BrandEntity brandEntity, BindingResult result) {
//若是实体类校验出现错误
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
//获取校验的所有错误结果
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 R.ok().put("data", brandEntity);
}
@PostMapping
public R updateBrand(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brandEntity, BindingResult result) {
//若是实体类校验出现错误
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
//获取校验的所有错误结果
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 R.ok().put("data", brandEntity);
}
}
OK,现在我们来进行测试:
添加品牌接口测试:故意携带brandId参数
修改接口测试:不携带brandId参数
三、全局异常捕捉(完美针对优雅入参异常响应)
3.1、场景介绍及思路分析
场景描述:我们在2.3章节当中可以看到对于原先的写法,写一个添加、更新接口,每一个接口的后面都需要跟上一个BindingResult,并且做的操作也是同样的就会产生代码冗余情况,我们是否可以来进行向AOP一样来对其统一做相同的处理呢?
解决方案:对于上面的问题我们可以采用全局异常捕捉器来进行统一异常响应返回。
实现思路:
1、编写异常处理类,使用@RestControllerAdvice
2、使用@ExceptionHandler
标注方法可以处理的异常来进行统一异常响应。
3.2、实现全局异常捕捉
对于JSR303的异常类我们如何确定呢?
可以看到在2.1节中,若是不使用BindingResult
时,在控制台抛出的异常就是MethodArgumentNotValidException
,那么我们可以对这个异常来进行全局异常捕捉。
接着我们来实现我们的全局异常处理器:
ExceptionControllerAdvice.java
:目前去绑定的异常有MethodArgumentNotValidException
(针对于JSR303的参数校验异常)、Throwable
(对于非参数校验异常情况)
package com.changlu.springbootjsr.exception;
import com.changlu.springbootjsr.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 异常控制器捕捉
* @Author: changlu
* @Date: 11:17 AM
*/
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.changlu.springbootjsr.controller")
//替代上方的两个注解
@RestControllerAdvice(basePackages = "com.changlu.springbootjsr.controller")
public class ExceptionControllerAdvice {
//捕捉参数校验异常(精确)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{}, 异常类型:{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(fieldError -> {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(400, "参数校验异常").put("data", errorMap);
}
//大范围(没有指定某个class的额外异常捕捉)
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
log.error("错误", throwable);
return R.error(500, "系统异常,请联系管理员");
}
}
最后我们来重构一下我们的BrandController
,看看它的现在变化:
package com.changlu.springbootjsr.controller;
import com.changlu.springbootjsr.entity.BrandEntity;
import com.changlu.springbootjsr.utils.R;
import com.changlu.springbootjsr.valid.AddGroup;
import com.changlu.springbootjsr.valid.UpdateGroup;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description: 商品控制器
* @Author: changlu
* @Date: 10:30 AM
*/
@RestController
@RequestMapping("/brand")
public class BrandController {
//添加接口
@PostMapping("/add")
public R addeBrand(@Validated({AddGroup.class}) @RequestBody BrandEntity brandEntity) {
return R.ok().put("data", brandEntity);
}
//更新接口
@PostMapping
public R updateBrand(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brandEntity) {
return R.ok().put("data", brandEntity);
}
}
注意:使用全局异常捕捉时,我们就无需在对应的请求实体类后添加BindingResult result
,因为一旦添加了就不会直接自然抛出异常,那么也就不会走我们的全局异常捕捉器。
至此,我们可以看到代码特别的简洁,符合我们的预期,现在我们来进行测试:测试参数与2.3的一致
添加接口:
更新接口: