文章目录
环境:
IDEA: 2021
JDK: 1.8
Spring-Boot-dependencies: 2.2.5.RELEASE
mysql: 5.7
mybatis-plus-boot-starter: 3.2.0
一、JSR303校验
1、简介
参数校验
是程序开发中必不可少的步骤。用户在前端页面上填写表单时,前端js程序会校验参数的合法性,当数据到了后端,为了防止恶意操作,保持程序的健壮性,后端同样需要对数据进行校验。后端参数校验最简单的做法是直接在业务方法里面进行判断,当判断成功之后再继续往下执行。但这样带给我们的是代码的耦合,冗余。当我们多个地方需要校验时,我们就需要在每一个地方调用校验程序,导致代码很冗余,且不美观。
那么如何优雅的对参数进行校验
呢?JSR303就是为了解决这个问题出现的。
2、相关注解
JSR303 是一套
JavaBean
参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。
注解如下:
注解 | 说明 |
---|---|
@NotNull | 注解元素必须是非空 |
@NotBlank | 注解元素不能是空格并且至少包含一个字符 |
@NotEmpty | 注解元素不能为null或空 |
@Email | 该字符串必须是格式正确的电子邮件地址。 |
@Null | 注解元素必须是空 |
@Digits | 带注释的元素必须是可接受范围内的数字 |
@Future | 带注释的元素必须是未来的瞬间、日期或时间。 |
@FutureOrPresent | 注释元素必须是当前或未来的瞬间、日期或时间。 |
@Past | 带注释的元素必须是过去的瞬间、日期或时间。 |
@PastOrPresent | 带注释的元素必须是过去或现在的瞬间、日期或时间。 |
@Max | 带注释的元素必须是一个数字,其值必须小于或等于指定的最大值。 |
@Min | 带注释的元素必须是一个数字,其值必须大于或等于指定的最小值。 |
@Pattern | 带注释的 {@code CharSequence} 必须匹配指定的正则表达式。正则表达式遵循 Java 正则表达式约定 |
@Size | 验证元素大小是否在指定范围内 |
@DecimalMax | 带注释的元素必须是一个数字,其值必须小于或等于指定的最大值。 |
@DecimalMin | 带注释的元素必须是一个数字,其值必须大于或等于指定的最小值。 |
@AssertTrue | 被注释的元素必须为true |
@AssertFalse | 被注释的元素必须为false |
@Positive | 被注解的元素必须是整数 |
@PositiveOrZero | 被注解元素必须是正数或0 |
Hibernate validator 在JSR303的基础上对校验注解进行了扩展,hibernate-validator官方文档
扩展注解如下:
例举几个:
注解 | 说明 |
---|---|
@URL | 被注释的元素必须是合法的URL |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@Range | 被注释的元素必须在合适的范围内 |
3、JSR303依赖包
<!--JSR303依赖包-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!--hibernate-validator依赖包,包含了JSR303的依赖包-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
二、JSR303自带的校验规则
1、在JavaBean上添加校验规则
package com.zhuang.mall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author mrzhuang
* @email 862627527@qq.com
* @date 2022-04-11 13:56:19
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须填写!")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(message = "logo url必须填写!")
@URL(message = "必须是合法的url!")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(message = "首字母必须填写!")
// 正则表达式不能写为"/^[a-zA-Z]$/"
@Pattern(regexp = "^[a-zA-Z]$" , message = "检索首字母必须是一个字母!")
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序必须填写!")
@Min(value = 0, message = "排序必须是大于等于0的整数!")
private Integer sort;
}
}
2、生效校验规则
2.1 controller返回的通用实体类R
/**
* Copyright (c) 2016-2019 人人开源 All rights reserved.
*
* https://www.renren.io
*
* 版权所有,侵权必究!
*/
package com.zhuang.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
/**
* @author mrzhuang
* @date 2022/4/25 8:12 PM
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", HttpStatus.SC_OK);
put("msg", "success");
}
public R setData(Object data){
put("data",data);
return this;
}
public <T> T getData(String key,TypeReference<T> typeReference){
Object data = get(key);
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public <T> T getData(TypeReference<T> typeReference){
Object data = get("data");
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, 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;
}
public Integer getCode(){
return (Integer) this.get("code");
}
}
2.2 方法一:在controller的方法中生效校验
package com.zhuang.mall.product.controller;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.zhuang.mall.product.entity.BrandEntity;
import com.zhuang.mall.product.service.BrandService;
import com.zhuang.common.utils.PageUtils;
import com.zhuang.common.utils.R;
import javax.naming.Binding;
import javax.validation.Valid;
/**
* 品牌
* @author mrzhuang
* @email 862627527@qq.com
* @date 2022-04-11 13:56:19
*/
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 保存
*/
//@Valid告诉spring mvc上传的数据需要校验,brandEntity中的@NotBlank规则就会生效!
//BindingResult会获取到错误的信息结果
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand, BindingResult result){
//判断result中是否有错误
if (result.hasErrors()) {
Map<String, String > map = new HashMap<>();
//获取到校验的错误结果
result.getFieldErrors().forEach((item)->{
//FiledError 获取到错误提示
String message = item.getDefaultMessage();
//获取错误的属性的名字
String field = item.getField();
map.put(field, message);
});
return R.error(400, "提交的数据不合法").put("data",map);
}else {
brandService.save(brand);
return R.ok();
}
}
测试:
使用Postman进行接口的测试。
错误的输入测试:
1、传入空的jason字符串:
{}
2、传入不合校验规则的Jason字符串:
{"name":"","logo":"123","sort":-1,"firstLetter":"ab"}
正确的输入测试:
传入的jason字符串为:
{"name":"小米","logo":"https://gss0.baidu.com/7LsWdDW5_xN3otqbppnN2DJv/forum/pic/item/fd039245d688d43f394f6821381ed21b0ff43b7b.jpg","sort":0,"firstLetter":"A"}
2.3 方法二:写一个异常处理类
对于第一种方法,代码的冗余度比较高。使用统一的异常处理类降低冗余、方便。
1、创建通用的异常枚举类
mall-common/src/main/java/com/zhuang/common/exception/BizCodeEnum.java
package com.zhuang.common.exception;
/**
* @author mrzhuang
* @date 2022/4/25 5:25 PM
*/
public enum BizCodeEnum {
UNKNOWN_EXCEPTION(10000,"系统未知错误"),
VALID_EXCEPTION(10001,"参数校验异常"),
private Integer code;
private String msg;
//枚举类必有的私有构造器
private BizCodeEnum(Integer code,String msg){
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
2、创建异常类
mall-product/src/main/java/com/zhuang/mall/product/exception/MallExceptionControllerAdvice.java
package com.zhuang.gulimall.product.exception;
import com.zhuang.common.exception.BizCodeEnum;
import com.zhuang.common.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 org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* @author mrzhuang
* @date 2022/4/24 8:54 PM
* description:集中处理所有的异常
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.zhuang.gulimall.product.controller")
public class MallExceptionControllerAdvice {
//指定处理的异常
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(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(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",errorMap);
}
//处理任意类型的异常,将异常抛出
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
log.error("错误:", throwable);
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(),BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
}
}
3、还原初始controller类中的方法
方法中的@Valid
注解需要加上!!!
/**
* 品牌
* @author mrzhuang
* @email 862627527@qq.com
* @date 2022-04-11 13:56:19
*/
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 保存
*/
//@Valid告诉spring mvc上传的数据需要校验,brandEntity中的@NotBlank规则就会生效!
//BindingResult会获取到错误的信息结果
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand){
brandService.save(brand);
return R.ok();
}
4、测试
输入错误的jason
字符串:
{"name":"","logo":"123","sort":-1,"firstLetter":"xx"}
输入正确的jason
字符串:
{"name":"小米","logo":"https://gss0.baidu.com/7LsWdDW5_xN3otqbppnN2DJv/forum/pic/item/fd039245d688d43f394f6821381ed21b0ff43b7b.jpg","sort":0,"showStatus":"0","firstLetter":"x"}
2、使用group校验
2.1 分组校验
分组校验
可以将JavaBean
的某个属性值
进行分组
,比如在进行更新操作或者添加操作的时候需要进行校验;
对于
brangId
我们需要在添加操作
的时候,可以不需要branId,因为采用的是自增长策略
,因此不需要有brandId,这时需要校验必须为空值。而对于更新操作
的时候,我们需要brandId,这时需要校验是否是空值。
2.1 @Validated与@Valid
@Validated与@Valid的比较
@Validated:
Spring提供的
支持分组校验
可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
由于无法加在成员属性(字段)上,所以无法单独完成级联校验,需要配合@Valid
@Valid:
JDK提供的(标准JSR-303规范)
不支持分组校验
可以用在方法、构造函数、方法参数和成员属性(字段)上
可以加在成员属性(字段)上,能够独自完成级联校验
2.2 定义接口,充当标识
1、添加AddGroup
与UpdateGroup
接口
package com.zhuang.common.valid;
/**
* @author mrzhuang
* @date 2022/4/25 9:18 PM
*/
public interface AddGroup {
}
package com.zhuang.common.valid;
/**
* @author mrzhuang
* @date 2022/4/25 9:18 PM
*/
public interface UpdateGroup {
}
2、给属性上添加分组
/**
* 品牌id
*/
@NotNull(message = "更新操作必须填写brandId!", groups = {UpdateGroup.class})
@Null(message = "添加操作必须为空值!", groups = {AddGroup.class})
@TableId
private Long brandId;
3、controller类中的方法
添加操作
@RequestMapping("/save")
public R save(@RequestBody @Validated({AddGroup.class}) BrandEntity brand){
brandService.save(brand);
return R.ok();
}
更新操作
@RequestMapping("/update")
public R update(@RequestBody @Validated({UpdateGroup.class}) BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
2.3 测试
添加操作:
输入的jason字符串:
{"brandId":"2000","name":"","logo":"123","sort":-1,"firstLetter":"xx"}
结果:
{
"msg": "参数校验异常",
"code": 10001,
"data": {
"brandId": "添加操作必须为空值!"
}
}
更新操作:
输入的jason字符串:
{"brandId":"","name":"","logo":"123","sort":-1,"firstLetter":"xx"}
结果:
{
"msg": "参数校验异常",
"code": 10001,
"data": {
"brandId": "更新操作必须填写brandId!"
}
}
注意:
从上面结果中看出@Validated注解无法校验属性上定义的规则!,而且当方法中同时使用@Validated与@Valid时,只会生效左边的第一个,也就是谁靠左就生效谁!!!
原因是:
没有加上默认分组 Default.class
。
解决方法:
1、@Validated({UpdateGroup.class)中的分组中加上默认分组
@Validated({AddGroup.class,Default.class
})
@Validated({UpdateGroup.class,Default.class
})
2、在写的充当标识的接口时继承默认分组
public interfaceAddGroup
extends Default
{
}
public interfaceUpdateGroup
extends Default
{
}
修改后的方法:
添加操作
@RequestMapping("/save")
public R save(@RequestBody @Validated({AddGroup.class,Default.class}) BrandEntity brand){
brandService.save(brand);
return R.ok();
}
更新操作
@RequestMapping("/update")
public R update(@RequestBody @Validated({UpdateGroup.class, Default.class}) BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
3、其他的校验规则
3.1 组序列
指定组与组之间的检验顺序,如果第一个组校验没过,就不会校验后面的组
@GroupSequence({UpdateGroup.class, AddGroup.class, Default.class})
public interface DefaultGroupSequence {
}
注意: 需要重新自定义一个接口。
3.2 级联验证
一个待验证的JavaBean类,其中又包含了一个待验证的对象。
需要在待验证的对象属性上加@Valid
注解
三、自定义校验规则
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
我们需要在showStatus
属性上添加一个自定义校验规则@ListValue
:输入的值只能为0或1
1、编写自定义注解
创建自定义注解ListValue,可以参考JSR303已有的注解的内容。
mall-common/src/main/java/com/zhuang/common/valid/ListValue.java
package com.zhuang.common.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @author mrzhuang
* @date 2022/4/26 10:23 AM
*/
@Documented
//约束校验方式
@Constraint(
validatedBy = {ListValueConstraintValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{com.chenxin.gulimail.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
//定义的值数组
int[] vals() default {};
}
我们还需要指定校验方式ListValueConstraintValidator
:校验我们输入的值是否符合要求
@Constraint(
validatedBy = {ListValueConstraintValidator.class}
)
创建ListValueConstraintValidator类,mall-common/src/main/java/com/zhuang/common/valid/ListValueConstraintValidator.java
查看validatedBy源码,可以看出validatedBy中的元素是ConstraintValidator<?, ?>的子类。
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
/**
* {@link ConstraintValidator} classes implementing the constraint. The given classes
* must reference distinct target types for a given {@link ValidationTarget}. If two
* {@code ConstraintValidator}s refer to the same type, an exception will occur.
* <p>
* At most one {@code ConstraintValidator} targeting the array of parameters of
* methods or constructors (aka cross-parameter) is accepted. If two or more
* are present, an exception will occur.
*
* @return array of {@code ConstraintValidator} classes implementing the constraint
*/
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
因此,我们写ListValueConstraintValidator类时,需要实现ConstraintValidator<?, ?>。
查看ConstraintValidator源码:
public interface ConstraintValidator<A extends Annotation, T> {
/**
* Initializes the validator in preparation for
* {@link #isValid(Object, ConstraintValidatorContext)} calls.
* The constraint annotation for a given constraint declaration
* is passed.
* <p>
* This method is guaranteed to be called before any use of this instance for
* validation.
* <p>
* The default implementation is a no-op.
*
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
default void initialize(A constraintAnnotation) {
}
/**
* Implements the validation logic.
* The state of {@code value} must not be altered.
* <p>
* This method can be accessed concurrently, thread-safety must be ensured
* by the implementation.
*
* @param value object to validate
* @param context context in which the constraint is evaluated
*
* @return {@code false} if {@code value} does not pass the constraint
*/
boolean isValid(T value, ConstraintValidatorContext context);
}
从上可以看出,ConstraintValidator中的泛型A为ListValue注解,T为获得的数据值value。
因此ListValueConstraintValidator类的内容为:
package com.zhuang.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* @author mrzhuang
* @date 2022/4/26 10:23 AM
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
Set<Integer> set = new HashSet<Integer>();
/**
* 初始化
* @param constraintAnnotation
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 真正的校验规则
* 判断是否校验成功
* @param integer
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
//判断输入的值是否是规定的值
return set.contains(integer);
}
}
2、编写配置文件ValidationMessages.properties
编写配置文件,输出message信息
mall-common/src/main/resources/ValidationMessages.properties
内容为:
com.zhuang.common.valid.ListValue.message=必须提交指定的值
3、测试
1、加上自定义的@ListValue注解
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals = {0, 1})
private Integer showStatus;
2、输入的jason字符串:
{"brandId":"","name":"小米","logo":"https://gss0.baidu.com/7LsWdDW5_xN3otqbppnN2DJv/forum/pic/item/fd039245d688d43f394f6821381ed21b0ff43b7b.jpg","sort":1,"showStatus":"2","firstLetter":"x"}
showStatus为2时,不是指定的值!校验错误!
结果:
{
"msg": "参数校验异常",
"code": 10001,
"data": {
"showStatus": "必须提交指定的值"
}
}
当设置showStatus为0或者1时,校验成功!
{
"msg": "success",
"code": 200
}