Java 中的异常和处理

1 异常分类

前言

程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?。


Java提供了更加优秀的解决办法:异常处理机制。


异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。

Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。


Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。


Java异常的分类和类结构图


Java标准裤内建了一些通用的异常,这些类以Throwable为顶层父类。


Throwable又派生出Error类和Exception类。


错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。


异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。


总体上我们根据Javac对异常的处理要求,将异常类分为2类。


非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。


检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。


需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。


初识异常


下面的代码会演示2个异常类型:ArithmeticException 和 InputMismatchException。前者由于整数除0引发,后者是输入的数据不能被转换为int类型引发。


package com.example;

import java. util .Scanner ;

public class AllDemo

{

      public static void main (String [] args )

      {

            System . out. println( "----欢迎使用命令行除法计算器----" ) ;

            CMDCalculate ();

      }

      public static void CMDCalculate ()

      {

            Scanner scan = new Scanner ( System. in );

            int num1 = scan .nextInt () ;

            int num2 = scan .nextInt () ;

            int result = devide (num1 , num2 ) ;

            System . out. println( "result:" + result) ;

            scan .close () ;

      }

      public static int devide (int num1, int num2 ){

            return num1 / num2 ;

      }

}

/*****************************************

 

----欢迎使用命令行除法计算器----

0

Exception in thread "main" java.lang.ArithmeticException : / by zero

     at com.example.AllDemo.devide( AllDemo.java:30 )

     at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )

     at com.example.AllDemo.main( AllDemo.java:12 )

 

----欢迎使用命令行除法计算器----

r

Exception in thread "main" java.util.InputMismatchException

     at java.util.Scanner.throwFor( Scanner.java:864 )

     at java.util.Scanner.next( Scanner.java:1485 )

     at java.util.Scanner.nextInt( Scanner.java:2117 )

     at java.util.Scanner.nextInt( Scanner.java:2076 )

     at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )

     at com.example.AllDemo.main( AllDemo.java:12 )

*****************************************/


异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。


异常最先发生的地方,叫做异常抛出点。


从上面的例子可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。


上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。


代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。但是为什么只throws了IOException呢?因为FileNotFoundException是IOException的子类,在处理范围内。


@Test

public void testException() throws IOException

{

    //FileInputStream的构造函数会抛出FileNotFoundException

    FileInputStream fileIn = new FileInputStream("E:\\a.txt");

     

    int word;

    //read方法会抛出IOException

    while((word =  fileIn.read())!=-1) 

    {

        System.out.print((char)word);

    }

    //close方法会抛出IOException

    fileIn.clos

}


异常处理的基本语法


在编写代码处理异常时,对于检查异常,有2种不同的处理方式:使用try…catch…finally语句块处理它。或者,在函数签名中使用throws 声明交给函数调用者caller去解决。


try…catch…finally语句块


try{

     //try块中放可能发生异常的代码。

     //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。

     //如果发生异常,则尝试去匹配catch块。

 

}catch(SQLException SQLexception){

    //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。

    //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。

    //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。

    //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。

    //如果try中没有发生异常,则所有的catch块将被忽略。

 

}catch(Exception exception){

    //...

}finally{

    

    //finally块通常是可选的。

   //无论异常是否发生,异常是否匹配被处理,finally都会执行。

   //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。

  //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 

}


需要注意的地方


1、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。


2、每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。


3、java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。

有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )

而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)


public static void main(String[] args){

        try {

            foo();

        }catch(ArithmeticException ae) {

            System.out.println("处理异常");

        }

}

public static void foo(){

        int a = 5/0;  //异常抛出点

        System.out.println("为什么还不给我涨工资!!!");  //////////////////////不会执行

}


throws 函数声明


throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。


throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。


采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。


public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN

     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。

}


finally块


finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。


良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。


需要注意的地方:


1、finally块没有处理异常的能力。处理异常的只能是catch块。


2、在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。


3、在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。


这是正常的情况,但是也有特例。关于finally有很多恶心,偏、怪、难的问题,我在本文最后统一介绍了,电梯速达->:finally块和return


throw 异常抛出语句


throw exceptionObject


程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。


throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。


public void save(User user)

{

      if(user  == null) 

          throw new IllegalArgumentException("User对象为空");

      //......

         

}


异常的链化


在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。


异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。


查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。


public class Throwable implements Serializable {

    private Throwable cause = this;

    

    public Throwable(String message, Throwable cause) {

        fillInStackTrace();

        detailMessage = message;

        this.cause = cause;

    }

     public Throwable(Throwable cause) {

        fillInStackTrace();

        detailMessage = (cause==null ? null : cause.toString());

        this.cause = cause;

    }

     

    //........

}


下面是一个例子,演示了异常的链化:从命令行输入2个int,将他们相加,输出。输入的数不是int,则导致getInputNumbers异常,从而导致add函数异常,则可以在add函数中抛出


一个链化的异常。


public static void main(String[] args)

{

     

    System.out.println("请输入2个加数");

    int result;

    try

    {

        result = add();

        System.out.println("结果:"+result);

    } catch (Exception e){

        e.printStackTrace();

    }

}

//获取输入的2个整数返回

private static List<Integer> getInputNumbers()

{

    List<Integer> nums = new ArrayList<>();

    Scanner scan = new Scanner(System.in);

    try {

        int num1 = scan.nextInt();

        int num2 = scan.nextInt();

        nums.add(new Integer(num1));

        nums.add(new Integer(num2));

    }catch(InputMismatchException immExp){

        throw immExp;

    }finally {

        scan.close();

    }

    return nums;

}

 

//执行加法计算

private static int add() throws Exception

{

    int result;

    try {

        List<Integer> nums =getInputNumbers();

        result = nums.get(0)  + nums.get(1);

    }catch(InputMismatchException immExp){

        throw new Exception("计算失败",immExp);  /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。

    }

    return  result;

}

 

/*

请输入2个加数

r 1

java.lang.Exception: 计算失败

    at practise.ExceptionTest.add(ExceptionTest.java:53)

    at practise.ExceptionTest.main(ExceptionTest.java:18)

Caused by: java.util.InputMismatchException

    at java.util.Scanner.throwFor(Scanner.java:864)

    at java.util.Scanner.next(Scanner.java:1485)

    at java.util.Scanner.nextInt(Scanner.java:2117)

    at java.util.Scanner.nextInt(Scanner.java:2076)

    at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)

    at practise.ExceptionTest.add(ExceptionTest.java:48)

    ... 1 more

 

*/


自定义异常


如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。


按照国际惯例,自定义的异常应该总是包含如下的构造函数:


  • 一个无参构造函数

  • 一个带有String参数的构造函数,并传递给父类的构造函数。

  • 一个带有String参数和Throwable参数,并都传递给父类构造函数

  • 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。


下面是IOException类的完整源代码,可以借鉴。


public class IOException extends Exception

{

    static final long serialVersionUID = 7818375828146090155L;

 

    public IOException()

    {

        super();

    }

 

    public IOException(String message)

    {

        super(message);

    }

 

    public IOException(String message, Throwable cause)

    {

        super(message, cause);

    }

 

     

    public IOException(Throwable cause)

    {

        super(cause);

    }

}


异常的注意事项


1、当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。


例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。



至于为什么?我想,也许下面的例子可以说明。


class Father

{

    public void start() throws IOException

    {

        throw new IOException();

    }

}

 

class Son extends Father

{

    public void start() throws Exception

    {

        throw new SQLException();

    }

}

/**********************假设上面的代码是允许的(实质是错误的)***********************/

class Test

{

    public static void main(String[] args)

    {

        Father[] objs = new Father[2];

        objs[0] = new Father();

        objs[1] = new Son();

 

        for(Father obj:objs)

        {

        //因为Son类抛出的实质是SQLException,而IOException无法处理它。

        //那么这里的try。。catch就不能处理Son中的异常。

        //多态就不能实现了。

            try {

                 obj.start();

            }catch(IOException)

            {

                 //处理IOException

            }

         }

   }

}


2、Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。


也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。


finally块和return


首先一个不容易理解的事实:在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。


public static void main(String[] args)

{

    int re = bar();

    System.out.println(re);

}

private static int bar() 

{

    try{

        return 5;

    } finally{

        System.out.println("finally");

    }

}

/*输出:

finally

*/

也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。


finally中的return 会覆盖 try 或者catch中的返回值。


public static void main(String[] args)

    {

        int result;

         

        result  =  foo();

        System.out.println(result);     /////////2

         

        result = bar();

        System.out.println(result);    /////////2

    }

 

    @SuppressWarnings("finally")

    public static int foo()

    {

        trz{

            int a = 5 / 0;

        } catch (Exception e){

            return 1;

        } finally{

            return 2;

        }

 

    }

 

    @SuppressWarnings("finally")

    public static int bar()

    {

        try {

            return 1;

        }finally {

            return 2;

        }

    }


finally中的return会抑制(消灭)前面try或者catch块中的异常


class TestException

{

    public static void main(String[] args)

    {

        int result;

        try{

            result = foo();

            System.out.println(result);           //输出100

        } catch (Exception e){

            System.out.println(e.getMessage());    //没有捕获到异常

        }

         

         

        try{

            result  = bar();

            System.out.println(result);           //输出100

        } catch (Exception e){

            System.out.println(e.getMessage());    //没有捕获到异常

        }

    }

     

    //catch中的异常被抑制

    @SuppressWarnings("finally")

    public static int foo() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }catch(ArithmeticException amExp) {

            throw new Exception("我将被忽略,因为下面的finally中使用了return");

        }finally {

            return 100;

        }

    }

     

    //try中的异常被抑制

    @SuppressWarnings("finally")

    public static int bar() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }finally {

            return 100;

        }

    }

}


finally中的异常会覆盖(消灭)前面try或者catch中的异常


class TestException

{

    public static void main(String[] args)

    {

        int result;

        try{

            result = foo();

        } catch (Exception e){

            System.out.println(e.getMessage());    //输出:我是finaly中的Exception

        }

         

         

        try{

            result  = bar();

        } catch (Exception e){

            System.out.println(e.getMessage());    //输出:我是finaly中的Exception

        }

    }

     

    //catch中的异常被抑制

    @SuppressWarnings("finally")

    public static int foo() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }catch(ArithmeticException amExp) {

            throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");

        }finally {

            throw new Exception("我是finaly中的Exception");

        }

    }

     

    //try中的异常被抑制

    @SuppressWarnings("finally")

    public static int bar() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }finally {

            throw new Exception("我是finaly中的Exception");

        }

         

    }

}


上面的3个例子都异于常人的编码思维,因此我建议:


  • 不要在fianlly中使用return。

  • 不要在finally中抛出异常。

  • 减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。

  • 将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。


2 Java 中 9 个处理 Exception 的最佳实践

在Java中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范对异常的处理的原因。而团队之间的这些规范往往是截然不同的。


本文给出几个被很多团队使用的异常处理最佳实践。


1. 在Finally块中清理资源或者使用try-with-resource语句


当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。


public void doNotCloseResourceInTry() {

    FileInputStream inputStream = null;

    try {

        File file = new File("./tmp.txt");

        inputStream = new FileInputStream(file);

        // use the inputStream to read a file

        // do NOT do this

        inputStream.close();

    } catch (FileNotFoundException e) {

        log.error(e);

    } catch (IOException e) {

        log.error(e);

    }

}


上述代码在没有任何exception的时候运行是没有问题的。但是当try块中的语句抛出异常或者自己实现的代码抛出异常,那么就不会执行最后的关闭语句,从而资源也无法释放。


合理的做法则是将所有清理的代码都放到finally块中或者使用try-with-resource语句。


public void closeResourceInFinally() {

    FileInputStream inputStream = null;

    try {

        File file = new File("./tmp.txt");

        inputStream = new FileInputStream(file);

        // use the inputStream to read a file

    } catch (FileNotFoundException e) {

        log.error(e);

    } finally {

        if (inputStream != null) {

            try {

                inputStream.close();

            } catch (IOException e) {

                log.error(e);

            }

        }

    }

}

 

public void automaticallyCloseResource() {

    File file = new File("./tmp.txt");

    try (FileInputStream inputStream = new FileInputStream(file);) {

        // use the inputStream to read a file

    } catch (FileNotFoundException e) {

        log.error(e);

    } catch (IOException e) {

        log.error(e);

    }

}


2. 指定具体的异常


尽可能的使用最具体的异常来声明方法,这样才能使得代码更容易理解。


public void doNotDoThis() throws Exception {

    ...

}

public void doThis() throws NumberFormatException {

    ...

}


如上,NumberFormatException字面上即可以看出是数字格式化错误。


3. 对异常进行文档说明


当在方法上声明抛出异常时,也需要进行文档说明。和前面的一点一样,都是为了给调用者提供尽可能多的信息,从而可以更好地避免/处理异常。


在Javadoc中加入throws声明,并且描述抛出异常的场景。


/**

 * This method does something extremely useful ...

 *

 * @param input

 * @throws MyBusinessException if ... happens

 */

public void doSomething(String input) throws MyBusinessException {

    ...

}


4. 抛出异常的时候包含描述信息


在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。


但这里并不是说要对错误信息长篇大论,因为本来Exception的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。


try {

    new Long("xyz");

} catch (NumberFormatException e) {

    log.error(e);

}


NumberFormatException即告诉了这个异常是格式化错误,异常的额外信息只需要提供这个错误字符串即可。当异常的名称不够明显的时候,则需要提供尽可能具体的错误信息。


5. 首先捕获最具体的异常


现在很多IDE都能智能提示这个最佳实践,当你试图首先捕获最笼统的异常时,会提示不能达到的代码。


当有多个catch块中,按照捕获顺序只有第一个匹配到的catch块才能执行。因此,如果先捕获IllegalArgumentException,那么则无法运行到对NumberFormatException的捕获。


public void catchMostSpecificExceptionFirst() {

    try {

        doSomething("A message");

    } catch (NumberFormatException e) {

        log.error(e);

    } catch (IllegalArgumentException e) {

        log.error(e)

    }

}


6. 不要捕获Throwable


Throwable是所有异常和错误的父类。你可以在catch语句中捕获,但是永远不要这么做。


如果catch了throwable,那么不仅仅会捕获所有exception,还会捕获error。而error是表明无法恢复的jvm错误。因此除非绝对肯定能够处理或者被要求处理error,不要捕获throwable。


public void doNotCatchThrowable() {

    try {

        // do something

    } catch (Throwable t) {

        // don't do this!

    }

}


7. 不要忽略异常


很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。


public void doNotIgnoreExceptions() {

    try {

        // do something

    } catch (NumberFormatException e) {

        // this will never happen

    }

}


但现实是经常会出现无法预料的异常或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。


合理的做法是至少要记录异常的信息。


public void logAnException() {

    try {

        // do something

    } catch (NumberFormatException e) {

        log.error("This should never happen: " + e);

    }

}


8. 不要记录并抛出异常


可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下:


try {

    new Long("xyz");

} catch (NumberFormatException e) {

    log.error(e);

    throw e;

}


这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下:


17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"

Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)

at java.lang.Long.parseLong(Long.java:589)

at java.lang.Long.(Long.java:965)

at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)

at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)


如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。


public void wrapException(String input) throws MyBusinessException {

    try {

        // do something

    } catch (NumberFormatException e) {

        throw new MyBusinessException("A message that describes the error.", e);

    }

}


因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。


9. 包装异常时不要抛弃原始的异常


捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。


需要注意的是,包装异常时,一定要把原始的异常设置为cause(Exception有构造方法可以传入cause)。否则,丢失了原始的异常信息会让错误的分析变得困难。


public void wrapException(String input) throws MyBusinessException {

    try {

        // do something

    } catch (NumberFormatException e) {

        throw new MyBusinessException("A message that describes the error.", e);

    }

}


3  改进异常处理的 6 条建议

合理地使用异常处理可以帮你节省数小时(甚至数天)调试时间。一个乘法异常会毁掉你的晚餐乃至周末计划。如果处置不及时,甚至对你的名誉都会造成影响。一个清晰的异常处理策略可以助你节省诊断、重现和问题纠正时间。下面是6条异常处理建议。


1. 使用一个系统全局异常类


不必为每种异常类型建立单独的类,一个就够了。确保这个异常类继承RuntimeException,这样可以减少类个数并且移除不必要的异常声明。


我知道你正在想什么:如果类型只有一个,那么怎么能知道异常具体是什么?我将如何追踪具体的属性?请继续阅读。


2. 使用枚举错误码


我们大多被教授的方法是将异常转为错误信息。这次查看日志文件时很好,(呃)但是这样也有缺点:


  1. 错误信息不会被翻译(除非你是Google)

  2. 错误信息不会转换为用户友好的文字

  3. 错误信息不能用编程的方式检测


将异常消息留给开发者定义也会出现同样的错误有多种不同的描述。


一个更好的办法是使用枚举表示异常类型。为每个错误分类创建一个枚举(付款、认证等),让枚举实现ErrorCode接口并作为异常的一个属性。


当抛出异常时,只要传入合适的枚举就可以了。


throw new SystemException(PaymentCode.CREDIT_CARD_EXPIRED);


现在如果需要测试异常只要比较异常代码和枚举就可以了。


catch (SystemException e) {

  if (e.getErrorCode() == PaymentCode.CREDIT_CARD_EXPIRED) {

  ...

  }

}


通过将错误码作为查找资源的key就可以方便地提供友好的国际化文本。


public class SystemExceptionExample3 {

 

    public static void main(String[] args) {

        System.out.println(getUserText(ValidationCode.VALUE_TOO_SHORT));

    }

 

    public static String getUserText(ErrorCode errorCode) {

        if (errorCode == null) {

            return null;

        }

        String key = errorCode.getClass().getSimpleName() + "__" + errorCode;

        ResourceBundle bundle = ResourceBundle.getBundle("com.northconcepts.exception.example.exceptions");

        return bundle.getString(key);

    }

 

}


3. 为枚举添加错误值


在很多时候可以为异常添加错误值,比如HTTP返回值。这种情况下,可以在ErrorCode接口添加一个getNumber方法并在每个枚举中实现这个方法。


public enum PaymentCode implements ErrorCode {

  SERVICE_TIMEOUT(101),

  CREDIT_CARD_EXPIRED(102),

  AMOUNT_TOO_HIGH(103),

  INSUFFICIENT_FUNDS(104);

 

  private final int number;

 

  private PaymentCode(int number) {

    this.number = number;

  }

 

  @Override

  public int getNumber() {

    return number;

  }

 

}


添加错误码可以是全局数值也可以每个枚举自己负责。你可以直接使用枚举里的ordinal()方法或者从文件或数据库加载。


4. 为异常添加动态属性


好的异常处理还应该记录相关数据而不仅仅是堆栈信息,这样可以在诊断错误和重现错误时节省大量时间。用户不会在你的应用停止工作时告诉你他们到底做了什么。


最简单的办法是给异常添加一个java.util.Map字段。新字段的职责就是通过名字保存相关数据。通过添加setter方法可以遵循流式接口。


可以像下面示例这样添加相关数据并抛出异常:


throw new SystemException(ValidationCode.VALUE_TOO_SHORT)

  .set("field", field)

  .set("value", value)

  .set("min-length", MIN_LENGTH);


5. 避免不必要的嵌套


冗长的堆栈信息不会有任何帮助,更糟糕的是会浪费你的时间和资源。重新抛出异常时调用静态函数而不是异常构造函数。封装的静态函数决定什么时候嵌套异常什么时候只要返回原来的实例。


public static SystemException wrap(Throwable exception, ErrorCode errorCode) {

  if (exception instanceof SystemException) {

    SystemException se = (SystemException)exception;

    if (errorCode != null && errorCode != se.getErrorCode()) {

      return new SystemException(exception.getMessage(), exception, errorCode);

    }

    return se;

  } else {

    return new SystemException(exception.getMessage(), exception, errorCode);

  }

}

 

public static SystemException wrap(Throwable exception) {

  return wrap(exception, null);

}


Your new code for rethrowing exceptions will look like the following.


catch (IOException e) {

  throw SystemException.wrap(e).set("fileName", fileName);

}


6. 使用带Web支持的集中式logger


再额外附赠一个建议。可能你情况很难向产品记录日志,这个麻烦可能来自多个中间商(很多开发者不能直接访问产品环境)。


在多服务器环境下情况可能会更糟。找到正确的服务器或者确定问题影响到了哪个服务器是一件非常令人头痛的事情。


我的建议是:


  1. 将你的日志记录到一个地方,推荐记录到数据库中。

  2. 通过Web浏览器访问数据库。


有很多方法和备选产品可以达成这一目标,log collector、远程logger、JMX agent、系统监视软件等。甚至可以自己写一个。重要的是要快速行动,一旦你达成了目标,你就可以:


  • 几秒钟之内定位错误

  • 为每个异常增加一个URL,可以记录或者发送email

  • 让你的伙伴可以在没有你的情况下定位错误原因

  • 避免测试人员为同一个bug添加多个记录。他们可以在bug记录里增加一条异常URL

  • 省钱

  • 让你的周末和名誉不受影响



猜你喜欢

转载自blog.csdn.net/aa46449521/article/details/79168506