全局异常处理

我们的程序在运行中,如果发生异常则会抛出两种错误,一种是exception,还有一种是error,前者能够在代码里面捕获,后者会直接扔给JVM。

exception一般可以在代码里面处理,但我们在写程序的时候不可能做到方方面面,所以有一些运行时异常无法在常规代码中处理,所以我们一般要在框架层去捕获RuntimeException。

在spring boot中,也为我们开发者提供了一个捕获全局异常Exception日志的工具。SpringBoot中有一个@ControllerAdvice的注解,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

实例代码如下:

@ControllerAdvice
public class TantiyuExceptionHandler {

     private Logger logger = LoggerFactory.getLogger(TantiyuExceptionHandler.class);    

    @ExceptionHandler(value =Exception.class)
    public String exceptionHandler(Exception e){
        logger.info("未知异常!原因是:{}",e);
           return e.getMessage();
    }
}

这里我们还可以定义一个继承自RuntimeException 的异常,如:TantiyuException,在代码中抛出自己所需的异常。

我们在代码中实现以上代码后,当有异常发生的时候,没有在代码中try-catch的异常将会被TantiyuExceptionHandler捕获,开发者可以处理这些异常。

通常,我们只需要将异常信息打印到控制台即可。

解决日志换行问题

首先我们来看一下打印到控制台默认的日志样式,日志实例如下:

java.lang.NullPointerException
    at cn.xxx.aaa.controller.XXXXController.logss(XXXController.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)

这种样式非常方便我们跟踪代码问题,查看容易又能迅速定位到自身的代码行,那么为何出产生问题呢?原因是目前大多数的Java系统都以分布式微服务作为系统架构,当问题发生的时候我们当然不可能一个服务一个服务的点开查看日志,通常的做法是将日志收集到一个集中的地方,然后通过某个终端来查看日志。通常我们会采用ELK组合,即:Elasticsearch + Logstash + Kibana 。

但这个组合在收集异常日志的时候,会以换行符标识新的一条日志,也就是上面传统的样式只能够现实:java.lang.NullPointerException。这就导致我们在查问题的时候无法找到更详细的日志信息,从而无从定位问题代码行,也就无从知道bug发生的地方。

因此,我们在打印日志之前,需要把换行符处理掉(当然可以通过配置来更改新日志标识,达到换行也能正常收录的目的,不过比较麻烦),换行后的日志就成了一坨,虽然有碍于快速查阅,不过有效的信息都会被日志服务收录,并不妨碍排查问题。

经过处理后的样式如下:

2021-08-24 20:05:09.796 ERROR [xxx-xxx,7ff86c98038670e0,7ff86c98038670e0,true] 2896 --- [nio-8093-exec-1] c.d.p.exception.ApiExceptionHandler      : 异常拦截:java.lang.NullPointerExceptionatcn.xxx.aaa.controller.XXXController.logss(XXXController.java:47)atsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)atjava.lang.reflect.Method.invoke(Method.java:498)atorg.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)atorg.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)atorg.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)atorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)atorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)atorg.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)atorg.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)atorg.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)atorg.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)atorg.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)atjavax.servlet.http.HttpServlet.service(HttpServlet.java:626)atorg.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)atjavax.servlet.http.HttpServlet.service(HttpServlet.java:733)

当然感兴趣的同学还可以将日志还原,java的日志规律性很强,很容易将日志还原回去,方便查阅。

那么,如何去掉日志中的换行符呢? 这里没啥技巧性,博主在这里直接贴出代码,方便大家解决问题。

已经经过测试的代码如下:

private Logger logger = LoggerFactory.getLogger(ApiExceptionHandler.class);

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RespWrapper globalExceptionHandler(HttpServletRequest request, Exception e) {
        //如果抛出的是系统自定义的异常则直接转换
        if (e instanceof CommonException) {
            log.error("异常拦截:{}", e.getMessage());
            CommonException ce = (CommonException) e;
            return new RespWrapper(ce.getMessage());
        } else {
            log.error("异常拦截:{} ", e.getMessage());
            String s = replaceBlank(getTrace(e));
            log.error("异常拦截:{}", s);
            return new RespWrapper(-11, e.getMessage() == null ? e.toString() : e.getMessage());
        }
    }

    @ExceptionHandler(value = CommonException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.IM_USED)
    public RespWrapper innerServiceException(HttpServletRequest request, CommonException e) {
        logger.info("异常信息:{}", e.getMessage());
        return new RespWrapper(e.getCode(), e.getMessage());
    }

    public static String replaceBlank(String str) {
        String dest = "";
        if (str!=null) {
            Pattern p = Pattern.compile("\\s*|\t|\r|\n");
            Matcher m = p.matcher(str);
            dest = m.replaceAll("");
        }
        return dest;
    }

    public static String getTrace(Throwable t) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);
        t.printStackTrace(writer);
        StringBuffer buffer = stringWriter.getBuffer();
        return buffer.toString();
    }

至此,我们成功解决spring全局异常的捕获问题,有解决使用elk日志方案带来的尴尬问题。

欢迎大家留言点评。