为什么要在服务器校验数据?
对于请求数据的校验通常会在前端数据进行校验,但是 “前端防君子、后端防小人”,为了避免恶意跳过前端使用类似postman等工具向后端发起请求导致数据不规范,所以可以使用JSR303便捷的校验后端数据的正确性。
1. JSR303简单使用
JSR303校验
JSR是Java Specification Requests的缩写,意思是Java规范提案,JSR-303是JAVA EE6中的一项子规范,叫做Bean Validation即,JSR 303,Bean Validation规范 ,为Bean验证定义了元数据模型和API。 默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展。
如何导入使用
maven导入坐标
<!-- JSR参数校验器 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
可以通过javax.validation.constraints
包下查看相应注解
列举常用注解的使用
注解 | 解释 |
---|---|
@NotNull | 属性不能为null,无法查检长度为0的字符串 |
@NotBlank | 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格 |
@NotEmpty | 该字段不能为null或"" |
被注释的元素必须是电子邮箱地址 | |
@Digits | 被注释的元素必须是数字 |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@Range | 被注释的元素必须在合适的范围内 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
使用步骤
① 在需要校验的实体类添加注解
先随便进入其中一个注解看看源码
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotNull.List.class)
@Documented
@Constraint(
validatedBy = {}
)
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotNull[] value();
}
}
可以看到 message
默认使用 javax.validation.constraints.NotNull.message
这个信息可以在 ValidationMessages_zh_CN.properties
下找到相应的 message
同理在添加注解的时候可以通过message
进行自定义信息覆盖掉默认的信息
javax.validation.constraints.AssertFalse.message = 只能为false
javax.validation.constraints.AssertTrue.message = 只能为true
javax.validation.constraints.DecimalMax.message = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message = 必须大于或等于{value}
javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message = 最大不能超过{value}
javax.validation.constraints.Min.message = 最小不能小于{value}
javax.validation.constraints.Negative.message = 必须是负数
javax.validation.constraints.NegativeOrZero.message = 必须是负数或零
javax.validation.constraints.NotBlank.message = 不能为空
javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.NotNull.message = 不能为null
javax.validation.constraints.Null.message = 必须为null
javax.validation.constraints.Past.message = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message = 必须是正数
javax.validation.constraints.PositiveOrZero.message = 必须是正数或零
javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间
接下来为实体类属性进行注解
/**
* 品牌实体类
*
* @author 兴趣使然的L
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id")
@Null(message = "新增不能指定id")
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@NotBlank
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
② 在对应controller的接口方法上加上校验注解@Valid
,开启校验
@PostMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
③ 对校验报错进行全局异常捕获处理
通过postman对接口进行测试 当不进行捕获处理时返回的结果如下
{
"timestamp": "2020-04-29T09:36:04.125+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "品牌名必须非空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
可以看到返回的信息又臭又长(虽然很详细),并非真正想要获取的信息,可以通过异常处理返回自己指定格式的数据。
/**
* 集中处理所有异常(全局异常处理)
*
* @author 兴趣使然的L
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.xqsr.gulimall.product.controller")
public class ExceptionControllerAdvice {
/**
* 对数据校验失败的异常进行捕获并返回数据
*/
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
// BindingResult 可以获取校验的结果
BindingResult bindingResult = e.getBindingResult();
// 自定义封装格式
Map<String,String> errorMap = new HashMap<>();
// 遍历属性错误信息通过自定义的map进行挨个封装
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
// 返回数据
return R.error(400, "参数格式校验失败").put("data",errorMap);
}
}
再次通过postman进行测试
2. 分组校验使用(适合多场景校验)
对于实体类的操作可能有新增和修改等操作,对于不同的操作可能会存在不同的情况,如新增时要求某一个属性不能为空,但修改时允许该属性可以为空,此时就需要按不同场景校验,此时就可以使用分组校验属性
group
使用步骤
① 创建分组接口:AddGroup.java
和UpdateGroup.java
通过上面的源码分析可以看到注解中具有 Class<?>[] groups() default {};
属性,可以看到这是一个类属性,所以需要创建类来标识分组,类中可以不带任何内容
/**
* 添加操作分组(用于验证操作时指定分组操作)
*
* @author 兴趣使然的L
*/
public interface AddGroup {
}
/**
* 修改操作分组(用于验证操作时指定分组操作)
*
* @author 兴趣使然的L
*/
public interface UpdateGroup {
}
② 在校验属性的注解上添加 group
属性分组
/**
* 品牌实体类
*
* @author 兴趣使然的L
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups={AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups={AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
private Integer sort;
}
③ 在controller的接口方法上添加 @Validated(XXX.class)
注解修饰参数
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
通过如上三个步骤就可以完成分组校验操作,对不同的操作可以进行不同的校验处理
3. 自定义校验注解使用
可能存在对于某一些属性的校验,使用给定的注解无法满足校验要求,此时就需要自定义注解
需求:对于实体类的showStatus
属性进行限定值只能在0和1中取值
使用步骤
① 创建自定义注解
/**
* 自定义验证注解
* ListValue 用于限制 Integer 类型的属性只能使用vals指定范围的值
*
* @author 兴趣使然的L
*/
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class }) // 这个注解用来指定校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
// 报错信息
String message() default "{com.xqsr.common.valid.ListValue.message}";
// 分组信息
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
// 自定义限制信息(只允许使用在vals数组内的值)
int[] vals() default { };
}
补充:@Constraint(validatedBy = { XXX1.class, XXX2.class })
可以匹配多个注解器
② 创建ValidationMessages.properties
用来自定义默认信息
可以看到上述的报错信息使用的是com.xqsr.common.valid.ListValue.message
(自定义信息)
com.xqsr.common.valid.ListValue.message=未使用指定范围的值
注意: 小bug会出现乱码问题,需要更改字符编码(可以在idea的设置中的文件编码处更改默认编码为UTF-8,更改后再创建properties文件即可)
③ 创建自定义的校验器(实现 ConstraintValidator<A extends Annotation, T>
接口,其中A指自定义的注解, T指该注解需要修饰的类型)
实现接口的两个方法:初始化方法 与 校验方法
因为需求是限定值的范围,所以这里直接使用HashSet集合进行校验
/**
* 自定义验证器
*
* @author 兴趣使然的L
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private final Set<Integer> set = new HashSet<>();
/**
* 初始化方法
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 校验方法
* @param value 需要校验的值
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
④ 使用注解修饰实体类的属性
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
// 通过自定义的vals属性限定只能是0或1
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
到这里自定义注解就已经可以实现了