开发环境
| 名称 | 版本 |
|---|---|
| 操作系统 | 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参数校验
源码
pom.xml
<!--jsr303 参数校验--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
简单的必填校验
参数类-Shop
package com.test.model;import lombok.Data;import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import java.math.BigDecimal;import java.time.LocalDate;@Datapublic class Shop {@NotNull(message = "id 不能为空")private Long id;@NotBlank(message = "name 不能为空")private String name;@NotNull(message = "创建日期不能为空")private LocalDate createDate;@NotNull(message = "金额不能为空")private BigDecimal amount;}
接口-valid
package com.test.controller;import com.test.model.Shop;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;@RestController@RequestMapping("/test")public class TestController {@PostMapping("/valid")public String valid(@RequestBody @Validated Shop param) {return "OK";}}
测试-接口-valid
Postman 调动
● URL:http://localhost:8001/test/valid
● 入参:
{"id":"","name":"","createDate":"","amount": ""}
● 返回结果,可以看到,返回了一大串报错结果
{"timestamp": "2023-04-15T03:03:01.051+0000","status": 400,"error": "Bad Request","errors": [{"codes": ["NotNull.shop.createDate","NotNull.createDate","NotNull.java.time.LocalDate","NotNull"],"arguments": [{"codes": ["shop.createDate","createDate"],"arguments": null,"defaultMessage": "createDate","code": "createDate"}],"defaultMessage": "创建日期不能为空","objectName": "shop","field": "createDate","rejectedValue": null,"bindingFailure": false,"code": "NotNull"},{"codes": ["NotNull.shop.id","NotNull.id","NotNull.java.lang.Long","NotNull"],"arguments": [{"codes": ["shop.id","id"],"arguments": null,"defaultMessage": "id","code": "id"}],"defaultMessage": "id 不能为空","objectName": "shop","field": "id","rejectedValue": null,"bindingFailure": false,"code": "NotNull"},{"codes": ["NotBlank.shop.name","NotBlank.name","NotBlank.java.lang.String","NotBlank"],"arguments": [{"codes": ["shop.name","name"],"arguments": null,"defaultMessage": "name","code": "name"}],"defaultMessage": "name 不能为空","objectName": "shop","field": "name","rejectedValue": "","bindingFailure": false,"code": "NotBlank"},{"codes": ["NotNull.shop.amount","NotNull.amount","NotNull.java.math.BigDecimal","NotNull"],"arguments": [{"codes": ["shop.amount","amount"],"arguments": null,"defaultMessage": "amount","code": "amount"}],"defaultMessage": "金额不能为空","objectName": "shop","field": "amount","rejectedValue": null,"bindingFailure": false,"code": "NotNull"}],"message": "Validation failed for object='shop'. Error count: 4","path": "/test/valid"}
使用 BindingResult 统计报错信息
测试-接口-valid 中,可以看到,返回的报错信息很冗余,这里我们使用 BindingResult 统计报错信息
package com.test.controller;import com.fasterxml.jackson.databind.util.JSONPObject;import com.test.model.Shop;import com.test.utils.CommonUtils;import io.micrometer.core.instrument.util.JsonUtils;import org.springframework.boot.context.properties.bind.BindResult;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import java.util.HashMap;import java.util.List;import java.util.Map;@RestController@RequestMapping("/test")public class TestController {@PostMapping("/valid")public String valid(@RequestBody @Validated Shop param, BindingResult result) {if(result.hasErrors()) {Map<String, String> map = new HashMap<>();List<FieldError> fieldErrors = result.getFieldErrors();fieldErrors.forEach(item -> {String field = item.getField();String defaultMessage = item.getDefaultMessage();map.put(field,defaultMessage);});return CommonUtils.convertToJson(map, true);}return "OK";}}
测试-使用 BindingResult 统计报错信息
● URL:http://localhost:8001/test/valid
● 请求参数:
{"id":"","name":"","createDate":"","amount": ""}
● 返回结果:
{"amount":"金额不能为空","name":"name 不能为空","id":"id 不能为空","createDate":"创建日期不能为空"}
使用异常处理统计报错信息
使用
BindingResult统计异常报错信息,会出现一个问题,就是每个接口都必须添加参数BindingResult result。另外一种办法,是添加一个异常处理类,统一处理参数异常。
添加这个异常处理不会和
BindingResult冲突- 如果接口添加了
BindingResult result,会以BindingResult的结果为准 如果接口没有添加
BindingResult result,以异常处理类返回的结果为准添加
GlobalExceptionHandler
import org.springframework.util.CollectionUtils;import org.springframework.validation.FieldError;import org.springframework.validation.ObjectError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.List;@RestControllerAdvicepublic class GlobalExceptionHandler {/*** 添加校验参数异常处理** @param e 异常信息* @return 参数异常提示*/@ExceptionHandler({MethodArgumentNotValidException.class})public String bindExceptionHandler(Throwable e) {//@Valid(或 @Validated )校验未通过时,会抛出 MethodArgumentNotValidException 异常if (e instanceof MethodArgumentNotValidException) {List<ObjectError> allErrors = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors();//String msg = StringUtils.collectionToDelimitedString(allErrors, ";");return getValidExceptionMsg(allErrors);}//if (e instanceof BindException) {// List<ObjectError> allErrors = ((BindException) e).getBindingResult().getAllErrors();// return StringUtils.collectionToDelimitedString(allErrors, ";");// //return getValidExceptionMsg(allErrors);//}return e.getMessage();}private String getValidExceptionMsg(List<ObjectError> errors) {if (!CollectionUtils.isEmpty(errors)) {StringBuilder sb = new StringBuilder();errors.forEach(error -> {if (error instanceof FieldError) {sb.append(((FieldError) error).getField()).append(":");}sb.append(error.getDefaultMessage()).append(";");});String msg = sb.toString();msg = msg.substring(0, msg.length() - 1);return msg;}return null;}}
- 测试接口
@PostMapping("/valid1")public String valid1(@RequestBody @Validated Shop param) {return "OK";}
{"id":"","name":"","createDate":"","amount": ""}
返回结果
name:name 不能为空;id:id 不能为空;amount:金额不能为空;createDate:创建日期不能为空
自定义校验
自定义校验逻辑-MyConstraintValidator
package com.test.valid;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;import java.util.HashSet;import java.util.Set;public class MyConstraintValidator implements ConstraintValidator<ArrayVal, Integer> {private Set<Integer> set = new HashSet<>();@Overridepublic void initialize(ArrayVal constraintAnnotation) {int[] value = constraintAnnotation.value();for (int i : value) {set.add(i);}}@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {return set.contains(value);}}
自定义校验类型-ArrayVal
package com.test.valid;import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.*;@Documented@Constraint(validatedBy = {MyConstraintValidator.class} )@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)public @interface ArrayVal {String message() default "{com.dp.annotation.ArrayVal.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};int[] value() default {};}
参数类-Shop
package com.test.model;import com.test.valid.ArrayVal;import lombok.Data;@Datapublic class Shop {@ArrayVal(value = {0, 1}, message = "字段必须为 1 或者 0")private Integer isDelete;}
测试-自定义校验
● URL:http://localhost:8001/test/valid
● 请求参数:
{"isDelete":3}
● 返回结果:
{"isDelete":"字段必须为 1 或者 0"}
校验分组
AddGroup
package com.test.valid;/*** 分组校验,添加*/public @interface AddGroup {}
UpdateGroup
package com.test.valid;/*** 分组校验,修改*/public @interface UpdateGroup {}
参数类-Shop-添加分组
package com.test.model;import com.test.valid.AddGroup;import com.test.valid.ArrayVal;import com.test.valid.UpdateGroup;import lombok.Data;import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import java.math.BigDecimal;import java.time.LocalDate;@Datapublic class Shop {@NotNull(message = "id 不能为空", groups = UpdateGroup.class)private Long id;@NotBlank(message = "name 不能为空", groups = {AddGroup.class, UpdateGroup.class})private String name;@NotNull(message = "创建日期不能为空", groups = AddGroup.class)private LocalDate createDate;@NotNull(message = "金额不能为空", groups = {AddGroup.class, UpdateGroup.class})private BigDecimal amount;@ArrayVal(value = {0, 1}, message = "字段必须为 1 或者 0")private Integer isDelete;}
接口-新增,修改
package com.test.controller;import com.fasterxml.jackson.databind.util.JSONPObject;import com.test.model.Shop;import com.test.utils.CommonUtils;import com.test.valid.AddGroup;import com.test.valid.UpdateGroup;import io.micrometer.core.instrument.util.JsonUtils;import org.springframework.boot.context.properties.bind.BindResult;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import java.util.HashMap;import java.util.List;import java.util.Map;@RestController@RequestMapping("/test")public class TestController {@PostMapping("/add")public String add(@RequestBody @Validated(AddGroup.class) Shop param, BindingResult result) {String errorMsg = checkError(result);if(null != errorMsg) {return errorMsg;}return "OK";}@PostMapping("/update")public String update(@RequestBody @Validated(UpdateGroup.class) Shop param, BindingResult result) {String errorMsg = checkError(result);if(null != errorMsg) {return errorMsg;}return "OK";}private String checkError (BindingResult result) {if(result.hasErrors()) {Map<String, String> map = new HashMap<>();List<FieldError> fieldErrors = result.getFieldErrors();fieldErrors.forEach(item -> {String field = item.getField();String defaultMessage = item.getDefaultMessage();map.put(field,defaultMessage);});return CommonUtils.convertToJson(map, true);}return null;}}
测试-新增
● URL:http://localhost:8001/test/add
● 请求参数:
{"id":"","name":"luoma","createDate":"2023-04-15","amount": "100.11","isDelete":null}
● 返回结果:
OK
● URL:http://localhost:8001/test/add
● 请求参数:
{"id":"","name":"luoma","createDate":"2023-04-15","amount": "","isDelete":null}
● 返回结果:
{"amount":"金额不能为空"}
● 可以看到,id 因为加入了 UpdateGroup 分组,不用必填。
● 其它 AddGroup 分组,必填。
● isDelete 因为没有加入分组,所以在有分组的校验中失效了。
测试-修改
● URL:http://localhost:8001/test/update
● 请求参数:
{"id":"1","name":"luoma","createDate":"","amount": "100.11","isDelete":nulll}
● 返回结果:
OK
● URL:http://localhost:8001/test/update
● 请求参数:
{"id":"","name":"luoma","createDate":"","amount": "100.11","isDelete":null}
● 返回结果:
{"amount":"金额不能为空"}
● 可以看到,createDate 因为加入了 AddGroup 分组,不用必填。
● 其它 UpdateGroup 分组,必填。
● isDelete 因为没有加入分组,所以在有分组的校验中失效了。
必填参数嵌套
/*** 保存数据* @param param 付款申请数据* @return 操作结果*/@ApiOperation("保存数据,发起工作流")@PostMapping("/save")public ResponseVO<Boolean> save(@RequestBody @Validated PayApplyEditDto param){Boolean result = true;return new ResponseVO<>(ResponseCode.OK, result);}
package com.luoma.finance.dto;import lombok.Data;import javax.validation.Valid;import java.util.List;/*** 参数类 - 付款申请保存操作* luoma - 2022年11月16日17:49:21*/@Datapublic class PayApplyEditDto {/*** 融资付款申请头信息数据*/@Validprivate PayApplyHeadEditDto headData;/*** 附件列表*/private List<AttachmnetDTO> listAttachmnet;}
package com.luoma.finance.dto;import lombok.Data;import javax.validation.constraints.NotBlank;/*** 参数类 - 付款申请保存操作 - 融资付款申请头信息表* luoma - 2022年11月16日17:49:21*/@Datapublic class PayApplyHeadEditDto {/*** 付款方式代码:BANKDEDUCTED、BANKWITHHOLD、NORMALPAYMENT、VIRTUALPAYMENT*/@NotBlank(message = "付款方式代码不能为空")private String payWayCode;/*** 付款方式名称:银行已扣、银行代扣、正常付款、虚拟支付,如果“Direct Debt”值为Y时,默认银行代扣*/@NotBlank(message = "付款方式名称不能为空")private String payWayName;/*** 申请事由:默认提款表的“申请事由”*/@NotBlank(message = "申请事由不能为空")private String applyReason;/*** 付款附言:默认融资产品表的“付款附言”,需发给收方信息*/private String paymentPostscript;/*** 是否推送直联,默认N,手工选择;校验规则:如果Direct Debt=Y,则此字段不能为Y*/@NotBlank(message = "是否推送直联不能为空")private String pushDirectConnection;}
@Validated 验证 List 参数失效问题
公共类-OUDTO
package com.luoma.finance.dto;import lombok.Getter;import lombok.Setter;import lombok.ToString;import javax.validation.constraints.NotBlank;@Getter@Setter@ToStringpublic class OUDTO {private int ouId;@NotBlank(message = "ouCode 不能为空")private String ouCode;@NotBlank(message = "ouName 不能为空")private String ouName;}
验证成功的情况
@PostMapping("/add")public ResponseVO add(@RequestBody @Validated OUDTO dto) {return ouConsumer.add(dto);}
http://localhost:18810/financing/api/ou/add
入参
{"ouCode": "","ouId": 0,"ouName": "test"}
返回
{"code": 400,"message": "ouCode 不能为空"}
验证失败的情况
@PostMapping("/adds")public ResponseVO adds(@RequestBody @Validated List<OUDTO> dtos){for (OUDTO dto : dtos) {ouConsumer.add(dto);}return new ResponseVO(ResponseCode.OK);}
http://localhost:18810/financing/api/ou/adds
入参
[{"ouCode": "0","ouId": 0,"ouName": ""},{"ouCode": "","ouId": 1,"ouName": "1"}]
直接进入的方法,验证失效
解决办法
ValidableList
package com.luoma.finance.util;import lombok.Data;import javax.validation.Valid;import java.util.*;/*** 用于解决使用 @Validated 无法验证 List 参数的问题* @param <E> 具体对象* luoma - 2022年11月4日18:13:05*/@Datapublic class ValidableList<E> implements List<E> {@Validprivate List<E> list = new LinkedList<>();@Overridepublic int size() {return list.size();}@Overridepublic boolean isEmpty() {return list.isEmpty();}@Overridepublic boolean contains(Object o) {return list.contains(o);}@Overridepublic Iterator<E> iterator() {return list.iterator();}@Overridepublic Object[] toArray() {return list.toArray();}@Overridepublic <T> T[] toArray(T[] a) {return list.toArray(a);}@Overridepublic boolean add(E e) {return list.add(e);}@Overridepublic boolean remove(Object o) {return list.remove(o);}@Overridepublic boolean containsAll(Collection<?> c) {return list.containsAll(c);}@Overridepublic boolean addAll(Collection<? extends E> c) {return list.addAll(c);}@Overridepublic boolean addAll(int index, Collection<? extends E> c) {return list.addAll(index, c);}@Overridepublic boolean removeAll(Collection<?> c) {return list.removeAll(c);}@Overridepublic boolean retainAll(Collection<?> c) {return list.retainAll(c);}@Overridepublic void clear() {list.clear();}@Overridepublic E get(int index) {return list.get(index);}@Overridepublic E set(int index, E element) {return list.set(index, element);}@Overridepublic void add(int index, E element) {list.add(index, element);}@Overridepublic E remove(int index) {return list.remove(index);}@Overridepublic int indexOf(Object o) {return list.indexOf(o);}@Overridepublic int lastIndexOf(Object o) {return list.lastIndexOf(o);}@Overridepublic ListIterator<E> listIterator() {return list.listIterator();}@Overridepublic ListIterator<E> listIterator(int index) {return list.listIterator(index);}@Overridepublic List<E> subList(int fromIndex, int toIndex) {return list.subList(fromIndex, toIndex);}}
List 修改为 ValidableList
@PostMapping("/adds")public ResponseVO adds(@RequestBody @Validated ValidableList<OUDTO> dtos){for (OUDTO dto : dtos) {ouConsumer.add(dto);}return new ResponseVO(ResponseCode.OK);}
http://localhost:18810/financing/api/ou/adds
入参
[{"ouCode": "0","ouId": 0,"ouName": "11"},{"ouCode": "","ouId": 1,"ouName": "1"}]
返回结果
{"code": 400,"message": "ouCode 不能为空"}