话不多说,直接上代码,优雅就完事儿了
org.projectlombok
lombok
1.18.10
javax.validation
validation-api
2.0.1.Final
org.hibernate
hibernate-validator
6.0.1.Final
在入参 Bean 前添加此注解 @Validated
import com.pph.model.request.ValidatedDemoReq;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author pph
* @date 2020/5/12 17:45
* @description
*/
@RestController
@RequestMapping("/validation")
public class ValidationTestController {
/**
* Bean 入参校验框架测试
*
* @param req 入参
* @return 结果
*/
@RequestMapping(value = "/test", method = RequestMethod.GET)
public Object test(@RequestBody @Validated ValidatedDemoReq req) {
return req.toString();
}
/**
* Bean 入参校验框架测试(分组)
*
* @param req 入参
* @return 结果
*/
@RequestMapping(value = "/test/group", method = RequestMethod.GET)
public Object test(@RequestBody @Validated(value = GroupTest.class) ValidatedDemoReq req) {
return req.toString();
}
}
实现 ConstraintValidator 接口并引用到注解中
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import static java.util.stream.Collectors.toList;
/**
* @author pph
* @date 2020/5/12 13:57
* @description 校验只能为指定的某些值
*/
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = Contains.ContainsValidator.class)
public @interface Contains {
String message() default "必须传入 contains 指定其中的某个值";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
/**
* 指定值数值
*
* @return 数组结果
*/
String[] contains() default {};
/**
* 是否区分大小写
*
* @return 结果
*/
boolean isCaseSensitive() default true;
/**
* 是否必传
*
* @return 结果
*/
boolean isMust() default true;
final class ContainsValidator implements ConstraintValidator {
/**
* 指定值集合
*/
private List contains;
/**
* 是否区分大小写
*/
private boolean isCaseSensitive = true;
/**
* 是否必传
*/
private boolean isMust = true;
@Override
public void initialize(Contains constraintAnnotation) {
// 数组去重转集合
this.contains = Arrays.stream(constraintAnnotation.contains())
.distinct()
.collect(toList());
this.isCaseSensitive = constraintAnnotation.isCaseSensitive();
this.isMust = constraintAnnotation.isMust();
}
@Override
public boolean isValid(String str, ConstraintValidatorContext context) {
// 非必传时 str 为 null 默认通过
if (!isMust
&& Objects.isNull(str)) {
return true;
}
// contains 不能指定 null,当 str 为 null 直接返回异常信息
if (Objects.isNull(str)) {
return false;
}
// 未指定值时,默认通过
if (contains.isEmpty()) {
return true;
}
// 不区分大小写,将所有字符串转大写再作比较
if (!isCaseSensitive) {
contains = contains.stream()
.map(String::toUpperCase)
.collect(toList());
return contains.contains(str.toUpperCase());
}
return contains.contains(str);
}
}
}
import com.pph.model.annotation.validation.Contains;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
/**
* @author pph
* @date 2020/5/11 15:13
* @description Validated 基本使用介绍
*/
@Data
public class ValidatedDemoReq {
/**
* 坑:不要跨类型使用注解,否则异常信息无法正常被显示,错误难以排查
*
* 常用自带注解与描述
* 1 | @Null | 被注释的元素必须为 null
* 2 | @NotNull | 被注释的元素必须不为 null(必传字段)
* 3 | @AssertTrue | 被注释的元素必须为 true
* 4 | @AssertFalse | 被注释的元素必须为 false
* 5 | @Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
* 6 | @Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
* 7 | @DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
* 8 | @DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
* 9 | @Size(max, min) | 被注释的元素的大小必须在指定的范围内
* 10 | @Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内
* 11 | @Past | 被注释的元素必须是一个过去的日期
* 12 | @Future | 被注释的元素必须是一个将来的日期
* 13 | @Pattern(value) | 被注释的元素必须符合指定的正则表达式
* 14 | @Email | 被注释的元素必须是电子邮箱地址
* 15 | @Length | 被注释的字符串的大小必须在指定的范围内
* 16 | @NotEmpty | 被注释的字符串的必须非空
* 17 | @Email | 被注释的元素必须是电子邮箱地址
* 18 | @Range | 被注释的元素必须在合适的范围内
* 19 | @NotBlank | 验证字符串非null,且长度必须大于 0
* 20 * | @Valid | 注意: 集合嵌套对象必须这样 List<@Valid Object>, 否则 Object 里的注解不生效
*
* 自定义注解(详情可以(ctrl + 左键) 点击跳转了解):
*
* @see com.pph.model.annotation.validation.Contains
*/
@Null(message = "id must be null!")
private Long id;
@NotBlank(message = "name can not be blank!")
@Pattern(regexp = "^[a-zA-Z]\\w{5,31}", message = "name 只能为英文或数字,且不能以数字开头,长度为 6-32 个字符")
private String name;
@NotNull(message = "手机号为必传字段")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3-9][0-9]{9}$", message = "手机号格式有误")
private String mobile;
/**
* 这是一个坑坑坑,注意
* 不要将 @Size 作用于 Long 等数值类型包装类,
* 否则框架捕获不到参数异常 MethodArgumentNotValidException.class,
* 从而走 Exception.class 异常信息返回
* 如果要指定数值类型大小请使用 @Range(min = 1, max = 100, message = "range 只能取 1-100")
*/
@Size(min = 6, max = 20, message = "size 只能取 6-20 个字符")
private String size;
@Range(min = 1, max = 100, message = "range 只能取 1-100")
private Long range;
@AssertTrue(message = "assertTrue must be true!")
private Boolean assertTrue;
@AssertFalse(message = "assertFalse must be true!")
private Boolean assertFalse;
@Min(value = 1, message = "min minimum must be 1!")
private Long min;
@Max(value = 100, message = "max maximum must be 1!")
private Long max;
@Digits(integer = 2, fraction = 2, message = "只能为 2 位数, 小数点只能保留 2 位!")
private Double digits;
/**
* 集合中嵌套对象时使用此注解,否则集合中的对象不会校验
*/
private List<@Valid ValidatedDemoReq> lists;
@Contains(contains = {"str1", "str2", "str3"}, isCaseSensitive = false, isMust = false,
message = "contains 必须为指定的值")
private String contains;
// ------------------------- group 分组使用
@NotBlank(message = "group1 can not be blank!", groups = GroupTest.class)
private String group1;
@NotBlank(message = "group2 can not be blank!", groups = GroupTest.class)
private String group2;
}
创建分组接口(只需要继承Default 接口即可)
import javax.validation.groups.Default;
/**
* @author pph
* @date 2020/5/12 17:07
* @description 入参校验分组测试
*/
public interface GroupTest extends Default {
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author pph
* @date 2020/5/11 10:08
* @description API 统一结果返回
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Results implements Serializable {
/**
* 请求返回码
*/
private Integer code;
/**
* 请求返回信息
*/
private String message;
/**
* 请求是否成功
*/
private Boolean success;
/**
* 请求返回数据
*/
private T data;
/**
* 当前时间戳
*/
private Long timestamp = System.currentTimeMillis();
public Results(Integer code, String message, Boolean success, T data) {
this.code = code;
this.message = message;
this.success = success;
this.data = data;
}
/**
* 正常结果返回
*
* @param data 返回数据
* @param 返回结果泛型
* @return result
*/
public static Results success(T data) {
return new Results<>(200, "请求成功!", true, data);
}
/**
* 异常结果返回
*
* @param message 异常信息
* @return result
*/
public static Results
import com.pph.utils.Results;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* @author pph
* @date 2020/5/11 14:14
* @description 统一结果返回
*/
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
// public ResponseAdvice(SkipResultsProcessing uris) {
// this.uris = uris;
// }
// private final SkipResultsProcessing uris;
@Override
public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class extends HttpMessageConverter>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// application-skip-results-processing.yml 配置跳过后置统一结果处理则直接返回
// if (uris.getUris()
// .stream()
// .anyMatch(u -> StringUtils.equals(u, request.getURI().getPath()))) {
// return body;
// }
// 如果结果已经是 Results 对象类型则直接返回
if (body instanceof Results) {
return body;
}
// 返回统一结果
return Results.success(body);
}
}
import com.pph.utils.Results;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author pph
* @date 2020/5/11 11:27
* @description 统一异常结果处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* Controller Bean 入参校验异常结果处理
*
* @param e 异常
* @return result
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Results> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
return Results.error(e.getBindingResult()
.getFieldError()
.getDefaultMessage());
}
/**
* 统一异常结果处理
*
* @return result
*/
@ExceptionHandler(Exception.class)
public Results> handleException(Exception e) {
return Results.error(e.getMessage());
}
}