In Spring you can set up "global" exception handler via @ControllerAdvice and @ExceptionHandler annotation. I'm trying to utilize this mechanism to have two global exception handlers:
RestControllerExceptionHandler - which should return error responses as json for any controller annotated with @RestControllerControllerExceptionHandler - which should print error message to the screen for any other controller (annottated with @Controller)The problem is that when I declare these two exception handlers spring always uses the ControllerExceptionHandler and never RestControllerExceptionHandler to handle the exception.
How to make this work ? BTW: I tried to use @Order annotation but this does not seem to work.
Here are my exception handlers:
// should handle all exception for classes annotated with
@ControllerAdvice(annotations = RestController.class)
public class RestControllerExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e) {
// below object should be serialized to json
ErrorResponse errorResponse = new ErrorResponse("asdasd");
return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
// should handle exceptions for all the other controllers
@ControllerAdvice(annotations = Controller.class)
public class ControllerExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleUnexpectedException(Exception e) {
return new ResponseEntity<String>("Unexpected exception, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
When I remove ControllerExceptionHandler than RestControllerExceptionHandler is correctly called by spring (only for classes annotated with @RestController).... but when I add ControllerExceptionHandler than all goes via ControllerExceptionHandler. Why?
After some deeper investigation it seems that the alphabetical order matters :/.
When I renamed my RestControllerExceptionHandler to ARestControllerExceptionHandler (which alphabetically precedes ControllerExceptionHandler) all works as expected! ARestControllerExceptionHandler correctly handles exceptions from RestControllers and ControllerExceptionHandler handles exception from other controllers.
I created a bug in spring for this: https://jira.spring.io/browse/SPR-15432
--- EDIT:
I received the answer for SPR-15432 where it is suggested that this case can be solved with @Order (org.springframework.core.annotation.Order) annotation or by implementing Ordered interface.
This did not work for me before, but it seems that I have imported wrong @Order annotation. (from log4j2 instead of spring). After fixing this it works. Fixed version is following:
// should handle all exception for classes annotated with
@ControllerAdvice(annotations = RestController.class)
@Order(1) // NOTE: order 1 here
public class RestControllerExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e) {
// below object should be serialized to json
ErrorResponse errorResponse = new ErrorResponse("asdasd");
return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// should handle exceptions for all the other controllers
@ControllerAdvice(annotations = Controller.class)
@Order(2) // NOTE: order 2 here
public class ControllerExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleUnexpectedException(Exception e) {
return new ResponseEntity<String>("Unexpected exception, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
That's happening because @RestController annotation is itself an @Controller as well so Spring is considering the @ControllerAdvice with annotation = Controller.class for both.
You may try another method to define the subset of Controllers that the @ControllerAdvice will have effect, so here are some solutions:
Create a new Annotation @NotRestController:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotRestController {
}
Mark controllers that are not @RestController with both @Controller and @NotRestController:
@Controller
@NotRestController
@RequestMapping("/controller")
public class SampleController {
}
Use NotRestController.class on ControllerExceptionHandler:
@ControllerAdvice(annotations = NotRestController.class)
public class ControllerExceptionHandler {
I've created a sample project with Solution 1 and shared it on Github:
https://github.com/brunocleite/springexceptionhandlertest
Move your @RestController classes into a package foo.bar and your @Controller classes into another package foo.nuk.
Use basePackages property of @ControllerAdvice instead of annotations:
@ControllerAdvice(basePackages={"foo.bar"})
public class RestControllerExceptionHandler {
@ControllerAdvice(basePackages={"foo.nuk"})
public class ControllerExceptionHandler {
Best regards!
To further build on the solution 1 of @bruno-leite:
You can define a meta-annotation so you only need to 1 annotation each time insted of 2:
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Controller;
import java.lang.annotation.*;
/**
* Alias annotation for @Controller so that @ControllerAdvice instances can
* target the web controllers separately from the rest controllers.
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
public @interface WebController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
Use it on the web controller(s):
@WebController
@RequestMapping("/customers")
public class CustomerController {
Finally define a @ControllerAdvice that only targets the web controllers like this:
@ControllerAdvice(annotations = WebController.class)
public class GlobalControllerAdvice {
@ModelAttribute("displayName")
public String getDisplayName(@AuthenticationPrincipal KeycloakAuthenticationToken principal) {
if (principal != null) {
String givenName = principal.getAccount().getKeycloakSecurityContext().getToken().getGivenName();
String familyName = principal.getAccount().getKeycloakSecurityContext().getToken().getFamilyName();
return givenName + " " + familyName;
} else {
return null;
}
}
}
(This example ensures that my Thymeleaf templates always have the displayName attribute, but can be easily adjusted for the exception handlers as well)