Spring List使用@Valid校验时如何获取无效元素的索引或ID
问题描述
使用@Valid注解对List集合类型参数做校验时,希望直接获取集合内校验不通过对象的定位信息,替代现有正则解析字段路径的实现。
现有实现
已完成基础校验逻辑开发,校验失败时默认返回响应格式如下:
{ "status": "BAD_REQUEST", "message": "Validation Error", "detailedMessage": "There are errors in the entered data", "timestamp": 1657896844868, "validationErrors": [ { "field": "items[0].name", "message": "The name is mandatory" }, { "field": "items[1].surname", "message": "The surname is mandatory" } ] }
当前通过正则表达式从items[0].name这类字段字符串中提取索引,供前端定位错误项高亮输入框,但该实现不够优雅。
预期效果
优先方案为不依赖数组索引,直接获取无效对象自带的业务id字段,返回如下格式响应,前端无需解析字符串即可精准定位无效对象;退而求其次的方案是单独返回数组索引字段,也优于正则解析的实现。
{ "status": "BAD_REQUEST", "message": "Validation Error", "detailedMessage": "There are errors in the entered data", "timestamp": 1657896844868, "validationErrors": [ { "field": "items[12345].name", "message": "The name is mandatory", "itemId": 12345 }, { "field": "items[12346].surname", "message": "The surname is mandatory", "itemId": 12346 } ] }
现有代码
- 请求类定义(Controller中通过
@Valid @RequestBody注解接收参数)
public class Request { @Valid @NotEmpty(message = "You must insert at least one item") private List<@Valid @NotNull Item> items; }
- 全局异常处理代码(捕获
MethodArgumentNotValidException构造校验错误响应)
@RestControllerAdvice public class BaseExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", "There are errors in the entered data"); List<ValidationError> errors = new ArrayList<>(); ex.getBindingResult().getAllErrors().forEach(error -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); ValidationError validationError = new ValidationError(fieldName, errorMessage); errors.add(validationError); }); errorResponse.setValidationErrors(errors); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } }
实现方案
两种需求都不需要靠正则硬解析字段路径实现,Spring校验框架本身暴露了足够的能力拿到所需信息。
方案1:直接返回集合元素索引
Bean Validation返回的List类型字段路径格式固定为字段名[索引].子属性,不需要写复杂正则,直接按字符位置截取就能拿到索引,比正则效率高且不会出现匹配异常。
修改异常处理器中的错误遍历逻辑即可:
ex.getBindingResult().getAllErrors().forEach(error -> { FieldError fieldError = (FieldError) error; String fieldName = fieldError.getField(); String errorMessage = fieldError.getDefaultMessage(); ValidationError validationError = new ValidationError(fieldName, errorMessage); if (fieldName.startsWith("items[")) { int indexStart = fieldName.indexOf('[') + 1; int indexEnd = fieldName.indexOf(']'); Integer itemIndex = Integer.parseInt(fieldName.substring(indexStart, indexEnd)); validationError.setItemIndex(itemIndex); } errors.add(validationError); });
只需要给ValidationError类补充itemIndex字段,就能直接把索引返回给前端,不需要额外解析。
方案2:返回元素业务ID(最优实现)
要拿到错误项的业务ID更简单,直接从校验上下文中拿到原始入参对象,根据索引取对应Item即可,完全不依赖路径解析逻辑:
@Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", "There are errors in the entered data"); List<ValidationError> errors = new ArrayList<>(); BindingResult bindingResult = ex.getBindingResult(); // 获取原始请求入参 Request targetRequest = (Request) bindingResult.getTarget(); List<Item> items = targetRequest.getItems(); bindingResult.getAllErrors().forEach(error -> { FieldError fieldError = (FieldError) error; String fieldName = fieldError.getField(); String errorMessage = fieldError.getDefaultMessage(); ValidationError validationError = new ValidationError(fieldName, errorMessage); if (fieldName.startsWith("items[")) { int indexStart = fieldName.indexOf('[') + 1; int indexEnd = fieldName.indexOf(']'); int itemIndex = Integer.parseInt(fieldName.substring(indexStart, indexEnd)); // 直接从原始入参列表中取出错误对应的Item对象 Item errorItem = items.get(itemIndex); // 设置业务ID validationError.setItemId(errorItem.getId()); // 如果需要把返回的field路径里的索引替换成业务ID,直接拼接即可 String customField = "items[" + errorItem.getId() + "]." + fieldName.substring(indexEnd + 2); validationError.setField(customField); } errors.add(validationError); }); errorResponse.setValidationErrors(errors); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
这种实现拿到的业务ID100%准确,不存在解析错位问题,前端拿到itemId后可以直接匹配对应项高亮,不需要做任何字符串解析。如果后续有多层嵌套集合的校验场景,只需要按层级解析每个[]中的索引,逐层从原始入参中取对应元素即可,逻辑通用。
内容的提问来源于stack exchange,提问作者Andrea
相关产品推荐
相关产品推荐

