SpringBoot-jsr303 参数校验

2023年04月15日 11:29 · 阅读(265) ·

开发环境

名称 版本
操作系统 Windows 10 X64
JDK JDK1.8(jdk-8u151-windows-x64)
IDEA IntelliJ IntelliJ IDEA 2021.2.3 (Community Edition)
Maven Maven 3.6.0
Spring Boot 2.2.10.RELEASE

参考

你还在手动写if else来进行参数校验吗?今天教你java项目中实现jsr303参数校验

【java工程师必会】springBoot的Controller层validation优雅处理入参校验

SpringBoot-通用参数校验

源码

SpringBoot-jsr303 参数校验.zip

pom.xml

  1. <!--jsr303 参数校验-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-validation</artifactId>
  5. </dependency>

简单的必填校验

参数类-Shop

  1. package com.test.model;
  2. import lombok.Data;
  3. import javax.validation.constraints.NotBlank;
  4. import javax.validation.constraints.NotNull;
  5. import java.math.BigDecimal;
  6. import java.time.LocalDate;
  7. @Data
  8. public class Shop {
  9. @NotNull(message = "id 不能为空")
  10. private Long id;
  11. @NotBlank(message = "name 不能为空")
  12. private String name;
  13. @NotNull(message = "创建日期不能为空")
  14. private LocalDate createDate;
  15. @NotNull(message = "金额不能为空")
  16. private BigDecimal amount;
  17. }

接口-valid

  1. package com.test.controller;
  2. import com.test.model.Shop;
  3. import org.springframework.validation.annotation.Validated;
  4. import org.springframework.web.bind.annotation.*;
  5. @RestController
  6. @RequestMapping("/test")
  7. public class TestController {
  8. @PostMapping("/valid")
  9. public String valid(@RequestBody @Validated Shop param) {
  10. return "OK";
  11. }
  12. }

测试-接口-valid

Postman 调动

● URL:http://localhost:8001/test/valid

● 入参:

  1. {
  2. "id":"",
  3. "name":"",
  4. "createDate":"",
  5. "amount": ""
  6. }

● 返回结果,可以看到,返回了一大串报错结果

  1. {
  2. "timestamp": "2023-04-15T03:03:01.051+0000",
  3. "status": 400,
  4. "error": "Bad Request",
  5. "errors": [
  6. {
  7. "codes": [
  8. "NotNull.shop.createDate",
  9. "NotNull.createDate",
  10. "NotNull.java.time.LocalDate",
  11. "NotNull"
  12. ],
  13. "arguments": [
  14. {
  15. "codes": [
  16. "shop.createDate",
  17. "createDate"
  18. ],
  19. "arguments": null,
  20. "defaultMessage": "createDate",
  21. "code": "createDate"
  22. }
  23. ],
  24. "defaultMessage": "创建日期不能为空",
  25. "objectName": "shop",
  26. "field": "createDate",
  27. "rejectedValue": null,
  28. "bindingFailure": false,
  29. "code": "NotNull"
  30. },
  31. {
  32. "codes": [
  33. "NotNull.shop.id",
  34. "NotNull.id",
  35. "NotNull.java.lang.Long",
  36. "NotNull"
  37. ],
  38. "arguments": [
  39. {
  40. "codes": [
  41. "shop.id",
  42. "id"
  43. ],
  44. "arguments": null,
  45. "defaultMessage": "id",
  46. "code": "id"
  47. }
  48. ],
  49. "defaultMessage": "id 不能为空",
  50. "objectName": "shop",
  51. "field": "id",
  52. "rejectedValue": null,
  53. "bindingFailure": false,
  54. "code": "NotNull"
  55. },
  56. {
  57. "codes": [
  58. "NotBlank.shop.name",
  59. "NotBlank.name",
  60. "NotBlank.java.lang.String",
  61. "NotBlank"
  62. ],
  63. "arguments": [
  64. {
  65. "codes": [
  66. "shop.name",
  67. "name"
  68. ],
  69. "arguments": null,
  70. "defaultMessage": "name",
  71. "code": "name"
  72. }
  73. ],
  74. "defaultMessage": "name 不能为空",
  75. "objectName": "shop",
  76. "field": "name",
  77. "rejectedValue": "",
  78. "bindingFailure": false,
  79. "code": "NotBlank"
  80. },
  81. {
  82. "codes": [
  83. "NotNull.shop.amount",
  84. "NotNull.amount",
  85. "NotNull.java.math.BigDecimal",
  86. "NotNull"
  87. ],
  88. "arguments": [
  89. {
  90. "codes": [
  91. "shop.amount",
  92. "amount"
  93. ],
  94. "arguments": null,
  95. "defaultMessage": "amount",
  96. "code": "amount"
  97. }
  98. ],
  99. "defaultMessage": "金额不能为空",
  100. "objectName": "shop",
  101. "field": "amount",
  102. "rejectedValue": null,
  103. "bindingFailure": false,
  104. "code": "NotNull"
  105. }
  106. ],
  107. "message": "Validation failed for object='shop'. Error count: 4",
  108. "path": "/test/valid"
  109. }

使用 BindingResult 统计报错信息

测试-接口-valid 中,可以看到,返回的报错信息很冗余,这里我们使用 BindingResult 统计报错信息

  1. package com.test.controller;
  2. import com.fasterxml.jackson.databind.util.JSONPObject;
  3. import com.test.model.Shop;
  4. import com.test.utils.CommonUtils;
  5. import io.micrometer.core.instrument.util.JsonUtils;
  6. import org.springframework.boot.context.properties.bind.BindResult;
  7. import org.springframework.validation.BindingResult;
  8. import org.springframework.validation.FieldError;
  9. import org.springframework.validation.annotation.Validated;
  10. import org.springframework.web.bind.annotation.*;
  11. import java.util.HashMap;
  12. import java.util.List;
  13. import java.util.Map;
  14. @RestController
  15. @RequestMapping("/test")
  16. public class TestController {
  17. @PostMapping("/valid")
  18. public String valid(@RequestBody @Validated Shop param, BindingResult result) {
  19. if(result.hasErrors()) {
  20. Map<String, String> map = new HashMap<>();
  21. List<FieldError> fieldErrors = result.getFieldErrors();
  22. fieldErrors.forEach(item -> {
  23. String field = item.getField();
  24. String defaultMessage = item.getDefaultMessage();
  25. map.put(field,defaultMessage);
  26. });
  27. return CommonUtils.convertToJson(map, true);
  28. }
  29. return "OK";
  30. }
  31. }

测试-使用 BindingResult 统计报错信息

● URL:http://localhost:8001/test/valid

● 请求参数:

  1. {
  2. "id":"",
  3. "name":"",
  4. "createDate":"",
  5. "amount": ""
  6. }

● 返回结果:

  1. {
  2. "amount":"金额不能为空",
  3. "name":"name 不能为空",
  4. "id":"id 不能为空",
  5. "createDate":"创建日期不能为空"
  6. }

使用异常处理统计报错信息

  • 使用 BindingResult 统计异常报错信息,会出现一个问题,就是每个接口都必须添加参数 BindingResult result

  • 另外一种办法,是添加一个异常处理类,统一处理参数异常。

  • 添加这个异常处理不会BindingResult 冲突

  • 如果接口添加了 BindingResult result,会以BindingResult 的结果为准
  • 如果接口没有添加 BindingResult result,以异常处理类返回的结果为准

  • 添加 GlobalExceptionHandler

  1. import org.springframework.util.CollectionUtils;
  2. import org.springframework.validation.FieldError;
  3. import org.springframework.validation.ObjectError;
  4. import org.springframework.web.bind.MethodArgumentNotValidException;
  5. import org.springframework.web.bind.annotation.ExceptionHandler;
  6. import org.springframework.web.bind.annotation.RestControllerAdvice;
  7. import java.util.List;
  8. @RestControllerAdvice
  9. public class GlobalExceptionHandler {
  10. /**
  11. * 添加校验参数异常处理
  12. *
  13. * @param e 异常信息
  14. * @return 参数异常提示
  15. */
  16. @ExceptionHandler({MethodArgumentNotValidException.class})
  17. public String bindExceptionHandler(Throwable e) {
  18. //@Valid(或 @Validated )校验未通过时,会抛出 MethodArgumentNotValidException 异常
  19. if (e instanceof MethodArgumentNotValidException) {
  20. List<ObjectError> allErrors = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors();
  21. //String msg = StringUtils.collectionToDelimitedString(allErrors, ";");
  22. return getValidExceptionMsg(allErrors);
  23. }
  24. //if (e instanceof BindException) {
  25. // List<ObjectError> allErrors = ((BindException) e).getBindingResult().getAllErrors();
  26. // return StringUtils.collectionToDelimitedString(allErrors, ";");
  27. // //return getValidExceptionMsg(allErrors);
  28. //}
  29. return e.getMessage();
  30. }
  31. private String getValidExceptionMsg(List<ObjectError> errors) {
  32. if (!CollectionUtils.isEmpty(errors)) {
  33. StringBuilder sb = new StringBuilder();
  34. errors.forEach(error -> {
  35. if (error instanceof FieldError) {
  36. sb.append(((FieldError) error).getField()).append(":");
  37. }
  38. sb.append(error.getDefaultMessage()).append(";");
  39. });
  40. String msg = sb.toString();
  41. msg = msg.substring(0, msg.length() - 1);
  42. return msg;
  43. }
  44. return null;
  45. }
  46. }
  • 测试接口
  1. @PostMapping("/valid1")
  2. public String valid1(@RequestBody @Validated Shop param) {
  3. return "OK";
  4. }
  1. {
  2. "id":"",
  3. "name":"",
  4. "createDate":"",
  5. "amount": ""
  6. }

返回结果

  1. name:name 不能为空;id:id 不能为空;amount:金额不能为空;createDate:创建日期不能为空

自定义校验

自定义校验逻辑-MyConstraintValidator

  1. package com.test.valid;
  2. import javax.validation.ConstraintValidator;
  3. import javax.validation.ConstraintValidatorContext;
  4. import java.util.HashSet;
  5. import java.util.Set;
  6. public class MyConstraintValidator implements ConstraintValidator<ArrayVal, Integer> {
  7. private Set<Integer> set = new HashSet<>();
  8. @Override
  9. public void initialize(ArrayVal constraintAnnotation) {
  10. int[] value = constraintAnnotation.value();
  11. for (int i : value) {
  12. set.add(i);
  13. }
  14. }
  15. @Override
  16. public boolean isValid(Integer value, ConstraintValidatorContext context) {
  17. return set.contains(value);
  18. }
  19. }

自定义校验类型-ArrayVal

  1. package com.test.valid;
  2. import javax.validation.Constraint;
  3. import javax.validation.Payload;
  4. import java.lang.annotation.*;
  5. @Documented
  6. @Constraint(validatedBy = {MyConstraintValidator.class} )
  7. @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface ArrayVal {
  10. String message() default "{com.dp.annotation.ArrayVal.message}";
  11. Class<?>[] groups() default {};
  12. Class<? extends Payload>[] payload() default {};
  13. int[] value() default {};
  14. }

参数类-Shop

  1. package com.test.model;
  2. import com.test.valid.ArrayVal;
  3. import lombok.Data;
  4. @Data
  5. public class Shop {
  6. @ArrayVal(value = {0, 1}, message = "字段必须为 1 或者 0")
  7. private Integer isDelete;
  8. }

测试-自定义校验

● URL:http://localhost:8001/test/valid

● 请求参数:

  1. {
  2. "isDelete":3
  3. }

● 返回结果:

  1. {"isDelete":"字段必须为 1 或者 0"}

校验分组

AddGroup

  1. package com.test.valid;
  2. /**
  3. * 分组校验,添加
  4. */
  5. public @interface AddGroup {}

UpdateGroup

  1. package com.test.valid;
  2. /**
  3. * 分组校验,修改
  4. */
  5. public @interface UpdateGroup {}

参数类-Shop-添加分组

  1. package com.test.model;
  2. import com.test.valid.AddGroup;
  3. import com.test.valid.ArrayVal;
  4. import com.test.valid.UpdateGroup;
  5. import lombok.Data;
  6. import javax.validation.constraints.NotBlank;
  7. import javax.validation.constraints.NotNull;
  8. import java.math.BigDecimal;
  9. import java.time.LocalDate;
  10. @Data
  11. public class Shop {
  12. @NotNull(message = "id 不能为空", groups = UpdateGroup.class)
  13. private Long id;
  14. @NotBlank(message = "name 不能为空", groups = {AddGroup.class, UpdateGroup.class})
  15. private String name;
  16. @NotNull(message = "创建日期不能为空", groups = AddGroup.class)
  17. private LocalDate createDate;
  18. @NotNull(message = "金额不能为空", groups = {AddGroup.class, UpdateGroup.class})
  19. private BigDecimal amount;
  20. @ArrayVal(value = {0, 1}, message = "字段必须为 1 或者 0")
  21. private Integer isDelete;
  22. }

接口-新增,修改

  1. package com.test.controller;
  2. import com.fasterxml.jackson.databind.util.JSONPObject;
  3. import com.test.model.Shop;
  4. import com.test.utils.CommonUtils;
  5. import com.test.valid.AddGroup;
  6. import com.test.valid.UpdateGroup;
  7. import io.micrometer.core.instrument.util.JsonUtils;
  8. import org.springframework.boot.context.properties.bind.BindResult;
  9. import org.springframework.validation.BindingResult;
  10. import org.springframework.validation.FieldError;
  11. import org.springframework.validation.annotation.Validated;
  12. import org.springframework.web.bind.annotation.*;
  13. import java.util.HashMap;
  14. import java.util.List;
  15. import java.util.Map;
  16. @RestController
  17. @RequestMapping("/test")
  18. public class TestController {
  19. @PostMapping("/add")
  20. public String add(@RequestBody @Validated(AddGroup.class) Shop param, BindingResult result) {
  21. String errorMsg = checkError(result);
  22. if(null != errorMsg) {
  23. return errorMsg;
  24. }
  25. return "OK";
  26. }
  27. @PostMapping("/update")
  28. public String update(@RequestBody @Validated(UpdateGroup.class) Shop param, BindingResult result) {
  29. String errorMsg = checkError(result);
  30. if(null != errorMsg) {
  31. return errorMsg;
  32. }
  33. return "OK";
  34. }
  35. private String checkError (BindingResult result) {
  36. if(result.hasErrors()) {
  37. Map<String, String> map = new HashMap<>();
  38. List<FieldError> fieldErrors = result.getFieldErrors();
  39. fieldErrors.forEach(item -> {
  40. String field = item.getField();
  41. String defaultMessage = item.getDefaultMessage();
  42. map.put(field,defaultMessage);
  43. });
  44. return CommonUtils.convertToJson(map, true);
  45. }
  46. return null;
  47. }
  48. }

测试-新增

● URL:http://localhost:8001/test/add

● 请求参数:

  1. {
  2. "id":"",
  3. "name":"luoma",
  4. "createDate":"2023-04-15",
  5. "amount": "100.11",
  6. "isDelete":null
  7. }

● 返回结果:

  1. OK

● URL:http://localhost:8001/test/add

● 请求参数:

  1. {
  2. "id":"",
  3. "name":"luoma",
  4. "createDate":"2023-04-15",
  5. "amount": "",
  6. "isDelete":null
  7. }

● 返回结果:

  1. {"amount":"金额不能为空"}

● 可以看到,id 因为加入了 UpdateGroup 分组,不用必填。
● 其它 AddGroup 分组,必填。
isDelete 因为没有加入分组,所以在有分组的校验中失效了。

测试-修改

● URL:http://localhost:8001/test/update

● 请求参数:

  1. {
  2. "id":"1",
  3. "name":"luoma",
  4. "createDate":"",
  5. "amount": "100.11",
  6. "isDelete":nulll
  7. }

● 返回结果:

  1. OK

● URL:http://localhost:8001/test/update

● 请求参数:

  1. {
  2. "id":"",
  3. "name":"luoma",
  4. "createDate":"",
  5. "amount": "100.11",
  6. "isDelete":null
  7. }

● 返回结果:

  1. {"amount":"金额不能为空"}

● 可以看到,createDate 因为加入了 AddGroup 分组,不用必填。
● 其它 UpdateGroup 分组,必填。
isDelete 因为没有加入分组,所以在有分组的校验中失效了。

必填参数嵌套

  1. /**
  2. * 保存数据
  3. * @param param 付款申请数据
  4. * @return 操作结果
  5. */
  6. @ApiOperation("保存数据,发起工作流")
  7. @PostMapping("/save")
  8. public ResponseVO<Boolean> save(@RequestBody @Validated PayApplyEditDto param){
  9. Boolean result = true;
  10. return new ResponseVO<>(ResponseCode.OK, result);
  11. }
  1. package com.luoma.finance.dto;
  2. import lombok.Data;
  3. import javax.validation.Valid;
  4. import java.util.List;
  5. /**
  6. * 参数类 - 付款申请保存操作
  7. * luoma - 2022年11月16日17:49:21
  8. */
  9. @Data
  10. public class PayApplyEditDto {
  11. /**
  12. * 融资付款申请头信息数据
  13. */
  14. @Valid
  15. private PayApplyHeadEditDto headData;
  16. /**
  17. * 附件列表
  18. */
  19. private List<AttachmnetDTO> listAttachmnet;
  20. }
  1. package com.luoma.finance.dto;
  2. import lombok.Data;
  3. import javax.validation.constraints.NotBlank;
  4. /**
  5. * 参数类 - 付款申请保存操作 - 融资付款申请头信息表
  6. * luoma - 2022年11月16日17:49:21
  7. */
  8. @Data
  9. public class PayApplyHeadEditDto {
  10. /**
  11. * 付款方式代码:BANKDEDUCTED、BANKWITHHOLD、NORMALPAYMENT、VIRTUALPAYMENT
  12. */
  13. @NotBlank(message = "付款方式代码不能为空")
  14. private String payWayCode;
  15. /**
  16. * 付款方式名称:银行已扣、银行代扣、正常付款、虚拟支付,如果“Direct Debt”值为Y时,默认银行代扣
  17. */
  18. @NotBlank(message = "付款方式名称不能为空")
  19. private String payWayName;
  20. /**
  21. * 申请事由:默认提款表的“申请事由”
  22. */
  23. @NotBlank(message = "申请事由不能为空")
  24. private String applyReason;
  25. /**
  26. * 付款附言:默认融资产品表的“付款附言”,需发给收方信息
  27. */
  28. private String paymentPostscript;
  29. /**
  30. * 是否推送直联,默认N,手工选择;校验规则:如果Direct Debt=Y,则此字段不能为Y
  31. */
  32. @NotBlank(message = "是否推送直联不能为空")
  33. private String pushDirectConnection;
  34. }

@Validated 验证 List 参数失效问题

公共类-OUDTO

  1. package com.luoma.finance.dto;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. import lombok.ToString;
  5. import javax.validation.constraints.NotBlank;
  6. @Getter
  7. @Setter
  8. @ToString
  9. public class OUDTO {
  10. private int ouId;
  11. @NotBlank(message = "ouCode 不能为空")
  12. private String ouCode;
  13. @NotBlank(message = "ouName 不能为空")
  14. private String ouName;
  15. }

验证成功的情况

  1. @PostMapping("/add")
  2. public ResponseVO add(@RequestBody @Validated OUDTO dto) {
  3. return ouConsumer.add(dto);
  4. }

http://localhost:18810/financing/api/ou/add

入参

  1. {
  2. "ouCode": "",
  3. "ouId": 0,
  4. "ouName": "test"
  5. }

返回

  1. {
  2. "code": 400,
  3. "message": "ouCode 不能为空"
  4. }

验证失败的情况

  1. @PostMapping("/adds")
  2. public ResponseVO adds(@RequestBody @Validated List<OUDTO> dtos){
  3. for (OUDTO dto : dtos) {
  4. ouConsumer.add(dto);
  5. }
  6. return new ResponseVO(ResponseCode.OK);
  7. }

http://localhost:18810/financing/api/ou/adds

入参

  1. [
  2. {
  3. "ouCode": "0",
  4. "ouId": 0,
  5. "ouName": ""
  6. },
  7. {
  8. "ouCode": "",
  9. "ouId": 1,
  10. "ouName": "1"
  11. }
  12. ]

直接进入的方法,验证失效

解决办法

ValidableList

  1. package com.luoma.finance.util;
  2. import lombok.Data;
  3. import javax.validation.Valid;
  4. import java.util.*;
  5. /**
  6. * 用于解决使用 @Validated 无法验证 List 参数的问题
  7. * @param <E> 具体对象
  8. * luoma - 2022年11月4日18:13:05
  9. */
  10. @Data
  11. public class ValidableList<E> implements List<E> {
  12. @Valid
  13. private List<E> list = new LinkedList<>();
  14. @Override
  15. public int size() {
  16. return list.size();
  17. }
  18. @Override
  19. public boolean isEmpty() {
  20. return list.isEmpty();
  21. }
  22. @Override
  23. public boolean contains(Object o) {
  24. return list.contains(o);
  25. }
  26. @Override
  27. public Iterator<E> iterator() {
  28. return list.iterator();
  29. }
  30. @Override
  31. public Object[] toArray() {
  32. return list.toArray();
  33. }
  34. @Override
  35. public <T> T[] toArray(T[] a) {
  36. return list.toArray(a);
  37. }
  38. @Override
  39. public boolean add(E e) {
  40. return list.add(e);
  41. }
  42. @Override
  43. public boolean remove(Object o) {
  44. return list.remove(o);
  45. }
  46. @Override
  47. public boolean containsAll(Collection<?> c) {
  48. return list.containsAll(c);
  49. }
  50. @Override
  51. public boolean addAll(Collection<? extends E> c) {
  52. return list.addAll(c);
  53. }
  54. @Override
  55. public boolean addAll(int index, Collection<? extends E> c) {
  56. return list.addAll(index, c);
  57. }
  58. @Override
  59. public boolean removeAll(Collection<?> c) {
  60. return list.removeAll(c);
  61. }
  62. @Override
  63. public boolean retainAll(Collection<?> c) {
  64. return list.retainAll(c);
  65. }
  66. @Override
  67. public void clear() {
  68. list.clear();
  69. }
  70. @Override
  71. public E get(int index) {
  72. return list.get(index);
  73. }
  74. @Override
  75. public E set(int index, E element) {
  76. return list.set(index, element);
  77. }
  78. @Override
  79. public void add(int index, E element) {
  80. list.add(index, element);
  81. }
  82. @Override
  83. public E remove(int index) {
  84. return list.remove(index);
  85. }
  86. @Override
  87. public int indexOf(Object o) {
  88. return list.indexOf(o);
  89. }
  90. @Override
  91. public int lastIndexOf(Object o) {
  92. return list.lastIndexOf(o);
  93. }
  94. @Override
  95. public ListIterator<E> listIterator() {
  96. return list.listIterator();
  97. }
  98. @Override
  99. public ListIterator<E> listIterator(int index) {
  100. return list.listIterator(index);
  101. }
  102. @Override
  103. public List<E> subList(int fromIndex, int toIndex) {
  104. return list.subList(fromIndex, toIndex);
  105. }
  106. }

List 修改为 ValidableList

  1. @PostMapping("/adds")
  2. public ResponseVO adds(@RequestBody @Validated ValidableList<OUDTO> dtos){
  3. for (OUDTO dto : dtos) {
  4. ouConsumer.add(dto);
  5. }
  6. return new ResponseVO(ResponseCode.OK);
  7. }

http://localhost:18810/financing/api/ou/adds

入参

  1. [
  2. {
  3. "ouCode": "0",
  4. "ouId": 0,
  5. "ouName": "11"
  6. },
  7. {
  8. "ouCode": "",
  9. "ouId": 1,
  10. "ouName": "1"
  11. }
  12. ]

返回结果

  1. {
  2. "code": 400,
  3. "message": "ouCode 不能为空"
  4. }

SpringBoot-jsr303 参数校验-国际化

SpringBoot-jsr303 参数校验-国际化