使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。
day051 记录日志(基本日志、高级日志、修改日志管理器配置、本地化、处理器、过滤器、格式化器、日志记录说明)
每个Java程序员都很熟悉在有问题的代码中插入一些System.out.println方法调用来帮助观察程序运行的操作过程。当然,一旦发现问题的根源,就要将这些语句从代码中删去。如果接下来又出现了问题,就需要再插入几个调用System.out.println方法的语句。记录日志API就是为了解决这个问题而设计的。下面先讨论这些API的优点。
•可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
•可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小。
•日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。
•日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
•日志记录可以采用不同的方式格式化,例如,纯文本或XML。
•应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp。
•在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。
1.基本日志
要生成简单的日志记录,可以使用全局日志记录器(globallogger)并调用其info方法:
Logger.getClobal().info("File->Openmenuitemselected");
在默认情况下,这条记录将会显示以下内容:
May10,201310:12:15 PM LogginglmageViewer fileOpen
INFO:File->Open menu item selected
但是,如果在适当的地方(如main开始)调用
Logger.getClobal().setLevel(Level.OFF);
将会取消所有的日志。
2.高级日志
从前面已经看到“虚拟日志”,下面继续看一下企业级(industrial-strength)日志。在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器。
可以调用getLogger方法创建或获取记录器:
private static final Logger myLogger=Logger.getLogger("com.mycompany.myapp"):
与包名类似,日志记录器名也具有层次结构。事实上,与包名相比,日志记录器的层次性更强。对于包来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器的父与子之间将共享某些属性。例如,如果对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别。
通常,有以下7个日志记录器级别:
•SEVERE
•WARNING
•INFO
•CONFIG
•FINE
•FINER
•FINEST
在默认情况下,只记录前三个级别。也可以设置其他的级別。例如,
logger.setLevel(Level.FINE);
现在,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)
下面有一些用来跟踪执行流的方法:
void entering(String dassName, 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)
例如:
int read(String file, String pattern)
{
1ogger.entering("com.mycompany.mylib.Reader", "read",
new Object口{ file, pattern });
...
1ogger.exiting("com.mycompany.mylib.Reader", "read", count):
return count;
}
这些调用将生成FINER级别和以字符串ENTRY和RETURN开始的日志记录。记录日志的常见用途是记录那些不可预料的异常。可以使用下面两个方法提供日志记录中包含的异常描述内容。
void throwing(String className, String methodName, Throwable t)
void log(Level 1, String message, Throwable t)
典型的用法是:
if (...)
{
IOException exception = new IOException(". . .");
1ogger.throwing("com•mycompany.mylib.Reader", "read", exception);
throw exception;
}
还有:
try
{
...
}
catch (IOException e)
{
Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
}
3.修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:
jre/lib/1ogging.properties
要想使用另一个配置文件,就要将java.utiUogging.config.file特性设置为配置文件的存储位置,并用下列命令启动应用程序:
java -Djava.util.logging.config.file-configFileMainClass
要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行
.level=INFO
可以通过添加以下内容来指定自己的日志记录级别
com.mycompany_myapp.level=FINE
也就是说,在日志记录器名后面添加后缀.level。
在稍后可以看到,日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到FINE级别的消息,就需要进行下列设置
java.util.logging.ConsoleHandler.level=FINE
4.本地化
我们可能希望将日志消息本地化,以便让全球的用户都可以阅读它。
本地化的应用程序包含资源包(resourcebundle)中的本地特定信息。资源包由各个地区(如美国或德国)的映射集合组成。例如,某个资源包可能将字符串“readingFile”映射成英文的“Readingfile”或者德文的“Achtung!Dateiwirdeingelesen”。一个程序可以包含多个资源包,一个用于菜单;其他用于日志消息。每个资源包都有一个名字(如com.mycompany.logmessages)。要想将映射添加到一个资源包中,需要为每个地区创建一个文件。英文消息映射位于com/mycompany/logmessages_en.properties文件中;德文消息映射位于com/mycompany/logmessages_de.properties文件中。(en和de是语言编码)。可以将这些文件与应用程序的类文件放在一起,以便ResourceBimdle类自动地对它们进行定位。这些文件都是纯文本文件,其组成如下所示:
readingFile=Achtung! Datei wird eingelesen
renamingFile=Datei wird umbenannt
....
在请求日志记录器时,可以指定一资源包:
Logger logger = Logger.getLogger(1oggerName, "com.mycompany.1ogmessages");
然后,为日志消息指定资源包的关键字,而不是实际的日志消息字符串。
logger.info("readi ngFi1e");
通常需要在本地化的消息中增加一些参数,因此,消息应该包括占位符{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 });
5.处理器
在默认情况下t日志记录器将记录发送到ConsoleHandler中,并由它输出到System.err流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“”)有一个ConsoleHandler。
与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为
java.uti1.1ogging.ConsoleHandler.level=INF0
要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。另外,还可以绕过配置文件,安装自己的处理器。
Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel (Level.FINE);
logger.setUseParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler):
在默认情况下,日志记录器将记录发送到自己的处理器和父处理器。我们的日志记录器是原始日志记录器(命名为“”)的子类,而原始日志记录器将会把所有等于或高于INFO级別的记录发送到控制台。然而,我们并不想两次看到这些记录。鉴于这个原因,应该将useParentHandlersM性设置为false。
要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个很有用的处理器,一个是FileHandler;另"h是SocketHandler。SocketHandler将记录发送到特定的主机和端口。而更令人感兴趣的是FileHandler,它可以收集文件中的记录。可以像下面这样直接将记录发送到默认文件的处理器:
FileHandler handler = newFileHandler();
1ogger.addHandler(handler);
这些记录被发送到用户主目录的javan.log文件中,n是文件名的唯一编号。如果用户系统没有主目录(例如,在Windows95/98/Me),文件就存储在C:\Window这样的默认位置上。在默认情况下,记录被格式化为XML。下面是一个典型的日志记录的形式:
〈record 〉
<date>2002-02-04T07:45:15</date>
<millis>1012837515710</nillis>
<sequence>l</sequence>
<1ogger>com.mycompany.myapp</logger〉
〈 level >INF0</1evel >
<class>com.mycompany.mylib.Reader</class>
<method>read</method>
<thread>10</thread>
<message>Reading file corejava.gif</message>
</record>
可以通过设置日志管理器配置文件中的不同参数
也冇可能不想使用默认的日志记录文件名,因此,应该使用另一种模式,例如,%h/myapp.log
如果多个应用程序(或者同一个应用程序的多个副本)使用同一个口志文件,就应该开启append标志。另外,应该在文件名模式中使用%u,以便每个应用程序创建日志的唯一副本。
开启文件循环功能也是一个不错的主意。日志文件以myapp.log.0,myapp.log.1,myapp.log.2,这种循环序列的形式出现3只要文件超出了大小限制,最旧的文件就会被删除,其他的文件将重新命名,同时创建一个新文件,其编号为0。
还可以通过扩展Handler类或StreamHandler类自定义处理器。在本节结尾的示例程序中就定义了这样一个处理器3这个处理器将在窗口中显示日志记录(如图所示)。
这个处理器扩展于StreamHandler类,并安装了一个流。这个流的write方法将流显示输出到文本框中。
class WindowHandler extends StreamHandler
{
public WindowHandler()
{
...
final JTextArea output = new]TextArea();
setOutputStream(new
OutputStreamO
{
public void write(int b) {}//not called
public void write(byte[] b, int off, int len)
{
output.append(new String(b, off, len));
}
});
}
...
}
使用这种方式只存在一个问题,这就是处理器会缓存记录,并且只有在缓存满的吋候才将它们写人流中,因此,需要覆盖publish方法,以便在处理器获得每个记录之后刷新缓冲区。
class WindowHandler extends StreamHandler
{
...
public void publish(LogRecord record)
{
super.publish(record);
flush();
}
}
如果希望编写更加复杂的流处理器,就应该扩展Handler类,并自定义publish、flush和close方法。
6.过滤器
在默认情况下,过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。另外,可以通过实现niter接口并定义下列方法来自定义过滤器。
boolean isLoggab1e(LogRecord record)
在这个方法中,可以利用自己喜欢的标准,对日志记录进行分析,返回true表示这些记录应该包含在日志中。例如,某个过滤器可能只对entering方法和exiting方法产生的消息感兴趣,这个过滤器可以调用record.getMessage()方法,并査看这个消息是否用ENTRY或RETURN开头。
要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了。注意,同一时刻最多只能有一个过滤器。
7.格式化器
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但是,也可以自定义格式。这需要扩展Formatter类并覆盖下面这个方法:
String format(LogRecord record)
可以根据自己的愿望对记录中的信息进行格式化,并返冋结果字符串。在format方法中,有可能会调用下面这个方法
String formatMessage(LogRecord record)
这个方法对记录中的部分消息进行格式化、参数替换和本地化应用操作。
很多文件格式(如XML)需要在已格式化的记录的前后加上一个头部和尾部在这个例子中,要覆盖下面两个方法:
String getHead(Handler h)
String getTail(Handler h)
最后,调用setFormatter方法将格式化器安装到处理器中。
8.日志记录说明
面对日志记录如此之多的可选项,很容易让人忘记最基本的东西。下面的“日志说明书”总结了一些最常用的操作。
1)为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,例如,com.mycompany.myprog,这是一种好的编程习惯。另外,可以通过调用下列方法得到日志记录器。
Logger logger= Logger.getLogger("com.mycompany.myprog");
为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:
private static final Logger logger = Logger.getLogger("com.mycompany.nyprog"):
2)默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件。但是正如前面所述,改变配置需要做相当多的工作。因此,最好在应用程序中安装一个更加适宜的默认配置。
下列代码确保将所有的消息记录到应用程序特定的文件中。可以将这段代码放置在应用程序的main方法中。
if (System,getProperty("java,util.logging.config.dass")== null
&& System.getPropertyC'java.util.logging.config.file")== null)
{
try
{
Logger.getLogger("").setLevel(Level.ALL);
final int L0C_R0TATI0N_C0UNT = 10;
Handler handler = new FileHandler('Wmyapp.log", 0, L0G_R0TATI0N_C0UNT):
Logger.getLogger("").addHandler(handler):
}
catch (IOException e)
{
logger.log(Level.SEVERE, "Can't create log file handler", e);
}
}
3)现在,可以记录自己想要的内容了。但需要牢记:所有级别为INFO、WARNING和SEVERE的消息都将显示到控制台上o因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE是一个很好的选择。当调用System.out.println时,实际上生成了下面的日志消息:
logger.fine("File open dialog canceled");
记录那些不可预料的异常也是一个不错的想法,例如:
try
{
...
}
catch (SonreException e)
{
logger.log(Level.FINE, "explanation", e);
}
下面的程序上述说明可实现:日志记录消息也显示在日志窗口中。
/**
*@author zzehao
*/
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.logging.*;
import javax.swing.*;
public class LogginglmageViewer
{
public static void main(String[] args)
{
if (System.getProperty("java.util.logging.config.class")== null
&& System.getProperty("java.uti1.1ogging.config.fi1e") == null)
{
try
{
Logger.getLogger("com.horstmann.corejava").setLevel(Level .ALL);
final int LOG_ROTATION_COUNT = 10;
Handler handler = new FileHandler("%h/LogginglmageViewer.1og", 0, LOG_ROTATION_COUNT);
Logger.getLogger("com.horstmann.corejava").addHandler(handler);
}
catch (IOException e)
{
Logger.getLogger("coin.horstmann.corejava").log(Level.SEVERE, "Can't create log file handler", e);
}
}
EventQueue.invokeLater(() ->
{
Handler windowHandler = new WindowHandler();
windowHandler.setLevel(Level.ALL);
Logger.getLogger("com.horstmann.corejava").addHandler(windowHandler);
JFrame frame = new ImageViewerFrame();
frame.setTitle("LogginglmageViewer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Logger.getLogger("com.horstmann.corejava").fine("Showing frame");
frame.setVisible(true);
});
}
}
class ImageViewerFrame 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("com.horstmann.corejava");
public ImageViewerFrame()
{
logger.entering("ImageViewerFrame","<init>");
setSize(DEFAULT_WIDTH,DEFAULT_HEIGHT);
//set up menu bar
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu menu = new JMenu("File");
menuBar.add(menu);
JMenuItem openItem = new JMenuItem("Open");
menu.add(openItem);
openItem.addActionListener(new FileOpenListener());
JMenuItem exitItem = new JMenuItem("Exit");
menu.add(exitItem);
exitItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
logger.fine("Exiting.");
System.exit(0);
}
});
//use a label to display the images
label = new JLabel ();
add(label );
logger.exiting("ImageViewerFrame", "<init>");
}
private class FileOpenListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
logger.entering("ImageViewerFrame.FileOpenListener", "actionPerformed", event);
//set up file chooser
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File ("."));
// accept all files ending with .gif
chooser.setFileFilter(new javax.swing.filechooser.FileFilter()
{
public boolean accept(File f)
{
return f.getName().toLowerCase().endsWith(".gif")||f.isDirectory();
}
public String getDescription()
{
return "GIFImages";
}
});
//show file chooser dialog
int r=chooser.showOpenDialog(ImageViewerFrame.this);
//if image file accepted,set it as icon of the label
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");
}
}
}
class WindowHandler extends StreamHandler
{
private JFrame frame;
public WindowHandler()
{
frame=new JFrame();
final JTextArea output=new JTextArea();
output.setEditable(false);
frame.setSize(200,200);
frame.add(new JScrollPane(output));
frame.setFocusableWindowState(false);
frame.setVisible(true);
setOutputStream(new OutputStream()
{
public void write(int b)
{
//not called
}
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();
}
}
运行的结果如下: