@ExceptionHandler for Error gets called only if there's no mapping for Exception

北城以北 提交于 2019-12-10 09:53:40

问题


Using spring-web-4.2.6, I have the following Controller and ExceptionHandler:

@ControllerAdvice
public class ExceptionsHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorDTO> HandleDefaultException(Exception ex) {
    ...
    }

    @ExceptionHandler(InternalError.class)
    public ResponseEntity<ErrorDTO> HandleInternalError(InternalError ex) {
    ...
    }
}

@RestController
@RequestMapping("/myController")
public class MyController {
    @RequestMapping(value = "/myAction", method = RequestMethod.POST)
    public boolean myAction() {
        throw new InternalError("");
    }
}

For some reason, the ExceptionsHandler's HandleDefaultException (for Exception.class) method is invoked, with an exception of type NestedServletException, instead of the HandleInternalError call.

When removing the default call, the IntenalError call is called with the proper InternalError exception.

I do not want to remove the default call as it is important to me to have a default handler to allow for a better experience for my users.

What am I missing here?

EDIT:

Apparently I'm using spring-web-4.3.3, without asking for it. I don't understand why exactly, here's my Gradle dependencies tree: http://pastebin.com/h6KXSyp2


回答1:


Spring MVC should only exhibit the behavior you describe with version 4.3 and above. See this JIRA issue. Previously, Spring MVC would not expose any Throwable values to @ExceptionHandler methods. See

  • ExceptionHandler doesn't work with Throwable

Since 4.3, Spring MVC will catch any Throwable thrown from your handler methods and wrap it in a NestedServletException, which it will then expose to the normal ExceptionHandlerExceptionResolver process.

Here's a short description of how it works:

  1. Checks if the handler method's @Controller class contains any @ExceptionHandler methods.
  2. If it does, tries to resolve one that can handle the Exception type (including NestedServletException). If it can, it uses that (there's some sorting if multiple matches are found). If it can't, and the Exception has a cause, it unwraps and tries again to find a handler for that. That cause might now be a Throwable (or any of its subtypes).
  3. If it doesn't. It gets all the @ControllerAdvice classes and tries to find a handler for the Exception type (including NestedServletException) in those. If it can, it uses that. If it can't, and the Exception has a cause, it unwraps it and tries again with that Throwable type.

In your example, your MyController throws an InternalError. Since this is not a subclass of Exception, Spring MVC wraps it in an NestedServletException.

MyController doesn't have any @ExceptionHandler methods, so Spring MVC skips it. You have a @ControllerAdvice annotated class, ExceptionsHandler, so Spring MVC checks that. The @ExceptionHandler annotated HandleDefaultException method can handle Exception, so Spring MVC chooses it to handle the NestedServletException.

If you remove that HandleDefaultException, Spring MVC won't find something that can handle Exception. It will then attempt to unwrap the NestedServletException and check for its cause. It'll then find the HandleInternalError which can handle that InternalError.

This is not an easy issue to deal with. Here are some options:

Create an @ExceptionHandler that handles NestedServletException and do the check for InternalError yourself.

@ExceptionHandler(NestedServletException.class)
public ResponseEntity<String> HandleNested(NestedServletException ex) {
    Throwable cause = ex.getCause();
    if (cause instanceof InternalError) {
        // deal with it
    } else if (cause instanceof OtherError) {
        // deal in some other way
    }
}

This is fine unless there's a bunch of different Error or Throwable types you want to handle. (Note that you can rethrow these if you can't or don't know how to handle them. Spring MVC will default to some other behavior, likely returning a 500 error code.)

Alternatively, you can take advantage of the fact that Spring MVC first checks the @Controller (or @RestController) class for @ExceptionHandler methods first. Just move the @ExceptionHandler method for InternalError into the controller.

@RestController
@RequestMapping("/myController")
public class MyController {
    @RequestMapping(value = "/myAction", method = RequestMethod.POST)
    public boolean myAction() {
        throw new InternalError("");
    }

    @ExceptionHandler(value = InternalError.class)
    public ResponseEntity<String> HandleInternalError(InternalError ex) {
         ...
    }
}

Now Spring will first attempt to find a handler for NestedServletException in MyController. It won't find any so it will unwrap NestedServletException and get an InternalError. It will try to find a handler for InternalError and find HandleInternalError.

This has the disadvantage that if multiple controllers' handler methods throw InternalError, you have to add an @ExceptionHandler to each. This might also be an advantage. Your handling logic will be closer to the thing that throws the error.



来源:https://stackoverflow.com/questions/40345227/exceptionhandler-for-error-gets-called-only-if-theres-no-mapping-for-exception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!