Java基础之《Java核心卷1》第7章

7. 异常、断言和日志

7.1 抛出异常

在Java中,异常对象都是派生于Throwable类的一个实例

Java中的异常层次结构

  • Throwable:所有的异常都是由Throwable继承而来,它有两个分支Error 和Exception
    • Error:Java运行时系统的内部错误和资源耗尽错误,应用程序不应该抛出这种类型的对象,而应该通知用户并尽可能安全终止
    • Exception:异常
      • IOException:程序本身没有问题, 但由于像I/O错误这类问题导致的异常属于其他异常
        常见的有:(1)试图在文件尾部后面读取数据;(2)试图打开一个不存在的文件;(3)试图根据给定的字符串查找Class 对象,而这个字符串表示的类并不存在
      • RuntimeException:程序错误导致的异常
        常见的有:(1)错误的类型转换;(2)数组访问越界;(3)访问null指针

非受查(unchecked)异常和受查( checked) 异常

派生于Error类或RuntimeException类的所有异常被称为非受查异常;所有其他的异常称为受查异常
编译器将核查是否为所有的受査异常提供了异常处理器

一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。如果方法没有声明所有可能发生的受查异常, 编译器就会发出一个错误消息。

RuntimeException这个名字很容易让人混淆。实际上, 现在讨论的所有错误都发生在运行时

  • 声明受查异常:告诉编译器有可能发生什么错误,例如

    public Fi1eInputStream(String name) throws FileNotFoundException
    //表明构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能抛出一个FileNotFoundException异常
    //一旦抛出异常,运行时系统就会开始搜索异常处理器, 以便知道如何处理FileNotFoundException对象
    

需要抛出异常的情况

1. 调用一个抛出受査异常的方法,例如,FileInputStream构造器
2. 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
3. 程序出现错误,例如a[-l]=0会抛出一个ArraylndexOutOffloundsException这样的非受查异常
4. Java虚拟机和运行时库出现的内部错误

如果有前2种情况,必须告诉调用这个方法的程序员有可能抛出异常
对于那些可能被他人使用的Java方法, 应该根据异常规范,在方法后用throws列出所有可能的异常,多个异常用逗号隔开
不能声明非受查异常

超类声明的受查异常要比子类更宽泛,如果超类没有声明异常,那么子类也不能声明异常

throw抛出异常
throw和throws是成对使用的,或者和catch联用

/**
* n是文件总长度,len是要读取的长度。当要读取的长度超过文件大小,需要抛出异常;
* 可以抛出更宽泛的IOException,也可以抛出子类EOFException
*/
if(n < len)
    throw new EOFException();
//EOFException("detailed problem"); 可以接收一个String 参数,用于更加具体地描述异常

Java 中,只能抛出Throwable子类的对象,而在C++ 中,却可以抛出任何类型的值

自定义异常

自定义异常应当应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(调用super.toString())

//注意:throw和throws都要出现
class FileFormatException extends IOException {
    public FileFormatException() {}
    public FileFormatException(String gripe) {
        super(gripe);
    }
}
public class Test{
    public static void main(String[] args) throws FileFormatException{
        int n = 10, len = 20;
        if(n < len) throw new FileFormatException("n < len");
    }
}
//运行后可以看到抛出的异常信息

API

//java.lang.Throwable 1.0
Throwable();//构造一个新的Throwable对象,这个对象没有详细的描述信息
Throwable(String message);//构造一个新的Throwable对象,这个对象带有特定的详细描述信息
//习惯上,所有派生的异常类都支持一个默认的构造器和一个带有详细描述信息的构造器
String getMessage();//获得Throwable对象的详细描述信息

7.2 捕获异常

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

try…catch捕获异常

如果try内捕获到了异常,那么剩余的语句不会执行,转去执行catch 子句中的处理器代码
如果抛出了一个catch中没有声明的异常,那么这个方法就会立刻退出
try…catch可以捕获多个异常,即多个catch语句

e.getMessage();//得到详细的错误信息
e.getClass().getName();//得到异常对象的实际类型
  • 合并catch语句:只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性

    try{
    	code that might throw exceptions
    }catch(FileNotFoundException | UnknownHostException e){
    	emergency action for missing files and unknown hosts
    }catch (IOException e){
    	emergency action for all other I/O problems
    }
    

再次抛出异常和异常链

try{
	access the database
}catch (SQLException e){
    Throwable se = new ServletException("database error");//并不关心是什么异常,只要知道是Servlet有问题就行
    se.initCause(e);//将原始异常设置为新异常的“原因”,保留原始异常的信息(非强制)
    throw se;//抛出
}

这样,当捕获到异常时,可以使用如下语句,获得原始异常的信息

Throwable e = se.getCause();

这种包装还可以用来处理不允许被抛出的异常,通过将它包装为其他异常
当想记录一下异常再抛出,可以

catch (Exception e){
	logger.log(level, message, e);
	throw e;
}

一个问题:在 Java SE 7 之前,如果 catch 中抛出的异常,比包含该语句的方法抛出的异常更宽泛,编译器会报错
现在编译器会跟踪发现e来自try块,如果try块里的异常是SQLException的实例,并且e在catch中没有改变,那么编译通过

finally子句

场景:建立了数据库连接后抛出异常,此时连接未释放,这时候就需要finally子句
不管是否有异常被捕获,finally中的语句都要被执行,可以将释放连接的语句写在finally中
(1)假设抛出异常,那么执行顺序就是:try中未抛出异常的代码 - catch语句 - finally语句
(2)假设抛出异常,且catch未能捕捉,那么执行顺序就是:try中未抛出异常的代码 - finally语句 - 将异常抛给方法的调用者

try{
    ...
}catch(){
    ...
}finally{
    ...
}

注意:(1)当finally中包含return语句时,它将覆盖try中的return
(2)finally语句中也可能抛出异常,如果原有异常是要抛给方法的调用者(即无catch或catch失败),此时也会覆盖掉原有的异常

带资源的try语句

try(Scanner in = new Scanner(new FileInputStream("E:/test.txt"), "UTF-8")){
    while(in.hasNext())
        System.out.println(in.next());
}
//结束时会自动调用in.close()

这个块正常退出时, 或者存在一个异常时, 都会调用in.close()方法, 就好像使用了finally 块一样
可以在 try(resource) 中指定多个资源,语句之间用分号(;)隔开

可以处理双重异常抛出:例如在finally语句中,try块和finally块都抛出异常,如果采用常规编程,那么try块的异常会被覆盖掉;
而采用带资源的try语句时,抛出的将是try内部的异常,而close()抛出异常会被抑制,即被自动捕获,通过addSuppressed方法增加到原异常中,getSuppressed方法获取

堆栈轨迹(stack trace):

假设在A()中调用了B(),在B()中打印堆栈轨迹,那么首先打印是B()自身,然后是A()

  • printStackTrace()
/**
* 获取堆栈轨迹
* PrintStream继承自字节输出流OutputStream支持写字节
* PrintWriter继承自字符输出流Writer支持写字符
* 所有的printStackTrace将执行printStackTrace(System.err),System.err是一个静态PrintString
*/
Throwable t = new Throwable();//Throwable所有异常处理超类
StringWriter out = new StringWriter();//创建了具有默认字符串缓冲区容量的字符串写入器
t.printStackTrace(new PrintWriter(out));//创建不带自动行刷新的新PrintWriter,并将内容放在out里面
String description = out.toString();
System.out.println(description);
  • getStackTrace()
/**
* 获取堆栈轨迹
*/
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame:frames)
    System.out.println("方法名:"+frame.getMethodName()+", 类名:"+frame.getClassName()+", 行数:"+
                       frame.getLineNumber()+", 文件名:"+frame.getFileName()+"----"+frame);
  • Thread.getAllStackTrace()
/**
* 静态方法:产生所有线程的堆栈轨迹
*/
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t:map.keySet()){
    StackTraceElement[] frames = map.get(t);
    for(StackTraceElement frame:frames)
        System.out.println(frame);
}
  • API
//java.lang.Throwable 1.0
Throwable(Throwable cause);//1.4 用给定的“原因” 构造一个Throwable对象
Throwable(String message, Throwable cause);//1.4 用给定的“原因” 构造一个Throwable对象
Throwable initCause(Throwable cause);//1.4 将这个对象设置为“原因”,若已经设置,则抛出一个异常。返回this引用。
Throwable getCause();//1.4 如果没有设置“原因”,则返回null
StackTraceElement[] getStackTrace();//1.4 获得构造这个对象时调用堆栈的跟踪。
void addSuppressed(Throwable t);//7 为这个异常增加一个“抑制”异常
Throwable[] getSuppressed();//7 得到这个异常的所有“抑制”异常

//java.lang.Exception 1.0
Exception(Throwable cause);//1.4 用给定的“原因”构造一个异常对象
Exception(String message, Throwable cause);//用给定的“原因”构造一个异常对象

//java.lang.RuntimeException 1.0
RuntimeException(Throwable cause);//1.4
RuntimeException(String message, Throwable cause);//1.4

//java.lang.StackTraceElement 1.4
String getFileName();//返回这个元素运行时对应的源文件名。如果这信息不存在,则返回null。
int getLineNumber();//返回这个元素运行时对应的源文件行数。如果这个信息不存在,则返回-1。
String getClassName();//返回这个元素运行时对应的类的完全限定名,如pack.Test
String getMethodName();//返回运行时对应的方法名。构造器名是<init>;静态初始化器名是<clinit>;这里无法区分同名的重载方法。
boolean isNativeMethod();//如果这个元素运行时在一个本地方法中, 则返回true
//本地方法就是java代码里面写的native方法,它没有方法体。是为了调用C/C++代码而写的
String toString();//如果存在的话,返回一个包含类名、方法名、文件名和行数的格式化字符串

7.3 使用异常机制的技巧

(1)异常处理不能代替简单的测试:
捕获异常所花费的时间比简单的 if 语句更加耗时,因此基本规则:只在异常情况下使用异常机制

(2)不要过分地细化异常
将 try…catch…try…catch… 转变为 try…catch…catch… try可以容纳大量语句,如果希望捕获不同异常,应当使用多条catch

(3)利用异常层次结构
应该抛出更加适当的子类或创建自己的异常类,而不是全部抛出RuntimeException 异常、或者只捕获Thowable 异常

(4)不要压制异常
在Java 中, 往往强烈地倾向关闭异常。理解:A()调用B(),而B()有受查异常(必须声明),那么B()应该写在A()的try语句中

(5)在检测错误时,“ 苛刻” 要比放任更好
例如, 当栈空时, Stack.pop是返回一个null , 还是抛出一个异常?——在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException 异常更好

(6)不要羞于传递异常
传递异常是指用throws将异常抛给上级调用,传递异常要比捕获这些异常更好

异常有“早抛出, 晚捕获”的倾向

7.4 断言

assert基础

使用大量 if() 形式的检查语句会使程序运行缓慢
断言机制允许在测试期间向代码中插入一些检査语句,当代码发布时,这些插人的检测语句将会被自动地移走

assert两种形式

//1. assert 条件;
assert x >= 0;//对x检查,不满足条件,抛出AssertionError异常
//2. assert 条件 : 表达式;
assert x >= 0 : x;//将x的实际值传给AssertionError的构造器,并转换成一个消息字符串
assert x >= 0 : "x >= 0";//将字符串"x >= 0"传给AssertionError的构造器,并转换成一个消息字符串
//表达式部分的唯一目的是产生一个消息字符串。AssertionError 对象并不存储表达式的值,因此,不可能在以后得到它

启用和禁用断言

在默认情况下, 断言被禁用

//-enableassertions或-ea选项启用
java -enableassertions MyApp
//在某个类或整个包中使用断言
java -ea:MyClass -ea:com.mycompany.mylib... MyApp;//开启MyClass类和com.mycompany.mylib包和它的子包中所有类的断言
//选项-disableassertions或-da禁用某个特定类和包的断言
java -ea:...	 -da:MyClass MyApp

有些类不是由类加载器加载, 而是直接由虚拟机加载;启用和禁用所有断言的-ea 和-da 开关不能应用到那些没有类加载器的“系统类”上。对于这些系统类来说, 需要使用-enablesystemassertions/-esa 开关启用断言

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

使用断言完成参数检查

在Java中,有3 种处理系统错误的机制:异常、日志、断言

断言失败是致命的、不可恢复的错误,只用于开发和测阶段
不应该使用断言向程序的其他部分通告发生了可恢复性的错误,断言只应该用于在测试阶段确定程序内部的错误位置

事实上, 由于可以使用断言,当方法被非法调用时, 将会出现难以预料的结果。有时候会拋出一个断言错误,有时候会产生一个null 指
针异常, 这完全取决于类加载器的配置

例如:

//很多程序员使用注释说明假设条件
if (i % 3 == 0)
else if (i % 3 == 1)
else //(i % 3 == 2)
//使用断言会更好一些
else{
	assert i % 3 == 2;
	...
}
//更进一步,应该对i的正负作断言
assert i >= 0;

API

//java.lang.ClassLoader 1.0
void setDefaultAssertionStatus(boolean b);//1.4 对于通过类加载器加载的所有类来说,设置默认启用/禁用
void setCIassAssertionStatus(String className, boolean b);//1.4 对于给定的类和它的内部类,启用或禁用断言
void setPackageAssertionStatus (String packageName, boolean b);//1.4 对于给定包和其子包中的所有类,启用或禁用断言
void clearAssertionStatus();//1.4 移去所有类和包的显式断言状态设置,并禁用所有通过这个类加载器加载的类的断言

7.5 日志

基础

基本日志

程序中常使用System.out.println来打印程序信息,日志可以取代这个功能
基本日志使用全局日志记录器(global logger)

import java.util.logging.Logger;
Logger.getGlobal().info("File->Open menu item selected");

输出:

十一月 15, 2022 9:40:03 下午 pack.Test main
信息: File->Open menu item selected
//即时间信息+类名+方法名+message

取消日志:

import java.util.logging.Level;
Logger.getGlobal().setLevel(Level.OFF);//这条语句之后的日志信息将不会被打印(这条语句之前的不影响)

高级日志

  • getLogger:创建或获取记录器

    private static final Logger myLogger = Logger.getLogger("pack.Test");
    //getLogger参数是自定义的日志名称,一般可以用当前类名来进行命名
    myLogger.warning("test");
    myLogger.info("test");
    myLogger.log(Level.INFO, "test");//指定级别
    //Logger.getGlobal().setLevel(Level.OFF);//将失效,得写成myLogger.setLevel(Level.OFF)
    

    未被任何变量引用的日志记录器可能会被垃圾回收,因此要用静态变量存储日志记录器的一个引用

  • 日志的层次结构

    日志记录器的层次性比包更强,父记录器和子记录器将共享某些属性,例如子记录器会继承父记录器的日志级别
    通常, 有以下7 个日志记录器级别

    SEVERE WARNING INFO CONFIG FINE FINER FINEST

    在默认情况下,只记录前三个级别。也就是说只有默认情况下,可以使用logger.severe, logger.waring, logger.info

  • 修改级别

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

    但是即便这样设置之后,还需要日志处理器的配置(见后面内容)才能生效

处理器

在默认情况下,日志记录器将记录发送到ConsoleHandler中, 并由它输出到System.err流中
子记录器将记录发送给父记录器,最终的处理器中有ConsoleHandler

对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值
在默认配置文件 “jre.lib.logging.properties” 中,它的默认设置为 “java.util.logging.ConsoleHandler.level = INFO”

  • 修改日志级别

    • 要同时改变日志记录器级别和处理器级别(但即便同时改变了默认配置中的.level和java.util.logging.ConsoleHandler.level,既没有看到效果)

    • 安装自己的处理器

      Logger logger = Logger.getLogger("pack");
      logger.setLevel(Level.FINE);
      logger.setUseParentHandlers(false);
      Handler handler = new ConsoleHandler();
      handler.setLevel(Level.FINE);
      logger.addHandler(handler);
      logger.fine("fine");
      

      日志管理器在VM启动过程中初始化,这在main执行之前完成

      如果在main中调System.setProperty(“java.util_logging.config_file”,file), 也会调用LogManager.readConfiguration()来重新初始化曰志管理器

默认情况, 日志记录器将记录发送到自己的处理器和父处理器。原始日志记录器将会把所有等于或高于INFO级別的记录发送到控制台
要想将日志记录发送到其他地方, 就要添加其他的处理器

  • FileHandler:收集文件中的记录

    Logger logger = Logger.getLogger("pack");
    FileHandler handler = new FileHandler();
    logger.addHandler(handler);
    logger.info("info");
    

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

  • SocketHandler:将记录发送到特定的主机和端口

  • 文件处理器配置参数
    在这里插入图片描述

    比较常用的是level和append;如果多个应用程序使用同一个口志文件, 就应该开启append标志
    另外, 应该在文件名模式中使用%u, 以便每个应用程序创建日志的唯一副本
    在这里插入图片描述

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

常用的日志方法

  • logp:如果虚拟机对执行过程进行了优化,就得不到准确的调用信息,logp 方法获得调用类和方法的确切位置

    void logp(Level l, String className, String methodName, String message);
    
  • entering和existing:跟踪执行流

    void entering(String className, String methodName);
    void entering(String className, String methodName, Object param);
    void entering(String className, String methodName, Object[] params);
    void exiting(String className, String methodName);
    void exiting(String className, String methodName, Object result);
    

    生成FINER级别和以字符串ENTRY和RETURN开始的日志记录,例如
    注意:这是FINER级别,如果想要显示,需要先修改日志器和处理器的等级

    int read (String file, String pattern){
        //生成进入时的信息
    	logger.entering("com.mycompany.mylib.Reader", "read", new Object[]{ file, pattern });
        //输出退出时的信息
        logger.exiting("com.mycompany.mylib.Reader", "read", count);
        return count ;
    }
    
  • 日志记录异常信息:throwing和log

    void throwing(String className, String methodName, Throwable t);
    void log(Level l, String message, Throwable t);
    

    用法:

    //用法1
    if (...){
        IOException exception = new IOException("...");
        logger.throwing("com.mycompany.mylib.Reader", "read", exception);
        throw exception;
    }
    //用法2
    try{
        ...
    }catch(IOException e){
    	Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
    }
    

    调用throwing 可以记录一条FINER级别的记录和一条以THROW 开始的信息

API

  • Logger
//java.util.logging.Logger 1.4
Logger getLogger(String loggerName);//获得给定名字的日志记录器。如果这个日志记录器不存在,创建一个日志记录器
Logger getLogger(String loggerName, String bundleName);//bundleName 用来查看本地消息的资源包名
void severe(String message);
void warning(String message);
void info(String message);
void config(String message);
void fine(String message);
void finer(String message);
void finest(String message);
//记录一个由方法名和给定消息指示级别的日志记录。
void entering(String className, String methodName);
void entering(String className, String methodName, Object param);
void entering(String className, String methodName, Object[] param);
void exiting(String className, String methodName);
void exiting(String className, String methodName, Object result);
//记录一个描述进入/退出方法的日志记录, 其中应该包括给定参数和返回值。
void throwing(String className, String methodName, Throwable t);//记录一个描述拋出给定异常对象的日志记录
void log(Level level, String message)
void log(Level level, String message, Object obj)
void log(Level level, String message, Object[] objs)
void log(Level level, String message, Throwable t)
//记录一个给定级别和消息的日志记录, 其中可以包括对象或者可抛出对象。要想包括对象, 消息中必须包含格式化占位符{0}、{1} 等
void logp(Level level, String className, String methodName, String message);
void logp(Level level, String className, String methodName, String message, Object obj);
void logp(Level level, String className, String methodName, String message, Object[] objs);
void logp(Level level, String className, String methodName, String message, Throwable t);
//记录一个给定级别、准确的调用者信息和消息的日志记录,其中可以包括对象或可抛出对象
void logrb(Level level, String className, String methodName, String bundleName, String message);
void logrb(Level level, String className, String methodName, String bundleName, String message, Object obj);
void logrb(Level level, String className, String methodName, 
           String bundleName, String message, Object[] objs);
void logrb(Level level, String className, String methodName, 
           String bundleName, String message, Throwable t);
//记录一个给定级别、准确的调用者信息、资源包名和消息的日志记录,其中可以包括对象或可拋出对象
Level getLevel();
Level setLevel(Level l);//获得和设置这个日志记录器的级别
Logger getParent();
Logger setParent(Logger l);//获得和设置这个日志记录器的父tl 志记录器
Handler[] getHandlers();//获得这个日志记录器的所有处理器
void addHandler(Handler h);
void removeHandler(Handler h);//增加或删除这个日志记录器中的一个处理器
boolean getUseParentHandlers();
void setUseParentHandlers(boolean b);//获得和设置“use parent handler”属性。如果这个属性是true,则日志记录器会将全部的日志记录转发给它的父处理器
Filter getFilter();
void setFilter(Filter f);//获得和设置这个日志记录器的过滤器
  • Handler
//java.util.logging.Handler 1.4
abstract void publish(LogRecord record);//将日志记录发送到希望的目的地
abstract void flush();//刷新所有已缓冲的数据
abstract void close();//刷新所有已缓冲的数据,并释放所有相关的资源
Filter getFilter();
void setFilter(Filter f );//获得和设置这个处理器的过滤器
Formatter getFormatter();
void setFormatter(Formatter f);//获得和设置这个处理器的格式化器。
Level getLevel();
void setLevel(Level l);//获得和设置这个处理器的级别
  • ConsoleHandler
//java.util.logging.ConsoleHandler 1.4
ConsoleHandler();//构造一个新的控制台处理器
  • FileHandler
//java.util.logging.FileHandler 1.4
FileHandler(String pattern);
FileHandler(String pattern, boolean append);
FileHandler(String pattern, int limit, int count);
FileHandler(String pattern, int limit, int count, boolean append);
/**
* 构造一个文件处理器
* 参数:pattern构造日志文件名的模式。参见表7-2列出的模式变量
* limit 在打开一个新日志文件之前,日志文件可以包含的近似最大字节数
* count 循环序列的文件数量
* append 新构造的文件处理器对象应该追加在一个已存在的日志文件尾部,则为true
*/
  • LogRecord
//java.util.logging.LogRecord 1.4
Level getLevel();//获得这个日志记录的记录级别
String getLoggerName();//获得正在记录这个日志记录的日志记录器的名字
ResourceBundle getresourceBundle();
String getresourceBundleName();//获得用于本地化消息的资源包或资源包的名字。如果没有获得,则返回null。
String getMessage();//获得本地化和格式化之前的原始消息
Object[] getParameters();//获得参数对象。如果没有获得,则返回null
Throwable getThrown();//获得被拋出的对象。如果不存在,则返回null
String getSourceClassName();
String getSourceMethodName();
//获得记录这个日志记录的代码区域。这个信息有可能是由日志记录代码提供的,也有可能是自动从运行时堆栈推测出来的。如果日志记录代码提供的值有误,或者运行时代码由于被优化而无法推测出确切的位置,这两个方法的返回值就有可能不准确
long getMillis();//获得创建时间。以毫秒为单位
long getSequenceNumber();//获得这个日志记录的唯一序列序号
int getThreadID();//获得创建这个日志记录的线程的唯一ID。这些ID是由LogRecord类分配的,并且与其他线程的ID无关
  • Filter
//java.util.logging.Filter 1.4
/boolean isLoggable(LogRecord record);//如果给定日志记录需要记录, 则返回true
  • Formatter
//java.util.logging.Formatter 1.4
abstract String format(LogRecord record);//返回对日志记录格式化后得到的字符串
String getHead(Handler h)
String getTail(Handler h);
//返回应该出现在包含日志记录的文档的开头和结尾的字符串。超类Formatter定义了这些方法,它们只返回空字符串。如果必要的话,可以对它们进行覆盖
String formatMessage(LogRecord record);//返回经过本地化和格式化后的日志记录的消息内容

本地化

(1)首先需要创建Resouce Bundle:参考博客:https://blog.csdn.net/weixin_34186950/article/details/88751212
在这里插入图片描述
在src目录下右键 - New - Resource Bundle - 设置base name - 点击“+”号 - 输出创建名称,如“en,zh”,逗号隔开
在zn和en文件中分别添加下列语句:
在这里插入图片描述

(2)Locale本地化

参考博客:https://blog.csdn.net/yu_duan_hun/article/details/123690100

如果中文乱码,可以设置File-setting-File Encoding中修改编码方式
将Global Encoding、Project Encoding、Default encoding for properties file三项设置为UTF-8,旁边的Transpareb…可以勾选

Main solution = new Main();
ResourceBundle rb = ResourceBundle.getBundle("demo",new Locale("zh","CN"));//getBundle(basename, new Locale(language, country)
String apple = rb.getString("apple");
Logger logger = Logger.getGlobal();//获取全局日志管理器
logger.log(Level.INFO, apple, new Object[]{"占位0"});

rb = ResourceBundle.getBundle("demo",Locale.US);
apple = rb.getString("apple");
logger.log(Level.INFO, apple, new Object[]{"占位0"});

输出:
在这里插入图片描述

(3)占位
占位是指在内容中添加{0}、{1}等标志,输出时,通过logger.log中的 new Object[]{“…”, “…”}替换,{0}由第1个字符串替换,那么{9}就要用第10个字符串替换,不能跳跃


其他

  • 过滤器

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

    boolean isLoggable(LogRecord record)
    

    例如,某个过滤器可能只对entering方法和exiting方法产生的消息感兴趣,可以调用record.getMessage()方法,并査看这个消息是否用ENTRY 或RETURN 开头

    setFilter方法可以将一个过滤器安装到一个日志记录器或处理器中
    同一时刻最多只能有一个过滤器

  • 格式化器

    ConsoleHandler 类和FileHandler 类可以生成文本和XML 格式的日志记录
    自定义格式需要扩展Formatter 类并覆盖format方法

    String format(LogRecord record)
    

    有时会调用formatMessage()方法,进行格式化、参数替换和本地化

    String formatMessage(LogRecord record)
    

    很多文件格式(如XML) 需要在已格式化的记录的前后加上一个头部和尾部,需要覆盖以下两个方法

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

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

示例

这个示例涉及到知识点主要是 增加处理器 和 文件选择弹窗,和日志本身关联一般

package pack;

import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.OutputStream;
import java.util.logging.*;

public class LoggingImageViewer {
    public static void main(String[] args){
        //1. 设置为将日志信息输出到指定文件中去
        if(System.getProperty("java.util.logging.config.class")==null && System.getProperty("java.util.logging.config")==null) {
            Logger.getLogger("pack").setLevel(Level.ALL);
            final int LOG_ROTATION_COUNT = 10;
            try {
                /**
                 * FileHandler(String pattern, int limit, int count, boolean append)
                 * pattern 构造日志文件名的模式。参见处理器小节中列出的模式变量;%h是系统属性user.home的值
                 * limit 日志文件可以包含的近似最大字节数。0表示无限制
                 * count 循环序列的文件数量。这里是使用文件轮换集,即到达每个文件的给定大小限制后,就新建文件,并在文件名添加"0"、"1"、"2"等
                 * append 新构造的文件处理器对象应该追加在一个已存在的日志文件尾部,则为true;这里未使用该参数
                 */
                Handler handler = new FileHandler("%h/LoggingImageViewer.log", 0, LOG_ROTATION_COUNT);
                Logger.getLogger("pack").addHandler(handler);//添加了处理器后,之后日志都将输出到该文件中去,%h默认路径是C:/user/用户名
            }catch (Exception e){
                Logger.getLogger("pack").log(Level.SEVERE, "Can't create log file handler", e);
            }
        }
        //2. EventQueue事件分发
        EventQueue.invokeLater(()->{
            //如果有需要,就在窗体的文本域中显示日志信息
            Handler windowHandler = new WindowHandler();//增加一个窗体,这个窗体上目前仅有一个文本域
            windowHandler.setLevel(Level.ALL);
            Logger.getLogger("pack").addHandler(windowHandler);
            Logger.getLogger("pack").info("info");//注意,"pack"日志器有两个处理器,也就是说上述语句将分别输出到前面的指定文件中和这里的窗体中
            //接下来显示图像
            JFrame frame = new ImageViewFrame();
            frame.setTitle("LoggingImageViewer");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//使用System.exit方法退出exit程序
            Logger.getLogger("pack").fine("Showing frame");
            frame.setVisible(true);
        });
    }
}

/**
 * WindowHandler作用:创建一个窗体(这里专门用于显示日志信息),窗体上有一个文本域,文本域允许滚动条
 */
//StreamHandler是基于流的日志Handler,继承了Handler; 此类主要作为基类,或支持实现其他日志Handlers所用的类
class WindowHandler extends StreamHandler {
    private JFrame frame;//窗体对象
    public WindowHandler(){
        frame = new JFrame();//初始化frame
        final JTextArea output = new JTextArea();//创建默认的文本域
        output.setEditable(false);
        frame.setSize(200, 200);
        frame.add(new JScrollPane(output));//允许滚动条
        frame.setFocusableWindowState(false);//设置焦点窗口false
        frame.setVisible(true);
        setOutputStream(new OutputStream(){
            public void write(int b){}
            public void write(byte[] b, int off, int len){
                output.append(new String(b, off, len));
            }
        });
    }
    public void publish(LogRecord record){
        if(!frame.isVisible()) return;
        super.publish(record);
        flush();
    }
}

/**
 * 一个新的窗体,有菜单条,用来筛选GIF文件
 */
class ImageViewFrame extends JFrame{
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 400;
    private JLabel label;//标签组件
    private static Logger logger = Logger.getLogger("pack");
    public ImageViewFrame(){
        logger.entering("ImageViewerFrame", "<init>");
        setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
        JMenuBar menuBar = new JMenuBar();//给窗口添加菜单栏
        setJMenuBar(menuBar);
        JMenu menu = new JMenu("File");//添加菜单"File"
        menuBar.add(menu);
        //打开文件选项
        JMenuItem openItem = new JMenuItem("Open");//再给菜单"File"添加item
        menu.add(openItem);
        openItem.addActionListener(new FileOpenListenter());//为打开选项添加事件
        //关闭文件选项
        JMenuItem exitItem = new JMenuItem("Exit");
        menu.add(exitItem);
        exitItem.addActionListener((event)->{
            logger.fine("Exiting.");
            System.exit(0);
        });
        label = new JLabel();
        add(label);
        logger.exiting("ImageViewerFrane", "<init>");
    }
    //内部类:打开文件事件类
    private class FileOpenListenter implements ActionListener{
        public void actionPerformed(ActionEvent event){
            logger.entering("ImageViewerFrame.FileOpenListener", "actionPerformed", event);
            //需要一个文件选择器,选择出所有以.gif结尾的文件
            JFileChooser chooser = new JFileChooser();
            chooser.setCurrentDirectory(new File("."));//设置当前路径
            //筛选GIF图像
            chooser.setFileFilter(new FileFilter(){
                public boolean accept(File f){
                    return f.getName().toLowerCase().endsWith(".gif") || f.isDirectory();
                }
                public String getDescription(){
                    return "GIF Images";
                }
            });
            //显示文件选择弹框
            int r = chooser.showOpenDialog(ImageViewFrame.this);
            if(r == JFileChooser.APPROVE_OPTION){
                String name = chooser.getSelectedFile().getPath();
                logger.log(Level.FINE, "Reading file {0}", name);//使用了占位符
                label.setIcon(new ImageIcon(name));
            }else logger.fine("File open dialog canceled.");
            logger.exiting("ImageViewerFrame.FileOpenListener", "actionPerformed");
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值