Search code examples
javaspringspring-aop

How to ignore default exception handling advice when it is already handled by another advice


I currently have a ExceptionAdvice class where it handles all the basic (400, 405, 404 and Other) Exceptions. For example I have a default advice where it handles all MethodArgumentNotValidExceptions and returns 400 Bad Request Error. For example

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Error handleBadRequestException(Exception exception) {
    return buildError(extractTriggerElement(exception), exception);
}

I also have a different point cut advice targeting one of the controller methods that handles MethodArgumentNotValidException because I need to create a custom error message for this case. Something like this

@AfterThrowing(pointcut = "execution(* 
package.controller.MyController*.updateSomething(*))", throwing = "ex")
private Error handleCustomError(MethodArgumentNotValidException ex) {
    return buildCustomError(ex);
}

The problem is that the controller advice gets called first but then it gets overwritten by the default advice so I get the default error message back. Is there a way to ignore the @ExceptionHandler from default advice when other advices have already handled it so I could get the customError message back?


Solution

  • You are misunderstanding @AfterThrowing:

    • You can only use that kind of advice in order to do something after a method exits with an exception and before it is thrown and maybe handled by another piece of code. You cannot change the application flow, e.g. catch the exception or manipulate the method result.

    • Furthermore, @AfterThrowing advices must return void for the reason I just explained. Your advice should not even compile but the compiler should yield the error "This advice must return void". At least this is what my AspectJ compiler does (I am using full AspectJ, not the "lite" version called Spring AOP, but the result should be the same.)

    • See the Spring AOP manual for more info about @AfterThrowing.

    Having explained that, what can you do? I will show you in a pure Java + AspectJ example so as to get Spring out of the equation. You can easily transfer the knowledge to Spring AOP and Spring MVC error handlers by yourself:

    What you need in order to change the application flow is an @Around advice. If you make that kind of advice catch the error in your special method(s) and return an error object, the default Spring exception handler will not even see that there was an exception because it was already caught by the aspect. I.e. the default handler will only handle all other errors not handled by the aspect.

    Here is some fully self-consistent and compileable sample code:

    Response classes:

    We use these in the sample application in order to emulate normal and error responses like in Spring.

    package de.scrum_master.app;
    
    public interface Response {
      String getMessage();
    }
    
    package de.scrum_master.app;
    
    public class NormalResponse implements Response {
      private String message = "OK";
    
      @Override
      public String getMessage() {
        return message;
      }
    
      @Override
      public String toString() {
        return "NormalResponse [message=" + message + "]";
      }
    }
    
    package de.scrum_master.app;
    
    public class ErrorResponse implements Response {
      private String message;
      private Exception exeception;
    
      public ErrorResponse(String message, Exception exeception) {
        this.message = message;
        this.exeception = exeception;
      }
    
      @Override
      public String getMessage() {
        return message;
      }
    
      public Exception getExeception() {
        return exeception;
      }
    
      @Override
      public String toString() {
        return "ErrorResponse [message=" + message + ", exeception=" + exeception + "]";
      }
    }
    

    Driver application:

    The application has two methods, both of which randomly produce normal or error responses. The method produceSpecialException() is the one we want handled by the aspect later.

    We emulate the default handler by try-catch blocks and then calling helper method defaultHandler(Exception e).

    package de.scrum_master.app;
    
    import java.util.Random;
    
    public class Application {
      private final static Random RANDOM = new Random();
    
      public Response produceException() throws Exception {
        if (RANDOM.nextBoolean())
          throw new Exception("normal error");
        return new NormalResponse();
      }
    
      public Response produceSpecialException() throws Exception {
        if (RANDOM.nextBoolean())
          throw new Exception("special error");
        return new NormalResponse();
      }
    
      public static ErrorResponse defaultHandler(Exception e) {
        return new ErrorResponse("default handler", e);
      }
    
      public static void main(String[] args) {
        Application application = new Application();
        for (int i = 0; i < 5; i++) {
          try {
            System.out.println(application.produceException());
          } catch (Exception e) {
            System.out.println(defaultHandler(e));
          }
          try {
            System.out.println(application.produceSpecialException());
          } catch (Exception e) {
            System.out.println(defaultHandler(e));
          }
        }
      }
    
    }
    

    Console log without aspect:

    ErrorResponse [message=default handler, exeception=java.lang.Exception: normal error]
    NormalResponse [message=OK]
    ErrorResponse [message=default handler, exeception=java.lang.Exception: normal error]
    ErrorResponse [message=default handler, exeception=java.lang.Exception: special error]
    NormalResponse [message=OK]
    NormalResponse [message=OK]
    ErrorResponse [message=default handler, exeception=java.lang.Exception: normal error]
    ErrorResponse [message=default handler, exeception=java.lang.Exception: special error]
    NormalResponse [message=OK]
    NormalResponse [message=OK]
    

    As you can see above, all errors are handled by the default handler. No surprise here.

    Aspect:

    The aspect handles the "special" errors only, ignores the others.

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    import de.scrum_master.app.ErrorResponse;
    import de.scrum_master.app.Response;
    
    @Aspect
    public class ErrorHandler {
      @Around("execution(de.scrum_master.app.Response produceSpecialException(..))")
      public Response handleError(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        try {
          return (Response) thisJoinPoint.proceed();
        }
        catch (Exception e) {
          return new ErrorResponse("aspect handler", e);
        }
      }
    }
    

    Console log with aspect:

    ErrorResponse [message=default handler, exeception=java.lang.Exception: normal error]
    ErrorResponse [message=aspect handler, exeception=java.lang.Exception: special error]
    NormalResponse [message=OK]
    ErrorResponse [message=aspect handler, exeception=java.lang.Exception: special error]
    ErrorResponse [message=default handler, exeception=java.lang.Exception: normal error]
    NormalResponse [message=OK]
    ErrorResponse [message=default handler, exeception=java.lang.Exception: normal error]
    ErrorResponse [message=aspect handler, exeception=java.lang.Exception: special error]
    NormalResponse [message=OK]
    NormalResponse [message=OK]
    

    As you can see above, now some errors are handled by the aspect handler ("special error") while all the others are still handled by the default handler ("normal error").