walkeros
walkeros

Reputation: 4942

Spring: Different exception handler for RestController and Controller

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:

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?

Upvotes: 14

Views: 16733

Answers (5)

Wim Deblauwe
Wim Deblauwe

Reputation: 26878

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)

Upvotes: 1

danna
danna

Reputation: 11

This was my solution. It eliminates the need to create additional annotations.

  1. Create 2 base controllers.
@Controller
public class BaseController

@Controller
public class BaseRestController
  1. Extend each controller with chosen base controller.
@Controller
public class UserController extends BaseController

@RestController
public class UserRestController extends BaseRestController
  1. Create 2 global controllers and include only the extended base class in each.
@ControllerAdvice(assignableTypes = BaseController.class)
public class GlobalController

@ControllerAdvice(assignableTypes = BaseRestController.class)
public class GlobalRestController

Upvotes: 1

user7015190
user7015190

Reputation: 9

Use order to fix error:

@Order(1)
@ControllerAdvice(annotations = RestController.class)
public class ErrorRestControllerAdvice {
}

@Order(2)
@ControllerAdvice(annotations = Controller.class)
public class ErrorControllerAdvice {
}

Upvotes: -4

walkeros
walkeros

Reputation: 4942

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);
  }
}

Upvotes: 16

Bruno Leite
Bruno Leite

Reputation: 541

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:


Solution 1

  1. Create a new Annotation @NotRestController:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
        public @interface NotRestController {
    }
    
  2. Mark controllers that are not @RestController with both @Controller and @NotRestController:

    @Controller
    @NotRestController
    @RequestMapping("/controller")
    public class SampleController {
    }
    
  3. 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


Solution 2

  1. Move your @RestController classes into a package foo.bar and your @Controller classes into another package foo.nuk.

  2. Use basePackages property of @ControllerAdvice instead of annotations:

    @ControllerAdvice(basePackages={"foo.bar"})
    public class RestControllerExceptionHandler {
    
    @ControllerAdvice(basePackages={"foo.nuk"})
    public class ControllerExceptionHandler {
    

Best regards!

Upvotes: 11

Related Questions