Spring Boot 2.x: Handling exceptions globally

Keywords: Java Spring JSON Attribute

Preface

Exception handling is an irrevocable obstacle in our daily development. How to handle exceptions gracefully in Spring Book project is the direction we need to study in this lesson.

Classification of anomalies

In a Spring Book project, we can divide exceptions into two types: the first is before the request reaches the Controller layer, and the second is the error in the project code after the request reaches the Controller layer. The first one can be divided into two types of errors: 1. Path errors 2. Similar to request mode errors, parameter types are not equal and similar to errors.

Define ReturnVO and ReturnCode

In order to maintain the uniformity of return values, we define the uniform return class ReturnVO, and an enumeration class ReturnCode that records error return codes and error messages. The specific error information and error codes are saved in response.properties and read by stream.

ReturnVO

public class ReturnVO {

    private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL);

    /**
     * Return code
     */
    private String code;

    /**
     * Return information
     */
    private String message;

    /**
     * Return data
     */
    private Object data;


    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    /**
     * Default construct to return the correct return code and information
     */
    public ReturnVO() {
        this.setCode(properties.getProperty(ReturnCode.SUCCESS.val()));
        this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg()));
    }

    /**
     * Return code, which needs to be defined in the enumeration
     * @param code
     */
    public ReturnVO(ReturnCode code) {
        this.setCode(properties.getProperty(code.val()));
        this.setMessage(properties.getProperty(code.msg()));
    }

    /**
     * Return data, return correct code and message by default
     * @param data
     */
    public ReturnVO(Object data) {
        this.setCode(properties.getProperty(ReturnCode.SUCCESS.val()));
        this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg()));
        this.setData(data);
    }

    /**
     * Returns the wrong code and custom error messages
     * @param code
     * @param message
     */
    public ReturnVO(ReturnCode code, String message) {
        this.setCode(properties.getProperty(code.val()));
        this.setMessage(message);
    }

    /**
     * Return custom code, message, and data
     * @param code
     * @param message
     * @param data
     */
    public ReturnVO(ReturnCode code, String message, Object data) {
        this.setCode(code.val());
        this.setMessage(message);
        this.setData(data);
    }

    @Override
    public String toString() {
        return "ReturnVO{" +
                "code='" + code + '\'' +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }
}

ReturnCode

Other error handling only needs to add the corresponding exception in the enumeration class, and the name of the enumeration should be defined as the name of the exception, so that other code can be changed directly. When adding a new exception, only the fields in the enumeration class and the attributes in the properties file can be added.

public enum ReturnCode {

    /** Successful operation */
    SUCCESS("SUCCESS_CODE", "SUCCESS_MSG"),

    /** operation failed */
    FAIL("FAIL_CODE", "FAIL_MSG"),

    /** Null pointer exception */
    NullPointerException("NPE_CODE", "NPE_MSG"),

    /** The return value of the custom exception is null */
    NullResponseException("NRE_CODE", "NRE_MSG"),

    /** Runtime exception */
    RuntimeException("RTE_CODE","RTE_MSG"),

    /** Request mode error exception */
    HttpRequestMethodNotSupportedException("REQUEST_METHOD_UNSUPPORTED_CODE","REQUEST_METHOD_UNSUPPORTED_MSG"),

    /** INTERNAL_ERROR */
    BindException("BIND_EXCEPTION_CODE","BIND_EXCEPTION_MSG"),

    /** Page path incorrect */
    UrlError("UE_CODE","UE_MSG");

    private ReturnCode(String value, String msg){
        this.val = value;
        this.msg = msg;
    }

    public String val() {
        return val;
    }

    public String msg() {
        return msg;
    }

    private String val;
    private String msg;
}

response.properties

Here I have customized some exceptions for later testing, and we need to define a lot of exceptions to improve in our actual project.

SUCCESS_CODE=2000
SUCCESS_MSG=Successful operation

FAIL_CODE=5000
FAIL_MSG=operation failed

NPE_CODE=5001
NPE_MSG=Null pointer exception

NRE_CODE=5002
NRE_MSG=The return value is null

RTE_CODE=5001
RTE_MSG=Runtime exception

UE_CODE=404
UE_MSG=Page path error

REQUEST_METHOD_UNSUPPORTED_CODE=4000
REQUEST_METHOD_UNSUPPORTED_MSG=Exceptions in request mode

BIND_EXCEPTION_CODE=4001
BIND_EXCEPTION_MSG=Request parameter binding failed

Path error handling

The path error handling method here is to implement the ErrorController interface, and then implement the getErrorPath() method:

/**
 * The request path is incorrect
 * @author yangwei
 * @since 2019-01-02 18:13
 */
@RestController
public class RequestExceptionHandler implements ErrorController {

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping("/error")
    public ReturnVO errorPage(){
        return new ReturnVO(ReturnCode.UrlError);
    }
}

Here we can test it:

Use Controller Advice to handle other types of exceptions

Similar to the errors in request parameters before arriving at Controller, the errors in request mode, data format and so on are all categorized as one kind. Here, we only show how the errors in request mode are handled.

/**
 * Global exception handling class
 * @author yangwei
 *
 * For global return to json, use Controller Advice if you want to return to ModelAndView
 * Inherited ResponseEntityExceptionHandler to capture exceptions similar to request-mode exceptions
 */
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL);

    /**
     * Rewrite handleException Internal to customize the process
     **/
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        //Exceptions are passed directly to the handlerException() method for processing, and the return value is OK to ensure a friendly return, rather than a 500 error code.
        return new ResponseEntity<>(handlerException(ex), HttpStatus.OK);
    }

    /**
     * Exception capture
     * @param e Captured exception
     * @return Encapsulated return objects
     **/
    @ExceptionHandler(Exception.class)
    public ReturnVO handlerException(Throwable e) {
        ReturnVO returnVO = new ReturnVO();
        String errorName = e.getClass().getName();
        errorName = errorName.substring(errorName.lastIndexOf(".") + 1);
        //If you do not define an exception, instead throw a runtime exception directly, you need to go into the following branches
        if (e.getClass() == RuntimeException.class) {
            returnVO.setMessage(properties.getProperty(valueOf("RuntimeException").msg()) +": "+ e.getMessage());
            returnVO.setCode(properties.getProperty(valueOf("RuntimeException").val()));
        } else {
            returnVO.setMessage(properties.getProperty(valueOf(errorName).msg()));
            returnVO.setCode(properties.getProperty(valueOf(errorName).val()));
        }
        return returnVO;
    }
}

Here we can test:

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @PostMapping(value = "/findAll")
    public Object findAll() {
        throw new RuntimeException("ddd");
    }

    @RequestMapping(value = "/findAll1")
    public ReturnVO findAll1(UserDO userDO) {
        System.out.println(userDO);
        return new ReturnVO(userService.findAll1());
    }

   @RequestMapping(value = "/test")
    public ReturnVO test() {
        throw new RuntimeException("Testing non-custom run-time exceptions");
    }
}

Access findAll directly in the browser, default to get method, here as we expect will throw an exception request way error:

Access findAll1?id=123ss. Here we report a parameter binding exception because the id attribute in UserDO we accept is of Integer type:

Access test to test non-custom run-time exceptions:

Combining AOP usage, putting public modules reduces code duplication

In our last lesson, we used AOP to do a simple operation for global exception handling. This lesson was perfected and put into our public module. When using it, we only need to import the jar package and configure the scanning package path in the startup class.

/**
 * Unified encapsulation of return values and exception handling
 *
 * @author vi
 * @since 2018/12/20 6:09 AM
 */
@Slf4j
@Aspect
@Order(5)
@Component
public class ResponseAop {

    @Autowired
    private GlobalExceptionHandler exceptionHandler;

    /**
     * Tangent point
     */
    @Pointcut("execution(public * indi.viyoung.viboot.*.controller..*(..))")
    public void httpResponse() {
    }

    /**
     * Circumferential incision
     */
    @Around("httpResponse()")
    public ReturnVO handlerController(ProceedingJoinPoint proceedingJoinPoint) {
        ReturnVO returnVO = new ReturnVO();
        try {
            Object proceed = proceedingJoinPoint.proceed();
            if (proceed instanceof ReturnVO) {
                returnVO = (ReturnVO) proceed;
            } else {
                returnVO.setData(proceed);
            }
        }  catch (Throwable throwable) {
            // Here we call directly the method we just wrote in handler
            returnVO =  exceptionHandler.handlerException(throwable);
        }
        return returnVO;
    }
}

After doing these preparations, we only need to do the following steps when dealing with exceptions in the future:

  • Introducing a common module jar package
  • Configure the scan package path on the startup class
  • If an exception is added, then edit the return code and information in properties after adding it to the enumeration class (note: the variable name of the enumeration class must be consistent with the exception name).

Public Number

Posted by psurrena on Wed, 31 Jul 2019 03:29:17 -0700