Log4J 主要由 Loggers (日志记录器)、Appenders(输出端)和Layout(日志格式化器)组成。其中 Loggers 控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式(输出到控制台、文件 等);Layout 控制日志信息的输出格式。
1、Loggers
日志记录器:负责收集处理日志记录,实例的命名就是类“XX”的full quailied name(类的全限定名), Logger的名字大小写敏感,其命名有继承机制:例如:name为com.ydlclass.service的logger会继承 name为com.ydlclass的logger,和JUL一致。Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接 或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。 JUL是不是也有一个名为.的根。
2、Appenders
Appender和JUL的Handler很像,用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地 有以下几种:输出端类型 | 作用 |
---|---|
ConsoleAppender | 将日志输出到控制台 |
FileAppender | 将日志输出到文件中 |
DailyRollingFileAppender | 将日志输出到一个日志文件,并且每天输出到一个新的文件 |
RollingFileAppender | 将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大 小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件 |
JDBCAppender | 把日志信息保存到数据库中 |
3、Layouts
可以自己自定义输出格式,log4j也有实现了些Layout,比如:PatternLayout,SimpleLayoutpackage com.example.test;
import org.apache.log4j.*;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.io.PrintWriter;
public class TestLog4j {
private static final Logger logger = Logger.getLogger(TestLog4j.class); // 日志记录输出
@Test
public void testLog4j(){
Logger root = Logger.getRootLogger();
ConsoleAppender consoleAppender = new ConsoleAppender();
consoleAppender.setWriter(new PrintWriter(System.out));
Layout layout = new Layout() {
@Override
public String format(LoggingEvent loggingEvent) {
return loggingEvent.getThreadName() + ">>>>" + loggingEvent.getMessage() + "\r\n";
}
@Override
public boolean ignoresThrowable() {
return false;
}
@Override
public void activateOptions() {
}
};
consoleAppender.setLayout(layout);
root.addAppender(consoleAppender);
logger.info("hello log4j"); // 日志级别
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃 和终止运行
logger.error("error"); // 错误信息,但不会影响系统运行
logger.warn("warn"); // 警告信息,可能会发生问题
logger.info("info"); // 程序运行信息,数据库的连接、网 络、IO操作等
logger.debug("debug"); // 调试信息,一般在开发阶段使 用,记录程序的变量、参数等
logger.trace("trace"); // 追踪信息,记录程序的所有流程 信息
}
}
有一些默认的实现类:Layout layout = new SimpleLayout();
package com.example.test;
import org.apache.log4j.*;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.io.PrintWriter;
public class TestLog4j {
private static final Logger logger = Logger.getLogger(TestLog4j.class); // 日志记录输出
@Test
public void testLog4j(){
Logger root = Logger.getRootLogger();
ConsoleAppender consoleAppender = new ConsoleAppender();
consoleAppender.setWriter(new PrintWriter(System.out));
Layout layout = new SimpleLayout();
consoleAppender.setLayout(layout);
root.addAppender(consoleAppender);
logger.info("hello log4j"); // 日志级别
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃 和终止运行
logger.error("error"); // 错误信息,但不会影响系统运行
logger.warn("warn"); // 警告信息,可能会发生问题
logger.info("info"); // 程序运行信息,数据库的连接、网 络、IO操作等
logger.debug("debug"); // 调试信息,一般在开发阶段使 用,记录程序的变量、参数等
logger.trace("trace"); // 追踪信息,记录程序的所有流程 信息
}
}
他的实现太简单了:
public String format(LoggingEvent event) {
this.sbuf.setLength(0);
this.sbuf.append(event.getLevel().toString());
this.sbuf.append(" - ");
this.sbuf.append(event.getRenderedMessage());
this.sbuf.append(LINE_SEP);
return this.sbuf.toString();
}
还有一个比较常用的Layout,就是PatternLayout这个实现类,能够根据特定的占位符进行转化,和JUL很像,但是又不一样,我们庖丁解牛研究一番,首先看他的构造器,构造器中如果传入一个pattern字符串,他会根据这个pattern创建一个链表,这个链表具体干什么咱们慢慢往后看:
public PatternLayout(String pattern) {
this.BUF_SIZE = 256;
this.MAX_CAPACITY = 1024;
this.sbuf = new StringBuffer(256);
this.pattern = pattern;
this.head = this.createPatternParser(pattern == null ? "%m%n" : pattern).parse();
}
将步骤拆解开来看,首先创建了一个解析器:
protected PatternParser createPatternParser(String pattern) {
return new PatternParser(pattern);
}
查看parse方法,这个方法比较复杂我们简化来看:
public PatternConverter parse() {
this.i = 0;
while(true) {
while(true) {
while(this.i < this.patternLength) {
char c = this.pattern.charAt(this.i++);
switch(this.state) {
case 0:
if (this.i == this.patternLength) {
this.currentLiteral.append(c);
} else if (c == '%') {
switch(this.pattern.charAt(this.i)) {
case '%':
this.currentLiteral.append(c);
++this.i;
break;
case 'n':
this.currentLiteral.append(Layout.LINE_SEP);
++this.i;
break;
default:
if (this.currentLiteral.length() != 0) {
this.addToList(new PatternParser.LiteralPatternConverter(this.currentLiteral.toString()));
}
this.currentLiteral.setLength(0);
this.currentLiteral.append(c);
this.state = 1;
this.formattingInfo.reset();
}
} else {
this.currentLiteral.append(c);
}
break;
case 1:
this.currentLiteral.append(c);
switch(c) {
case '-':
this.formattingInfo.leftAlign = true;
break;
case '.':
this.state = 3;
break;
default:
if (c >= '0' && c <= '9') {
this.formattingInfo.min = c - 48;
this.state = 4;
} else {
this.finalizeConverter(c);
}
}
case 2:
default:
break;
case 3:
this.currentLiteral.append(c);
if (c >= '0' && c <= '9') {
this.formattingInfo.max = c - 48;
this.state = 5;
break;
}
LogLog.error("Error occured in position " + this.i + ".\n Was expecting digit, instead got char \"" + c + "\".");
this.state = 0;
break;
case 4:
this.currentLiteral.append(c);
if (c >= '0' && c <= '9') {
this.formattingInfo.min = this.formattingInfo.min * 10 + (c - 48);
} else if (c == '.') {
this.state = 3;
} else {
this.finalizeConverter(c);
}
break;
case 5:
this.currentLiteral.append(c);
if (c >= '0' && c <= '9') {
this.formattingInfo.max = this.formattingInfo.max * 10 + (c - 48);
} else {
this.finalizeConverter(c);
this.state = 0;
}
}
}
if (this.currentLiteral.length() != 0) {
this.addToList(new PatternParser.LiteralPatternConverter(this.currentLiteral.toString()));
}
return this.head;
}
}
}
而finalizeConverter做的工作大家就能看的很清楚了:
protected void finalizeConverter(char c) {
PatternConverter pc = null;
switch(c) {
case 'C':
pc = new PatternParser.ClassNamePatternConverter(this.formattingInfo, this.extractPrecisionOption());
this.currentLiteral.setLength(0);
break;
case 'D':
case 'E':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'Y':
case 'Z':
case '[':
case '\\':
case ']':
case '^':
case '_':
case '`':
case 'a':
case 'b':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'n':
case 'o':
case 'q':
case 's':
case 'u':
case 'v':
case 'w':
default:
LogLog.error("Unexpected char [" + c + "] at position " + this.i + " in conversion patterrn.");
pc = new PatternParser.LiteralPatternConverter(this.currentLiteral.toString());
this.currentLiteral.setLength(0);
break;
case 'F':
pc = new PatternParser.LocationPatternConverter(this.formattingInfo, 1004);
this.currentLiteral.setLength(0);
break;
case 'L':
pc = new PatternParser.LocationPatternConverter(this.formattingInfo, 1003);
this.currentLiteral.setLength(0);
break;
case 'M':
pc = new PatternParser.LocationPatternConverter(this.formattingInfo, 1001);
this.currentLiteral.setLength(0);
break;
case 'X':
String xOpt = this.extractOption();
pc = new PatternParser.MDCPatternConverter(this.formattingInfo, xOpt);
this.currentLiteral.setLength(0);
break;
case 'c':
pc = new PatternParser.CategoryPatternConverter(this.formattingInfo, this.extractPrecisionOption());
this.currentLiteral.setLength(0);
break;
case 'd':
String dateFormatStr = "ISO8601";
String dOpt = this.extractOption();
if (dOpt != null) {
dateFormatStr = dOpt;
}
Object df;
if (dateFormatStr.equalsIgnoreCase("ISO8601")) {
df = new ISO8601DateFormat();
} else if (dateFormatStr.equalsIgnoreCase("ABSOLUTE")) {
df = new AbsoluteTimeDateFormat();
} else if (dateFormatStr.equalsIgnoreCase("DATE")) {
df = new DateTimeDateFormat();
} else {
try {
df = new SimpleDateFormat(dateFormatStr);
} catch (IllegalArgumentException var7) {
LogLog.error("Could not instantiate SimpleDateFormat with " + dateFormatStr, var7);
df = (DateFormat)OptionConverter.instantiateByClassName("org.apache.log4j.helpers.ISO8601DateFormat", DateFormat.class, (Object)null);
}
}
pc = new PatternParser.DatePatternConverter(this.formattingInfo, (DateFormat)df);
this.currentLiteral.setLength(0);
break;
case 'l':
pc = new PatternParser.LocationPatternConverter(this.formattingInfo, 1000);
this.currentLiteral.setLength(0);
break;
case 'm':
pc = new PatternParser.BasicPatternConverter(this.formattingInfo, 2004);
this.currentLiteral.setLength(0);
break;
case 'p':
pc = new PatternParser.BasicPatternConverter(this.formattingInfo, 2002);
this.currentLiteral.setLength(0);
break;
case 'r':
pc = new PatternParser.BasicPatternConverter(this.formattingInfo, 2000);
this.currentLiteral.setLength(0);
break;
case 't':
pc = new PatternParser.BasicPatternConverter(this.formattingInfo, 2001);
this.currentLiteral.setLength(0);
break;
case 'x':
pc = new PatternParser.BasicPatternConverter(this.formattingInfo, 2003);
this.currentLiteral.setLength(0);
}
this.addConverter((PatternConverter)pc);
}
addConverter就是一个典型的链表结构的构建了:
protected void addConverter(PatternConverter pc) {
this.currentLiteral.setLength(0);
this.addToList(pc);
this.state = 0;
this.formattingInfo.reset();
}
private void addToList(PatternConverter pc) {
if (this.head == null) {
this.head = this.tail = pc;
} else {
this.tail.next = pc;
this.tail = pc;
}
}
构建完转化器链表之后,就是循环这个链表,一次处理对应的占位符了,他的核心的格式化的方法也是format方法,在format方法中是通过一个转化器链来完成转化的:
public String format(LoggingEvent event) {
if (this.sbuf.capacity() > 1024) {
this.sbuf = new StringBuffer(256);
} else {
this.sbuf.setLength(0);
}
for(PatternConverter c = this.head; c != null; c = c.next) {
c.format(this.sbuf, event);
}
return this.sbuf.toString();
}
这里就是通过一个pattern字符串,这个字符串可能张这个样子(%-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n),使用
createPatternParser().parse()方法构建一个处理器的链表,这个每个处理器处理一个占位符比如(%d)。进入PatternLayout.format()方法,会进入c.format,这是我们会进入一个抽象类PatternConverter中的format方法,里边的核心就是如下代码:重要是String s = this.convert(e); 发现convert是个抽象方法,所以此时会根据不同的占位符选择不同的处理器。
public void format(StringBuffer sbuf, LoggingEvent e) {
String s = this.convert(e);
if (s == null) {
if (0 < this.min) {
this.spacePad(sbuf, this.min);
}
} else {
int len = s.length();
if (len > this.max) {
sbuf.append(s.substring(len - this.max));
} else if (len < this.min) {
if (this.leftAlign) {
sbuf.append(s);
this.spacePad(sbuf, this.min - len);
} else {
this.spacePad(sbuf, this.min - len);
sbuf.append(s);
}
} else {
sbuf.append(s);
}
}
}
%m 输出代码中指定的日志信息
%p 输出日志级别,及 DEBUG、INFO 等
%n 换行符(Windows平台的换行符为 "\n",Unix 平台为 "\n")
%r 输出自应用启动到输出该 log 信息耗费的毫秒数
%c 输出打印语句所属的类的全名
%t 输出产生该日志的线程全名
%d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如: %d{yyyy年MM月dd日HH:mm:ss}
%l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如: Test.main(Test.java:10)
%F 输出日志消息产生时所在的文件名称
%L 输出代码中的行号
%% 输出一个 "%" 字符
可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对 其方式。如:
%5c 输出category名称,最小宽度是5,category<5,默认的情况下右 对齐
%-5c 输出category名称,最小宽度是5,category<5,"-"号指定左 对齐,会有空格
%.5c 输出category名称,最大宽度是5,category>5,就会将左边多 出的字符截掉,<5不会有空格
%20.30c category名称<20补空格,并且右对齐,>30字符,就从左边 交远销出的字符截掉
尝试写一个:
package com.example.test;
import org.apache.log4j.*;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.io.PrintWriter;
public class TestLog4j {
private static final Logger logger = Logger.getLogger(TestLog4j.class); // 日志记录输出
@Test
public void testLog4j(){
Logger root = Logger.getRootLogger();
ConsoleAppender consoleAppender = new ConsoleAppender();
consoleAppender.setWriter(new PrintWriter(System.out));
Layout layout = new PatternLayout("%-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n");
consoleAppender.setLayout(layout);
root.addAppender(consoleAppender);
logger.info("hello log4j"); // 日志级别
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃 和终止运行
logger.error("error"); // 错误信息,但不会影响系统运行
logger.warn("warn"); // 警告信息,可能会发生问题
logger.info("info"); // 程序运行信息,数据库的连接、网 络、IO操作等
logger.debug("debug"); // 调试信息,一般在开发阶段使 用,记录程序的变量、参数等
logger.trace("trace"); // 追踪信息,记录程序的所有流程 信息
}
}