Spring Boot跨参数校验返回false时抛出ConstraintViolationException问题求助
我在StackOverflow上查阅了大量和ConstraintViolationException相关的帖子,都没找到解决方案。我当前使用Spring Boot v2.3.3版本开发Web应用,已经集成了spring-boot-starter-validation依赖。我希望通过自定义跨参数约束注解实现密码校验功能,参考了Baeldung的相关教程,以下是我的ConstraintValidator实现类代码:
@SupportedValidationTarget(PARAMETERS) public class ValidPasswordValidator implements ConstraintValidator<ValidPassword, Object[]> { private ValidateRequestTypeEnum requestType; @Autowired private PasswordValidatorService passwordValidatorService; @Override public void initialize(ValidPassword constraintAnnotation) { this.requestType = constraintAnnotation.requestType(); SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this); } @Override public boolean isValid(Object[] value, ConstraintValidatorContext context) { SomeUser someUser = (SomeUser) value[0]; try { passwordValidatorService.validate(someUser, this.requestType); } catch (ConstraintViolationException e) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(e.getMessage()).addConstraintViolation(); return false; // 改为true时不会抛出异常 } return true; } }
现在遇到的问题是:只要isValid()方法返回false,即便我已经在catch块里处理了自定义的业务异常,还是会抛出ConstraintViolationException。就算我把业务异常换成其他类型的异常,再在catch块中捕获处理,行为也完全一致。
以下是我其他相关类的实现代码和异常堆栈:
ValidPassword注解
@Constraint(validatedBy = ValidPasswordValidator.class) @Target({METHOD, CONSTRUCTOR}) @Retention(RUNTIME) @Documented public @interface ValidPassword { String message() default ""; ValidateRequestTypeEnum requestType(); Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Controller类
@Validated @RestController public class SomeController { @ValidPassword(requestType = CREATE_USER) @GetMapping("/create-user") public void createUser(@RequestBody SomeUser someUser) { System.out.println("Create User Mapping"); System.out.println(someUser.toString()); } }
SomeUser模型类
public class SomeUser { private int id; private String name; private String password; // 已省略getter、setter方法 // 已省略toString()方法 }
PasswordValidatorService接口
public interface PasswordValidatorService { void validate(SomeUser someUser, ValidateRequestTypeEnum requestType) throws ConstraintViolationException; }
PasswordLengthValidatorImpl实现类
@Service public class PasswordLengthValidatorImpl implements PasswordValidatorService { @Override public void validate(SomeUser someUser, ValidateRequestTypeEnum requestType) throws ConstraintViolationException { switch (requestType) { case CREATE_USER: System.out.println("From Create_User block"); if (someUser.getPassword().length() < 8) { throw new ConstraintViolationException("Password length less than 8", new HashSet<>()); } break; default: System.out.println("---Default switch block---"); } } }
异常堆栈信息
javax.validation.ConstraintViolationException: createUser.<cross-parameter>: Password length less than 8 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] ... 省略其余堆栈
你观察到的异常是Spring方法校验的默认机制导致的,并非你catch块没有捕获到自定义抛出的异常:
当标注了@Validated的类下的方法触发校验时,Spring的MethodValidationInterceptor会拦截方法执行,收集所有ConstraintValidator返回false时对应的约束违例信息,统一封装成ConstraintViolationException抛出。你在catch块里向ConstraintValidatorContext中塞入的错误信息,就是最终异常里的提示内容。
根据你的业务需求可以选择两种处理方式:
方式1:保留框架校验逻辑,全局捕获异常返回友好提示
新增全局异常处理器,直接捕获框架抛出的ConstraintViolationException,封装成前端可以识别的错误格式返回:
@RestControllerAdvice public class GlobalValidationExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<Map<String, String>> handleValidationException(ConstraintViolationException e) { Map<String, String> errorInfo = new HashMap<>(); for (ConstraintViolation<?> violation : e.getConstraintViolations()) { errorInfo.put("message", violation.getMessage()); } return ResponseEntity.badRequest().body(errorInfo); } }
方式2:自定义业务异常,绕过框架自动抛异常逻辑
如果不想使用框架默认的异常抛出逻辑,可以修改校验器实现,校验不通过时直接抛自定义业务异常,isValid方法固定返回true即可:
// 首先定义自定义业务异常 public class PasswordValidationException extends RuntimeException { public PasswordValidationException(String message) { super(message); } } // 修改Validator的isValid方法 @Override public boolean isValid(Object[] value, ConstraintValidatorContext context) { SomeUser someUser = (SomeUser) value[0]; try { passwordValidatorService.validate(someUser, this.requestType); } catch (ConstraintViolationException e) { throw new PasswordValidationException(e.getMessage()); } return true; } // 全局异常处理器捕获自定义异常 @ExceptionHandler(PasswordValidationException.class) public ResponseEntity<Map<String, String>> handlePasswordValidationException(PasswordValidationException e) { Map<String, String> errorInfo = new HashMap<>(); errorInfo.put("message", e.getMessage()); return ResponseEntity.badRequest().body(errorInfo); }
额外优化建议:你的PasswordValidatorService实现类中没必要抛出ConstraintViolationException,直接抛自定义业务异常可以避免和框架原生异常混淆,代码可读性更高。
内容的提问来源于stack exchange,提问作者sam

