Java异常分类

异常处理 - 图1Throwable是异常体系的根,它继承自Object。Throwable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

而Exception则是运行时的错误,它可以被捕获并处理。
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:

  • NullPointerException:对某个null的对象调用方法或字段
  • IndexOutOfBoundsException:数组索引越界

Exception又分为两大类:

  • unchecked异常:RuntimeException以及它的子类;
    • 无能为力、引起注意型。针对此类异常,程序无法处理,如字段超长等导致的SQLException,即使做再多尝试也没用,一般处理方式是保存异常现场,供开发解决。
    • 力所能及、坦然处置型。如发生未授权异常(UnAuthorizedException),程序可跳转至权限申请页面。
  • checked异常:非RuntimeException(包括IOException、ReflectiveOperationException等等)

    • 可预测异常(Predicted Exception):常见的可预测异常包括IndexOutOfBoundsException、NullPointerException等,基于对代码的性能和稳定性要求,此类异常不应该被产生和抛出,而应该提前做好边界检查、空指针判断等处理。
    • 需捕获异常(Caution Exception),例如在使用Dubbo框架进行RPC调用时产生的远程服务超时异常DubboTimeoutException,此类异常是客户端必须显示处理的异常,不能因为服务端的异常导致客户端不可用,此时处理方案可以是重试或者降级处理等。
    • 可透出异常(Ignored Exception),主要是指框架和系统产生的且会自行处理的异常,而程序无需关心。例如Spring框架中抛出的NoSuchRequestHandlingMethodException异常,Spring框架会自己完成异常处理,默认将自身抛出的异常自动映射到合适的状态码,比如启动防护机制跳转到404页面。

      try代码块

      try-catch-finally是处理异常的三部曲。当存在try时,可以只有catch,也可以只有finally,就是不能只有try。
  • try代码块:监视代码执行过程,一旦发生异常直接跳转到catch,如果没有catch就直接跳转到finally。

  • catch代码块:可选执行的代码块,如果没有任何异常就不会执行,如果发现异常则进行处理或向上抛出。
  • finally代码块:必选执行的代码块,不管是否发生异常,即使发生OutOfMemoryError也会执行,通常处理善后的工作。如果没有执行finally,有三种可能:
    • 没有进入try代码块
    • 进入try代码块,但是代码运行中出现了死循环或死循环
    • 进入try代码块,但是执行了System.exit()操作

注意,finally是在return表达式运行后执行的,此时将要return的结果已经被暂存起来,待finally代码块执行结束后再将之前暂存的结果返回。

日志

日志规范

日志级别,针对不同场景,日志被分为五种不同的级别,按照重要程度由低到高排序:

  • DEBUG级别日志记录对调试程序有帮助的信息。
  • INFO级别日志用来记录程序运行现场,虽然此处并未发生错误,但是对排查其他错误具有指导意义。
  • WARN级别日志也可以用来记录程序运行现场,但是更加偏向于表明此处有出现潜在错误的可能。
  • ERROR级别日志表明当前程序运行发生了错误,需要被关注。但是当前发生的错误没有影响系统的继续运行。
  • FATAL级别日志表明当前程序运行出现了严重的错误事件,并且将会导致应用的中断。

    预先判断日志级别

    对DEBUG、INFO级别的日志,必须使用条件输出或者占位符的方式打印。
    正确的打印日志方式: ```java // 使用条件判断形式 if(logger.isDebugEnabled()){ logger.debug(“Processing trade with id: “+id+” and symbol: “+symbol); }

// 使用占位符形式 logger.debug(“Processing trade with id: {} and symbol: {}”, id, symbol);

  1. <a name="NPBfx"></a>
  2. #### 避免无效日志打印
  3. 生产环境禁止输出DEBUG日志,并且有选择地输出INFO日志。<br />使用INFO、WARN级别来记录业务行为信息的时候,一定要控制日志输出量,以免磁盘空间不足。同时要为日志文件设置合理的生命周期,及时清理过期的日志。<br />避免重复打印,务必在日志配置文件中设置additivity=false,实例如下:
  4. ```java
  5. <logger name="com.taobao.ecrm.member.config" additivity="false">

区别对待错误日志

WARN、ERROR都是与错误相关的日志级别,但不要一发生错误就笼统的输出ERROR级别日志。ERROR级别只记录系统逻辑错误、异常或者违反重要的业务规则,其他错误都可以归为WARN级别。

保证记录内容完整

日志记录的内容包括现场上下文与异常堆栈信息,所以打印时需要注意以下两点:

  1. 记录异常时一定要输出异常堆栈,例如logger.error(“xxx”+e.getMessage(), e);
  2. 日志中如果要输出对象实例,要确保实例类重写了toString方法,否则只会输出对象的hashCode值,没有实际意义。

    日志框架

    日志框架分为三大部分,包括日志门面、日志适配器、日志库。利用门面设计模式,即Facade来进行解耦,使日志使用变得更加简单。
    image.png

    日志门面

    日志门面只提供一套接口规范,自身不负责日志功能的实现,目的是让使用者不需要关注底层具体是哪个日志库来负责日志打印及具体的使用细节等。目前使用最广泛的日志门面有两种:slf4j和commons-logging。

    日志库

    它具体实现了日志的相关功能,主流的日志库包括log4j、log-jdk、logback。

    日志适配器

    日志适配器分两种场景:

  3. 日志门面适配器,因为slf4j规范是后来提出的,在此之前的日志库是没有实现slf4j接口的,例如log4j;所以,在工程中要想使用slf4j+log4j,就要额外一个适配器(slf4j-log4j12)来解决接口不兼容的问题。

  4. 日志库适配器,在一些老的工程里,一开始为了开发简单而直接使用了日志库API来完成日志打印,随着时间的推移想将原来直接调用日志库的模式改为业界标准的门面模式(例如slf4j+logback组合),但老工程代码里打印日志的地方太多,难以改动,所以需要一个适配器来完成从就日志库的API到slf4j的路由,这样在不改动原有代码的情况下也能使用slf4j来统一管理日志,而且后续自由替换具体日志库也不是问题。

日志集成

新工程推荐slf4j+logback模式。因为logback自身实现了slf4j的借口,无须额外引入适配器,另外logback是log4j 的升级版,具备比log4j更多的优点,集成配置:

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j-api.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback-classic.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>${logback-core.version}</version>
        </dependency>

老工程需要根据使用的日志库来确定适配器,通常情况下老工程使用的都是log4j,因此以log4j为例,集成配置:

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j-api.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j-log4j12.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>

如果老代码中直接使用了log4j日志库提供的接口来打印日志,则还需要引入日志库适配器:

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>${log4j-over-slf4j.version}</version>
        </dependency>

使用日志:

public static final Logger logger = LoggerFactory.getLogger(Abc.class);

logger被定义为static变量,是因为这个logger与当前类绑定,避免每次都new一个对象,造成资源浪费,甚至引发OutOfMemoryError问题。