Java Logging之JUL系列——Handler

前面的文章我们提到过,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,我们下篇文章再详细介绍。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 在现实生活中,记录日志非常重要。银行转账时会有转账记录;飞机飞行过程中,会有黑盒子(飞行数据记录器)记录飞行过程中...
    chliar阅读 754评论 1 0
  • 本文章是我大概三年前,在上家单位使用 Python 工作时结合官方文档做的整理。现在 Python 官方文档听说已...
    好吃的野菜阅读 215,999评论 14 232
  • 上文提到过,LogRecord可以理解为是一个DTO,那么LogRecord里面到底存储了哪些数据呢?我们可以看一...
    codecrazy阅读 1,495评论 0 1
  • 杂英纷已积,含芳独暮春; 还如故园树,忽忆故园人。
    苍山暮雪阅读 404评论 1 4
  • 入春以来,频繁变换的天气,短短数日就将春夏秋冬一起经历。几次低温过后,一场大雨,千树万树梨花开遍,鲜活的世界充满了...
    秋水伊人_44ad阅读 230评论 0 0