本系列博客基于B站谷粒商城,只作为本人学习总结使用。这里我会比较注重业务逻辑的编写和相关配置的流程。有问题可以评论或者联系我互相交流。原视频地址谷粒商城雷丰阳版。本人git仓库地址Draknessssw的谷粒商城
为某个实体类(brandEntity)的某个字段进行数据校验,首先添加相关注解@NotNull,@NotBlank,@URL等等
在需要校验的Controller上添加@Valid注解开启校验
而某些校验规则会在不同的情况下触发,甚至会有@Null和@NotNull同时出现的情况。这时候就可以对校验规则进行分组。而组是不需要实现的接口。
package com.xxxx.common.validator.group;
/**
* 新增数据 Group
*
* @author Mark [email protected]
*/
public interface AddGroup {
}
package com.xxxx.common.validator.group;
/**
* 更新数据 Group
*
* @author Mark [email protected]
*/
public interface UpdateGroup {
}
而后对实体类的相应字段指定对应校验规则的组
package com.xxxx.gulimall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import com.xxxx.common.valid.ListValue;
import com.xxxx.common.valid.UpdateStatusGroup;
import com.xxxx.common.validator.group.AddGroup;
import com.xxxx.common.validator.group.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author draknessssw
* @email [email protected]
* @date 2022-06-18 03:26:29
*/
@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地址
*/
@NotEmpty(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(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
/**
* 排序
*/
@NotNull(message = "排序必须为正整数", groups = {AddGroup.class})
@Min(value = 0, message = "排序必须为正整数", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;
}
然后在相应需要校验实体类字段的方法使用@Validated注解进行指定校验字段的校验规则组
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/*, BindingResult result*/){
// if (result.hasErrors()) {
// Map map = new HashMap<>();
// // 获取校验的错误结果
// result.getFieldErrors().forEach((item) -> {
// // 获取到错误提示FieldError
// String message = item.getDefaultMessage();
// // 获取错误的属性名字
// String field = item.getField();
// map.put(field, message);
// });
// return R.ok().error(400, "提交的数据不合法").put("data", map);
// }else {
// brandService.save(brand);
// return R.ok();
// }
brandService.save(brand);
return R.ok();
}
假设一个字段规定使用0和1两种状态
随便点进去一个注解,发现他们的接口均有这些信息
所以我们的自定义注解也需要指定一个接口,这里指定在公共类
package com.xxxx.common.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;
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})// 可以标注在哪些位置 方法、参数、构造器
@Retention(RUNTIME)// 可以在什么时候获取到
@Documented//
@Constraint(validatedBy = {ListValueConstraintValidator.class})// 使用哪个校验器进行校验的(这里不指定,在初始化的时候指定)
public @interface ListValue {
// 默认会找ValidationMessages.properties
String message() default "{com.xxxx.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 可以指定数据只能是vals数组指定的值
int[] vals() default {};
}
缺失依赖,导入依赖
<!--自定义注解-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
错误消息提示可以自己写一个配置类来指定
我这里存在一个转码的问题,所以转成了utf-8的消息字符,也可以在设置里面设置编码格式
com.xxxx.common.valid.ListValue.message=\u5FC5\u987B\u63D0\u4EA4\u6307\u5B9A\u7684\u503C
而我们的自定义注解还有vals属性,加入自定义接口即可
接下来编写校验器
主要是对定义的vals属性进行初始化和数据校验
package com.xxxx.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
if (vals != null && vals.length != 0) {
for (int val : vals) {
set.add(val);
}
}
}
/**
* 校验逻辑
* @param value 需要校验的值
* @param context 上下文
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);// 如果set length==0,会返回false
}
}
这个类用来集中扫描异常进行处理
package com.xxxx.gulimall.product.exception;
import com.xxxx.common.exception.BizCodeEnume;
import com.xxxx.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;
@Slf4j
//@RestController
//@ControllerAdvice
//相当于以上两个的复合注解
@RestControllerAdvice(basePackages = "com.xxxx.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
/**
* 统一处理异常,可以使用Exception.class先打印一下异常类型来确定具体异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{}, 异常类型:{}", e.getMessage(), e.getClass());
BindingResult result = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
// 获取校验的错误结果
result.getFieldErrors().forEach((item) -> {
// 获取错误的属性名字 + 获取到错误提示FieldError
errorMap.put(item.getField(), item.getDefaultMessage());
});
return R.ok().error(BizCodeEnume.VALID_EXCEPTION.getCode(), BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleValidException(Throwable throwable) {
log.error("Throwable错误,未处理:" + throwable);
return R.ok().error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
首先,@RestControllerAdvice相当于@RestController和@ControllerAdvice的复合注解,@ControllerAdvice用来集中处理异常,具体的处理交给@ExceptionHandler。而@ExceptionHandler的value属性可以指定具体的异常,最后统一返回一个json数据,一般是错误消息,状态码,错误数据。
可以抽取一个公共枚举类编写错误信息,同时编写set和get方法便于后续操作。
package com.xxxx.common.exception;
/**
* 错误码和错误信息定义类
* 1.错误码定义规则为5为数字
* 2.前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用001:系统未知
* 异常
* 3.维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10:通用
* 001:参数格式校验
* 002:短信验证频率太高
* 11:商品
* 12:订单
* 13:购物车
* 14:物流
* 21:库存
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000, "系统未知异常"),
VALID_EXCEPTION(10001, "参数格式验证失败"),
SMS_CODE_EXCEPTION(10002, "验证码获取频率太高,请稍后再试"),
TO_MANY_REQUEST(10002, "请求流量过大,请稍后再试"),
IDEMPOTENT_TOKEN_CREATE_EXCEPTION(10003, "令牌创建失败"),
IDEMPOTENT_TOKEN_VERIFY_EXCEPTION(10003, "令牌校验失败"),
PRODUCT_UP_EXCEPTION(11000, "商品上架异常"),
USER_EXIST_EXCEPTION(15001, "存在相同的用户"),
PHONE_EXIST_EXCEPTION(15002, "存在相同的手机号"),
LOGINACCT_PASSWORD_EXCEPTION(15003, "账号或密码错误"),
NO_STOCK_EXCEPTION(21000, "商品库存不足");
private int code;
private String msg;
BizCodeEnume(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
要是处理一个具体的异常(如方法参数无效异常),那么错误的状态码和消息可以通过自定义的枚举类中定义后获取,而具体的错误的属性名和提示可以通过遍历捕获的异常结果来获取。
处理不了的异常可以写一个总的异常处理方法来处理所有未捕获的异常。