文章目录
- 前言
- 一、引入validation 依赖
- 二、validation中的注解说明
-
- (1)@Validated
- (2)@Valid
- (3)@NotNull
- (4)@NotBlank
- (5)@NotEmpty
- (6)@Pattern
- (7) @Email
- (8)@Size
- 三、validation的使用
-
- 1.一般校验
- 2.嵌套验证
- 3.分组校验
- 4.自定义校验注解
- 5.反射加自定义注解实现动态校验
- 四、validation 校验失败处理
- 总结
前言
在项目开发过程,后端经常会对前端传递的参数进行各种校验,只有在校验通过时,才会执行后续的业务代码,否则抛出异常信息给前端。当参数较多时,刚开始会使用大量的 if…else…来逐一对参数进行检验,或则使用策略模式来方法来减少 if…else…的检验代码。但是将这些代码写在控制层或则业务层都会让代码过于臃肿。而且如果后期参数校验变了,或则参数不需要校验时,还需要大量删除,或修改检验参数的逻辑代码,因此采用 validation来对参数进行校验简化代码。
一、引入validation 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.3.1</version>
</dependency>
二、validation中的注解说明
(1)@Validated
用于在 Spring 控制器层方法参数进行验证
@PostMapping("/validator")
public String ValidatorTest(@RequestBody @Validated UserParam user) {
return "success";
}
(2)@Valid
是 Java 标准的一部分,用于声明需要对嵌套对象进行递归验证。在 Spring 框架中,通常用于处理复杂对象结构的验证,如验证对象中的集合或对象引用的属性
@Data
public class EmpParam implements Serializable {
@NotBlank(message = "员工姓名不能为空")
private String empName;
@NotBlank(message = "员工手机号不能为空")
private String empPhone;
@Valid
@NotNull(message = "部门信息不能为空")
private DeptParam deptParam;
}
@Data
public class DeptParam implements Serializable {
@NotNull(message = "部门ID不能为空")
private Long deptId;
@NotBlank(message = "部门名称不能为空")
private String deptName;
}
(3)@NotNull
注解用于检查被注解的元素值不为 null。适用于字符串、集合、Map 等任何对象类型,但不适用于基本数据类型(如 int、long 等)
常用于检查对象是否为 null
@NotNull(message = "员工ID不能为空")
private Long empId;
@NotNull(message = "部门信息不能为空")
private DeptParam deptParam;
(4)@NotBlank
用于检查被注解的字符串元素不为 null 且去除两端空白字符后长度大于 0。只适用于字符串类型。
@NotBlank(message = "员工身份证号不能为空")
private String empIdCard;
(5)@NotEmpty
注解用于检查被注解的元素不为 null 且不为空,适用于字符串、集合、Map 等。
如果是字符串,相当于同时检查 null 和长度大于 0
@NotEmpty
private List<String> emails;
(6)@Pattern
指定字段必须符合指定的正则表达式
@Pattern(regexp ="^1[3|4|5|6|7|8|9][0-9]d{8}$")
private String phone
(7) @Email
指定字段必须符合Email格式。
@Email
private String email;
(8)@Size
指定字段的长度范围
@Size(min = 6, max = 20)
private String password;
三、validation的使用
1.一般校验
一般校验:这里指的是,后端参数是一个实体类对象来接收参数,并且校验实体类中各个属性(被注解标注过的)
@RestController
@RequestMapping("/test")
public class ValidatorTestController {
@PostMapping("/validator")
public String ValidatorTest(@RequestBody @Validated UserParam user) {
UserParam contractParam = new UserParam();
BeanUtils.copyProperties(user, contractParam);
System.out.println(contractParam);
return "success";
}
}
实体类中的属性分别添加注解进行校验
@Data
public class UserParam {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度在6-20位之间")
private String password;
@NotBlank(message = "手机号不能为空")
private String phone;
@NotBlank(message = "身份证不能为空")
private String idCard;
@NotBlank(message = "身份证姓名不能为空")
private String idCardName;
@NotBlank(message = "车牌号不能为空")
private String carNumber;
@NotBlank(message = "银行卡号不能为空")
private String bankCardNumber;
}
用postman测试注解是生效的。注意响应的结果,是将所有参数没校验通过都一并响应给前端,方便前端处理。
2.嵌套验证
嵌套检验:后端接收参数是个实体类对象,并且实体类对象中还有一个实体或者是一个List类型对象
比如:添加员工信息时,为员工分配部门
控制层一定得使用 @Validated标注参数
@RestController
@RequestMapping("/emp")
public class EmpController {
@PostMapping("/add")
public String addEmp(@RequestBody @Validated EmpParam empParam) {
System.out.println(empParam);
return "success";
}
}
员工实体类参数:DeptParam 必须加上 @Valid注解才能校验 DeptParam 中的字段(被注解标注的)
@Data
public class EmpParam implements Serializable {
@NotBlank(message = "员工姓名不能为空")
private String empName;
@NotBlank(message = "员工手机号不能为空")
private String empPhone;
@NotBlank(message = "员工邮箱不能为空")
private String empEmail;
@NotBlank(message = "员工身份证号不能为空")
private String empIdCard;
@Valid
@NotNull(message = "部门信息不能为空")
private DeptParam deptParam;
}
@Data
public class DeptParam implements Serializable {
@NotNull(message = "部门ID不能为空")
private Long deptId;
@NotBlank(message = "部门名称不能为空")
private String deptName;
}
使用postman测试
3.分组校验
分组校验指的是,不通场景,参数检验方式不同。比如添加员工时候,由于empId在数据库是自增的,所以添加员工时 empParam 参数中empId 可以为空,但是在修改用户时,就要求empId不能为空。前提是 emp 新增和修改接口来接收前端的参数都是同一个 empParam 类
(1)自定义分组接口
public interface ValidationGroup {
interface Create extends Default{}
interface Update extends Default{}
}
(2)实体类中属性添加校验分组
@Data
public class EmpParam implements Serializable {
@NotNull(message = "员工ID不能为空", groups = {ValidationGroup.Update.class}) //注意查看添加接口和修改接口验证分组校验是否正确
private Long empId;
@NotBlank(message = "员工姓名不能为空",groups = {ValidationGroup.Create.class})
private String empName;
@NotBlank(message = "员工手机号不能为空")
private String empPhone;
@NotBlank(message = "员工邮箱不能为空")
private String empEmail;
@NotBlank(message = "员工身份证号不能为空")
private String empIdCard;
@Valid
@NotNull(message = "部门信息不能为空")
private DeptParam deptParam;
}
(3)控制层测试类
@RestController
@RequestMapping("/emp")
public class EmpController {
@PostMapping("/add")
public String addEmp(@RequestBody @Validated({ValidationGroup.Create.class}) EmpParam empParam) {
System.out.println(empParam);
return "success";
}
@PutMapping("/update")
public String updateEmp(@RequestBody @Validated(ValidationGroup.Update.class) EmpParam empParam) {
System.out.println(empParam);
return "success";
}
}
(4)使用posman测试
4.自定义校验注解
从上述方法中发现 validation这个框架所提供的校验注解,只是一些基本的判空,字符长度的校验。在日常开发中可能需要校验:比如手机号不能为空同时,必须是11位数字,以及添加用户时,用户手机号必须唯一(需要查询数据库)等等。显然是不满足日常开发的需求。因此我们可以根据 自身的需求来做校验注解。
接下来我们将自定义一个注解来专门校验身份证号
@Constraint(validatedBy = IdCardNumberValidator.class) // @IdCardNumber 注解校验类的具体实现类
@Target({ElementType.FIELD,ElementType.METHOD}) // 表示注解可以应用于字段和方法上
@Retention(RetentionPolicy.RUNTIME) // 表示注解在运行时保留,因此可以通过反射机制读取。强调,可以通过反射获取,
public @interface IdCardNumber {
String message() default "身份证错误";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
}
IdCardNameValidator 校验类来实现 ConstraintValidator接口即可
import com.personal.validation.annotations.IdCardName;
import com.personal.validation.utils.ValidatorUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class IdCardNameValidator implements ConstraintValidator<IdCardName, String> {
@Override
public void initialize(IdCardName constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String idCardName, ConstraintValidatorContext constraintValidatorContext) {
return ValidatorUtils.isValidName(idCardName);
}
}
校验工具类
public class ValidatorUtils {
//判断身份证号是否有效
/**
* '^':表示匹配字符串的开头
* '[1-9]':表示匹配1-9之间的数字,确保身份证号码的前6位不为0
* '\d{5}':表示匹配5位数字,确保身份证号码的前6位为数字,用于匹配地区码
* '(18|19|([23]\d))':表示匹配18或19或20-23之间的数字,确保身份证号码的前2位为18或19或20-23之间的数字,用于匹配年份
* '\d{2}':表示接下来的2位是月份,范围是01-12之间的数字
* '((0[1-9])|(1[0-2]))':表示匹配月份,范围是01-12之间的数字
* '(([0-2][1-9])|10|20|30|31)':表示日期,前两位0-2表示01-29,10、20、30、31分别单独列出
* '\d{3}':表示匹配3位数字,确保身份证号码的第18位为数字,用于匹配顺序码
* '[0-9Xx]':表示匹配0-9或X或x,确保身份证号码的第17位为数字或X或x,用于匹配校验位
*
* @param idCard
* @return
*/
public static boolean isValidIdCard(String idCard) {
return Pattern.matches("^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$", idCard);
}
}
实体类上加上 @IdCardNumber 注解
@Data
public class UserParam {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度在6-20位之间")
private String password;
@NotBlank(message = "手机号不能为空")
private String phone;
@NotBlank(message = "身份证不能为空")
@IdCardNumber(message = "身份证不正确")// 自定义注解来校验,并不会跟原有注解冲突
private String idCard;
@NotBlank(message = "身份证姓名不能为空")
private String idCardName;
@NotBlank(message = "车牌号不能为空")
private String carNumber;
@NotBlank(message = "银行卡号不能为空")
private String bankCardNumber;
}
测试自定义注解是生效的,虽然身份证没空,但是校验没能通过的自定义注解的校验类
以上只是简单写了一个案例,如何自定义一个注解,以后可以自定义一个注解来校验前端传递过来的参数必须在数据库是唯一的,例如手机号,不能多个用户使用同个手机号,身份证也是如此。
5.反射加自定义注解实现动态校验
现在有这样一个场景,系统中客户表中有两个类型客户:个体工商户和企业客户两类,系统的客户认证只是一个接口,但是呢,企业客户和个体客户校验信息不一样,企业校验是,企业名称,企业信用代码,法人,营业执照等。个体工商户验证时客户身份证,名字,电话等。简单来说就是通过一个 authType 认证类型 字段 来判断那些那些参数不能为空
(1)添加客户实体类参数
如果前端传递的 authType =1,就校验 SingleCustomerParam 对象不能为空,并且 SingleCustomerParam 类中被 @NotBlank注解标注也不能为空。如果authType = 2,就检验 FirmCustomerParam 对象不能为空,并且 FirmCustomerParam 类中被 @NotBlank注解标注也不能为空。如果authType != 1 或则 2 就返回认证类型错误给前端
@Data
public class AddCustomerParam implements Serializable {
/**
* 客户类型 1个体工商户 2:企业客户
*/
@NotBlank(message = "客户类型不能为空")
private String authType;
/**
* 1个体工商户
*/
private SingleCustomerParam singleCustomerParam;
/**
* 2企业客户
*/
private FirmCustomerParam firmCustomerParam;
}
SingleCustomerParam 实体类参数类
@Data
public class SingleCustomerParam implements Serializable {
/**
* 客户名称
*/
// @NotBlank(message = "客户名称不能为空") //
private String customerName;
/**
* 客户身份证照片
*/
@NotBlank(message = "客户身份证照片不能为空")
private String idCardImg;
/**
* 认证图片和视频
*/
@NotBlank(message = "认证图片和视频不能为空")
private String verifyImg;
/**
* 客户真是姓名
*/
@NotBlank(message = "客户真是姓名不能为空")
private String realName;
/**
* 客户身份证号
*/
@NotBlank(message = "客户身份证号不能为空")
private String idCard;
/**
* 客户手机号
*/
@NotBlank(message = "客户手机号不能为空")
private String phone;
}
FirmCustomerParam 实体参数类
@Data
public class FirmCustomerParam implements Serializable {
/**
* 企业营业执照
*/
@NotBlank(message = "企业营业执照不能为空")
private String businessLicense;
/**
* 企业名称
*/
@NotBlank(message = "企业名称不能为空")
private String firmName;
/**
* 企业信用代码
*/
@NotBlank(message = "企业信用代码不能为空")
private String creditCode;
/**
* 企业地址
*/
@NotBlank(message = "企业地址不能为空")
private String address;
/**
* 企业联系人
*/
@NotBlank(message = "企业联系人不能为空")
private String linkman;
/**
* 企业联系人电话
*/
@NotBlank(message = "企业联系人电话不能为空")
private String mobile;
}
(2)自定义参数校验注解 @CustomerParamTwo
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomerParamValidatorTwo.class)//注解校验类
public @interface CustomerParamTwo {
String message() default "Invalid customer parameters";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
CustomerParamValidatorTwo 实现 ConstraintValidator接口 重写具体校验规则
import com.personal.validation.annotations.CustomerParamTwo;
import com.personal.validation.param.AddCustomerParam;
import com.personal.validation.param.FirmCustomerParam;
import com.personal.validation.param.SingleCustomerParam;
import com.personal.validation.utils.ValidatorUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.constraints.NotBlank;
import java.lang.reflect.Field;
public class CustomerParamValidatorTwo implements ConstraintValidator<CustomerParamTwo, AddCustomerParam> {
@Override
public void initialize(CustomerParamTwo constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(AddCustomerParam customerParam, ConstraintValidatorContext constraintValidatorContext) {
String authType =customerParam.getAuthType();
if ("1".equals(authType)) {
if(customerParam.getSingleCustomerParam() instanceof SingleCustomerParam){
return customerParam.getSingleCustomerParam() != null
&& isValidSingleCustomerParam(customerParam.getSingleCustomerParam(), constraintValidatorContext);
}else
//当authType = 1 时,singleCustomerParam不能为空
addConstraintViolation(constraintValidatorContext, "singleCustomerParam","个人客户信息不能为空");
return false;
}else if ("2".equals(authType)) {
if(customerParam.getFirmCustomerParam() instanceof FirmCustomerParam){
return customerParam.getFirmCustomerParam() != null
&& isValidFirmCustomerParam(customerParam.getFirmCustomerParam(), constraintValidatorContext);
}else
//当authType = 2 时,firmCustomerParam不能为空
addConstraintViolation(constraintValidatorContext, "firmCustomerParam","企业客户信息不能为空");
return false;
}
//单独校验authType类型
if(!("1".equals(authType) || "2".equals(authType) || "".equals(authType))){
addConstraintViolation(constraintValidatorContext, "authType","认证类型不正确");
return false;
}
return false;
}
private boolean isValidSingleCustomerParam(SingleCustomerParam param, ConstraintValidatorContext context) {
boolean isValid = true ;
Field[] fields = SingleCustomerParam.class.getDeclaredFields();//反射获取对象的所有字段
for (Field field : fields) {
if (field.isAnnotationPresent(NotBlank.class)) {//获取属性上是否标注了校验注解
// 如果字段有 @NotBlank 注解,则进行非空验证
if (!validateField(param, field, context)) {
isValid = false;
}
}
}
if(!ValidatorUtils.isValidPhoneNumber(param.getPhone())){
addConstraintViolation(context, "phone","手机号不正确");
isValid = false;
}
return isValid;
}
private boolean isValidFirmCustomerParam(FirmCustomerParam param, ConstraintValidatorContext context) {
boolean isValid = true ;
Field[] fields = FirmCustomerParam.class.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(NotBlank.class)) {
// 如果字段有 @NotBlank 注解,则进行非空验证
if (!validateField(param, field, context)) {
isValid = false;
}
}
}
return isValid;
}
// 字段校验不通过后,获取校验错误信息
private boolean validateField(Object param, Field field, ConstraintValidatorContext context) {
try {
field.setAccessible(true);
Object value = field.get(param);
if (value == null || value.toString().trim().isEmpty()) {
String message = field.getAnnotation(NotBlank.class).message();
addConstraintViolation(context, field.getName(),message);
return false;
}
return true;
} catch (IllegalAccessException e) {
return false;
}
}
//将所有校验不通过的错误信息,封装返回给前端
private void addConstraintViolation(ConstraintValidatorContext context, String field, String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(field)
.addConstraintViolation();
}
}
校验控制层接口类
@RestController
@RequestMapping("/customer")
public class CustomerController {
/**
* @param addCustomerParam
* @return
*/
@PostMapping("/add")
public String addCustomer(@RequestBody @Validated AddCustomerParam addCustomerParam) {
System.out.println(addCustomerParam);
// 客户认证参数全部交给自定义注解校验,我们后续只关注业务代码,将 检验逻辑 与 业务代码逻辑 分离
return "add customer success";
}
}
在校验类加上自定义的注解 @CustomerParamTwo
@Data
@CustomerParamTwo
public class AddCustomerParam implements Serializable {
/**
* 客户类型 1个体工商户 2:企业客户
*/
@NotBlank(message = "客户类型不能为空")
private String authType;
/**
* 1个体工商户
*/
private SingleCustomerParam singleCustomerParam;
/**
* 2企业客户
*/
private FirmCustomerParam firmCustomerParam;
}
接下来我们进行测试:
测试authType 为空 或则 authType !=1 或则 2
authType = 1 校验 singleCustomerParam 个体工商户参数
校验成功:
authType = 2 校验 firmCustomerParam 企业客户参数
四、validation 校验失败处理
从上述案例中,校验失败错误信息原本抛出的异常不是如此的,validation 校验异常需要自己手动拦截并封装错误信息返回。接下来将如何捕获 validation 的异常信息,并封装成一个对象返回给前端
(1)自定义封装结果类
public class R {
private int code;
private String message;
private Object data;
public R() {
}
public R(int code, String message) {
this.code = code;
this.message = message;
}
public static R ok() {
return new R(200, "Success");
}
public static R fail(int code, String message) {
return new R(code, message);
}
// Getters and setters
// You may want to add additional methods for data handling
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
(2)自定义ValidationException 异常
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
(3)自定义拦截器
直接在官网复制 validation 拦截器
@ControllerAdvice
public class CustomGlobalExceptionHandler {
// Handle ConstraintViolationException
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public R handleValidationException(ConstraintViolationException ex) {
List<String> errors = new ArrayList<>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getPropertyPath() + ": " + violation.getMessage());
}
return R.fail(HttpStatus.BAD_REQUEST.value(), errors.toString());
}
// Handle MethodArgumentTypeMismatchException
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
String error = ex.getName() + " should be of type " + ex.getRequiredType().getName();
return R.fail(HttpStatus.BAD_REQUEST.value(), error);
}
// Handle BindException
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public R handleBindException(BindException ex) {
List<String> errors = new ArrayList<>();
for (FieldError error : ex.getFieldErrors()) {
errors.add(error.getField() + ": " + error.getDefaultMessage());
}
return R.fail(HttpStatus.BAD_REQUEST.value(), errors.toString());
}
// Handle custom ValidationException
@ExceptionHandler(ValidationException.class) // 在这里进行拦截的
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public R handleValidationException(ValidationException ex) {
return R.fail(HttpStatus.BAD_REQUEST.value(), ex.getMessage()); //拦截运行错误时的状态吗以及错误信息
}
// Handle generic exceptions
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public R handleException(Exception ex) {
return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
}
}
总结
例如:以上就是关于日常开发过程参数校验及封装错误信息返回给前端