本笔记内容为尚硅谷谷粒商城JSR303数据校验和集中异常处理部分
目录
一、简介
二、SR303数据校验使用步骤
1、引入依赖
2、给参数对象添加校验注解
常见的注解
3、接口参数前增加@Valid 开启校验
三、异常的统一处理
四、分组解决校验
1、创建Groups
2、添加分组
五、自定义校验注解
1、创建约束规则
2、创建约束校验器
3、自定义校验注解校验失败后的返回信息
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
后端在处理前端传过来的数据时,尽管前端表单已经加了校验逻辑,但是作为严谨考虑,在后端对接口传输的数据做校验也必不可少。
使用spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。
Bean Validation 中内置的 constraint
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的 constraint
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内
示例:
Controller 中需要校验的参数Bean前添加 @Valid 开启校验功能,紧跟在校验的Bean后添加一个BindingResult,BindingResult封装了前面Bean的校验结果。
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if (result.hasErrors())
{
Map map=new HashMap<>();
//1.获取校验错误结果
result.getFieldErrors().forEach((item)->{
//获取到错误提示
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();
}
参数校验不通过时,会抛出 BingBindException 异常,可以在统一异常处理中,做统一处理,这样就不用在每个需要参数校验的地方都用 BindingResult 获取校验结果了。
可以在common中新建一个枚举用于存放我们异常类型
例:
package com.atguigu.common.exception;
/**
* @Description: 错误状态码枚举
*
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 002:短信验证码频率太高
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
* 15:用户
*
*
*
**/
public enum BizCodeEnum {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
TO_MANY_REQUEST(10002,"请求流量过大,请稍后再试"),
SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),
PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
USER_EXIST_EXCEPTION(15001,"存在相同的用户"),
PHONE_EXIST_EXCEPTION(15002,"存在相同的手机号"),
NO_STOCK_EXCEPTION(21000,"商品库存不足"),
LOGINACCT_PASSWORD_EXCEPTION(15003,"账号或密码错误"),
;
private Integer code;
private String message;
BizCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
在exception包下创建一个异常类
例:
package com.atguigu.gulimall.product.exception;
import com.atguigu.common.exception.BizCodeEnum;
import com.atguigu.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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* Description: 集中处理异常
*
*/
@Slf4j
/*
@RestController
@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
*/
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControlleAdvice {
//捕获异常指定异常 MethodArgumentNotValidException
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map map=new HashMap<>();
bindingResult.getFieldErrors().forEach((item)->{
map.put(item.getField(),item.getDefaultMessage());
});
return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(), BizCodeEnum.VAILD_EXCEPTION.getMessage()).put("data",map);
}
//任意异常类型
@ExceptionHandler(value = Throwable.class)
public R handException(Throwable throwable){
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMessage());
}
}
然后只需要校验的参数Bean前添加 @Valid 开启校验功能就可以了
例:
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
新增和修改对于实体的校验规则是不同的,例如id是自增的时,新增时id要为空,修改则必须不为空;新增和修改,若用的恰好又是同一种实体,那就需要用到分组校验。
校验注解都有一个groups属性,可以将校验注解分组。
从源码可以看出 groups 是一个Class>类型的数组,那么就可以创建一个Groups
或
package com.xxxx.common.validator.group;
/**
* 新增数据 Group
*/
public interface AddGroup {
}
package com.xxxx.common.validator.group;
/**
* 更新数据 Group
*
*/
public interface UpdateGroup {
}
给参数对象的校验注解添加分组
例:
package com.atguigu.gulimall.product.entity;
import com.atguigu.common.valid.AddGroup;
import com.atguigu.common.valid.ListValue;
import com.atguigu.common.valid.UpdateGroup;
import com.atguigu.common.valid.UpdateStatusGroup;
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.*;
/**
* 品牌
*
*/
@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})
@ListValue(vals = {0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups = {AddGroup.class})
//@Pattern自定义可以传入正则表达式
@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 中原先的@Valid不能指定分组 ,需要替换成@Validated
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(/*@Valid*/ @Validated({AddGroup.class}) @RequestBody BrandEntity brand/*, BindingResult result*/){
/*if (result.hasErrors())
{
Map map=new HashMap<>();
//1.获取校验错误结果
result.getFieldErrors().forEach((item)->{
//获取到错误提示
String message = item.getDefaultMessage();
//获取到错误的属性名
String field=item.getField();
map.put(field,message);
});
return R.error(400,"提交数据不合法").put("data",map);
}else{
brandService.save(brand);
}*/
brandService.save(brand);
return R.ok();
}
虽然JSR303和springboot-validator 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要自定义校验注解。
假设一个字段规定使用0和1两种状态
点进去@NotNull查看 ,发现这些接口都有这些相同的信息
我们可以跟着创建约束规则ListValue
/**
* @Description: 自定义注解
**/
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class }) /*关联注解器-可以指定多个*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
// 默认会找ValidationMessages.properties
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
// 可以指定数据只能是vals数组指定的值
int[] vals() default { };
}
新建自定义校验器ListValuaConstraintValidator,并实现ConstraintValidator
/**
* @Description: 校验器
**/
public class ListValueConstraintValidator implements ConstraintValidator {
private Set set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 判断是否效验成功
* @param value 需要效验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
//判断是否有包含的值
boolean contains = set.contains(value);
return contains;
}
}
新建配置文件ValidationMessages.properties,定义我们自定义校验注解校验失败后的返回信息
com.xxx.common.valid.ListValue.message = 必须提交指定参数
例:
com.atguigu.common.valid.ListValue.message=必须提交指定的值
记得在刚刚编写好的自定义注解ListVaue中关联我们自定义的检验器
最后我们在使用@ListValue时就可以固定设定只能接收哪些值了
测试
JSR303 * 1)、给Bean添加校验注解:javax.validation.constraints,并定义自己的message提示 * 2)、开启校验功能@Valid * 效果:校验错误以后会有默认的响应; * 3)、给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果 * 4)、分组校验(多场景的复杂校验) * 1)、 @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class}) * 给校验注解标注什么情况需要进行校验 * 2)、@Validated({AddGroup.class}) * 3)、默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效; * * 5)、自定义校验 * 1)、编写一个自定义的校验注解 * 2)、编写一个自定义的校验器 ConstraintValidator * 3)、关联自定义的校验器和自定义的校验注解 * @Documented * @Constraint(validatedBy = { ListValueConstraintValidator.class【可以指定多个不同的校验器,适配不同类型的校验】 }) * @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) * @Retention(RUNTIME) * public @interface ListValue {统一的异常处理 * @ControllerAdvice * 1)、编写异常处理类,使用@ControllerAdvice。 * 2)、使用@ExceptionHandler标注方法可以处理的异常。
结束!