使用@javax.validation.Valid时如何以正确的方式抛出自定义异常?

Gar*_*hoi 10 java validation spring

使用时如何以正确的方式抛出自定义异常@javax.validation.Valid

@Valid在控制器中使用,并@AssertTrue验证请求正文字段。

public ResponseEntity<Foo> createFoo(
    @Valid @RequestBody Foo FooRequest ...
Run Code Online (Sandbox Code Playgroud)
    @AssertTrue()
    public boolean isFooValid() {
        if (invalid)
            return false;
        ...
    }

Run Code Online (Sandbox Code Playgroud)

但是,我想在某些情况下抛出自定义的异常类。

    @AssertTrue()
    public boolean isFooValid() {
        if (invalid)
            return false;
        ...

        // note below
        if (invalidInAnotherCondition)
            throw new CustomizedException(...);
    }

Run Code Online (Sandbox Code Playgroud)

我知道这不是@Valid在控制器中使用的理想方式,并且@AssertTrue。尽管如此,因为我可以创建自己的 Exception 类,其中包含自定义的错误信息,并且可以方便地使用@Valid.

然而错误发生了。

javax.validation.ValidationException: HV000090: Unable to access isFooValid
    at org.hibernate.validator.internal.util.ReflectionHelper.getValue(ReflectionHelper.java:245)
    at org.hibernate.validator.internal.metadata.location.GetterConstraintLocation.getValue(GetterConstraintLocation.java:89)
    at org.hibernate.validator.internal.engine.ValueContext.getValue(ValueContext.java:235)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:549)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:515)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:485)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:447)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:397)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:173)
    at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:117)
    at org.springframework.boot.autoconfigure.validation.ValidatorAdapter.validate(ValidatorAdapter.java:70)
    at org.springframework.validation.DataBinder.validate(DataBinder.java:889)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.validateIfApplicable(AbstractMessageConverterMethodArgumentResolver.java:266)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:137)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:269)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:78)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:133)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:130)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:249)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:78)
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:99)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:376)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.reflect.InvocationTargetException: null
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.hibernate.validator.internal.util.ReflectionHelper.getValue(ReflectionHelper.java:242)
    ... 70 common frames omitted
Caused by: com.finda.services.finda.common.exception.CustomizedException: 'df282e0d-1205-4574-adaa-0af819af66c0' 
    at ...
    ... 75 common frames omitted
Run Code Online (Sandbox Code Playgroud)

我认为发生这种情况是因为最初,@AssertTrue它本身抛出了自己的异常,并且要通过内部逻辑进行处理;但是,自定义抛出的异常是不可接受的,这可以在Caused by: java.lang.reflect.InvocationTargetException: nulljavax.validation.ValidationException: HV000090: Unable to access isFooValid

所以我的最后一个问题如下:

我可以绕过这个错误,仍然抛出自定义异常吗?

我真的很感谢您提前阅读这篇长文。

Edw*_*rzo 13

考虑下面的示例,我实现了类似于您所要求的内容:

@RestController
@RequestMapping("/accounts")
public class SavingsAccountController {

   private final BankAccountService accountService;

   @Autowired
   public SavingsAccountController(SavingsAccountService accountService) {
       this.accountService = accountService;
   }

   @PutMapping("withdraw")
   public ResponseEntity<AccountBalance> onMoneyWithdrawal(@RequestBody @Validated WithdrawMoney withdrawal, BindingResult errors) {

       //this is the validation barrier
       if (errors.hasErrors()) {
           throw new ValidationException(errors);
       }

       double balance = accountService.withdrawMoney(withdrawal);
       return ResponseEntity.ok(new AccountBalance(
               withdrawal.getAccountNumber(), balance));
   }

   @PutMapping("save")
   public ResponseEntity<AccountBalance> onMoneySaving(@RequestBody @Validated SaveMoney savings, BindingResult errors) {

       //this is the validation barrier
       if (errors.hasErrors()) {
           throw new ValidationException(errors);
       }

       double balance = accountService.saveMoney(savings);
       return ResponseEntity.ok(new AccountBalance(
               savings.getAccountNumber(), balance));
   }
}
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,我们使用 Bean Validation 来检查用户的 DTO 是否包含有效信息。DTO 中发现的任何错误都通过BindingResult错误变量提供,开发人员可以从中提取验证阶段出现问题的所有详细信息。

为了使开发人员更容易处理这种模式,在上面的代码中,我简单地将 包装到一个知道如何提取验证错误详细信息的BindingResult自定义中。ValidationException

public class ValidationException extends RuntimeException {

   private final BindingResult errors;

   public ValidationException(BindingResult errors) {
       this.errors = errors;
   }

   public List<String> getMessages() {
       return getValidationMessage(this.errors);
   }


   @Override
   public String getMessage() {
       return this.getMessages().toString();
   }


   //demonstrate how to extract a message from the binging result
   private static List<String> getValidationMessage(BindingResult bindingResult) {
       return bindingResult.getAllErrors()
               .stream()
               .map(ValidationException::getValidationMessage)
               .collect(Collectors.toList());
   }

   private static String getValidationMessage(ObjectError error) {
       if (error instanceof FieldError) {
           FieldError fieldError = (FieldError) error;
           String className = fieldError.getObjectName();
           String property = fieldError.getField();
           Object invalidValue = fieldError.getRejectedValue();
           String message = fieldError.getDefaultMessage();
           return String.format("%s.%s %s, but it was %s", className, property, message, invalidValue);
       }
       return String.format("%s: %s", error.getObjectName(), error.getDefaultMessage());
   }

}
Run Code Online (Sandbox Code Playgroud)

请注意,在我的控制器定义中,我没有使用 Bean Validation 的@Valid注释,而是使用 Spring 对应的@Validated注释,但在底层 Spring 将使用 Bean Validation。

如何序列化自定义异常?

在上面的代码中,ValidationException当有效负载无效时将抛出。控制器应该如何为客户端创建响应?

有多种方法可以处理这个问题,但也许最简单的解决方案是定义一个注释为 的类@ControllerAdvice。在这个带注释的类中,我们将为我们想要处理的任何特定异常放置异常处理程序,并将它们转换为有效的响应对象以返回给我们的客户端:

@ControllerAdvice
public class ExceptionHandlers {

   @ExceptionHandler
   public ResponseEntity<ErrorModel> handle(ValidationException ex) {
       return ResponseEntity.badRequest()
                            .body(new ErrorModel(ex.getMessages()));
   }

   //...
}
Run Code Online (Sandbox Code Playgroud)

我用 Spring 编写了一些有关此技术和其他验证技术的其他示例,以防您有兴趣阅读更多相关内容