Java’s exception architecture

erDiagram
Throwable ||--o{ Error : impl
Throwable || --o{ Exception : impl

Throwable is the top-level class with exceptions and errors.

  • Error

    Errors that cannot be handled in a program, indicating that a serious error occurred in the running application. Half of these errors are caused by the JVM. Common ones include NoClassDefFoundError and OutOfMemoryError

  • Exception

    Unchecked Exception: Unchecked exception: Unchecked exception: unchecked exception: unchecked exception: unchecked exception: unchecked exception: unchecked exception: unchecked exception

    • Checked Exceptions

    Exceptions that must be handled by the compiler must be caught by try-catch or thrown by the throws statement. Exceptions such as ClassNotFoundException and NoSuchMethodException fail to pass compilation.

    • Unchecked Exception (Unchecked Exception)

    Exceptions that the compiler does not check for and does not require to be handled include RuntimeException and subclasses of RuntimeException (NullPointerException, IllegalArgumentException, etc.).

Global exception handling

Spring Mvc uses the @ExceptionHandler annotation to handle exceptions thrown by the control layer

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
	Class<? extends Throwable>[] value() default {};
}
Copy the code

ExceptionHandler Only one method value() can fill in values from the Class array inherited from Throwable, which means that one ExceptionHandler can handle multiple exceptions or errors

Let’s start by creating a custom exception that will take a message argument

public class CustomException extends RuntimeException{

    public CustomException(String message) {
        super(message); }}Copy the code
  • @ RestController and @ Controller

    Use the following in the control layer

    @RestController
    @Slf4j
    public class DomainController {
    
        @ExceptionHandler(CustomException.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ResponseEntity<Object> domainExceptionHandler(CustomException e){
            log.error("exception from DomainController", e);
            return ResponseEntity.status(500).body(e.getMessage());
        }
    
        @GetMapping("/domain")
        public void test(int code){
            if(code == 1) {throw new CustomException("Custom exceptions"); }}}Copy the code

    The request interface passes in code = 1, and you can see the console print

    exception from  DomainController
    Copy the code

    Exceptions are handled by ExceptionHandler. This method can only handle exceptions of a single controller

  • @ ControllerAdvice and @ RestControllerAdvice

    @ControllerAdvice and @RestControllerAdvice can handle more than one controller at a time. The scope can be adjusted in the following ways, because in some cases we don’t want the exception controller to handle exceptions from some third party framework

    // Process all @RestController annotations
    @ControllerAdvice(annotations = RestController.class)
    public class ExampleAdvice1 {}
    
    // Process all controllers in the package path
    @ControllerAdvice(basePackages = "org.example.controllers")
    public class ExampleAdvice2 {}
    
    // Specify a specific class to handle
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
    public class ExampleAdvice3 {}
    Copy the code

    Note: Annotations and basePackages take effect only if the controllers being processed have been scanned as Spring beans

In most cases we only need to configure global exception handling, that is, @RestControllerAdvice for all controllers. The @ExceptionHandler configured for a single controller takes precedence over the global one. We can take advantage of this to handle special exceptions or some customization requirements (preferably less customization, which can cause maintenance difficulties later in the project).

About the Error

We now define an @ExceptionHandler to handle all exceptions, as follows

    @ExceptionHandler({Exception.class})
    public ResponseEntity<Object> handler(Exception e){
        log.error("error happened ", e);
        return ResponseEntity.status(500).body(e.getMessage());
    }
Copy the code

Raises an Error when changing the test code code = 2

    @GetMapping("/domain")
    public void test(int code){
        if(code == 1) {throw new CustomException("Custom exceptions");
        }
        if(code == 2) {throw new AssertionError("code is 2"); }}Copy the code

We’ve already seen the difference between Error and Exception. If we throw an Error here, our Exception handler should not handle it, because we only handle all exceptions, not the Error. Start the project call interface, and the console gets the following message

error happened Caused by: java.lang.AssertionError: code is 2
Copy the code

You can see that the AssertionError is handled by the exception handler because, after Spring 4.3, the doDispatch method of the DispatcherServlet handles errors thrown from handlers, Make them available to the @ExceptionHandler method and other scenarios.

.catch (Exception ex) {
    dispatchException = ex;
}
catch (Throwable err) {
    // As of 4.3, we're processing Errors thrown from handler methods as well,
    // making them available for @ExceptionHandler methods and other scenarios.
    dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); .Copy the code

Uniform result return

In a real project we usually define a uniform return structure, usually as follows

@Getter
@Builder
public class ResponseData<T> {

    private long timestamp;
    private int status;
    private String message;
    private T data;
}
Copy the code

Instead of writing repetitive wraparound code on each Controller, we can define a uniform return handling that implements the ResponseBodyAdvice interface

@RestControllerAdvice
@Slf4j
public class ResponseHandler  implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return null; }}Copy the code

Two methods are provided

  • supports

    This method takes two arguments

    • returnType: Controller return value type
    • converterType: HTTP message converter type,

    BeforeBodyWrite is executed only if this method returns true. We can use this method for configuration such as making certain interfaces not use a uniform return structure with a custom @ignoreBodyadvice annotation.

    @Override
    public boolean supports(final @NotNull MethodParameter methodParameter,
                            finalClass<? extends HttpMessageConverter<? >> aClass) { Class<? > clz = methodParameter.getDeclaringClass();if (method == null) {
            return false;
        } 
        // Check whether the annotation exists
        if (clz.isAnnotationPresent(IgnoreBodyAdvice.class)) {
            return false;
        } else
            return! method.isAnnotationPresent(IgnoreBodyAdvice.class); }Copy the code
  • beforeBodyWrite

    This method takes six arguments and is called before the Write method of HttpMessageConverter

    • body: The message returned
    • returnType: The return value type of controller
    • selectedContentType: The selected message type, such asapplication/json
    • selectedConverterType: HTTP message converter type, for exampleStringHttpMessageConverter
    • request: Current request
    • response: Current response

    Common usages are as follows

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    if(body == null) {return ResponseData.builder().message("ok").build();
    } else if(body instanceof ResponseData){
        return body;
    } else {
        returnResponseData.builder().data(body).build(); }}Copy the code

This is just the simplest example. In a real project, there may be many other criteria, which can be added according to the project situation.

This is the end of this article, if you have any questions or find errors can leave a comment or email [email protected], thank you