springmvc/springboot global exception handling and custom exception handling

Keywords: Java Lombok Apache github

Preface

Exception handling has always been a big part of project development, but few people pay attention to exception handling. Often it's a simple try/catch all the exceptions, and then a simple print StackTrace, which uses logger at most to print down the log or re-throw the exceptions, and some have custom exceptions, but it's still in the controller to catch exceptions, which requires catch (exception 1) catch (exception 2) to be particularly cumbersome and easy to leak.

In fact, springmvc in Controller Advice has provided a way to handle exceptions globally, and we can use aop to handle exceptions uniformly, so that everywhere we only need to pay attention to our business, not exception handling, and throw exceptions can also use spring's transactions, it only Transaction rollback occurs only when an exception is detected.

Important Notes

  1. The following relevant code uses lombok, but I don't know what it can be used by Baidu under lombok.
  2. Use the Builder Model

Unified exception handling

Here we use Spring mvc's Controller Advice to do unified exception handling

import com.sanri.test.testmvc.dto.ResultEntity;
import com.sanri.test.testmvc.exception.BusinessException;
import com.sanri.test.testmvc.exception.RemoteException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.ArrayList;
import java.util.List;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @Value("${project.package.prefix:com.sanri.test}")
    protected String packagePrefix;

    /**
     * Handling business exceptions
     * @param e
     * @return
     */
    @ExceptionHandler(BusinessException.class)
    public ResultEntity businessException(BusinessException e){
        printLocalStackTrack(e);
        return e.getResultEntity();
    }

    @ExceptionHandler(RemoteException.class)
    public ResultEntity remoteException(RemoteException e){
        ResultEntity parentResult = e.getParent().getResultEntity();
        ResultEntity resultEntity = e.getResultEntity();
        //Business errors are returned to the front end, but remote call exceptions need to be printed out in the console
        log.error(parentResult.getReturnCode()+":"+parentResult.getMessage()
                +" \n -| "+resultEntity.getReturnCode()+":"+resultEntity.getMessage());

        printLocalStackTrack(e);

        //Merge two result sets to return
        ResultEntity merge = ResultEntity.err(parentResult.getReturnCode())
                .message(parentResult.getMessage()+" \n  |- "+resultEntity.getReturnCode()+":"+resultEntity.getMessage());
        return merge;
    }

    /**
     * Printing involves only the exception stack for project class calls
     * @param e
     */
    private void printLocalStackTrack(BusinessException e) {
        StackTraceElement[] stackTrace = e.getStackTrace();
        List<StackTraceElement> localStackTrack = new ArrayList<>();
        StringBuffer showMessage = new StringBuffer();
        if (ArrayUtils.isNotEmpty(stackTrace)) {
            for (StackTraceElement stackTraceElement : stackTrace) {
                String className = stackTraceElement.getClassName();
                int lineNumber = stackTraceElement.getLineNumber();
                if (className.startsWith(packagePrefix)) {
                    localStackTrack.add(stackTraceElement);
                    showMessage.append(className + "(" + lineNumber + ")\n");
                }
            }
            log.error("Business exceptions:" + e.getMessage() + "\n" + showMessage);
        } else {
            log.error("Business exceptions,No call stack " + e.getMessage());
        }
    }

    /**
     * Exception handling, which can bind multiple
     * @return
     */
    @ExceptionHandler(Exception.class)
    public ResultEntity result(Exception e){
        e.printStackTrace();
        return ResultEntity.err(e.getMessage());
    }
}

Unified Return Value

  • Usually we define a unified return, so that the front end can parse the return value, like this.
package com.sanri.test.testmvc.dto;

import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

/**
 * Ordinary message return
 * @param <T>
 */
@Data
@ToString
public class ResultEntity<T> implements Serializable {
    private String returnCode = "0";
    private String message;
    private T data;
    public ResultEntity() {
        this.message = "ok";
    }
    public ResultEntity(T data) {
        this();
        this.data = data;
    }
    public static ResultEntity ok() {
        return new ResultEntity();
    }
    public static ResultEntity err(String returnCode) {
        ResultEntity resultEntity = new ResultEntity();
        resultEntity.returnCode = returnCode;
        resultEntity.message = "fail";
        return resultEntity;
    }
    public static ResultEntity err() {
        return err("-1");
    }
    public ResultEntity message(String msg) {
        this.message = msg;
        return this;
    }
    public ResultEntity data(T data) {
        this.data = data;
        return this;
    }
}

Custom exception

  • Custom exceptions, as far as my current work experience is concerned, are generally three types.

    1. The first is business anomaly, that is, the given input can not meet the business conditions, such as time expiration, name duplication, ID card is wrong, and so on.
    2. The second is the exception when calling a third-party system, which is also a business exception.
    3. The third is the fatal error of the system, which is usually wrong, but it needs to be handled well in the stage of testing and development. Online error can only be said to the user that the system is wrong, and then the development of log checking to see the error.

Business exceptions

For business anomalies, we sometimes need to number errors, because the front end needs to get the number to do some page Jump work, and customers can also tell the operation number when they complain about errors, and then can do the response; but most of the time, there is no need for the error number, at this time. A number can be generated randomly. We can specify a number to define the error number. For example, 0 is normal, 1-100 is general error, 101-1000 is system A, 1000-2000 is system B, and then more than 10,000 is random code.

import com.sanri.test.testmvc.dto.ResultEntity;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * System business exceptions (root exceptions), exceptional number segment is:
 * 0 : Success
 * 1 ~ 9999 Abnormal segment of built-in system
 * 10000 ~ 99999 Custom Exception Segment
 * 100000 ~ Integer.MAX_VALUE Dynamic Abnormal Segment
 */
public class BusinessException extends RuntimeException {
    protected ResultEntity resultEntity;
    protected static final int  MIN_AUTO_CODE = 100000;

    public static BusinessException create(String message) {
        int value= (int) (MIN_AUTO_CODE + Math.round((Integer.MAX_VALUE - MIN_AUTO_CODE) * Math.random()));
        return create(value + "",message);
    }

    public static BusinessException create(String returnCode,String message){
        if(StringUtils.isBlank(returnCode)){
            return create(message);
        }
         BusinessException businessException = new BusinessException();
         businessException.resultEntity = ResultEntity.err(returnCode).message(message);
         return businessException;
    }

    public static BusinessException create(ExceptionCause exceptionCause ,Object...args){
        ResultEntity resultEntity = exceptionCause.result();
        String message = resultEntity.getMessage();

        if(ArrayUtils.isNotEmpty(args)){
            String [] argsStringArray = new String [args.length];
            for (int i=0;i<args.length;i++) {
                Object arg = args[i];
                argsStringArray[i] = ObjectUtils.toString(arg);
            }
            String formatMessage = String.format(message, argsStringArray);
            resultEntity.setMessage(formatMessage);
        }

        BusinessException businessException = new BusinessException();
        businessException.resultEntity = resultEntity;
        return businessException;
    }

    @Override
    public String getMessage() {
        return resultEntity.getMessage();
    }

    public ResultEntity getResultEntity() {
        return resultEntity;
    }
}

Remote call exception

Remote call exceptions are usually returned to us with error codes and error messages. At this time, we can define a remote exceptions and regard business exceptions as parent exceptions. At this time, the error structure will be like this, for example.

Errors in Insurance Business 
  -| E007 Effective Date Must Be More Than Current Date 

The code is as follows, using the Builder Design pattern, if you do not know this design pattern, you can Baidu by yourself.

import com.sanri.test.testmvc.dto.ResultEntity;
import com.sun.deploy.net.proxy.RemoveCommentReader;

public class RemoteException  extends BusinessException{
    private BusinessException parent;

    private RemoteException(BusinessException parent) {
        this.parent = parent;
    }

    /**
     * Create remote exceptions
     * @param parent
     * @param remoteCode
     * @param remoteMessage
     * @return
     */
    public static RemoteException create(BusinessException parent,String remoteCode,String remoteMessage){
        RemoteException remoteException = new RemoteException(parent);
        remoteException.resultEntity = ResultEntity.err(remoteCode).message(remoteMessage);
        return remoteException;
    }

    /**
     * Easy to create remote information
     * @param parent
     * @param remoteMessage
     * @return
     */
    public static RemoteException create(BusinessException parent,String remoteMessage){
        return create(parent,"remoteError",remoteMessage);
    }

    public static RemoteException create(String localMessage,String remoteCode,String remoteMessage){
        return new Builder().localMessage(localMessage).remoteCode(remoteCode).remoteMessage(remoteMessage).build();
    }
    public static RemoteException create(String localMessage,String remoteMessage){
        return new Builder().localMessage(localMessage).remoteMessage(remoteMessage).build();
    }

    public static class Builder{
        private String localCode;
        private String localMessage;

        private String remoteCode;
        private String remoteMessage;

        public Builder localCode(String localCode){
            this.localCode = localCode;
            return this;
        }
        public Builder localMessage(String localMessage){
            this.localMessage = localMessage;
            return this;
        }
        public Builder remoteCode(String remoteCode){
            this.remoteCode = remoteCode;
            return this;
        }
        public Builder remoteMessage(String remoteMessage){
            this.remoteMessage = remoteMessage;
            return this;
        }

        public RemoteException build(){
            BusinessException businessException = BusinessException.create(localCode, localMessage);
            RemoteException remoteException = RemoteException.create(businessException,remoteCode,remoteMessage);
            return remoteException;
        }
    }

    public BusinessException getParent() {
        return parent;
    }
}

Elegant throw exception

I've seen many projects throw new Business Exception (...) in this way when throwing new exceptions.

  • We don't need to expose exceptional constructors, so we can
BusinessException.create("Duplicate name, please re-enter");
  • Or we can use enumeration to add a method to the enumeration class to create exceptions that require an error number.

Usage method:

throw SystemMessage.NOT_LOGIN.exception();

Code Definition:

import com.sanri.test.testmvc.dto.ResultEntity;

public interface ExceptionCause<T extends Exception> {
    T exception(Object... args);

    ResultEntity result();
}
import com.sanri.test.testmvc.dto.ResultEntity;

public enum  SystemMessage implements ExceptionCause<BusinessException> {  
    NOT_LOGIN(4001,"Not logged in or session Invalid"),
    PERMISSION_DENIED(4002,"No privileges"),
    DATA_PERMISSION_DENIED(4007,"No data privileges"),
    SIGN_ERROR(4003,"Signature error,Your signature string is [%s]")
    ;
    ResultEntity resultEntity = new ResultEntity();

    private SystemMessage(int returnCode,String message){
        resultEntity.setReturnCode(returnCode+"");
        resultEntity.setMessage(message);
    }

    @Override
    public BusinessException exception(Object...args) {
        return BusinessException.create(this,args);
    }

    @Override
    public ResultEntity result() {
        return resultEntity;
    }

    /**
     * Result return of custom message
     * @param args
     * @return
     */
    public ResultEntity result(Object ... args){
        String message = resultEntity.getMessage();
        resultEntity.setMessage(String.format(message,args));
        return resultEntity;
    }

    public String getReturnCode(){
        return resultEntity.getReturnCode();
    }
}
  • We can further encapsulate it and convert it into assertions, depending on personal preferences, which can be used in this way, just write an example, generally the login is intercepted in the filter.
assertLogin();

/**
 * Asserting whether the user is logged in  
 */
public void assertLogin(){
    // Get the current user from session or redis or auth2 or shiro or SSO 
    User user = xxx.get();
    if(user == null){
        throw SystemMessage.NOT_LOGIN.exception();
    }
}

Demonstration of usage

@RestController
public class ExceptionController {

    /**
     * Static exception display, fixed error code
     */
    @GetMapping("/staticException")
    public void staticException(){

        throw SystemMessage.ACCESS_DENIED.exception("No permission");
    }

    /**
     * Dynamic exceptions, the front end does not care about error codes
     */
    @GetMapping("/dynamicException")
    public void dynamicException(){

        throw BusinessException.create("Name duplication, please use another name");
    }

    /**
     * Third party call exception, need to display hierarchical exception
     */
    @GetMapping("/remoteException")
    public void remoteException(){

        //Analog Remote Error
        String remoteCode = "E007";
        String remoteMessage = "Effective date must be greater than the current date";

        throw RemoteException.create("A business call error",remoteCode,remoteMessage);
    }
}

github project code

The above code can be downloaded to my github and related projects can be directly run, ready to use.
https://gitee.com/sanri/example/tree/master/test-mvc

sanri-tools tool

Creation is not easy, I hope you can promote my gadget, very practical solution to some of the troubles in the project, welcome to github dot star, fork
github address: https://gitee.com/sanri/sanri-tools-maven
Blog address: https://blog.csdn.net/sanri1993/article/details/98664034

Posted by DasHaas on Sun, 08 Sep 2019 07:05:07 -0700