1、前言
在控制器类的方法里自己写校验逻辑代码当然也可以,只是代码比较丑陋,有点“low”。业界有更好的处理方法,分别阐述如下。
2、PathVariable校验
@GetMapping("/path/{group:[a-zA-Z0-9_]+}/{userid}")@ResponseBodypublic String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) {return group + ":" + userid;}
用法是:路径变量:正则表达式。当请求URI不满足正则表达式时,客户端将收到404错误码。不方便的地方是,不能通过捕获异常的方式,向前端返回统一的、自定义格式的响应参数。
3、方法参数校验
@GetMapping("/validate1")@ResponseBodypublic String validate1(@Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,@Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age) {return "validate1";}
如果前端传递的参数不满足规则,则抛出异常。注解Size、Min、Max来自validation-api.jar,更多注解参见相关标准小节。
4、表单对象/VO对象校验
当参数是VO时,可以在VO类的属性上添加校验注解。
public class User {@Size(min = 1,max = 10,message = "姓名长度必须为1到10")private String name;@NotEmptyprivate String firstName;@Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100")private Integer age;@Future@JSONField(format="yyyy-MM-dd HH:mm:ss")private Date birth;。。。}
其中,Future注解要求必须是相对当前时间来讲“未来的”某个时间。
@PostMapping("/validate2")@ResponseBodypublic User validate2(@Valid @RequestBody User user){return user;}
5、自定义校验规则
5.1 自定义注解校验
需要自定义一个注解类和一个校验类。
import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.*;@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.PARAMETER,ElementType.FIELD})@Constraint(validatedBy = FlagValidatorClass.class)public @interface FlagValidator {// flag的有效值,多个使用,隔开String values();// flag无效时的提示内容String message() default "flag必须是预定义的那几个值,不能随便写";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}
import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;public class FlagValidatorClass implements ConstraintValidator<FlagValidator,Object> {/*** FlagValidator注解规定的那些有效值*/private String values;@Overridepublic void initialize(FlagValidator flagValidator) {this.values = flagValidator.values();}/*** 用户输入的值,必须是FlagValidator注解规定的那些值其中之一。* 否则,校验不通过。* @param value 用户输入的值,如从前端传入的某个值*/@Overridepublic boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {// 切割获取值String[] value_array = values.split(",");Boolean isFlag = false;for (int i = 0; i < value_array.length; i++){// 存在一致就跳出循环if (value_array[i] .equals(value)){isFlag = true; break;}}return isFlag;}}
使用自定义的注解:
public class User {// 前端传入的flag值必须是1或2或3,否则校验失败@FlagValidator(values = "1,2,3")private String flag ;。。。}
5.2 分组校验
import org.hibernate.validator.constraints.Length;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;public class Resume {public interface Default {}public interface Update {}@NotNull(message = "id不能为空", groups = Update.class)private Long id;@NotNull(message = "名字不能为空", groups = Default.class)@Length(min = 4, max = 10, message = "name 长度必须在 {min} - {max} 之间", groups = Default.class)private String name;@NotNull(message = "年龄不能为空", groups = Default.class)@Min(value = 18, message = "年龄不能小于18岁", groups = Default.class)private Integer age;。。。}/*** 使用Defaul分组进行验证* @param resume* @return*/@PostMapping("/validate5")public String addUser(@Validated(value = Resume.Default.class) @RequestBody Resume resume) {return "validate5";}/*** 使用Default、Update分组进行验证* @param resume* @return*/@PutMapping("/validate6")public String updateUser(@Validated(value = {Resume.Update.class, Resume.Default.class}) @RequestBody Resume resume) {return "validate6";}}
建立了两个分组,名称分别为Default、Update。POST方法提交时使用Defaut分组的校验规则,PUT方法提交时同时使用两个分组规则。
6、异常拦截器
通过设置全局异常处理器,统一向前端返回校验失败信息。
import com.scj.springbootdemo.WebResult;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.util.CollectionUtils;import org.springframework.validation.ObjectError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import javax.validation.ConstraintViolation;import javax.validation.ConstraintViolationException;import java.util.List;import java.util.Set;/*** 全局异常处理器*/@ControllerAdvicepublic class GlobalExceptionHandler {private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 用来处理bean validation异常* @param ex* @return*/@ExceptionHandler(ConstraintViolationException.class)@ResponseBodypublic WebResult resolveConstraintViolationException(ConstraintViolationException ex){WebResult errorWebResult = new WebResult(WebResult.FAILED);Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();if(!CollectionUtils.isEmpty(constraintViolations)){StringBuilder msgBuilder = new StringBuilder();for(ConstraintViolation constraintViolation :constraintViolations){msgBuilder.append(constraintViolation.getMessage()).append(",");}String errorMessage = msgBuilder.toString();if(errorMessage.length()>1){errorMessage = errorMessage.substring(0,errorMessage.length()-1);}errorWebResult.setInfo(errorMessage);return errorWebResult;}errorWebResult.setInfo(ex.getMessage());return errorWebResult;}@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseBodypublic WebResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){WebResult errorWebResult = new WebResult(WebResult.FAILED);List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();if(!CollectionUtils.isEmpty(objectErrors)) {StringBuilder msgBuilder = new StringBuilder();for (ObjectError objectError : objectErrors) {msgBuilder.append(objectError.getDefaultMessage()).append(",");}String errorMessage = msgBuilder.toString();if (errorMessage.length() > 1) {errorMessage = errorMessage.substring(0, errorMessage.length() - 1);}errorWebResult.setInfo(errorMessage);return errorWebResult;}errorWebResult.setInfo(ex.getMessage());return errorWebResult;}}
7、相关标准
JSR 303 是Bean验证的规范 ,Hibernate Validator 是该规范的参考实现,它除了实现规范要求的注解外,还额外实现了一些注解。
validation-api-1.1.0.jar 包括如下约束注解:
hibernate-validator-5.3.6.jar 包括如下约束注解:
8、同时校验2个或更多个字段/参数
常见的场景之一是,查询某信息时要输入开始时间和结束时间。显然,结束时间要≥开始时间。可以在查询VO类上使用自定义注解,下面的例子来自这里。划重点:@ValidAddress使用在类上。
@ValidAddresspublic class Address {@NotNull@Size(max = 50)private String street1;@Size(max = 50)private String street2;@NotNull@Size(max = 10)private String zipCode;@NotNull@Size(max = 20)private String city;@Valid@NotNullprivate Country country;// Getters and setters}public class Country {@NotNull@Size(min = 2, max = 2)private String iso2;// Getters and setters}@Documented@Target(TYPE)@Retention(RUNTIME)@Constraint(validatedBy = { MultiCountryAddressValidator.class })public @interface ValidAddress {String message() default "{com.example.validation.ValidAddress.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}public class MultiCountryAddressValidatorimplements ConstraintValidator<ValidAddress, Address> {public void initialize(ValidAddress constraintAnnotation) {}@Overridepublic boolean isValid(Address address,ConstraintValidatorContext constraintValidatorContext) {Country country = address.getCountry();if (country == null || country.getIso2() == null || address.getZipCode() == null) {return true;}switch (country.getIso2()) {case "FR":return // Check if address.getZipCode() is valid for Francecase "GR":return // Check if address.getZipCode() is valid for Greecedefault:return true;}}}
