开发环境
名称 | 版本 |
---|---|
操作系统 | 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;
@Data
public 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;
@RestControllerAdvice
public 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<>();
@Override
public void initialize(ArrayVal constraintAnnotation) {
int[] value = constraintAnnotation.value();
for (int i : value) {
set.add(i);
}
}
@Override
public 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;
@Data
public 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;
@Data
public 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
*/
@Data
public class PayApplyEditDto {
/**
* 融资付款申请头信息数据
*/
@Valid
private 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
*/
@Data
public 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
@ToString
public 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
*/
@Data
public class ValidableList<E> implements List<E> {
@Valid
private List<E> list = new LinkedList<>();
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
@Override
public void clear() {
list.clear();
}
@Override
public E get(int index) {
return list.get(index);
}
@Override
public E set(int index, E element) {
return list.set(index, element);
}
@Override
public void add(int index, E element) {
list.add(index, element);
}
@Override
public E remove(int index) {
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}
@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
@Override
public 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 不能为空"
}