SoFunction
Updated on 2025-03-03

How to handle global exceptions in SpringBoot

Preface

In the development of SpringBoot, in order to improve the robustness of program operation, we often need to handle various program exceptions, but if each exception is processed separately, this will introduce a large number of business-independent exception handling codes, increasing the coupling of the program. At the same time, it will become more difficult to change the exception handling logic in the future. This article will show you how to handle global exceptions gracefully.

In order to achieve global interception, two annotations provided in Spring, @RestControllerAdvice and @ExceptionHandler are used here. Together, exceptions generated in the program can be intercepted and handled separately according to different exception types. Next, I will first introduce how to use these two annotations to elegantly complete the handling of global exceptions, and then explain the principle behind this.

How to implement global interception?

1.1 Custom exception handling class

In the following example, we inherited the ResponseEntityExceptionHandler and annotated this class with @RestControllerAdvice. Then we combined the @ExceptionHandler to define different exception handling methods for different exception types.

Here we can see that the exception I handle is a custom exception, and I will introduce it later.

  • The ResponseEntityExceptionHandler wraps the processing of various SpringMVC exceptions that may be thrown when processing requests, and the processing results are encapsulated into a ResponseEntity object.
  • ResponseEntityExceptionHandler is an abstract class. Usually we need to define an exception handling class annotated with @RestControllerAdvice annotation to inherit from ResponseEntityExceptionHandler.
  • ResponseEntityExceptionHandler defines a method for each exception handling. If the default processing cannot meet your needs, you can rewrite the processing of a certain exception.
@Log4j2  
@RestControllerAdvice  
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {  

    /**
      * Define the exception to be caught. There can be multiple @ExceptionHandler({}) *
      * @param request request
      * @param e exception
      * @param response response
      * @return Response Results
      */  
    @ExceptionHandler()  
    public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {  
        AuroraRuntimeException exception = (AuroraRuntimeException) e;  

       if (() == ResponseCode.USER_INPUT_ERROR) {  
           (HttpStatus.BAD_REQUEST.value());  
       } else if (() == ) {  
           (());  
       } else {  
           (HttpStatus.INTERNAL_SERVER_ERROR.value());  
       }  

        return new GenericResponse((), null, ());  
    }  

    @ExceptionHandler()  
    public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {  
        ("token exception", e);  
        (());  
        return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);  
    }  

}

1.2 Defining exception code

Here are several common exception codes, which are mainly used to distinguish different situations when throwing custom exceptions.

@Getter  
public enum ResponseCode {  

    SUCCESS(0, "Success"),  

    INTERNAL_ERROR(1, "Internal Server Error"),  

    USER_INPUT_ERROR(2, "User input error"),  

    AUTHENTICATION_NEEDED(3, "Token expires or is invalid"),  

    FORBIDDEN(4, "No access"),  

    TOO_FREQUENT_VISIT(5, "Visit too frequently, please take a break");  

    private final int code;  

    private final String message;  

    private final  status;  

    ResponseCode(int code, String message,  status) {  
         = code;  
         = message;  
         = status;  
    }  

    ResponseCode(int code, String message) {  
        this(code, message, .INTERNAL_SERVER_ERROR);  
    }  

}

1.3 Custom exception class

Here I define an AuroraRuntimeException exception, which is the exception used in the above exception handling function.

Each exception instance will have a corresponding exception code, which is just defined earlier.

@Getter  
public class AuroraRuntimeException extends RuntimeException {  

    private final ResponseCode code;  

    public AuroraRuntimeException() {  
        super(("%s", ResponseCode.INTERNAL_ERROR.getMessage()));  
         = ResponseCode.INTERNAL_ERROR;  
    }  

    public AuroraRuntimeException(Throwable e) {  
        super(e);  
         = ResponseCode.INTERNAL_ERROR;  
    }  

    public AuroraRuntimeException(String msg) {  
        this(ResponseCode.INTERNAL_ERROR, msg);  
    }  

    public AuroraRuntimeException(ResponseCode code) {  
        super(("%s", ()));  
         = code;  
    }  

    public AuroraRuntimeException(ResponseCode code, String msg) {  
        super(msg);  
         = code;  
    }  

}

1.4 Custom return type

In order to ensure the uniform return of each interface, a return type is specifically defined here.

@Getter  
@Setter  
public class GenericResponse<T> {  

    private int code;  

    private T data;  

    private String message;  

    public GenericResponse() {};  

    public GenericResponse(int code, T data) {  
         = code;  
         = data;  
    }  

    public GenericResponse(int code, T data, String message) {  
        this(code, data);  
         = message;  
    }  

    public GenericResponse(ResponseCode responseCode) {  
         = ();  
         = null;  
         = ();  
    }  

    public GenericResponse(ResponseCode responseCode, T data) {  
        this(responseCode);  
         = data;  
    }  

    public GenericResponse(ResponseCode responseCode, T data, String message) {  
        this(responseCode, data);  
         = message;  
    }  
}

Actual test exception

In the following example, we want to obtain user information. If the user's information does not exist, an exception can be directly thrown. This exception will be caught by the global exception handling method we defined above, and then different processing and return are completed according to different exception encodings.

public User getUserInfo(Long userId) {  
    // some logic

    User user = ().selectByPrimaryKey(userId);  
    if (user == null) {  
        throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "User ID does not exist");  
    }

    // some logic
    ....
}

The above completes the entire global exception handling process. Next, let’s focus on why @RestControllerAdvice and @ExceptionHandler can intercept exceptions generated in the program?

What is the principle behind global interception?

The @ControllerAdvice annotation will be mentioned below. Simply put, the difference between @RestControllerAdvice and @ControllerAdvice is similar to the difference between @RestController and @Controller. The @RestControllerAdvice annotation includes @ControllerAdvice annotation and @ResponseBody annotation.

public class DispatcherServlet extends FrameworkServlet {
    // ......
    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);

        // Focus on        initHandlerExceptionResolvers(context);

        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }
    // ......
}

In the initHandlerExceptionResolvers(context) method, all beans that implement the HandlerExceptionResolver interface will be obtained and saved. There is a bean of type ExceptionHandlerExceptionResolver. During the application startup process, this bean will obtain all bean objects annotated by @ControllerAdvice annotation for further processing. The key code is here:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
        implements ApplicationContextAware, InitializingBean {
    // ......
    private void initExceptionHandlerAdviceCache() {
        // ......
        List&lt;ControllerAdviceBean&gt; adviceBeans = (getApplicationContext());
        (adviceBeans);

        for (ControllerAdviceBean adviceBean : adviceBeans) {
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(());
            if (()) {
                // Find all ExceptionHandler annotations methods and save them as an ExceptionHandlerMethodResolver object to cache them                (adviceBean, resolver);
                if (()) {
                    ("Detected @ExceptionHandler methods in " + adviceBean);
                }
            }
            // ......
        }
    }
    // ......
}

When the Controller throws an exception, the DispatcherServlet parses the exception through ExceptionHandlerExceptionResolver, and ExceptionHandlerExceptionResolver parses the exception through ExceptionHandlerMethodResolver. The method of finally parsing the exception and finding the applicable @ExceptionHandler annotation is here:

public class ExceptionHandlerMethodResolver {
    // ......
    private Method getMappedMethod(Class&lt;? extends Throwable&gt; exceptionType) {
        List&lt;Class&lt;? extends Throwable&gt;&gt; matches = new ArrayList&lt;Class&lt;? extends Throwable&gt;&gt;();
        // Find all the methods that apply to the exception thrown by Controller, such as the exception thrown by Controller        // is AuroraRuntimeException (inherited from RuntimeException), then @ExceptionHandler() and        // All methods marked with @ExceptionHandler() apply to this exception        for (Class&lt;? extends Throwable&gt; mappedException : ()) {
            if ((exceptionType)) {
                (mappedException);
            }
        }
        if (!()) {
        /* Here we find the most applicable method through sorting. The sorting rules are based on the depth of the thrown exception relative to the declared exception, for example
     The exception thrown by the Controller is AuroraRuntimeException (inherited from RuntimeException), then AuroraRuntimeException
     The depth of the @ExceptionHandler() declaration is 0,
     The depth of the @ExceptionHandler() declaration is 2, so
     The @ExceptionHandler() annotation method will be ranked first */
            (matches, new ExceptionDepthComparator(exceptionType));
            return ((0));
        }
        else {
            return null;
        }
    }
    // ......
}

This is the process of the entire @RestControllerAdvice processing. Combined with @ExceptionHandler, flexible processing of different exceptions is completed.

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.