一、使用

1.1 字段注解

可以使用@NotEmpty@NotNull等存在于javax.validation.constraints包下的校验注解,参照对应的校验规则,选择合适的注解使用。

  1. /**
  2. * 品牌名
  3. */
  4. @NotBlank(message = "品牌名必须提交")
  5. private String name;

1.2 默认的校验提示信息

存在于ValidationMessages_zh_CN.properties文件中,根据不同的校验注解有不同的默认提示信息,我们可以在对应注解使用时,使用其message属性指定其对应的合适提示信息。

  1. @NotBlank(message = "品牌名必须提交")

1.3 开启校验

在实体接收参数的位置使用@Valid注解来开启参数校验。如下:

  1. /**
  2. * 保存
  3. */
  4. @RequestMapping("/save")
  5. //@RequiresPermissions("product:brand:save")
  6. public R save(@Valid @RequestBody BrandEntity brand){
  7. brandService.save(brand);
  8. return R.ok();
  9. }

通过接口调用,可以看到对应的校验注解已经生效:

  1. {
  2. "timestamp": "2022-01-15T11:59:03.537+0000",
  3. "status": 400,
  4. "error": "Bad Request",
  5. "errors": [
  6. {
  7. "codes": [
  8. "NotNull.brandEntity.sort",
  9. "NotNull.sort",
  10. "NotNull.java.lang.Integer",
  11. "NotNull"
  12. ],
  13. "arguments": [
  14. {
  15. "codes": [
  16. "brandEntity.sort",
  17. "sort"
  18. ],
  19. "arguments": null,
  20. "defaultMessage": "sort",
  21. "code": "sort"
  22. }
  23. ],
  24. "defaultMessage": "不能为null",
  25. "objectName": "brandEntity",
  26. "field": "sort",
  27. "rejectedValue": null,
  28. "bindingFailure": false,
  29. "code": "NotNull"
  30. },
  31. {
  32. "codes": [
  33. "NotBlank.brandEntity.name",
  34. "NotBlank.name",
  35. "NotBlank.java.lang.String",
  36. "NotBlank"
  37. ],
  38. "arguments": [
  39. {
  40. "codes": [
  41. "brandEntity.name",
  42. "name"
  43. ],
  44. "arguments": null,
  45. "defaultMessage": "name",
  46. "code": "name"
  47. }
  48. ],
  49. "defaultMessage": "品牌名必须提交",
  50. "objectName": "brandEntity",
  51. "field": "name",
  52. "rejectedValue": "",
  53. "bindingFailure": false,
  54. "code": "NotBlank"
  55. },
  56. {
  57. "codes": [
  58. "NotEmpty.brandEntity.logo",
  59. "NotEmpty.logo",
  60. "NotEmpty.java.lang.String",
  61. "NotEmpty"
  62. ],
  63. "arguments": [
  64. {
  65. "codes": [
  66. "brandEntity.logo",
  67. "logo"
  68. ],
  69. "arguments": null,
  70. "defaultMessage": "logo",
  71. "code": "logo"
  72. }
  73. ],
  74. "defaultMessage": "不能为空",
  75. "objectName": "brandEntity",
  76. "field": "logo",
  77. "rejectedValue": null,
  78. "bindingFailure": false,
  79. "code": "NotEmpty"
  80. },
  81. {
  82. "codes": [
  83. "NotEmpty.brandEntity.firstLetter",
  84. "NotEmpty.firstLetter",
  85. "NotEmpty.java.lang.String",
  86. "NotEmpty"
  87. ],
  88. "arguments": [
  89. {
  90. "codes": [
  91. "brandEntity.firstLetter",
  92. "firstLetter"
  93. ],
  94. "arguments": null,
  95. "defaultMessage": "firstLetter",
  96. "code": "firstLetter"
  97. }
  98. ],
  99. "defaultMessage": "不能为空",
  100. "objectName": "brandEntity",
  101. "field": "firstLetter",
  102. "rejectedValue": null,
  103. "bindingFailure": false,
  104. "code": "NotEmpty"
  105. }
  106. ],
  107. "message": "Validation failed for object='brandEntity'. Error count: 4",
  108. "path": "/product/brand/save"
  109. }

但是返回的提示信息并没有按照我们预先规定好的返回数据格式返回,所以需要对返回结果进行封装。

1.4 校验结果封装

在入参的位置,加一个参数BindingResult,可以在参数校验时,将校验结果接收,

  • 并可调用其hasErrors()方法判断其是否存在不合法的参数校验结果,
  • 还可调用其getFieldErrors()获取出现不合法参数的字段结果集合FieldError
    • 通过对校验出的字段结果进行遍历,
      • 调用其getDefaultMessage()方法获取错误提示信息
      • 调用其getField()方法获取出现不合法参数的字段名
  1. /**
  2. * 保存
  3. */
  4. @RequestMapping("/save")
  5. //@RequiresPermissions("product:brand:save")
  6. public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
  7. if (result.hasErrors()) {
  8. Map<String, String> map = new HashMap<>();
  9. // 获取校验的错误结果
  10. result.getFieldErrors().forEach(
  11. item -> {
  12. // 获取到对应错误的提示信息
  13. String message = item.getDefaultMessage();
  14. // 获取到对应错误的属性名字
  15. String field = item.getField();
  16. map.put(field, message);
  17. }
  18. );
  19. return R.error(400, "提交的数据不合法").put("data", map);
  20. } else {
  21. brandService.save(brand);
  22. return R.ok();
  23. }
  24. }

再次调用此接口,可以看到封装后的返回结果:

  1. {
  2. "msg": "提交的数据不合法",
  3. "code": 400,
  4. "data": {
  5. "name": "品牌名必须提交",
  6. "logo": "不能为空",
  7. "sort": "不能为null",
  8. "firstLetter": "不能为空"
  9. }
  10. }

二、统一异常处理降低侵入

可以通过统一的异常处理类,来减少代码的重复编写以及降低代码侵入。

  1. /**
  2. * 统一异常处理
  3. */
  4. @Slf4j
  5. @RestControllerAdvice(basePackages = "com.maomaochong.gulimall.product.controller")
  6. public class GulimallExceptionControllerAdvice {
  7. /**
  8. * 处理参数不合法异常
  9. * @param e
  10. * @return
  11. */
  12. @ExceptionHandler(MethodArgumentNotValidException.class)
  13. public R handleValidException(MethodArgumentNotValidException e) {
  14. log.error("数据校验出现问题{}, 异常类型:{}", e.getMessage(), e.getClass());
  15. // 获取校验结果
  16. BindingResult bindingResult = e.getBindingResult();
  17. Map<String, String> errorMap = new HashMap<>();
  18. bindingResult.getFieldErrors().forEach(
  19. item -> {
  20. errorMap.put(item.getField(), item.getDefaultMessage());
  21. }
  22. );
  23. return R.error(400, "数据校验出现问题").put("data", errorMap);
  24. }
  25. }

三、使用分组校验

我们可以借助于校验注解的groups注解,对注解进行分组,根据不同的场景使用不同的分组来做校验,让校验注解更加灵活。

3.1 创建分组接口

要使用分组校验功能,需要先创建对应分组的接口,作为分组的标识。只需要创建接口不需要其他额外的操作。

  1. /**
  2. * 参数分组校验标识 新增
  3. */
  4. public interface AddGroup {
  5. }

3.2 给校验注解指定分组

在对应的校验注解上使用其groups属性,传入对应分组的接口class名即可实现对接口的分组。一个注解可以同时属于多个分组,只需要在此属性中传入对应多个分组的class即可。

  1. /**
  2. * 品牌id
  3. */
  4. @NotNull(message = "修改时必须指定id", groups = {UpdateGroup.class})
  5. @Null(message = "新增不能指定id", groups = AddGroup.class)
  6. @TableId
  7. private Long brandId;

3.3 指定当前使用分组

我们需要在使用的位置,使用新的开启校验的注解@Validated,并在此注解的value属性中指定分组接口标识。

  1. /**
  2. * 保存
  3. */
  4. @RequestMapping("/save")
  5. //@RequiresPermissions("product:brand:save")
  6. public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
  7. ...
  8. }
  9. /**
  10. * 修改
  11. */
  12. @RequestMapping("/update")
  13. //@RequiresPermissions("product:brand:update")
  14. public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
  15. brandService.updateById(brand);
  16. return R.ok();
  17. }

可以看到,当新增时若携带了品牌id属性,则会触发校验:

  1. {
  2. "msg": "参数格式校验失败",
  3. "code": 10001,
  4. "data": {
  5. "brandId": "新增不能指定id"
  6. }
  7. }

当修改时若未携带品牌id属性,也会触发校验:

  1. {
  2. "msg": "参数格式校验失败",
  3. "code": 10001,
  4. "data": {
  5. "brandId": "修改时必须指定id"
  6. }
  7. }

注意:

  • 实体中没有加分组的属性,在指定了分组校验后不会生效,只会在不指定分组的情况下才会生效。

四、自定义校验

4.1 编写自定义校验注解

需要遵循JSR303规范,必须至少包含message()groups()payload()三部分。

创建自定义注解@ListValue

  1. /**
  2. * 自定义校验注解
  3. */
  4. @Documented
  5. @Constraint(validatedBy = { ListValueConstraintValidator.class }) // 指定自定义校验器
  6. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) // 指定可使用位置
  7. @Retention(RUNTIME) // 可以在运行时获取到
  8. public @interface ListValue {
  9. /**
  10. * 指定校验失败的提示信息
  11. * 默认在 ValidationMessages_zh_CN.properties 文件中
  12. * @return
  13. */
  14. String message() default "{com.maomaochong.common.valid.ListValue.message}";
  15. /**
  16. * 指定分组信息
  17. * @return
  18. */
  19. Class<?>[] groups() default { };
  20. /**
  21. * 负载信息
  22. * @return
  23. */
  24. Class<? extends Payload>[] payload() default { };
  25. /**
  26. * 可接受的值
  27. * @return
  28. */
  29. int[] vals() default {};
  30. }

4.2 编写自定义校验器

自定义的校验器需要实现ConstraintValidator<A extends Annotation, T>接口,并指定对应泛型:

  • 泛型一:指定对应关联的自定义注解
  • 泛型二:指定支持的处理目标类型

    1. /**
    2. * 自定义校验器
    3. */
    4. public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
    5. private Set<Integer> set = new HashSet<>();
    6. /**
    7. * 初始化方法
    8. * @param constraintAnnotation
    9. */
    10. @Override
    11. public void initialize(ListValue constraintAnnotation) {
    12. // 获取允许的值
    13. int[] vals = constraintAnnotation.vals();
    14. // 加入集合
    15. for (int val : vals) {
    16. set.add(val);
    17. }
    18. }
    19. /**
    20. * 判断是否校验成功
    21. * @param value
    22. * @param context
    23. * @return
    24. */
    25. @Override
    26. public boolean isValid(Integer value, ConstraintValidatorContext context) {
    27. return set.contains(value);
    28. }
    29. }

    4.3 关联校验器与自定义注解

    在自定义注解中,使用@Constraint(validatedBy = { })来关联自定义注解的校验器。

    1. @Constraint(validatedBy = { ListValueConstraintValidator.class })

    注意:

  • 我们可以通过指定多个不同的校验器,来实现对不同类型数据的校验的适配

    4.4 使用

    1. /**
    2. * 显示状态[0-不显示;1-显示]
    3. */
    4. @ListValue(vals = {0, 1}, groups = {AddGroup.class})
    5. private Integer showStatus;