前面的文章我们提到过,Handler是真正执行日志输出操作的地方,JUL中的Handler由java.util.logging.Handler抽象类来表示。有两个实现类直接继承自Handler,分别是StreamHandler和MemoryHandler,而StreamHandler又有三个直接子类分别是ConsoleHandler,FileHandler以及SocketHandler。
Handler中有一个最核心的抽象方法就是publish(),该方法的声明如下所示:
public abstract void publish(LogRecord record);
Handler的作用就是用来将日志输出到外部的,不同的Handler能够将日志输出到不同的地方。StreamHandler能够将日志通过一个OutputStream进行输出,StreamHandler的三个不同的子类即使用了不同的OutputStream对象。
ConsoleHandler会将日志输出到控制台,对应的OutputStream对象是System.err
FileHandler会将日志输出到文件,对应的OutputStream对象是FileOutputStream
SocketHandler会将日志输出到网络套接字,对应的OutputStream对象是Socket.getOutputStream()。
MemoryHandler的实现跟StreamHandler的实现不同,它不是将日志写入输出流,而是将日志输出到一个内存缓冲区中,本文后面会详细介绍MemoryHandler。
我们先来介绍一下Handler中公共的一些属性。Handler抽象类中含有如下get/set方法:
Level getLevel()
Filter getFilter()
Formatter getFormatter()
String getEncoding()
ErrorManager getErrorManager()
getLevel()是用来获取Handler的级别的,之前提到过,不仅Logger对象有级别,Handler中也有级别,如果需要进行输出的日志信息的级别(即LogRecord中的级别)低于Hander中的级别时,也不会有实际的输出操作。
getFilter()是用来获取Handler的过滤器的,跟Level类似,不仅Logger对象可以设置过滤器,Handler中也能设置过滤器。
getFormatter()用来获取格式化输出器,Handler是用来对日志进行实际输出的组件,但是用什么样的格式进行输出需要借助于Formatter格式化器,不同的Formatter输出的信息格式是不一样的,JUL中提供了两种内置的Formatter,一种是SimpleFormater,另一种是XMLFormatter,我们也可以实现自己的格式化器,只需要继承自java.util.logging.Formatter抽象类,并重写它的String format(LogRecord logRecord)方法。关于Formatter的信息我们后文再进行介绍。
getEncoding()获取字符编码信息,由于Handler是用来对日志信息进行实际输出操作的,因此在输出的过程中需要指定字符编码方式。
getErrorManager()返回错误处理器,errorManager用来处理日志记录过程中发生的异常信息。
ConsoleHander会将日志信息输出到控制台,在我们通过Logger.getLogger(String name)方法拿到日志记录器实例之后,我们可以对该日志记录器进行显示地设置,比如设置logger的级别为INFO,设置logger的Handler为ConsoleHander,设置ConsoleHandler的级别为INFO,设置ConsoleHandler的Formatter为SimpleFormatter。代码如下所示:
public class JavaLogging {
private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
static {
logger.setLevel(Level.INFO);
Handler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.INFO);
consoleHandler.setFormatter(new SimpleFormatter());
logger.addHandler(consoleHandler);
}
public static void main(String[] args) {
logger.info("Hello, Java Logging");
}
}
这样一来就会将INFO及其以上级别的日志信息以SimpleFormatter的格式输出到控制台。最终控制台输出如下:
Aug 12, 2018 4:20:44 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
Aug 12, 2018 4:20:44 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
我们发现一条日志被输出了两遍。至于为什么会输出两遍我们下一篇文章中会进行详细的分析,现在我们只关注一点:日志确实按照我们设置的以SimpleFormatter的格式将INFO级别的日志信息输出到了控制台。
FileHandler会将日志输出到文件中,我们可以修改上面的代码,给日志记录器Logger配置一个FileHandler,从而将日志信息输出到文件中,我们也顺便把Formatter设置为XMLFormatter,看看XMLFormatter输出来的格式是什么样的。代码如下:
public class JavaLogging {
private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
static {
logger.setLevel(Level.INFO);
Handler fileHandler = null;
try {
fileHandler = new FileHandler("mylog.txt");
} catch (IOException e) {
e.printStackTrace();
}
fileHandler.setLevel(Level.INFO);
fileHandler.setFormatter(new XMLFormatter());
logger.addHandler(fileHandler);
}
public static void main(String[] args) {
logger.info("Hello, Java Logging");
}
}
运行程序之后会在当前目录下生成一个mylog.txt文件,文件内容如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2018-08-12T16:31:06</date>
<millis>1534062666182</millis>
<sequence>0</sequence>
<logger>cn.codecrazy.study.logging.JavaLogging</logger>
<level>INFO</level>
<class>cn.codecrazy.study.logging.JavaLogging</class>
<method>main</method>
<thread>1</thread>
<message>Hello, Java Logging</message>
</record>
</log>
可以看到,由于我们给Logger指定的Handler是FileHandler,因此日志信息输出到了我们指定的文件中,由于我们设置了Formatter为XMLFormatter,因此最终的日志信息是以XML的格式展示的。
FileHandler给我们提供了多种不同的配置方式,如根据pattern配置文件名格式,配置文件数目,配置文件大小,配置是否将信息追加到已有的文件中等等。具体有哪些配置方式我们后面介绍JUL的配置文件时再详细介绍。
SocketHandler会将日志信息发送到网络服务器,首先我们写一个简单的网络服务器,监听在本地,端口号为8888,代码如下所示:
public class LoggingServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
while(true) {
Socket socket = serverSocket.accept();
Runnable task = () -> handleSocket(socket);
Executors.newFixedThreadPool(3).submit(task);
}
}
private static void handleSocket(Socket socket) {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
之后再设置我们的日志记录器的Hander为SocketHandler,并指定主机和端口号,代码如下所示:
public class JavaLogging {
private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
static {
logger.setLevel(Level.INFO);
Handler socketHandler = null;
try {
socketHandler = new SocketHandler("localhost", 8888);
} catch (IOException e) {
e.printStackTrace();
}
socketHandler.setLevel(Level.INFO);
socketHandler.setFormatter(new XMLFormatter());
logger.addHandler(socketHandler);
}
public static void main(String[] args) {
logger.info("Hello, Java Logging");
}
}
之后先运行LoggingServer,再运行JavaLogging,我们在LoggingServer的控制台能看到如下输出:
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2018-08-12T17:13:30</date>
<millis>1534065210654</millis>
<sequence>0</sequence>
<logger>cn.codecrazy.study.logging.JavaLogging</logger>
<level>INFO</level>
<class>cn.codecrazy.study.logging.JavaLogging</class>
<method>main</method>
<thread>1</thread>
<message>Hello, Java Logging</message>
</record>
</log>
说明我们的SocketHandler将我们的日志信息以XML格式发送到了本地主机,监听端口为8888的网络服务器,我们的网络服务器接收到信息之后简单地将其在控制台打印了出来。
MemoryHandler直接继承自Handler,该Handler在内部维护了一个LogRecord数组,即一个内存缓冲区,通过MemoryHandler的publish()方法输出的日志信息首先进入内存缓冲区中,缓冲区如果满了的话,新进入的日志信息从缓冲区的头部开始覆盖,形成一个循环。只有到了一定条件的时候才通过“target Handler”向外部进行输出。MemoryHandler的部分属性如下所示:
private final static int DEFAULT_SIZE = 1000;
private volatile Level pushLevel;
private int size;
private Handler target;
private LogRecord buffer[];
int start, count;
MemoryHandler除了有其他Handler都有的level属性之外还多了一个pushLevel属性,该属性与是否将日志信息交给target Handler进行输出有关。
buffer[]是用来缓存通过MemoryHandler的publish方法写入的LogRecord对象的,其他Handler的publish方法是直接将LogRecord输出到系统外部,而MemoryHandler是先将LogRecord缓存在内部。
size属性用来设置buffer数组的大小的,默认大小是DEFAULT_SIZE(即1000),也就是说默认情况下,如果缓存了1000个LogRecord之后还没有将日志信息发送到外部,那么后面进来的LogRecord将从缓冲区的头部开始覆盖,start和count就是用来控制循环操作buffer[]的。
target属性就是当达到一定条件时需要最终将缓冲区的日志信息输出到外部的Handler,我们在创建MemoryHandler的时候一般来说需要设置该属性,否则那些日志信息只是驻留在内存中,而不会进入外部系统,比如文件,控制台或者网络套接字等等。
我们可以看一下MemoryHandler中的publish方法:
public synchronized void publish(LogRecord record) {
if (!isLoggable(record)) {
return;
}
int ix = (start+count)%buffer.length;
buffer[ix] = record;
if (count < buffer.length) {
count++;
} else {
start++;
start %= buffer.length;
}
if (record.getLevel().intValue() >= pushLevel.intValue()) {
push();
}
}
可以看到,方法中的前面部分是用来将LogRecord存入buffer缓冲区中的,重点关注最后的一个if语句,如果日志信息的级别高于或者等于MemoryHandler的pushLevel,那么就要执行push方法。push方法就是将buffer中的LogRecord全部发送到target Handler,push方法代码如下:
public synchronized void push() {
for (int i = 0; i < count; i++) {
int ix = (start+i)%buffer.length;
LogRecord record = buffer[ix];
target.publish(record);
}
// Empty the buffer.
start = 0;
count = 0;
}
循环调用target Handler的publish方法将缓冲区的LogRecord全部发送出去,并清空缓冲区。
默认情况下,MemoryHandler会将LogRecord缓存起来,直到遇到某个LogRecord的级别高于或者等于pushLevel,这时会将缓冲区中的所有LogRecord全部发送到目标Handler进行输出,并清空缓冲区。默认情况下pushLevel的值为Level.SEVERE。
我们可以通过配置pushLevel来改变触发push操作的时机,比如配置成LogRecord级别高于WARNING时就push。我们也可以实现自己的MemoryHandler,这样就可以更灵活地按照我们的业务需求设置触发push的操作。使用MemoryHandler的代码如下所示:
public class JavaLogging {
private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
static {
logger.setLevel(Level.INFO);
Handler memoryHandler = new MemoryHandler(new ConsoleHandler(), 100, Level.WARNING);
memoryHandler.setLevel(Level.INFO);
memoryHandler.setFormatter(new SimpleFormatter());
logger.addHandler(memoryHandler);
}
public static void main(String[] args) {
logger.info("Hello, Java Logging");
logger.severe("severe");
}
}
我们给logger设置了一个MemoryHandler,该MemoryHandler的目标handler是ConsoleHander,缓冲区大小设置为100,pushLevel级别设置为Level.WARNING。
如果我们注释掉了logger.server那一行,那么logger.info那一行的信息是不会通过MemoryHandler输出到控制台的,因为没有达到push的触发条件,即没有收到级别高于或者等于WARNING级别的LogRecord,而一旦执行到logger.severe那一行,就会触发push,从而将缓冲区中的所有LogRecord输出到控制台。
我们实际执行的过程中会发现,就算注释掉了logger.severe那一行,控制台还是输出了一行日志信息:
Aug 12, 2018 6:10:37 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
需要注意的是,这个日志的输出不是通过MemoryHandler的targe Handler输出来的,而是直接通过另一个ConsoleHandler输出来的。为什么还会有另一个ConsoleHandler对日志进行输出呢?这涉及到JUL中的日志记录器层级关系——Logger Hierarchy,我们下篇文章再详细介绍。