一、添加依赖
<!--参数校验-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
二、校验注解分类
1.空和非空检查
@NotBlank:只能用于字符串不为null和"",并且字符串调用trim()方法后的length要大于0。
@NotNull:不能为null。
@Null:必须为null。
@NotEmpty:集合对象元素不能为0,集合不能为空。
2.数值检查
@DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
@DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
@Digits(integer,fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内。
@Positive:被注释的元素必须是正数。
@PositiveOrZero:被注释的元素必须是正数或0。
@Negative:被注释的元素必须是负数。
@NegativeOrZero:被注释的元素必须是负数或0。
@Max(value):被注释的元素的值只能小于或等于该值。
@Min(value):被注释的元素的值只能大于或等于该值。
@Range(min,max):被注释的元素必须在min和max之间。
@Length(min,max): 被注释的字符串的长度必须在min和max之间。
3.Boolean 值检查
@AssertTrue:被注释的元素必须为true。
@AssertFalse:被注释的元素必须为false。
4.长度检查
@Size(min,max):判断字符串、集合、数组、Map的长度是否在min和max之间。
5.日期检查
@Past:被注释的元素的日期必须是过去的日期。
@PathOrPresent:被注释的元素的日期必须是过去或现在的日期。
@Future:被注释的元素的日期必须是将来的日期。
@FutureOrPresent:被注释的元素的日期必须是将来或现在的日期。
6.其他
@URL:判断被注释的字符串必须是一个有效的URL。
@SafeHtml:判断提交的HTML是否安全。
三、@Valid和@Validated的区别
@Valid:可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。支持嵌套校验,不支持分组校验。
@Validated:可以添加在类、方法参数、普通方法上,不能用在成员变量上。不支持嵌套校验,支持分组校验。
总的来说,绝大多数场景下,我们使用 @Validated 注解即可。
四、普通测试
1.创建一个用于测试的实体类
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 用户登录请求类
*/
@Data
public class UserLoginRequest {
/**
* 账号
*/
@NotBlank(message = "账号不能为空")
@Length(min = 2, max = 20, message = "账号的长度为2-20位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号的格式为大小写字母和数字")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码的长度为4-16位")
private String password;
}
2.创建测试控制器
import com.example.quartzdemo.request.UserLoginRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 测试
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
/**
* 登录
*
* @param userLoginRequest 使用@Validated校验输入项
*/
@PostMapping("/login")
public String login(@Validated UserLoginRequest userLoginRequest) {
log.info("userLoginRequest:{}", userLoginRequest);
return "登录验证通过";
}
}
3.启动程序,进行测试
我们在postman上进行接口的测试
没有传入password参数,错误异常提示了密码不能为空。
我们传入的username不是大小写字母或数字,错误异常日志提示了账号格式为大小写字母或数字。
我们正确填写了账号和密码,验证通过。
五、分组校验测试
1.我们先创建两个分组接口,一个用于新增一个用于修改。
因为新增和修改的需要的参数不一定相同,比如新增的时候不需要ID,但是修改的时候必须要有ID参数,所以需要使用分组来实现我们的需求。
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 新增用户分组
*/
public interface AddUserGroup {
}
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 修改用户分组
*/
public interface UpdateUserGroup {
}
2.修改用户实体,在实体类中加上新增和修改的分组
import com.example.quartzdemo.interfaces.AddUserGroup;
import com.example.quartzdemo.interfaces.UpdateUserGroup;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 用户登录请求类
*/
@Data
public class UserLoginRequest {
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空", groups = UpdateUserGroup.class)
private Long id;
/**
* 账号
*/
@NotBlank(message = "账号不能为空", groups = {AddUserGroup.class, UpdateUserGroup.class})
@Length(min = 2, max = 20, message = "账号的长度为2-20位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号的格式为大小写字母和数字")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空", groups = {AddUserGroup.class, UpdateUserGroup.class})
@Length(min = 4, max = 16, message = "密码的长度为4-16位")
private String password;
}
我们设置ID只能是修改的时候才会判断是否存在,账号和密码在新增和修改的时候都要判断存在。
3.在控制器中测试
import com.example.quartzdemo.interfaces.AddUserGroup;
import com.example.quartzdemo.interfaces.UpdateUserGroup;
import com.example.quartzdemo.request.UserLoginRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 测试
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
/**
* 登录
*
* @param userLoginRequest 使用@Validated校验输入项
*/
@PostMapping("/login")
public String login(@Validated UserLoginRequest userLoginRequest) {
log.info("userLoginRequest:{}", userLoginRequest);
return "登录验证通过";
}
/**
* 新增用户
*
* @param userLoginRequest 使用@Validated校验输入项 校验的注解中加上了新增分组的参数
*/
@PostMapping("/add")
public String add(@Validated(AddUserGroup.class) UserLoginRequest userLoginRequest) {
log.info("userLoginRequest:{}", userLoginRequest);
return "新增用户验证通过";
}
/**
* 修改用户
*
* @param userLoginRequest 使用@Validated校验输入项 校验的注解中加上了修改分组的参数
*/
@PostMapping("/update")
public String update(@Validated(UpdateUserGroup.class) UserLoginRequest userLoginRequest) {
log.info("userLoginRequest:{}", userLoginRequest);
return "修改用户验证通过";
}
}
我们继续在postman上进行接口的测试
可以看到我们在新增的时候没有传入ID参数校验通过了。
接下来我们现在调试修改的接口,没有传递ID参数。
我们在调试修改接口的时候,没有传递ID参数,验证不能通过了。
最后我们加上ID参数,验证通过。
六、错误提示的友好处理。
1.创建校验不通过的枚举类
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 业务层异常枚举
*/
public enum ServiceExceptionEnum {
SUCCESS(0, "成功"),
ERROR(1, "失败"),
SYS_ERROR(1000, "服务端发生异常"),
MISSING_REQUEST_PARAM_ERROR(1001, "参数缺失"),
INVALID_REQUEST_PARAM_ERROR(1002, "请求参数不合法");
private final String message;
private final int code;
ServiceExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}
public String getMessage() {
return message;
}
public int getCode() {
return code;
}
}
2.统一返回结果实体类
package com.example.quartzdemo.common;
import com.example.quartzdemo.enums.ServiceExceptionEnum;
import java.io.Serializable;
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 统一返回结果实体类
*/
public class CommonResult<T> implements Serializable {
/**
* 错误码
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 返回数据
*/
private T data;
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> CommonResult<T> success(T data) {
CommonResult<T> commonResult = new CommonResult<>();
commonResult.setCode(ServiceExceptionEnum.SUCCESS.getCode());
commonResult.setMessage(ServiceExceptionEnum.SUCCESS.getMessage());
commonResult.setData(data);
return commonResult;
}
/**
* 失败
*
* @param message
* @param <T>
* @return
*/
public static <T> CommonResult<T> error(String message) {
CommonResult<T> commonResult = new CommonResult<>();
commonResult.setCode(ServiceExceptionEnum.ERROR.getCode());
commonResult.setMessage(message);
return commonResult;
}
/**
* 失败
*
* @param message
* @param <T>
* @return
*/
public static <T> CommonResult<T> error(int code, String message) {
CommonResult<T> commonResult = new CommonResult<>();
commonResult.setCode(code);
commonResult.setMessage(message);
return commonResult;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "CommonResult{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}
3.创建全局异常处理类
package com.example.quartzdemo.exception;
import com.example.quartzdemo.common.CommonResult;
import com.example.quartzdemo.enums.ServiceExceptionEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
/**
* @author qinxun
* @date 2023-06-14
* @Descripion: 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 MissingServletRequestParameterException 异常
* <p>
* SpringMVC 参数不正确
*/
@ResponseBody
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {
log.error("[missingServletRequestParameterExceptionHandler]", ex);
// 包装 CommonResult 结果
return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
}
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
log.error("[constraintViolationExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(constraintViolation.getMessage());
}
// 包装 CommonResult 结果
return CommonResult.error(
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
@ResponseBody
@ExceptionHandler(value = BindException.class)
public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
log.info("========进入了 bindException======");
log.error("[bindExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 CommonResult 结果
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public CommonResult MethodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) {
log.info("-----------------进入了 MethodArgumentNotValidException-----------------");
log.error("[MethodArgumentNotValidException]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 CommonResult 结果
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
/**
* 处理其它 Exception 异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
// 记录异常日志
log.error("[exceptionHandler]", e);
// 返回 ERROR CommonResult
return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
ServiceExceptionEnum.SYS_ERROR.getMessage());
}
}
我们重新做postman上进行调试
我们可以看到错误异常的的提示很友好了。