引入依赖
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
全局处理异常
我们做项目的时候,有时候请求会返回http400或者500的状态码,可能是参数校验失败或者程序本身抛出异常,其实我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码和异常信息去区分系统的异常情况。在实际的项目开发中,通常会用统一异常处理来返回一个更友好的提示。
我们先定义一个Result,统一所有接口的返回结果结构
mport java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Result<T> implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1326625485688697147L;
public static final int ERROR = -1;
public static final int SUCCESS = 0;
// @ApiModelProperty("状态码(可选)")
private int status;
// @ApiModelProperty("提示消息")
private String msg;
// @ApiModelProperty("响应数据")
private T data;
//@JsonCreator
protected Result(@JsonProperty("status") int status, @JsonProperty("msg") String msg, @JsonProperty("data") T data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public static <T> Result<T> create(int statusCode, String message, T data) {
return new Result<>(statusCode, message, data);
}
public static <T> Result<T> success(T data, String msg) {
return create(SUCCESS, msg, data);
}
public static <T> Result<T> success(T data) {
return create(SUCCESS, "操作成功", data);
}
public static <T> Result<T> success(String message) {
return create(SUCCESS, message, null);
}
public static <T> Result<T> error(String message) {
return create(ERROR, message, null);
}
public static <T> Result<T> error() {
return create(ERROR, "操作失败", null);
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
public int getStatus() {
return status;
}
@Override
public String toString() {
return "Result{" + "data=" + data + ", status=" + status + ", msg='" + msg + '\'' + '}';
}
}
其次,定义GlobalExceptionHandler 全局处理异常类,一般参数校验失败会抛出MethodArgumentNotValidException或者ConstraintViolationException异常,我们就会捕获这些异常,去做统一的异常处理
mport org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
LOGGER.error(e.getMessage(), e);
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
return Result.error(msg);
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleConstraintViolationException(ConstraintViolationException ex) {
LOGGER.error(e.getMessage(), e);
return Result.error( ex.getMessage());
}
@ExceptionHandler(Exception.class)
public Result exception(Exception e) {
// 打印出错误日志
LOGGER.error(e.getMessage(), e);
return Result.error("这是一个全局处理异常 ,"+e.getMessage());
}
}
基本校验
需要在contrller类加@Validated注解,然后在方法参数上加@NotBlank等各种参数校验注解
@RestController
@RequestMapping(value = "/test")
@Validated
public class TestController {
@GetMapping(value = "/test1")
public Result test1(@RequestParam @NotBlank @Length(min = 2, max = 10) String param1,@RequestParam @NotNull @Min(0) @Max(100) Integer param2){
return Result.success("aaa");
}
@PostMapping(value = "/test3")
public Result test3(@RequestBody UserSearchDto userSearchDto) throws Exception{
return Result.success("ccccc");
}
}
一般post请求的话,@RequestBody请求参数是一个对象,我们也可以在dto里面加上注解
@Data
public class UserSearchDto {
@ApiModelProperty(value = "年龄")
@NotNull
@Min(0)
@Max(150)
private Integer age;
/*分页参数*/
@ApiModelProperty(value = "当前页")
private String currentPage;
@ApiModelProperty(value = "页大小")
private String pageSize;
}
下面在列一下常用的注解
注解 | 作用类型 | 来源 | 说明 |
---|---|---|---|
@Null | 任何类型 | 属性必须为null | |
@NotNull | 任何类型 | 属性不能为null | |
@NotEmpty | 集合 | hibernate validator扩展注解 | 集合不能为null,且size大于0 |
@NotBlank | 字符串、字符 | hibernate validator扩展注解 | 字符类不能为null,且去掉空格之后长度大于0 |
@AssertTrue | Boolean、boolean | 布尔属性必须是true | |
@Min | 数字类型(原子和包装) | 限定数字的最小值(整型) | |
@Max | 同@Min | 限定数字的最大值(整型) | |
@DecimalMin | 同@Min | 限定数字的最小值(字符串,可以是小数) | |
@DecimalMax | 同@Min | 限定数字的最大值(字符串,可以是小数) | |
@Range | 数字类型(原子和包装) | hibernate validator扩展注解 | 限定数字范围(长整型) |
@Length(min=,max=) | 字符串 | hibernate validator扩展注解 | 限定字符串长度 |
@Size | 集合 | 限定集合大小 | |
@Past | 时间、日期 | 必须是一个过去的时间或日期 | |
@Future | 时期、时间 | 必须是一个未来的时间或日期 | |
字符串 | hibernate validator扩展注解 | 必须是一个邮箱格式 | |
@Pattern | 字符串、字符 | 正则匹配字符串 |
集合校验
如果我们需要批量新增的时候,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数。包装List类型,并声明@Valid注解
public class ValidationList<E> implements List<E> {
@Delegate // @Delegate是lombok注解
@Valid // 一定要加@Valid注解
public List<E> list = new ArrayList<>();
// 一定要记得重写toString方法
@Override
public String toString() {
return list.toString();
}
}
@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校验不通过,会抛出NotReadablePropertyException,同样可以使用统一异常进行处理。
比如,我们需要一次性保存多个对象,Controller层的方法可以这么写:
@PostMapping(value = "/saveList")
public Result test4(@RequestBody @Validated({Insert.class}) ValidationList<FireHistoryForestryAddDto> list) {
return Result.success("ccccc");
}
分组校验
比如说有时侯新增接口和更新接口用的是同一个dto,但是只有更新的时候id字段才需要验证,这个时候就需要分组验证
定义分组Insert接口(新增接口使用),如何类推
public interface Insert {
}
然后再接口指定Insert.class
@PostMapping(value = "/test3")
public Result test3(@RequestBody @Validated({Insert.class}) FireHistoryForestryAddDto dto) throws Exception{
return Result.success("ccccc");
}
在参数校验处指定Insert.class
@Data
public class FireHistoryForestryAddDto {
/**
* 编号
*/
@NotBlank(groups = { Update.class, Delete.class}, message = "编号不能为空")
private String id;
@NotBlank(groups = {Insert.class}, message = "火灾名称不能为空")
private String name;
/**
* 行政编码
*/
@NotBlank(groups = {Insert.class}, message = "行政编码不能为空")
private String districtCode;
}
自定义校验
当自带的校验规则不满足,我们所需时,我们可以自定义校验规则,比如我们需要一个日期格式的校验器,这个需要两步。
首先,自定义约束注解
/**
* 判断时间类型字符串是否正确【注解】
*
* @author: Jreffer
* @date: 2022年3月7日13:12:52
*/
@Target({TYPE, ANNOTATION_TYPE, FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {DateCorrectValidator.class})
public @interface DateCorrect {
boolean required() default true;
String message() default "日期格式错误";
String value() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String format() default "yyyy-MM-dd";
}
实现ConstraintValidator接口编写约束校验器
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.text.SimpleDateFormat;
/**
* 判断日期类型【校验器】
*
* @author: Jreffer
* @date: 2022年3月7日13:12:52
*/
public class DateCorrectValidator implements ConstraintValidator<DateCorrect, String> {
private DateCorrect dateTime;
@Override
public void initialize(DateCorrect dateTime) {
this.dateTime = dateTime;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离
if (value == null) {
return true;
}
String format = dateTime.format();
if (value.length() != format.length()) {
return false;
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
try {
simpleDateFormat.parse(value);
} catch (Exception e) {
return false;
}
return true;
}
}
这样我们就可以使用@DateCorrect注解了