Java SpringBoot

1、前言

在控制器类的方法里自己写校验逻辑代码当然也可以,只是代码比较丑陋,有点“low”。业界有更好的处理方法,分别阐述如下。

2、PathVariable校验

  1. @GetMapping("/path/{group:[a-zA-Z0-9_]+}/{userid}")
  2. @ResponseBody
  3. public String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) {
  4. return group + ":" + userid;
  5. }

用法是:路径变量:正则表达式。当请求URI不满足正则表达式时,客户端将收到404错误码。不方便的地方是,不能通过捕获异常的方式,向前端返回统一的、自定义格式的响应参数。

3、方法参数校验

  1. @GetMapping("/validate1")
  2. @ResponseBody
  3. public String validate1(
  4. @Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
  5. @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age) {
  6. return "validate1";
  7. }

如果前端传递的参数不满足规则,则抛出异常。注解Size、Min、Max来自validation-api.jar,更多注解参见相关标准小节。

4、表单对象/VO对象校验

当参数是VO时,可以在VO类的属性上添加校验注解。

  1. public class User {
  2. @Size(min = 1,max = 10,message = "姓名长度必须为1到10")
  3. private String name;
  4. @NotEmpty
  5. private String firstName;
  6. @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100")
  7. private Integer age;
  8. @Future
  9. @JSONField(format="yyyy-MM-dd HH:mm:ss")
  10. private Date birth;
  11. 。。。
  12. }

其中,Future注解要求必须是相对当前时间来讲“未来的”某个时间。

  1. @PostMapping("/validate2")
  2. @ResponseBody
  3. public User validate2(@Valid @RequestBody User user){
  4. return user;
  5. }

5、自定义校验规则

5.1 自定义注解校验

需要自定义一个注解类和一个校验类。

  1. import javax.validation.Constraint;
  2. import javax.validation.Payload;
  3. import java.lang.annotation.*;
  4. @Documented
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Target({ElementType.PARAMETER,ElementType.FIELD})
  7. @Constraint(validatedBy = FlagValidatorClass.class)
  8. public @interface FlagValidator {
  9. // flag的有效值,多个使用,隔开
  10. String values();
  11. // flag无效时的提示内容
  12. String message() default "flag必须是预定义的那几个值,不能随便写";
  13. Class<?>[] groups() default {};
  14. Class<? extends Payload>[] payload() default {};
  15. }
  1. import javax.validation.ConstraintValidator;
  2. import javax.validation.ConstraintValidatorContext;
  3. public class FlagValidatorClass implements ConstraintValidator<FlagValidator,Object> {
  4. /**
  5. * FlagValidator注解规定的那些有效值
  6. */
  7. private String values;
  8. @Override
  9. public void initialize(FlagValidator flagValidator) {
  10. this.values = flagValidator.values();
  11. }
  12. /**
  13. * 用户输入的值,必须是FlagValidator注解规定的那些值其中之一。
  14. * 否则,校验不通过。
  15. * @param value 用户输入的值,如从前端传入的某个值
  16. */
  17. @Override
  18. public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
  19. // 切割获取值
  20. String[] value_array = values.split(",");
  21. Boolean isFlag = false;
  22. for (int i = 0; i < value_array.length; i++){
  23. // 存在一致就跳出循环
  24. if (value_array[i] .equals(value)){
  25. isFlag = true; break;
  26. }
  27. }
  28. return isFlag;
  29. }
  30. }

使用自定义的注解:

  1. public class User {
  2. // 前端传入的flag值必须是1或2或3,否则校验失败
  3. @FlagValidator(values = "1,2,3")
  4. private String flag ;
  5. 。。。
  6. }

5.2 分组校验

  1. import org.hibernate.validator.constraints.Length;
  2. import javax.validation.constraints.Min;
  3. import javax.validation.constraints.NotNull;
  4. public class Resume {
  5. public interface Default {
  6. }
  7. public interface Update {
  8. }
  9. @NotNull(message = "id不能为空", groups = Update.class)
  10. private Long id;
  11. @NotNull(message = "名字不能为空", groups = Default.class)
  12. @Length(min = 4, max = 10, message = "name 长度必须在 {min} - {max} 之间", groups = Default.class)
  13. private String name;
  14. @NotNull(message = "年龄不能为空", groups = Default.class)
  15. @Min(value = 18, message = "年龄不能小于18岁", groups = Default.class)
  16. private Integer age;
  17. 。。。
  18. }
  19. /**
  20. * 使用Defaul分组进行验证
  21. * @param resume
  22. * @return
  23. */
  24. @PostMapping("/validate5")
  25. public String addUser(@Validated(value = Resume.Default.class) @RequestBody Resume resume) {
  26. return "validate5";
  27. }
  28. /**
  29. * 使用Default、Update分组进行验证
  30. * @param resume
  31. * @return
  32. */
  33. @PutMapping("/validate6")
  34. public String updateUser(@Validated(value = {Resume.Update.class, Resume.Default.class}) @RequestBody Resume resume) {
  35. return "validate6";
  36. }
  37. }

建立了两个分组,名称分别为Default、Update。POST方法提交时使用Defaut分组的校验规则,PUT方法提交时同时使用两个分组规则。

6、异常拦截器

通过设置全局异常处理器,统一向前端返回校验失败信息。

  1. import com.scj.springbootdemo.WebResult;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.util.CollectionUtils;
  5. import org.springframework.validation.ObjectError;
  6. import org.springframework.web.bind.MethodArgumentNotValidException;
  7. import org.springframework.web.bind.annotation.ControllerAdvice;
  8. import org.springframework.web.bind.annotation.ExceptionHandler;
  9. import org.springframework.web.bind.annotation.ResponseBody;
  10. import javax.validation.ConstraintViolation;
  11. import javax.validation.ConstraintViolationException;
  12. import java.util.List;
  13. import java.util.Set;
  14. /**
  15. * 全局异常处理器
  16. */
  17. @ControllerAdvice
  18. public class GlobalExceptionHandler {
  19. private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  20. /**
  21. * 用来处理bean validation异常
  22. * @param ex
  23. * @return
  24. */
  25. @ExceptionHandler(ConstraintViolationException.class)
  26. @ResponseBody
  27. public WebResult resolveConstraintViolationException(ConstraintViolationException ex){
  28. WebResult errorWebResult = new WebResult(WebResult.FAILED);
  29. Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
  30. if(!CollectionUtils.isEmpty(constraintViolations)){
  31. StringBuilder msgBuilder = new StringBuilder();
  32. for(ConstraintViolation constraintViolation :constraintViolations){
  33. msgBuilder.append(constraintViolation.getMessage()).append(",");
  34. }
  35. String errorMessage = msgBuilder.toString();
  36. if(errorMessage.length()>1){
  37. errorMessage = errorMessage.substring(0,errorMessage.length()-1);
  38. }
  39. errorWebResult.setInfo(errorMessage);
  40. return errorWebResult;
  41. }
  42. errorWebResult.setInfo(ex.getMessage());
  43. return errorWebResult;
  44. }
  45. @ExceptionHandler(MethodArgumentNotValidException.class)
  46. @ResponseBody
  47. public WebResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){
  48. WebResult errorWebResult = new WebResult(WebResult.FAILED);
  49. List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();
  50. if(!CollectionUtils.isEmpty(objectErrors)) {
  51. StringBuilder msgBuilder = new StringBuilder();
  52. for (ObjectError objectError : objectErrors) {
  53. msgBuilder.append(objectError.getDefaultMessage()).append(",");
  54. }
  55. String errorMessage = msgBuilder.toString();
  56. if (errorMessage.length() > 1) {
  57. errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
  58. }
  59. errorWebResult.setInfo(errorMessage);
  60. return errorWebResult;
  61. }
  62. errorWebResult.setInfo(ex.getMessage());
  63. return errorWebResult;
  64. }
  65. }

7、相关标准

JSR 303 是Bean验证的规范 ,Hibernate Validator 是该规范的参考实现,它除了实现规范要求的注解外,还额外实现了一些注解。
validation-api-1.1.0.jar 包括如下约束注解:
SpringBoot 参数校验/参数验证常用方法总结 - 图1
hibernate-validator-5.3.6.jar 包括如下约束注解:
SpringBoot 参数校验/参数验证常用方法总结 - 图2

8、同时校验2个或更多个字段/参数

常见的场景之一是,查询某信息时要输入开始时间和结束时间。显然,结束时间要≥开始时间。可以在查询VO类上使用自定义注解,下面的例子来自这里。划重点:@ValidAddress使用在类上。

  1. @ValidAddress
  2. public class Address {
  3. @NotNull
  4. @Size(max = 50)
  5. private String street1;
  6. @Size(max = 50)
  7. private String street2;
  8. @NotNull
  9. @Size(max = 10)
  10. private String zipCode;
  11. @NotNull
  12. @Size(max = 20)
  13. private String city;
  14. @Valid
  15. @NotNull
  16. private Country country;
  17. // Getters and setters
  18. }
  19. public class Country {
  20. @NotNull
  21. @Size(min = 2, max = 2)
  22. private String iso2;
  23. // Getters and setters
  24. }
  25. @Documented
  26. @Target(TYPE)
  27. @Retention(RUNTIME)
  28. @Constraint(validatedBy = { MultiCountryAddressValidator.class })
  29. public @interface ValidAddress {
  30. String message() default "{com.example.validation.ValidAddress.message}";
  31. Class<?>[] groups() default {};
  32. Class<? extends Payload>[] payload() default {};
  33. }
  34. public class MultiCountryAddressValidator
  35. implements ConstraintValidator<ValidAddress, Address> {
  36. public void initialize(ValidAddress constraintAnnotation) {
  37. }
  38. @Override
  39. public boolean isValid(Address address,
  40. ConstraintValidatorContext constraintValidatorContext) {
  41. Country country = address.getCountry();
  42. if (country == null || country.getIso2() == null || address.getZipCode() == null) {
  43. return true;
  44. }
  45. switch (country.getIso2()) {
  46. case "FR":
  47. return // Check if address.getZipCode() is valid for France
  48. case "GR":
  49. return // Check if address.getZipCode() is valid for Greece
  50. default:
  51. return true;
  52. }
  53. }
  54. }