How to deal with global exception in microservice

Keywords: Spring SQL Java

In microservices, global exception handling is an objective problem that must be solved. If these exceptions are not handled well, some unfriendly error messages will be displayed to users, such as null pointer exceptions, SQL execution errors and other exceptions, which are certainly incomprehensible to customers, thus greatly reducing the user experience.

Common solutions to global exception handling
  • Define a class to handle exceptions. The Controller that needs to handle exceptions directly inherits this class to get exception handling methods. Although this method can solve the problem, it is extremely inflexible, because it is obviously not good to use inheritance mechanism only to obtain a default method.
  • Change this base class into an interface, and provide the default implementation of this method (that is, the default method in the interface, java8 starts to support the default implementation of the interface method). Almost all controllers need exception handling, so the Controller needs to write the implementation xxxexception, which is obviously not very good. Moreover, this way depends on the syntax of java8, which is a big limitation.
  • Use @ controlleradvise + @ expectionhandler, and then implement some global exception handling in this way. @Controller advise, as the default annotation in Spring, provides exception capture for all controllers (within the scope of your project package scanning).
Define default return entity
public class JsonEntity<T> implements java.io.Serializable {
    private static final long serialVersionUID = -1771426378340695807L;
    T data;
    private int status = 200;
    private String message;

    public JsonEntity() {
    }

    public JsonEntity(T data) {
        this.data = data;
    }
	// Omit getter/setter
	
    @Override
    public String toString() {
        return "JsonEntity{" +
               "status=" + status +
               ", message='" + message + '\'' +
               ", data=" + data +
               '}';
    }
}

Define ExceptionResponse. When an exception occurs, the returned entity is placed in the data of JsonEntity
public class ExceptionResponse implements Serializable {
    private static final long serialVersionUID = -5599209786899732586L;
    private String errorMessage;
    private String classType;
	// Omit getter/setter
}
Define BaseException, inherit RuntimeException
@Slf4j
public abstract class BaseException extends RuntimeException implements Serializable {
    private static final long serialVersionUID = 5047454682872098494L;
    private int responseStatus = HttpStatus.INTERNAL_SERVER_ERROR.value();
    private Object[] parameters = null;
    private Logger logger = LoggerFactory.getLogger(BaseException.class);

    public BaseException() {
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }

    public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public BaseException(Object[] parameters) {
        this.parameters = parameters;
    }

    public BaseException(String message, Object[] parameters) {
        super(message);
        this.parameters = parameters;
    }

    public static int determineResponseStatus(Throwable throwable) {
        if (throwable instanceof BaseException) {
            return ((BaseException) throwable).responseStatus();
        }
        return HttpStatus.INTERNAL_SERVER_ERROR.value();
    }

    public static int determineResponseStatus(Throwable throwable, int defaultValue) {
        if (throwable == null) {
            return defaultValue;
        }
        return determineResponseStatus(throwable);
    }

    public BaseException responseStatus(int responseStatus) {
        this.responseStatus = responseStatus;
        return this;
    }

    public int responseStatus() {
        return responseStatus;
    }

    public String getMessage(String fmt) {
        if (ObjectUtil.isEmpty(parameters)) {//NOPMD
            return fmt;
        }
        return String.format(fmt, parameters);
    }

    public String getMessage() {
        return (super.getMessage() == null ? "" : super.getMessage()) + formatParameters();
    }

    public String getLocalizedMessage() {
        return getLocalizedMessage(LocaleContextHolder.getLocale());
    }

    public String getLocalizedMessage(Locale locale) {
        Assert.notNull(locale);
        return getFromMessageSource(super.getMessage(), parameters, locale);
    }

    private String getFromMessageSource(String messageCode, Object[] params, Locale locale) {
        try {
            MessageSource messageSource = SpringContextHelper.afterSpringFullyStarted().getBeanSilently(MessageSource.class);
            if (messageSource != null) {
                return messageSource.getMessage(messageCode, params, locale);
            }
        } catch (Exception e) {
            logger.trace("error: {}, fallback to non-localized code, code: {}", e.getMessage(), messageCode);
        }
        logger.trace("messageSource absent, fallback to non-localized message, code: {}", messageCode);
        return getMessage();
    }

    private String formatParameters() {
        StringBuffer sb = new StringBuffer();
        if (!isEmpty(parameters)) {
            sb.append(" [params: ");
            for (int i = 0; i < parameters.length; i++) {
                sb.append(i).append(":").append(parameters[i]);
                if (i < parameters.length - 1) {
                    sb.append(", ");
                }
            }
            sb.append("] ");
        }
        return sb.toString();
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void throwIf(boolean condition) {
        if (condition) {
            throw this;
        }
    }
Define BizException, inherit BaseException to handle business exceptions
public class BizException extends BaseException {
    private static final long serialVersionUID = 4328407468288938158L;

    public BizException() {
    }

    public BizException(String message) {
        super(message);
    }

    public BizException(String message, int status) {
        super(message);
        this.responseStatus(status);
    }

    public BizException(String message, Throwable cause) {
        super(message, cause);
    }

    public BizException(String message, Throwable cause, int status) {
        super(message, cause);
        this.responseStatus(status);

    }

    public BizException(Throwable cause) {
        super(cause);
    }

    public BizException(Throwable cause, int status) {
        super(cause);
        this.responseStatus(status);
    }

    public BizException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public BizException(Object[] params) {
        super(params);
    }

    public BizException(String message, Object[] params) {
        super(message, params);
    }

    public static BaseException ofMessage(String message) {
        return new BizException(message);
    }
}

Define GlobalExceptionHandler and use @ controlleradvise
@Controller
@ControllerAdvice
public class GlobalExceptionHandler {
    private static Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @Autowired(required = false)
    private ResponseStatusExceptionResolver responseStatusExceptionResolver;

    @Autowired(required = false)
    private DefaultHandlerExceptionResolver defaultHandlerExceptionResolver;

    @PostConstruct
    public final void postInit() {
        if (responseStatusExceptionResolver == null) {
            responseStatusExceptionResolver = new ResponseStatusExceptionResolver();
        }
        if (defaultHandlerExceptionResolver == null) {
            defaultHandlerExceptionResolver = new DefaultHandlerExceptionResolver();
        }
    }

    protected ResponseEntity jsonResponse(HttpServletRequest request, HttpServletResponse response, int status, Exception e) {//NOPMD
        JsonEntity<ExceptionResponse> resp = ResponseHelper.createInstance(ExceptionUtil.getExceptionResponse(request, e));
        resp.setStatus(status);
        resp.setMessage(ExceptionUtil.getErrorMessage(e));
        return new ResponseEntity<>(resp, HttpStatus.valueOf(status));
    }
    
    @ExceptionHandler(value = Exception.class)
    @Order(Ordered.LOWEST_PRECEDENCE)
    public final Object defaultErrorHandler(HttpServletRequest req, HttpServletResponse response, @Nullable Object handler, Exception e) {
        if (e instanceof InternalHttpException) {
            log.warn(e.getMessage());
        } else {
            log.error(e.getMessage(), e);
        }
        ErrorWrapperResponse wrapperResponse = new ErrorWrapperResponse(response);

        ModelAndView modelAndView = responseStatusExceptionResolver.resolveException(req, wrapperResponse, handler, e);
        if (modelAndView == null) {
            modelAndView = defaultHandlerExceptionResolver.resolveException(req, wrapperResponse, handler, e);
        }
        int status;

        if (modelAndView == null) {
            if (e instanceof BaseException) {
                status = ((BaseException) e).responseStatus();
            } else {
                status = HttpStatus.INTERNAL_SERVER_ERROR.value();
            }
            modelAndView = new ModelAndView();
            req.setAttribute("javax.servlet.error.exception", e);
        } else {
            status = wrapperResponse.getStatus();
        }

        if (WebUtil.isAjax(req)) {
            return jsonResponse(req, response, status, e);
        } else {
            return htmlResponse(req, response, modelAndView, status, e);
        }
    }

    private Object htmlResponse(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView, int status, Exception e) {//NOPMD
        modelAndView.addObject("requestId", WebUtil.getRequestId());
        modelAndView.addObject("javax.servlet.error.exception", e);
        modelAndView.addObject("errorStatus", status);
        modelAndView.setStatus(HttpStatus.valueOf(status));
        switch (status) {
            case 400:
            case 401:
            case 403:
            case 404:
            case 405:
            case 410:
            case 415:
            case 500:
                modelAndView.setViewName("error/" + status);
                break;
            default:
                modelAndView.setViewName("error/500");
                break;
        }
        return modelAndView;
    }

    private static class ErrorWrapperResponse extends HttpServletResponseWrapper {

        private int status = 200;

        private String message;

        private boolean hasErrorToSend = false;

        ErrorWrapperResponse(HttpServletResponse response) {
            super(response);
        }

        @Override
        public void sendError(int status) throws IOException {
            sendError(status, null);
        }

        @Override
        public void sendError(int status, String message) throws IOException {
            this.status = status;
            this.message = message;
            this.hasErrorToSend = true;
        }

        @Override
        public int getStatus() {
            if (this.hasErrorToSend) {
                return this.status;
            } else {
                return super.getStatus();
            }
        }

        public String getMessage() {
            return this.message;
        }

        public boolean hasErrorToSend() {
            return this.hasErrorToSend;
        }
    }
}
Reference articles
123 original articles published, 83 praised, 270000 visitors+
Private letter follow

Posted by dniry on Wed, 04 Mar 2020 02:35:09 -0800