1-07 异常、断言和日志

LAST UPDATE:2020/10/31


参考:

  1. JAVA核心技术卷Ⅰ
  2. 廖雪峰-JAVA教程-异常

第7章 异常、断言和日志

  • 异常处理(exception handing)

  • 存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。

  • 捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!

  • 项目中一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。

    BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:

    其他业务类型的异常就可以从BaseException派生

    自定义的BaseException应该提供多个构造方法

7.1 处理错误

  • 如果由于出现错误而使得某些操作没有完成,程序应该:
    • 返回到一种安全状态,并能够让用户执行一些其他的命令
    • 或者允许用户保存所有操作的结果,并以妥善的方式终止程序。
  • 异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
  • 错误类型
    • 用户输入错误
    • 设备错误
    • 物理限制
    • 代码错误
  • 对于方法中的一个错误,传统的做法是返回一个特殊的错误码。但是,并不是在任何情况下都能返回一个错误码。有可能无法明确地将有效数据与无效数据加以区分。
  • 在Java中,如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。
    • 在这种情况下,方法并不返回任何值,而是抛出( throw)一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回任何值。此外,调用这个方法的代码也将无法继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)。
7.1.1 异常分类
graph TB Z[Object] A[Thowable] B[Error] C[Exception] D[IOException] E[RuntimeException] Z-->A A-->B A-->C C-->D C-->E
  • 在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。
    • 如果Java中内置的异常类不能够满足需求,用户可以创建自己的异常类。
  • 所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支: Error 和Exception
    • Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。
    • 在设计Java程序时,需要关注Exception层次结构。这个层次结构又分解为两个分支:
      • 一个分支派生于RuntimeException;
      • 另一个分支包含其他异常。
      • 派生于RuntimeException的异常包含下面几种情况:
        • 错误的类型转换。
        • 数组访问越界。
        • 访问null指针。
      • 不是派生于RuntimeException的异常包括:
        • 试图在文件尾部后面读取数据。
        • 试图打开一个不存在的文件。
        • 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
  • "如果出现RuntimeException异常,那么就一定是你的问题
  • 派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常
    • 编译器将核查是否为所有的受查异常提供了异常处理器。
7.1.2 声明受查异常
  • 一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。

  • 方法应该在其首部声明所有可能抛出的异常。

  • 在遇到下面4种情况时应该抛出异常

    • 调用一个抛出受查异常的方法,例如,FileInputStream 构造器。

    • 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常

    • 程序出现错误,例如,a[- 1]=0会抛出一个ArrayIndexOutOfBoundsException这样的
      非受查异常。

    • Java虚拟机和运行时库出现的内部错误。

    如果出现前两种情况之一,则必须告诉调用这个方法的程序员有可能抛出异常如果没有处理器捕获这个异常,当前执行的线程就会结束。

  • 对于那些可能被他人使用的Java方法,应该根据异常规范( exception specification), 在
    方法的首部声明这个方法可能抛出的异常。

  • 如果一个方法有可能抛出多个受查异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。

  • 不需要声明Java的内部错误,即从Error继承的错误

  • 也不应该声明从RuntimeException继承的那些非受查异常。这些运行时错误完全在我们的控制之下。

  • 总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。

  • 除了声明异常之外,还可以捕获异常。

  • 如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用

    • 特别需要说明的是,如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
  • 如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,
    则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。

7.1.3 如何抛出异常
  • EOFException异常:“在输入过程中,遇到了一个未预期的EOF后的信号”。
  • EOFException类还有一个含有一个字符串型参数的构造器。这个构造器可以更加细致的
    描述异常出现的情况。
  • 在这种情况下:
    • 1)找到一个合适的异常类。
    • 2)创建这个类的一个对象。
    • 3)将对象抛出。
      一旦方法抛出了异常,这个方法就不可能返回到调用者。
7.1.4 创建异常类
  • 我们需要做的只是定义一个派生于Exception的类,或者派生于Exception 子类的类。

  • 习惯上,定义的类应该包含两个构造器

    • 一个是默认的构造器;

    • 另一个是带有详细描述信息的构造器

      超类Throwable的toString方法将会打印出这些详细信息,这在调试中非常有用)。

7.2 捕获异常

7.2.1 捕获异常
  • 如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台
    上打印出异常信息,其中包括异常的类型和堆栈的内容。

  • 要想捕获一个异常,必须设置try/catch语句块。

  • 如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么

    • 程序将跳过try 语句块的其余代码。
    • 程序将执行catch子句中的处理器代码。

    如果在try 语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
    如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法
    就会立刻退出(希望调用者为这种类型的异常设计了catch子句)。

  • 编译器严格地执行throws说明符。如果调用了一个抛出受查异常的方法,就必
    须对它进行处理,或者继续传递。

    • 通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。
    • 如果想传递一个异常,就必须在方法的首部添加一个throws 说明符,以便告知调用者这个方法可能会抛出异常。
    • 如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕
      获方法代码中出现的每一个受查异常不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。
7.2.2 捕获多个异常
  • 在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。

  • 异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用

    //得到详细的错误信息(如果有的话)
    e.getMessage();
    //得到异常对象的实际类型。
    e.getClass().getName();
    
  • 在Java SE 7中,同一个catch子句中可以捕获多个异常类型。

    • 只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。

    • 捕获多个异常时,异常变量隐含为final变量

      • 例如,不能在以下子句体中为e赋不同的值:

        catch (FileNotFoundException | UnknownHostException e){...}
        
  • 捕获多个异常不仅会让你的代码看起来更简单,还会更高效。生成的字节码只包含一个对应公共catch子句的代码块。

7.2.3 再次抛出异常与异常链
  • 在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。

    try
    {
    	access the database
    }
    catch (SQLException e)
    {
    	Throwable se = new ServletException("database error");
    	se.initCause(e);
    	throw se;
    }
    
    //当捕获到异常时,就可以使用下面这条语句重新得到原始异常:
    Throwable e = se.getCause();
    
  • 强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

  • 如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并将它包装成一个运行时异常。

  • 有时你可能只想记录一个异常,再将它重新抛出,而不做任何改变:

    try
    {
    	access the database
    }
    catch (Exception e)
    {
    	logger.log(level,message, e);
    	throw e;
    }
    
7.2.4 finally子句
  • 不管是否有异常被捕获,finally子句中的代码都被执行。
  • try语句可以只有finally子句,而没有catch子句
  • 事实上,我们认为在需要关闭资源时,使用finally子句是一种不错的选择。
  • 当finally子句包含return语句时,将会出现一种意想不到的结果。
    • 假设利用return语句从try 语句块中退出。在方法返回前,finally子句的内容将被执行。
    • 如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。
  • 现在,假设在try语句块中的代码抛出了一些非IOException的异常,这些异常只有这个
    方法的调用者才能够给予处理。执行finally语句块,并调用close方法。而close方法本身也有可能抛出IOException异常。当出现这种情况时,原始的异常将会丢失,转而抛出close方法的异常。
    这会有问题,因为第一个异常很可能更有意思。如果你想做适当的处理,重新抛出原来
    的异常,代码会变得极其繁琐。
7.2.5 带资源的try语句
  • 对于以下代码模式:

    open a resource
    try
    {
    	work with the resource
    }
    finally
    {
    	close the resource
    }
    

    假设资源属于一个实现了AutoCloseable接口的类,Java SE 7为这种代码模式提供了一
    个很有用的快捷方式。AutoCloseable接口有一个方法:

    void close() throws Exception
    

    另外,还有一个Closeable接口。这是AutoCloseable的子接口,也包含一个close
    方法。不过,这个方法声明为抛出一个IOException。

  • 带资源的try 语句(try-with-resources)的最简形式为:

    try (Resource res = . . .)
    {
    	work with res
    }
    

    try块退出时,会自动调用res.close()

  • 下面给出一个典型的例子,这里要读取一个文件中的所有单词:

    try (Scanner in = new Scanner(new Fi1eInputStream(" /usr/share/dict/words")),"UTF-8"")
    {
        while (in.hasNext())
        	out.println(in.next().toUpperCase());
    }
    

    这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了
    finally块一样。

  • 还可以指定多个资源。例如:

    try (Scanner in = new Scanner(new FileInputStream(" /usr/share/dict/words"),"UTF-8");
    Printwriter out = new Printwriter("out.txt"))
    {
    	while (in.hasNext())
    		out.println(in.next().toUpperCase());
    }
    

    不论这个块如何退出,in和 out都会关闭。如果你用常规方式手动编程,就需要两个嵌
    套的 try/finally 语句。

  • 上一节已经看到,如果try块抛出一个异常,而且 close方法也抛出一个异常,这就会带
    来一个难题。带资源的try语句可以很好地处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可以调用getSuppressed方法,它会得到从close方法抛出并被抑制的异常列表。
    你肯定不想采用这种常规方式编程。

  • 只要需要关闭资源,就要尽可能使用带资源的try语句。

  • 带资源的try 语句自身也可以有catch子句和一个finally子句。这些子句会在关闭资源之后执行。不过在实际中,一个try语句中加入这么多内容可能不是一个好主意。

7.2.6 分析堆栈轨迹元素
  • 堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中调用的特定位置。

    • 当JAVA程序正常终止,而没有捕获异常时,这个列表哦就会显示出来。
  • 可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。

  • 一种更灵活的方法是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组。

    • StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。
  • 静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈轨迹。

    Map<Thread, StackTraceElement[]> map = Thread.getA11StackTraces();
    for (Thread t : map.keySet())
    {
        StackTraceElement[] frames = map.get(t);
    	analyze frames
    }
    

7.3 使用异常机制的技巧

  • 异常处理不能代替简单的测试

    • 与执行简单测试相比,捕获异常所花费的时间大大超过了前者。
    • 原则:只在异常情况下使用异常机制。
  • 不要过分细化异常

  • 利用异常层次结构

    • 不要只抛出RuntimcException异常。应该寻找更加适当的子类或创建自己的异常类。
    • 不要只捕获Thowable异常,否则,会使程序代码更难读、更难维护。
  • 不要压制异常

  • 在检测错误时,”苛刻“要比放任更好。

  • 不要羞于传递异常

    最后两个可以归纳为:”早抛出,晚捕获

7.4 使用断言

  • 在一个具有自我保护能力的程序中,断言很常用。
  • Java断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
  • 对可恢复的错误不能使用断言,而应该抛出异常。
  • 断言很少被使用,更好的方法是编写单元测试。

7.4.1 断言的概念
  • 断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。

  • Java语言引入了关键字assert。这个关键字有两种形式:
    assert 条件;

    assert 条件:表达式;
    这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。
    在第二种形式中,表达式将被传入AssertionError 的构造器,并转换成一个消息字符串。

    注释:“表达式部分的唯一目的是产生一个消息字符串。AssertionError对象并不存储
    表达式的值,因此,不可能在以后得到它。

7.4.2 启用和禁用断言
  • 在默认情况下,断言被禁用。可以在运行程序时用-enableassertions或-ea选项启用:

    java -enableassertions MyApp
    

    需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器(class loader)的功能。当断言被禁用时,类加载器将跳过断言代码,因此,不会降低程序运行的速度。

  • 也可以在某个类或整个包中使用断言,例如:

    java -ea:MyClass -ea:com.mycompany.mylib...MyApp
    

    这条命令将开启MyClass类以及在com.mycompany.mylib包和它的子包中的所有类的断
    言。选项-ea将开启默认包中的所有类的断言。

  • 也可以用选项-disableassertions或-da禁用某个特定类和包的断言:

    java -ea:... -da:MyClass MyApp
    
  • 有些类不是由类加载器加载,而是直接由虚拟机加载。可以使用这些开关有选择地启用
    或禁用那些类中的断言。
    然而,启用和禁用所有断言的-ea和-da开关不能应用到那些没有类加载器的“系统类”
    上。

    对于这些系统类来说,需要使用-enablesystemassertions/-esa开关启用断言。
    在程序中也可以控制类加载器的断言状态。

7.4.3 使用断言完成参数检查
  • 在Java语言中,给出了3种处理系统错误的机制:

    • 抛出一个异常
    • 日志
    • 使用断言
  • 什么时候应该选择使用断言呢?请记住下面几点:

    • 断言失败是致命的、不可恢复的错误。
    • 断言检查只用于开发和测阶段

    因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作
    为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。

7.4.4 为文档假设使用断言

7.5 记录日志

  • 断言是一种测试和调试阶段所使用的战术性工具,而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。
  • Commons Logging
    • Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
    • 它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
    • 是使用最广泛的日志模块;
    • API非常简单;
    • 可以自动检测并使用其他日志模块。
  • 其实SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
    • SLF4J和Logback可以取代Commons Logging和Log4j;
    • 始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。

  • 记录日志API的优点
    • 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这
      个操作也很容易。
    • 可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小。
    • 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。
    • 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准
      丢弃那些无用的记录项。
    • 日志记录可以采用不同的方式格式化,例如,纯文本或XML。
    • 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,
      例如,com.mycompany.myapp.
    • 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换
      这个配置。
7.5.1 基本日志
  • 要生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其info方法:

    Logger.getGlobal().info("File->0pen menu item selected");
    

    在默认情况下,这条记录将会显示以下内容:

    May 10,2013 10:12:15 PM LogginglmageViewer fileOpen
    INFO: File->Open menu item selected
    

    但是,如果在适当的地方(如main开始)调用

    Logger.getGlobal().setLevel(Level.OFF);
    

    将会取消所有的日志。

7.5.2 高级日志
  • 可以调用getLogger方法创建或获取记录器:

    private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
    

    提示:未被任何变量引用的日志记录器可能会被垃圾回收。为了防止这种情况发生,要像上面的例子中一样,用一个静态变量存储日志记录器的一个引用。)

  • 与包名类似,日志记录器名也具有层次结构。

    • 事实上,与包名相比,日志记录器的层次性更强。
    • 对于包来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器
      的父与子之间将共享某些属性。
  • 通常,有以下7个日志记录器级别:

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST

    在默认情况下,只记录前三个级别。也可以设置其他的级别。例如,

    //现在,FINE和更高级别的记录都可以记录下来。
    logger.setLevel(Level.FINE);
    

    另外,还可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别
    的记录。
    对于所有的级别有下面几种记录方法:

    logger.warning(message);
    logger.fine(message);
    

    同时,还可以使用log方法指定级别,例如:

    logger.log(Level.FINE,message);
    
  • 默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。但是,
    如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用logp方法获得调用类和方法的确切位置,这个方法的签名为:

    void logp(Level 1, String className,String methodName, String message)
    
  • //其他重要函数
    entering
    throwing
    log
    
7.5.3 修改日志管理器配置
  • 可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:

    jre/lib/logging.properties
    
  • 要想使用另一个配置文件,就要将java.util.logging.config.file特性设置为配置文件的存
    储位置,并用下列命令启动应用程序:

    java -Djava.util.logging.config.file=configFile MainClass
    
    
  • 要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行

    .level=INFO
    
  • 可以通过添加以下内容来指定自己的日志记录级别

    //也就是说,在日志记录器名后面添加后缀.level。
    com.mycompany.myapp.level=FINE
    
  • 在稍后可以看到,日志记录并不将消息发送到控制台上,这是处理器的任务。另外,
    理器也有级别。要想在控制台上看到FINE级别的消息,就需要进行下列设置

    java.util.logging.ConsoleHandler.level=FINE
    
7.5.4 本地化
  • 本地化的应用程序包含资源包(resource bundle)中的本地特定信息。资源包由各个地区
    (如美国或德国)的映射集合组成。

    • 例如,某个资源包可能将字符串“readingFile”映射成英文的“Reading file”或者德文的“Achtung! Datei wird eingelesen”.
  • 一个程序可以包含多个资源包,一个用于菜单;其他用于日志消息。每个资源包都有一
    个名字(如com.mycompany.logmessages)。

    • 要想将映射添加到一个资源包中,需要为每个地区创建一个文件。英文消息映射位于com/mycompany/logmessages_en.properties文件中;德文消息映射位于com/mycompany/logmessages_de.properties文件中。( en和 de是语言编码)。
      可以将这些文件与应用程序的类文件放在一起,以便ResourceBundle类自动地对它们进行定位。这些文件都是纯文本文件,其组成如下所示:

      readingFile=Achtung!Datei wird eingelesen
      renamingFile=Datei wird umbenannt
      
    • 在请求日志记录器时,可以指定一个资源包:

    Logger logger = Logger.getLogger(loggerName,"com.mycompany.logmessages");
    

    然后,为日志消息指定资源包的关键字,而不是实际的日志消息字符串。

    logger.info("readingFile");
    

    通常需要在本地化的消息中增加一些参数,因此,消息应该包括占位符{0}、{1}等。例如,要想在日志消息中包含文件名,就应该用下列方式包括占位符:

    Reading file {0}.
    Achtung! Datei {0} wird eingelesen.
    

    然后,通过调用下面的一个方法向占位符传递具体的值:

    logger.log(Level.INFO,"readingFile",fileName);
    logger.log(Level.INFO,"renamingFile",new Object[] { oldName,newName };
    
7.5.5 处理器
  • 在默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到System.err
    流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器有一个ConsoleHandler。

  • 与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日
    志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为

    java.util.logging.ConsoleHandler.level=INFO
    

    要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。
    另外,还可以绕过配置文件,安装自己的处理器。

  • 在默认情况下,日志记录器将记录发送到自己的处理器和父处理器。我们的日志记录
    器是原始日志记录器的子类,而原始日志记录器将会把所有等于或高于INFO级别的记录发送到控制台。

    • 然而,我们并不想两次看到这些记录。鉴于这个原因,应该将useParentHandlers属性设置为false。
  • 要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个很
    有用的处理器,

    • 一个是FileHandler:可以收集文件中的记录。

      • 可以像下面这样直接将记录发送到默认文件的处理器:

        FileHandler handler = new FileHandler();
        logger.addHandler(handler);
        

        这些记录被发送到用户主目录的javan.log文件中,n是文件名的唯一编号。
        在默认情况下,记录被格式化为XML。

    • 另一个是SocketHandler。SocketHandler将记录发送到特定的主机和端口。

  • 可以通过设置日志管理器配置文件中的不同参数(请参看表7-1),或者利用其他的构造
    器来修改文件处理器的默认行为。

    Java核心技术读书笔记_07 异常、断言和日志_2

    也有可能不想使用默认的日志记录文件名,因此,应该使用另一种模式,例如,%h/
    myapp.log(有关模式变量的解释请参看表7-2)。

Java核心技术读书笔记_07 异常、断言和日志_3

  • 如果多个应用程序(或者同一个应用程序的多个副本)使用同一个日志文件,就应该开
    启append标志。另外,应该在文件名模式中使用%u,以便每个应用程序创建日志的唯一副本。

  • 开启文件循环功能也是一个不错的主意。日志文件以myapp.log.0,myapp.log.1,myapp.log.2,这种循环序列的形式出现。只要文件超出了大小限制,最旧的文件就会被删除,其他的文件将重新命名,同时创建一个新文件,其编号为0。

    很多程序员将日志记录作为辅助文档提供给技术支持员工。如果程序的行为有误,
    用户就可以返回查看日志文件以找到错误的原因。在这种情况下,应该开启“append"
    标志,或使用循环日志,也可以两个功能同时使用。

  • 还可以通过扩展Handler类或StreamHandler类自定义处理器。

7.5.6 过滤器
  • 在默认情况下,过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以
    有一个可选的过滤器来完成附加的过滤。另外,可以通过实现Filter接口并定义下列方法来
    自定义过滤器。

    boolean isLoggable(LogRecord record)
    

    在这个方法中,可以利用自己喜欢的标准,对日志记录进行分析,返回true表示这些记
    录应该包含在日志中。例如,某个过滤器可能只对entering方法和exiting方法产生的消息
    感兴趣,这个过滤器可以调用record.getMessage()方法,并查看这个消息是否用ENTRY或RETURN开头。

  • 要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以
    了。注意,同—时刻最多只能有一个过滤器。

7.5.7 格式化器
  • ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但是,也可
    以自定义格式。这需要扩展Formatter类并覆盖下面这个方法:

    String format(LogRecord record)
    

    可以根据自己的愿望对记录中的信息进行格式化,并返回结果字符串。在format方法
    中,有可能会调用下面这个方法

    String formatMessage(LogRecord record)
    

    这个方法对记录中的部分消息进行格式化、参数替换和本地化应用操作。

  • 很多文件格式(如XML)需要在已格式化的记录的前后加上一个头部和尾部。在这个例
    子中,要覆盖下面两个方法:

    String getHead(Handler h)
    String getTail(Handler h)
    

    最后,调用setFormatter方法将格式化器安装到处理器中。

7.5.8 日志记录说明
  • 为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程
    序包一样的名字,例如,com.mycompany.myprog,这是一种好的编程习惯。另外,可以通过调用下列方法得到日志记录器。

    Logger logger =Logger.getLogger("com.mycompany.myprog");
    

    为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:

    private static final Logger logger = Logger.getLogger("com.mycompany.myprog");
    
  • 默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。

  • 所有级别为INFO、WARNING和SEVERE的消息都将显示到控制台上。因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE是一个很好的选择。

7.6 调试技巧

  • 可以用下面的方法打印或记录任意变量的值:

    System.out.printIn("x="+ x);
    

    Logger.getGlobal().info("x=" + x);
    

    如果x是一个数值,则会被转换成等价的字符串。如果x是一个对象,那么Java就会调
    用这个对象的toString方法。要想获得隐式参数对象的状态,就可以打印this对象的状态。

  • 日志代理(logging proxy)是一个子类的对象,它可以截获方法调用,并进行日志记
    录,然后调用超类中的方法。

  • 利用Throwable类提供的printStackTrace方法,可以从任何一个异常对象中获得堆栈
    情况。

    • 不一定要通过捕获异常来生成堆栈轨迹。只要在代码的任何位置插入下面这条语句就可以获得堆栈轨迹:

      Thread.dumpStack();
      
  • 一般来说,堆栈轨迹显示在System.err上。也可以利用printStackTrace(PrintWriters)
    方法将它发送到一个文件中。另外,如果想记录或显示堆栈轨迹,就可以采用下面的方式,将它捕获到一个字符串中:

    Stringwriter out = new Stringwriter();
    new Throwable().printStackTrace(new Printwriter(out));
    String description = out.toString();
    
  • 7)通常,将一个程序中的错误信息保存在一个文件中是非常有用的。然而,错误信息
    被发送到System.err中,而不是System.out中。因此,不能够通过运行下面的语句获取它们:

    java MyProgram> errors.txt
    

    而是采用下面的方式捕获错误流:

    java MyProgram 2> errors.txt
    

    要想在同一个文件中同时捕获System.err和System.out,需要使用下面这条命令

    java MyProgram 1>errors.txt 2>&1
    

    这条命令将工作在bash和Windows shell 中。

  • 让非捕获异常的堆栈轨迹出现在System.err中并不是一个很理想的方法。

  • 要想观察类的加载过程,可以用-verbose标志启动Java虚拟机。

  • -Xlint选项告诉编译器对一些普遍容易出现的代码问题进行检查。

  • Java虚拟机增加了对Java应用程序进行监控(monitoring)和管理(management)的支持。它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。

  • 可以使用jmap实用工具获得一个堆的转储,其中显示了堆中的每个对象。使用命
    令如下:

    jmap -dump:format-b,file=dumpFileName processID
    jhat dumpFileName
    

    然后,通过浏览器进入localhost:7000,将会运行一个网络应用程序,借此探查转储对象
    时堆的内容。
    13)如果使用-Xprof标志运行Java虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。剖析信息将发送给System.out。输出结果中还会显示哪些方法是由
    即时编译器编译的。

原文地址:https://www.cnblogs.com/nojacky/p/13909189.html