- 浏览: 3231 次
- 性别:
- 来自: 上海
文章分类
最新评论
Log4j
1.1. 背景
在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析工作;跟踪代码运行时轨迹,作为日后审计的依据;担当集成开发环境中的调试器的作用,向文件或控制台打印代码的调试信息。
最普通的做法就是在代码中嵌入许多的打印语句,这些打印语句可以输出到控制台或文件中,比较好的做法就是构造一个日志操作类来封装此类操作,而不是让一系列的打印语句充斥了代码的主体。
1.2. Log4j简介
在强调可重用组件开发的今天,除了自己从头到尾开发一个可重用的日志操作类外,Apache为我们提供了一个强有力的日志操作包-Log4j。
Log4j是Apache的一个开放源代码项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务 器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就 是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
此外,通过Log4j其他语言接口,您可以在C、C+ +、.Net、PL/SQL程序中使用Log4j,其语法和用法与在Java程序中一样,使得多语言分布式系统得到一个统一一致的日志组件模块。而且,通 过使用各种第三方扩展,您可以很方便地将Log4j集成到J2EE、JINI甚至是SNMP应用中。
Log4j有三个主要的组件:
Loggers(记录器),Appenders (输出源)和Layouts(布局),这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出。综合使用这三个组件可以轻松的记录信息的类型和 级别,并可以在运行时控制日志输出的样式和位置。下面对三个组件分别进行说明:
1、 Loggers
Loggers组件在此系统中被分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的,DEBUG < INFO < WARN < ERROR < FATAL,分别用来指定这条日志信息的重要程度,明白这一点很重要,这里Log4j有一个规则:假设Loggers级别为P,如果在Loggers中发 生了一个级别Q比P高,则可以启动,否则屏蔽掉。
假设你定义的级别是info,那么error和warn的日志可以显示而比他低的debug信息就不显示了。
Java程序举例来说:
***建立Logger的一个实例,命名为“com.foo”***
Logger logger = Logger.getLogger("com.foo");
***"com.foo"是实例进行命名,也可以任意***
***设置logger的级别。通常不在程序中设置logger的级别。一般在配置文件中设置。***
logger.setLevel(Level.INFO);
Logger barlogger = Logger.getLogger("com.foo.Bar");
***下面这个请求可用,因为WARN >= INFO***
logger.warn("Low fuel level.");
***下面这个请求不可用,因为DEBUG < INFO***
logger.debug("Starting search for nearest gas station.");
***命名为“com.foo.bar”的实例barlogger会继承实例“com.foo”的级别。因此,下面这个请求可用,因为INFO >= INFO***
barlogger.info("Located nearest gas station.");
***下面这个请求不可用,因为DEBUG < INFO***
barlogger.debug("Exiting gas station search");
这里“是否可用”的意思是能否输出Logger信息。
在对Logger实例进行命名时,没有限制,可以取任意自己感兴趣的名字。一般情况下建议以类的所在位置来命名Logger实例,这是目前来讲比较有效的Logger命名方式。这样可以使得每个类建立自己的日志信息,便于管理。比如:
static Logger logger = Logger.getLogger(ClientWithLog4j.class.getName());
2、Appenders
禁用与使用日志请求只是Log4j其中的一个小小的地方,Log4j日志系统允许把日志输出到不同的地方,如控制台(Console)、文件(Files)、根据天数或者文件大小产生新的文件、以流的形式发送到其它地方等等。
其语法表示为:
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
配置时使用方式为:
log4j.appender.appenderName = fully.qualified.name.of.appender.class
log4j.appender.appenderName.option1 = value1
…
log4j.appender.appenderName.option = valueN
这样就为日志的输出提供了相当大的便利。
3、Layouts
有时用户希望根据自己的喜好格式化自己的日志输出。Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供了 四种日志输出样式,如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式等等。
其语法表示为:
org.apache.log4j.HTMLLayout(以HTML表格形式布局),
org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
配置时使用方式为:
log4j.appender.appenderName.layout =fully.qualified.name.of.layout.class
log4j.appender.appenderName.layout.option1 = value1
…
log4j.appender.appenderName.layout.option = valueN
4 . Log4j的配置
以上是从原理方面说明Log4j的使用方法,在具体Java编程使用Log4j可以参照以下示例:
1、 建立Logger实例:
语法表示:public static Logger getLogger( String name)
实际使用:static Logger logger = Logger.getLogger(ServerWithLog4j.class.getName ()) ;
2、 读取配置文件:
获得了Logger的实例之后,接下来将配置Log4j使用环境:
语法表示:
BasicConfigurator.configure():自动快速地使用缺省Log4j环境。
PropertyConfigurator.configure(String configFilename):读取使用Java的特性文件编写的配置文件。
DOMConfigurator.configure(String filename):读取XML形式的配置文件。
实际使用:
PropertyConfigurator.configure("ServerWithLog4j.properties");
3、 插入日志信息
完成了以上连个步骤以后,下面就可以按日志的不同级别插入到你要记录日志的任何地方了。
语法表示:
Logger.debug(Object message);//调试信息
Logger.info(Object message);//一般信息
Logger.warn(Object message);//警告信息
Logger.error(Object message);//错误信息
Logger.fatal(Object message);//致命错误信息
实际使用:logger.info("ServerSocket before accept: " + server);
5. 配置过程
在实际编程时,要使Log4j真正在系统中运行事先还要对配置文件进行定义。定义步骤就是对Logger、Appender及Layout的分别使用。
Log4j支持两种配置文件格式,一种是XML格式的文件,一种是java properties(key=value)【Java特性文件(键=值)】。下面我们介绍使用Java特性文件做为配置文件的方法
具体如下:
1、配置根Logger,其语法为:
log4j.rootLogger = [ level ] , appenderName1, appenderName2, …
level : 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。Log4j建议只使用四个级别,优 先级从高到低分别是ERROR、WARN、INFO、DEBUG。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定 义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。
appenderName:就是指定日志信息输出到哪个地方。您可以同时指定多个输出目的地。
例如:log4j.rootLogger=info,A1,B2,C3
2、配置日志信息输出目的地,其语法为:
log4j.appender.appenderName = fully.qualified.name.of.appender.class //
"fully.qualified.name.of.appender.class" 可以指定下面五个目的地中的一个:
1.org.apache.log4j.ConsoleAppender(控制台)
2.org.apache.log4j.FileAppender(文件)
3.org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
4.org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
5.org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
1.ConsoleAppender选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
Target=System.err:默认情况下是:System.out,指定输出控制台
2.FileAppender 选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt:指定消息输出到mylog.txt文件。
Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
3.DailyRollingFileAppender 选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt:指定消息输出到mylog.txt文件。
Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
DatePattern='.'yyyy-ww:每周滚动一次文件,即每周产生一个新的文件。当然也可以指定按月、周、天、时和分。即对应的格式如下:
1)'.'yyyy-MM: 每月
2)'.'yyyy-ww: 每周
3)'.'yyyy-MM-dd: 每天
4)'.'yyyy-MM-dd-a: 每天两次
5)'.'yyyy-MM-dd-HH: 每小时
6)'.'yyyy-MM-dd-HH-mm: 每分钟
4.RollingFileAppender 选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt:指定消息输出到mylog.txt文件。
Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
MaxFileSize=100KB: 后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。
MaxBackupIndex=2:指定可以产生的滚动文件的最大数。
实际应用:
log4j.appender.A1=org.apache.log4j.ConsoleAppender //这里指定了日志输出的第一个位置A1是控制台ConsoleAppender
3、配置日志信息的格式,其语法为:
A. log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class
"fully.qualified.name.of.layout.class" 可以指定下面4个格式中的一个:
1.org.apache.log4j.HTMLLayout(以HTML表格形式布局),
2.org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
3.org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
4.org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
1.HTMLLayout 选项
LocationInfo=true:默认值是false,输出java文件名称和行号
Title=my app file: 默认值是 Log4J Log Messages.
2.PatternLayout 选项
ConversionPattern=%m%n :指定怎样格式化指定的消息。
3.XMLLayout 选项
LocationInfo=true:默认值是false,输出java文件和行号
实际应用:
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
B. log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n
这里需要说明的就是日志信息格式中几个符号所代表的含义:
-X号: X信息输出时左对齐;
%p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
%d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%r: 输出自应用启动到输出该log信息耗费的毫秒数
%c: 输出日志信息所属的类目,通常就是所在类的全名
%t: 输出产生该日志事件的线程名
%l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
%%: 输出一个"%"字符
%F: 输出日志消息产生时所在的文件名称
%L: 输出代码中的行号
%m: 输出代码中指定的消息,产生的日志具体信息
%n: 输出一个回车换行符,Windows平台为"\r\n",Unix平台为"\n"输出日志信息换行
可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。如:
1)%20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,默认的情况下右对齐。
2)%-20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,"-"号指定左对齐。
3)%.30c:指定输出category的名称,最大的宽度是30,如果category的名称大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
4)%20.30c:如果category的名称小于20就补空格,并且右对齐,如果其名称长于30字符,就从左边交远销出的字符截掉。
这里上面三个步骤是对前面Log4j组件说明的一个简化;下面给出一个具体配置例子,在程序中可以参照执行:
log4j.rootLogger=INFO,A1,B2
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n
根据上面的日志格式,某一个程序的输出结果如下:
0 INFO 2003-06-13 13:23:46968 ClientWithLog4j Client socket: Socket[addr=localhost/127.0.0.1,port=8002,localport=2014]
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server says: 'Java server with log4j, Fri Jun 13 13:23:46 CST 2003'
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j GOOD
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server responds: 'Command 'HELLO' not understood.'
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j HELP
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server responds: 'Vocabulary: HELP QUIT'
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j QUIT
4. # 当输出信息于回滚文件时
log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender //指定以文件的方式输出日志
log4j.appender.ROLLING_FILE.Threshold=ERROR
log4j.appender.ROLLING_FILE.File=rolling.log //文件位置,也可以用变量${java.home}、rolling.log
log4j.appender.ROLLING_FILE.Append=true
log4j.appender.ROLLING_FILE.MaxFileSize=10KB //文件最大尺寸
log4j.appender.ROLLING_FILE.MaxBackupIndex=1 //备份数
log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
6. Log4j比较全面的配置
LOG4J的配置之简单使它遍及于越来越多的应用中了:Log4J配置文件实现了输出到控制台、文件、回滚文件、发送日志邮件、输出到数据库日志表、自定义标签等全套功能。择其一二使用就够用了,
log4j.rootLogger=DEBUG,CONSOLE,A1,im
log4j.addivity.org.apache=true
# 应用于控制台
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.Threshold=DEBUG
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
#log4j.appender.CONSOLE.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[thread] n%c[CATEGORY]%n%m[MESSAGE]%n%n
#应用于文件
log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=file.log
log4j.appender.FILE.Append=false
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
# Use this layout for LogFactor 5 analysis
# 应用于文件回滚
log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender
log4j.appender.ROLLING_FILE.Threshold=ERROR
log4j.appender.ROLLING_FILE.File=rolling.log //文件位置,也可以用变量${java.home}、rolling.log
log4j.appender.ROLLING_FILE.Append=true //true:添加 false:覆盖
log4j.appender.ROLLING_FILE.MaxFileSize=10KB //文件最大尺寸
log4j.appender.ROLLING_FILE.MaxBackupIndex=1 //备份数
log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
#应用于socket
log4j.appender.SOCKET=org.apache.log4j.RollingFileAppender
log4j.appender.SOCKET.RemoteHost=localhost
log4j.appender.SOCKET.Port=5001
log4j.appender.SOCKET.LocationInfo=true
# Set up for Log Facter 5
log4j.appender.SOCKET.layout=org.apache.log4j.PatternLayout
log4j.appender.SOCET.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[thread]%n%c[CATEGORY]%n%m[MESSAGE]%n%n
# Log Factor 5 Appender
log4j.appender.LF5_APPENDER=org.apache.log4j.lf5.LF5Appender
log4j.appender.LF5_APPENDER.MaxNumberOfRecords=2000
# 发送日志给邮件
log4j.appender.MAIL=org.apache.log4j.net.SMTPAppender
log4j.appender.MAIL.Threshold=FATAL
log4j.appender.MAIL.BufferSize=10
log4j.appender.MAIL.From=web@www.wuset.com
log4j.appender.MAIL.SMTPHost=www.wusetu.com
log4j.appender.MAIL.Subject=Log4J Message
log4j.appender.MAIL.To=web@www.wusetu.com
log4j.appender.MAIL.layout=org.apache.log4j.PatternLayout
log4j.appender.MAIL.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
# 用于数据库
log4j.appender.DATABASE=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.DATABASE.URL=jdbc:mysql://localhost:3306/test
log4j.appender.DATABASE.driver=com.mysql.jdbc.Driver
log4j.appender.DATABASE.user=root
log4j.appender.DATABASE.password=
log4j.appender.DATABASE.sql=INSERT INTO LOG4J (Message) VALUES ('[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n')
log4j.appender.DATABASE.layout=org.apache.log4j.PatternLayout
log4j.appender.DATABASE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
log4j.appender.A1=org.apache.log4j.DailyRollingFileAppender
log4j.appender.A1.File=SampleMessages.log4j
log4j.appender.A1.DatePattern=yyyyMMdd-HH'.log4j'
log4j.appender.A1.layout=org.apache.log4j.xml.XMLLayout
#自定义Appender
log4j.appender.im = net.cybercorlin.util.logger.appender.IMAppender
log4j.appender.im.host = mail.cybercorlin.net
log4j.appender.im.username = username
log4j.appender.im.password = password
log4j.appender.im.recipient = corlin@cybercorlin.net
log4j.appender.im.layout=org.apache.log4j.PatternLayout
log4j.appender.im.layout.ConversionPattern =[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
Log4J将写日志功能抽象成七个核心类/接口:Logger、LoggerRepository、Level、LoggingEvent、Appender、Layout、ObjectRender。其类图如下:
更详细的,实现Log4J主要功能相关的类图:
其实Log4J最核心的也就5个类:Logger用于对日志记录行为的抽象,提供记录不同级别日志的接口;Level对日志级别的抽象;Appender是对记录日志形式的抽象;Layout是对日志行格式的抽象;而LoggingEvent是对一次日志记录过程中所能取到信息的抽象。另外两个LoggerRepository是Logger实例的容器,而ObjectRender是对日志实例的解析接口,它们主要提供了一种扩展支持。
简单的一次记录日志过程的序列图如下:
即获取Logger实例->判断Logger实例对应的日志记录级别是否要比请求的级别低->若是调用forceLog记录日志->创建LoggingEvent实例->将LoggingEvent实例传递给Appender->Appender调用Layout实例格式化日志消息->Appender将格式化后的日志信息写入该Appender对应的日志输出中。
包含Log4J其他模块类的更详细序列图如下:
在简单的介绍了Log4J各个模块类的作用后,以下将详细的介绍各个模块的具体作用以及代码实现。
Logger类
Logger是对记录日志动作的抽象,它提供了记录不同级别日志的接口,日志信息可以包含异常信息也可以不包含:
1 public void debug(Object message) {
2 if(isLevelEnabled(Level.DEBUG)) {
3 forceLog(FQCN, Level.DEBUG, message, null);
4 }
5 }
6 public void debug(Object message, Throwable cause) {
7 if(isLevelEnabled(Level.DEBUG)) {
8 forceLog(FQCN, Level.DEBUG, message, cause);
9 }
10 }
11 protected void forceLog(String fqcn, Level level, Object message, Throwable t) {
12 callAppenders(new LoggingEvent(fqcn, this, level, message, t));
13 }
Logger类包含Level信息 ,如果当前Logger未设置Level值,它也可以中父节点中继承下来,该值可以用来控制该Logger可以记录的日志级别:
1 protected Level level;
2 public Level getEffectiveLevel() {
3 for(Logger logger = this; logger != null; logger = logger.parent) {
4 if(logger.level != null) {
5 return logger.level;
6 }
7 }
8 return null;
9 }
10 public boolean isLevelEnabled(Level level) {
11 return level.isGreaterOrEqual(this.getEffectiveLevel());
12 }
13 public boolean isDebugEnabled() {
14 return isLevelEnabled(Level.DEBUG);
15 }
Logger是一个命名的实体,其名字一般用”.”分割以体现不同Logger的层次关系,其中Level和Appender信息可以从父节点中获取,因而Logger类中还具有name和parent属性。
1 private String name;
2 protected Logger parent;
在某些情况下,我们希望某些Logger只将日志记录到特定的Appender中,而不想记录在父节点中的Appender中,Log4J为这种需求提供了additivity属性,即对当前Logger节点,如果其additivity属性设置为false,则该Logger不会继承父节点的Appender信息,但是其子节点依然会继承该Logger的Appender信息,除非子节点的additivity属性也设置成了false。
1 private boolean additive = true;
2 public void callAppenders(LoggingEvent event) {
3 int writes = 0;
4
5 for(Logger logger = this; logger != null; logger = logger.parent) {
6 synchronized(logger) {
7 if(logger.appenders != null) {
8 writes += logger.appenders.appendLoopOnAppenders(event);
9 }
10 if(!logger.additive) {
11 break;
12 }
13 }
14 }
15
16 if(writes == 0) {
17 System.err.println("No Appender is configed.");
18 }
19 }
最后,为了支持国际化,Log4J还提供了两个l7dlog()方法,通过指定的key,以从资源文件中获取消息内容。为了使用这两个方法,需要设置资源文件。同样,资源文件也是可以从父节点中继承的。
1 private ResourceBundle resourceBundle;
2 public void l7dlog(Level level, String key, Throwable cause) {
3 if(isLevelEnabled(level)) {
4 String message = getResourceBundleString(key);
5 if(message == null) {
6 message = key;
7 }
8 forceLog(FQCN, level, message, cause);
9 }
10 }
11
12 public void l7dlog(Level level, String key, Object[] params, Throwable cause) {
13
14 if(pattern == null) {
15 message = key;
16 } else {
17 message = MessageFormat.format(pattern, params);
18 }
19
20 }
21
22 protected String getResourceBundleString(String key) {
23 ResourceBundle rb = getResourceBundle();
24
25 return rb.getString(key);
26 }
27 public ResourceBundle getResourceBundle() {
28 for(Logger logger = this; logger != null; logger = logger.parent) {
29 if(logger.resourceBundle != null) {
30 return logger.resourceBundle;
31 }
32 }
33 return null;
34 }
另外,在实际开发中经常会遇到要把日志信息同时写到不同地方,如同时写入文件和控制台,因而一个Logger实例中可以包含多个Appender,为了管理多个Appender,Log4J抽象出了AppenderAttachable接口,它定义了几个用于管理多个Appender实例的方法,这些方法由AppenderAttachableImpl类实现,而Logger会实例化AppenderAttachableImpl,并将这些方法代理给该实例:
1 public interface AppenderAttachable {
2 public void addAppender(Appender newAppender);
3 public Enumeration getAllAppenders();
4 public Appender getAppender(String name);
5 public boolean isAttached(Appender appender);
6 void removeAllAppenders();
7 void removeAppender(Appender appender);
8 void removeAppender(String name);
9 }
RootLogger类
在Log4J中,所有Logger实例组成一个单根的树状结构,由于Logger实例的根节点有一点特殊:它的名字为“root”,它没有父节点,它的Level字段必须设值以防止其他Logger实例都没有设置Level值的情况。基于这些考虑,Log4J通过继承Logger类实现了RootLogger类,它用于表达所有Logger实例的根节点:
1 public final class RootLogger extends Logger {
2 public RootLogger(Level level) {
3 super("root");
4 setLevel(level);
5 }
6 public final Level getChainedLevel() {
7 return level;
8 }
9 public final void setLevel(Level level) {
10 if (level == null) {
11 LogLog.error("You have tried to set a null level to root.",
12 new Throwable());
13 } else {
14 this.level = level;
15 }
16 }
17 }
NOPLogger类
有时候,为了测试等其他需求,我们希望Logger本身不做什么事情,Log4J为这种需求提供了NOPLogger类,它继承自Logger,但是基本上的方法都为空。
Level类
Level是对日志级别的抽象,目前Log4J支持的级别有FATAL、ERROR、WARN、INFO、DEBUG、TRACE,从头到尾一次级别递减,另外Log4J还支持两种特殊的级别:ALL和OFF,它们分别表示打开和关闭日志功能。
1 public static final int OFF_INT = Integer.MAX_VALUE;
2 public static final int FATAL_INT = 50000;
3 public static final int ERROR_INT = 40000;
4 public static final int WARN_INT = 30000;
5 public static final int INFO_INT = 20000;
6 public static final int DEBUG_INT = 10000;
7 public static final int TRACE_INT = 5000;
8 public static final int ALL_INT = Integer.MIN_VALUE;
9
10 public static final Level OFF = new Level(OFF_INT, "OFF", 0);
11 public static final Level FATAL = new Level(FATAL_INT, "FATAL", 0);
12 public static final Level ERROR = new Level(ERROR_INT, "ERROR", 3);
13 public static final Level WARN = new Level(WARN_INT, "WARN", 4);
14 public static final Level INFO = new Level(INFO_INT, "INFO", 6);
15 public static final Level DEBUG = new Level(DEBUG_INT, "DEBUG", 7);
16 public static final Level TRACE = new Level(TRACE_INT, "TRACE", 7);
17 public static final Level ALL = new Level(ALL_INT, "ALL", 7);
每个Level实例包含了该Level代表的int值(一般是从级别低到级别高一次增大)、该Level的String表达、该Level和系统Level的对应值。
1 protected transient int level;
2 protected transient String levelStr;
3 protected transient int syslogEquivalent;
4 protected Level(int level, String levelStr, int syslogEquivalent) {
5 this.level = level;
6 this.levelStr = levelStr;
7 this.syslogEquivalent = syslogEquivalent;
8 }
Level类主要提供了判断哪个Level级别更高的方法isGreaterOrEqual()以及将int值或String值转换成Level实例的toLevel()方法:
1 public boolean isGreaterOrEqual(Level level) {
2 return this.level >= level.level;
3 }
4 public static Level toLevel(int level) {
5 return toLevel(level, DEBUG);
6 }
7 public static Level toLevel(int level, Level defaultLevel) {
8 switch(level) {
9 case OFF_INT: return OFF;
10 case FATAL_INT: return FATAL;
11 case ERROR_INT: return ERROR;
12 case WARN_INT: return WARN;
13 case INFO_INT: return INFO;
14 case DEBUG_INT: return DEBUG;
15 case TRACE_INT: return TRACE;
16 case ALL_INT: return ALL;
17 }
18 return defaultLevel;
19 }
另外,由于对相同级别的Level实例来说,它必须是单例的,因而Log4J对序列化和反序列化做了一些处理。即它的三个成员都是transient,真正序列化和反序列化的代码自己写,并且加入readResolve()方法的支持,以保证反序列化出来的相同级别的Level实例是相同的实例。
1 private void readObject(final ObjectInputStream input) throws IOException, ClassNotFoundException {
2 input.defaultReadObject();
3 level = input.readInt();
4 syslogEquivalent = input.readInt();
5 levelStr = input.readUTF();
6 if(levelStr == null) {
7 levelStr = "";
8 }
9 }
10 private void writeObject(final ObjectOutputStream output) throws IOException {
11 output.defaultWriteObject();
12 output.writeInt(level);
13 output.writeInt(syslogEquivalent);
14 output.writeUTF(levelStr);
15 }
16 private Object readResolve() throws ObjectStreamException {
17 if(this.getClass() == Level.class) {
18 return toLevel(level);
19 }
20 return this;
21 }
如果要实现自己的Level类,可以继承自Level,并且实现相应的静态toLevel()方法即可。关于如何实现自己的Level类将会在配置文件相关小节中详细讨论。
LoggerRepository类
LoggerRepository从概念以及字面上来说它就是一个Logger实例的容器:一方面相同名字的Logger实例只需要创建一次,在后面的使用中,只需要从这个容器中取即可;另一方面,Logger容器可以存放从配置文件中解析出来的信息,从而使配置信息可以无缝的应用到Log4J内部系统中;最后Logger容器还为维护Logger的树状层次结构提供了方面,每个Logger只维护父节点的信息,有了Logger容器的存在则可以很容易的找到一个新的Logger实例的父节点;关于Logger容器将在下一节中详细讲解。
LoggingEvent类
LoggingEvent个人感觉用LoggingContext更合适一些,它是对一次日志记录时哪能获取到的数据的封装。它包含了以下信息以提供Layout在format()方法中使用:
1. fqnOfCategoryClass:日志记录接口(默认为Logger)的类全名,该信息主要用于计算日志记录点的源文件、调用方法以及行号等位置信息。
2. locationInfo:通过fqnOfCategoryClass计算位置信息,位置信息的计算由LocationInfo类实现,这些信息可以提供给Layout使用。
3. logger:目前来看主要是通过Logger实例取得LogRepository实例,并通过LogRepository取得注册的ObjectRender实例,如果有的话。
4. loggerName:当前日志记录的Logger名称,提供给Layout使用。
5. threadName:当前线程名,提供给Layout使用。
6. level:当前日志的级别,提供给Layout使用。
7. message:当前日志类,一般是String类型,但是也可以通过注册ObjectRender,然后传入相应的其他对象类型。
8. renderedMessage:经过ObjectRender处理后的日志信息,提供给Layout使用。
9. throwableInfo:异常信息,如果存在的话,提供给Layout使用。
10. timestamp:创建LoggingEvent实例的时间,提供给Layout使用。
11. 其他相对不常用的信息将会在后面小节中讲解。
LoggingEvent只是一个简单的数据对象(DO),因而其实现还是比较简单的,即在创建实例时将数据提供给它,在其他类(Layout等)使用它时通过getXXX()方法取数据。不过还是有几个方法可以简单的讲解一下。
LocationInfo类计算位置信息
LocationInfo所指的位置信息主要包括记录日志所在的源文件名、类名、方法名、所在源文件的行号。
1 transient String lineNumber;
2 transient String fileName;
3 transient String className;
4 transient String methodName;
5 //fully.qualified.classname.of.caller.methodName(Filename.java:line)
6 public String fullInfo;
我们知道在异常栈中每一条记录都包含了方法调用对应的这些信息,Log4J的这些信息正是利用了这个原理,即通过构建一个Throwable实例,而后在该Throwable的栈信息中解析出来的:
1 public LocationInfo getLocationInformation() {
2 if (locationInfo == null) {
3 locationInfo = new LocationInfo(new Throwable(),
4 fqnOfCategoryClass);
5 }
6 return locationInfo;
7 }
以上Throwable一般会产生如下异常栈:
1 java.lang.Throwable
2
3 at org.apache.log4j.PatternLayout.format(PatternLayout.java:413)
4 at org.apache.log4j.FileAppender.doAppend(FileAppender.java:183)
5 at org.apache.log4j.Category.callAppenders(Category.java:131)
6 at org.apache.log4j.Category.log(Category.java:512)
7 at callers.fully.qualified.className.methodName(FileName.java:74)
8
因而我们就可以通过callers.fully.qualified.className信息来找到改行信息,这个className信息即是传入的fqnOfCategoryClass。
如果当前JDK版本是1.4以上,我们就可以通过JDK提供的一些方法来查找:
1 getStackTraceMethod = Throwable.class.getMethod("getStackTrace",
2 noArgs);
3 Class stackTraceElementClass = Class
4 .forName("java.lang.StackTraceElement");
5 getClassNameMethod = stackTraceElementClass.getMethod(
6 "getClassName", noArgs);
7 getMethodNameMethod = stackTraceElementClass.getMethod(
8 "getMethodName", noArgs);
9 getFileNameMethod = stackTraceElementClass.getMethod("getFileName",
10 noArgs);
11 getLineNumberMethod = stackTraceElementClass.getMethod(
12 "getLineNumber", noArgs);
13
14 Object[] noArgs = null;
15 Object[] elements = (Object[]) getStackTraceMethod.invoke(t,
16 noArgs);
17 String prevClass = NA;
18 for (int i = elements.length - 1; i >= 0; i--) {
19 String thisClass = (String) getClassNameMethod.invoke(
20 elements[i], noArgs);
21 if (fqnOfCallingClass.equals(thisClass)) {
22 int caller = i + 1;
23 if (caller < elements.length) {
24 className = prevClass;
25 methodName = (String) getMethodNameMethod.invoke(
26 elements[caller], noArgs);
27 fileName = (String) getFileNameMethod.invoke(
28 elements[caller], noArgs);
29 if (fileName == null) {
30 fileName = NA;
31 }
32 int line = ((Integer) getLineNumberMethod.invoke(
33 elements[caller], noArgs)).intValue();
34 if (line < 0) {
35 lineNumber = NA;
36 } else {
37 lineNumber = String.valueOf(line);
38 }
39 StringBuffer buf = new StringBuffer();
40 buf.append(className);
41 buf.append(".");
42 buf.append(methodName);
43 buf.append("(");
44 buf.append(fileName);
45 buf.append(":");
46 buf.append(lineNumber);
47 buf.append(")");
48 this.fullInfo = buf.toString();
49 }
50 return;
51 }
52 prevClass = thisClass;
53 }
否则,则需要我们通过字符串查找的方式来查找:
1 String s;
2 // Protect against multiple access to sw.
3 synchronized (sw) {
4 t.printStackTrace(pw);
5 s = sw.toString();
6 sw.getBuffer().setLength(0);
7 }
8 int ibegin, iend;
9 ibegin = s.lastIndexOf(fqnOfCallingClass);
10 if (ibegin == -1)
11 return;
12 // See bug 44888.
13 if (ibegin + fqnOfCallingClass.length() < s.length()
14 && s.charAt(ibegin + fqnOfCallingClass.length()) != '.') {
15 int i = s.lastIndexOf(fqnOfCallingClass + ".");
16 if (i != -1) {
17 ibegin = i;
18 }
19 }
20
21 ibegin = s.indexOf(Layout.LINE_SEP, ibegin);
22 if (ibegin == -1)
23 return;
24 ibegin += Layout.LINE_SEP_LEN;
25
26 // determine end of line
27 iend = s.indexOf(Layout.LINE_SEP, ibegin);
28 if (iend == -1)
29 return;
30
31 // VA has a different stack trace format which doesn't
32 // need to skip the inital 'at'
33 if (!inVisualAge) {
34 // back up to first blank character
35 ibegin = s.lastIndexOf("at ", iend);
36 if (ibegin == -1)
37 return;
38 // Add 3 to skip "at ";
39 ibegin += 3;
40 }
41 // everything between is the requested stack item
42 this.fullInfo = s.substring(ibegin, iend);
对于通过字符串查找到的fullInfo值,在获取其他单个值时还需要做相应的字符串解析:
className:
1 // Starting the search from '(' is safer because there is
2 // potentially a dot between the parentheses.
3 int iend = fullInfo.lastIndexOf('(');
4 if (iend == -1)
5 className = NA;
6 else {
7 iend = fullInfo.lastIndexOf('.', iend);
8
9 // This is because a stack trace in VisualAge looks like:
10
11 // java.lang.RuntimeException
12 // java.lang.Throwable()
13 // java.lang.Exception()
14 // java.lang.RuntimeException()
15 // void test.test.B.print()
16 // void test.test.A.printIndirect()
17 // void test.test.Run.main(java.lang.String [])
18 int ibegin = 0;
19 if (inVisualAge) {
20 ibegin = fullInfo.lastIndexOf(' ', iend) + 1;
21 }
22
23 if (iend == -1)
24 className = NA;
25 else
26 className = this.fullInfo.substring(ibegin, iend);
fileName:
1
2 int iend = fullInfo.lastIndexOf(':');
3 if (iend == -1)
4 fileName = NA;
5 else {
6 int ibegin = fullInfo.lastIndexOf('(', iend - 1);
7 fileName = this.fullInfo.substring(ibegin + 1, iend);
8 }
lineNumber:
1 int iend = fullInfo.lastIndexOf(')');
2 int ibegin = fullInfo.lastIndexOf(':', iend - 1);
3 if (ibegin == -1)
4 lineNumber = NA;
5 else
6 lineNumber = this.fullInfo.substring(ibegin + 1, iend);
methodName:
1 int iend = fullInfo.lastIndexOf('(');
2 int ibegin = fullInfo.lastIndexOf('.', iend);
3 if (ibegin == -1)
4 methodName = NA;
5 else
6 methodName = this.fullInfo.substring(ibegin + 1, iend);
ObjectRender接口
Log4J中,对传入的message实例,如果是非String类型,会先使用注册的ObjectRender(在LogRepository中查找注册的ObjectRender信息)处理成String后返回,若没有找到相应的ObjectRender,则使用默认的ObjectRender,它只是调用该消息实例的toString()方法。
1 public Object getMessage() {
2 if (message != null) {
3 return message;
4 } else {
5 return getRenderedMessage();
6 }
7 }
8 public String getRenderedMessage() {
9 if (renderedMessage == null && message != null) {
10 if (message instanceof String)
11 renderedMessage = (String) message;
12 else {
13 LoggerRepository repository = logger.getLoggerRepository();
14
15 if (repository instanceof RendererSupport) {
16 RendererSupport rs = (RendererSupport) repository;
17 renderedMessage = rs.getRendererMap()
18 .findAndRender(message);
19 } else {
20 renderedMessage = message.toString();
21 }
22 }
23 }
24 return renderedMessage;
25 }
ThrowableInformation类
ThrowableInformation类用以处理异常栈信息,即通过Throwable实例获取异常栈字符串数组。同时还支持自定义的ThrowableRender(在LogRepository中设置),默认的ThrowableRender通过系统printStackTrace()方法来获取信息:
1 if (throwable != null) {
2 this.throwableInfo = new ThrowableInformation(throwable, logger);
3 }
4 ThrowableRenderer renderer = null;
5 if (category != null) {
6 LoggerRepository repo = category.getLoggerRepository();
7 if (repo instanceof ThrowableRendererSupport) {
8 renderer = ((ThrowableRendererSupport) repo)
9 .getThrowableRenderer();
10 }
11 }
12 if (renderer == null) {
13 rep = DefaultThrowableRenderer.render(throwable);
14 } else {
15 rep = renderer.doRender(throwable);
16 }
17 public static String[] render(final Throwable throwable) {
18 StringWriter sw = new StringWriter();
19 PrintWriter pw = new PrintWriter(sw);
20 try {
21 throwable.printStackTrace(pw);
22 } catch (RuntimeException ex) {
23 }
24 pw.flush();
25 LineNumberReader reader = new LineNumberReader(new StringReader(
26 sw.toString()));
27 ArrayList lines = new ArrayList();
28 try {
29 String line = reader.readLine();
30 while (line != null) {
31 lines.add(line);
32 line = reader.readLine();
33 }
34 } catch (IOException ex) {
35 if (ex instanceof InterruptedIOException) {
36 Thread.currentThread().interrupt();
37 }
38 lines.add(ex.toString());
39 }
40 String[] tempRep = new String[lines.size()];
41 lines.toArray(tempRep);
42 return tempRep;
43 }
Layout类
Layout负责将LoggingEvent中的信息格式化成一行日志信息。对不同格式的日志可能还需要提供头和尾等信息。另外有些Layout不会处理异常信息,此时ignoresThrowable()方法返回false,并且异常信息需要Appender来处理,如PatternLayout。
1 public abstract class Layout implements OptionHandler {
2 public final static String LINE_SEP = System.getProperty("line.separator");
3 public final static int LINE_SEP_LEN = LINE_SEP.length();
4 abstract public String format(LoggingEvent event);
5 public String getContentType() {
6 return "text/plain";
7 }
8 public String getHeader() {
9 return null;
10 }
11 public String getFooter() {
12 return null;
13 }
14 abstract public boolean ignoresThrowable();
15 }
Layout的实现比较简单,如SimpleLayout对一行日志信息只是打印日志级别信息以及日志信息。
1 public class SimpleLayout extends Layout {
2 StringBuffer sbuf = new StringBuffer(128);
3 public SimpleLayout() {
4 }
5 public void activateOptions() {
6 }
7 public String format(LoggingEvent event) {
8 sbuf.setLength(0);
9 sbuf.append(event.getLevel().toString());
10 sbuf.append(" - ");
11 sbuf.append(event.getRenderedMessage());
12 sbuf.append(LINE_SEP);
13 return sbuf.toString();
14 }
15 public boolean ignoresThrowable() {
16 return true;
17 }
18 }
关于Layout更详细的信息将会在以后小节中介绍。
Appender接口
Appender负责定义日志输出的目的地,它可以是控制台(ConsoleAppender)、文件(FileAppender)、JMS服务器(JmsLogAppender)、以Email的形式发送出去(SMTPAppender)等。Appender是一个命名的实体,另外它还包含了对Layout、ErrorHandler、Filter等引用:
1 public interface Appender {
2 void addFilter(Filter newFilter);
3 public Filter getFilter();
4 public void clearFilters();
5 public void close();
6 public void doAppend(LoggingEvent event);
7 public String getName();
8 public void setErrorHandler(ErrorHandler errorHandler);
9 public ErrorHandler getErrorHandler();
10 public void setLayout(Layout layout);
11 public Layout getLayout();
12 public void setName(String name);
13 public boolean requiresLayout();
14 }
简单的,在配置文件中,Appender会注册到Logger中,Logger在写日志时,通过继承机制遍历所有注册到它本身和其父节点的Appender(在additivity为true的情况下),调用doAppend()方法,实现日志的写入。在doAppend方法中,若当前Appender注册了Filter,则doAppend还会判断当前日志时候通过了Filter的过滤,通过了Filter的过滤后,如果当前Appender继承自SkeletonAppender,还会检查当前日志级别时候要比当前Appender本身的日志级别阀门要打,所有这些都通过后,才会将LoggingEvent实例传递给Layout实例以格式化成一行日志信息,最后写入相应的目的地,在这些操作中,任何出现的错误都由ErrorHandler字段来处理。
SkeletonAppender类
目前Log4J实现的Appender都继承自SkeletonAppender类,该类对Appender接口提供了最基本的实现,并且引入了Threshold的概念,即所有的比当前Appender定义的日志级别阀指要大的日志才会记录下来。
1 public abstract class AppenderSkeleton implements Appender, OptionHandler {
2 protected Layout layout;
3 protected String name;
4 protected Priority threshold;
5 protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();
6 protected Filter headFilter;
7 protected Filter tailFilter;
8 protected boolean closed = false;
9 public AppenderSkeleton() {
10 super();
11 }
12 public void activateOptions() {
13 }
14 abstract protected void append(LoggingEvent event);
15 public boolean isAsSevereAsThreshold(Priority priority) {
16 return ((threshold == null) || priority.isGreaterOrEqual(threshold));
17 }
18 public synchronized void doAppend(LoggingEvent event) {
19 if (closed) {
20 LogLog.error("Attempted to append to closed appender named ["
21 + name + "].");
22 return;
23 }
24 if (!isAsSevereAsThreshold(event.getLevel())) {
25 return;
26 }
27 Filter f = this.headFilter;
28 FILTER_LOOP: while (f != null) {
29 switch (f.decide(event)) {
30 case Filter.DENY:
31 return;
32 case Filter.ACCEPT:
33 break FILTER_LOOP;
34 case Filter.NEUTRAL:
35 f = f.getNext();
36 }
37 }
38 this.append(event);
39 }
40 public void finalize() {
41 if (this.closed)
42 return;
43 LogLog.debug("Finalizing appender named [" + name + "].");
44 close();
45 }
46 }
SkeletonAppender实现了doAppend()方法,它首先检查日志级别是否要比threshold要大;然后如果注册了Filter,则使用Filter对LoggingEvent实例进行过滤,如果Filter返回Filter.DENY则doAppend()退出,否则执行append()方法,该方法由子类实现。
在Log4J中,Filter组成一条链,它定了以decide()方法,由子类实现,若返回DENY则日志不会被记录、NEUTRAL则继续检查下一个Filter实例、ACCEPT则Filter通过,继续执行后面的写日志操作。使用Filter可以为Appender加入一些出了threshold以外的其他逻辑,由于它本身是链状的,而且它的执行是横跨在Appender的doAppend方法中,因而这也是一个典型的AOP的概念。Filter的实现将会在下一小节中讲解。
SkeletonAppender还重写了finalize()方法,这是因为Log4J本身作为一个组件,它可能还是通过其他组件如commons-logging或slf4j组件间接的引入,因而使用它的程序不应该对它存在依赖的,然而在程序退出之前所有的Appender需要调用close()方法以释放它所占据的资源,为了不在使用Log4J的程序手动的close()的方法,以减少Log4J代码的侵入性,因而Log4J将close()的方法调用加入到finalize()方法中,即在垃圾回收器回收Appender实例时就会调用它的close()方法。
WriterAppender类和ConsoleAppender类
WriterAppender将日志写入Java IO中,它继承自SkeletonAppender类。它引入了三个字段:immediateFlush,指定没写完一条日志后,即将日志内容刷新到设备中,虽然这么做会有一点性能上的损失,但是如果不怎么做,则会出现在程序异常终止的时候无法看到部分日志信息,而经常这些丢失的日志信息要用于分析为什么会出现异常终止的情况,因而一般推荐将该值设置为true,即默认值;econding用于定义日志文本的编码方式;qw定义写日志的writer,它可以是文件或是控制台等Java IO支持的流。
在写日志文本前,WriterAppender还会做其他检查,如该Appender不能已经closed、qw和layout必须有值等,而后才可以将layout格式化后的日志行写入设备中。若layout本身不处理异常问题,则有Appender处理异常问题。最后如果每行日志需要刷新,则调用刷新操作。
1 public class WriterAppender extends AppenderSkeleton {
2 protected boolean immediateFlush = true;
3 protected String encoding;
4 protected QuietWriter qw;
5 public WriterAppender() {
6 }
7 public WriterAppender(Layout layout, OutputStream os) {
8 this(layout, new OutputStreamWriter(os));
9 }
10 public WriterAppender(Layout layout, Writer writer) {
11 this.layout = layout;
12 this.setWriter(writer);
13 }
14 public void append(LoggingEvent event) {
15 if (!checkEntryConditions()) {
16 return;
17 }
18 subAppend(event);
19 }
20 protected boolean checkEntryConditions() {
21 if (this.closed) {
22 LogLog.warn("Not allowed to write to a closed appender.");
23 return false;
24 }
25 if (this.qw == null) {
26 errorHandler
27 .error("No output stream or file set for the appender named ["
28 + name + "].");
29 return false;
30 }
31 if (this.layout == null) {
32 errorHandler.error("No layout set for the appender named [" + name
33 + "].");
34 return false;
35 }
36 return true;
37 }
38 protected void subAppend(LoggingEvent event) {
39 this.qw.write(this.layout.format(event));
40 if (layout.ignoresThrowable()) {
41 String[] s = event.getThrowableStrRep();
42 if (s != null) {
43 int len = s.length;
44 for (int i = 0; i < len; i++) {
45 this.qw.write(s[i]);
46 this.qw.write(Layout.LINE_SEP);
47 }
48 }
49 }
50 if (shouldFlush(event)) {
51 this.qw.flush();
52 }
53 }
54 public boolean requiresLayout() {
55 return true;
56 }
57 }
ConsoleAppender继承自WriterAppender,它只是简单的将System.out或System.err实例传递给WriterAppender以构建相应的writer,最后实现将日志写入到控制台中。
Filter类
在Log4J中,Filter组成一条链,它定了以decide()方法,由子类实现,若返回DENY则日志不会被记录、NEUTRAL则继续检查下一个Filter实例、ACCEPT则Filter通过,继续执行后面的写日志操作。使用Filter可以为Appender加入一些出了threshold以外的其他逻辑,由于它本身是链状的,而且它的执行是横跨在Appender的doAppend方法中,因而这也是一个典型的AOP的概念。
1 public abstract class Filter implements OptionHandler {
2 public Filter next;
3 public static final int DENY = -1;
4 public static final int NEUTRAL = 0;
5 public static final int ACCEPT = 1;
6 public void activateOptions() {
7 }
8 abstract public int decide(LoggingEvent event);
9 public void setNext(Filter next) {
10 this.next = next;
11 }
12 public Filter getNext() {
13 return next;
14 }
15 }
Log4J本身提供了四个Filter:DenyAllFilter、LevelMatchFilter、LevelRangeFilter、StringMatchFilter。
DenyAllFilter只是简单的在decide()中返回DENY值,可以将其应用在Filter链尾,实现如果之前的Filter都没有通过,则该LoggingEvent没有通过,类似或的操作:
1 public class DenyAllFilter extends Filter {
2 public int decide(LoggingEvent event) {
3 return Filter.DENY;
4 }
5 }
StringMatchFilter通过日志消息中的字符串来判断Filter后的状态:
1 public class StringMatchFilter extends Filter {
2 boolean acceptOnMatch = true;
3 String stringToMatch;
4 public int decide(LoggingEvent event) {
5 String msg = event.getRenderedMessage();
6 if (msg == null || stringToMatch == null)
7 return Filter.NEUTRAL;
8 if (msg.indexOf(stringToMatch) == -1) {
9 return Filter.NEUTRAL;
10 } else { // we've got a match
11 if (acceptOnMatch) {
12 return Filter.ACCEPT;
13 } else {
14 return Filter.DENY;
15 }
16 }
17 }
18 }
LevelMatchFilter判断日志级别是否和设置的级别匹配以决定Filter后的状态:
1 public class LevelMatchFilter extends Filter {
2 boolean acceptOnMatch = true;
3 Level levelToMatch;
4 public int decide(LoggingEvent event) {
5 if (this.levelToMatch == null) {
6 return Filter.NEUTRAL;
7 }
8 boolean matchOccured = false;
9 if (this.levelToMatch.equals(event.getLevel())) {
10 matchOccured = true;
11 }
12 if (matchOccured) {
13 if (this.acceptOnMatch)
14 return Filter.ACCEPT;
15 else
16 return Filter.DENY;
17 } else {
18 return Filter.NEUTRAL;
19 }
20 }
21 }
LevelRangeFilter判断日志级别是否在设置的级别范围内以决定Filter后的状态:
1 public class LevelRangeFilter extends Filter {
2 boolean acceptOnMatch = false;
3 Level levelMin;
4 Level levelMax;
5 public int decide(LoggingEvent event) {
6 if (this.levelMin != null) {
7 if (event.getLevel().isGreaterOrEqual(levelMin) == false) {
8 return Filter.DENY;
9 }
10 }
11 if (this.levelMax != null) {
12 if (event.getLevel().toInt() > levelMax.toInt()) {
13 return Filter.DENY;
14 }
15 }
16 if (acceptOnMatch) {
17 return Filter.ACCEPT;
18 } else {
19 return Filter.NEUTRAL;
20 }
21 }
22 }
总结
这一系列终于是结束了。本文主要介绍了Log4J核心类的实现和他们之间的交互关系。涉及到各个模块本身的其他详细信息将会在接下来的小节中详细介绍,如LogRepository与配置信息、Appender类结构的详细信息、Layout类结构的详细信息以及部分LoggingEvent提供的高级功能。而像Level、Logger本身,由于内容不多,已经在这一小节中全部介绍完了
转自:http://www.blogjava.net/DLevin/archive/2012/06/28/381667.html。
在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析工作;跟踪代码运行时轨迹,作为日后审计的依据;担当集成开发环境中的调试器的作用,向文件或控制台打印代码的调试信息。
最普通的做法就是在代码中嵌入许多的打印语句,这些打印语句可以输出到控制台或文件中,比较好的做法就是构造一个日志操作类来封装此类操作,而不是让一系列的打印语句充斥了代码的主体。
1.2. Log4j简介
在强调可重用组件开发的今天,除了自己从头到尾开发一个可重用的日志操作类外,Apache为我们提供了一个强有力的日志操作包-Log4j。
Log4j是Apache的一个开放源代码项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务 器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就 是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
此外,通过Log4j其他语言接口,您可以在C、C+ +、.Net、PL/SQL程序中使用Log4j,其语法和用法与在Java程序中一样,使得多语言分布式系统得到一个统一一致的日志组件模块。而且,通 过使用各种第三方扩展,您可以很方便地将Log4j集成到J2EE、JINI甚至是SNMP应用中。
Log4j有三个主要的组件:
Loggers(记录器),Appenders (输出源)和Layouts(布局),这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出。综合使用这三个组件可以轻松的记录信息的类型和 级别,并可以在运行时控制日志输出的样式和位置。下面对三个组件分别进行说明:
1、 Loggers
Loggers组件在此系统中被分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的,DEBUG < INFO < WARN < ERROR < FATAL,分别用来指定这条日志信息的重要程度,明白这一点很重要,这里Log4j有一个规则:假设Loggers级别为P,如果在Loggers中发 生了一个级别Q比P高,则可以启动,否则屏蔽掉。
假设你定义的级别是info,那么error和warn的日志可以显示而比他低的debug信息就不显示了。
Java程序举例来说:
***建立Logger的一个实例,命名为“com.foo”***
Logger logger = Logger.getLogger("com.foo");
***"com.foo"是实例进行命名,也可以任意***
***设置logger的级别。通常不在程序中设置logger的级别。一般在配置文件中设置。***
logger.setLevel(Level.INFO);
Logger barlogger = Logger.getLogger("com.foo.Bar");
***下面这个请求可用,因为WARN >= INFO***
logger.warn("Low fuel level.");
***下面这个请求不可用,因为DEBUG < INFO***
logger.debug("Starting search for nearest gas station.");
***命名为“com.foo.bar”的实例barlogger会继承实例“com.foo”的级别。因此,下面这个请求可用,因为INFO >= INFO***
barlogger.info("Located nearest gas station.");
***下面这个请求不可用,因为DEBUG < INFO***
barlogger.debug("Exiting gas station search");
这里“是否可用”的意思是能否输出Logger信息。
在对Logger实例进行命名时,没有限制,可以取任意自己感兴趣的名字。一般情况下建议以类的所在位置来命名Logger实例,这是目前来讲比较有效的Logger命名方式。这样可以使得每个类建立自己的日志信息,便于管理。比如:
static Logger logger = Logger.getLogger(ClientWithLog4j.class.getName());
2、Appenders
禁用与使用日志请求只是Log4j其中的一个小小的地方,Log4j日志系统允许把日志输出到不同的地方,如控制台(Console)、文件(Files)、根据天数或者文件大小产生新的文件、以流的形式发送到其它地方等等。
其语法表示为:
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
配置时使用方式为:
log4j.appender.appenderName = fully.qualified.name.of.appender.class
log4j.appender.appenderName.option1 = value1
…
log4j.appender.appenderName.option = valueN
这样就为日志的输出提供了相当大的便利。
3、Layouts
有时用户希望根据自己的喜好格式化自己的日志输出。Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供了 四种日志输出样式,如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式等等。
其语法表示为:
org.apache.log4j.HTMLLayout(以HTML表格形式布局),
org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
配置时使用方式为:
log4j.appender.appenderName.layout =fully.qualified.name.of.layout.class
log4j.appender.appenderName.layout.option1 = value1
…
log4j.appender.appenderName.layout.option = valueN
4 . Log4j的配置
以上是从原理方面说明Log4j的使用方法,在具体Java编程使用Log4j可以参照以下示例:
1、 建立Logger实例:
语法表示:public static Logger getLogger( String name)
实际使用:static Logger logger = Logger.getLogger(ServerWithLog4j.class.getName ()) ;
2、 读取配置文件:
获得了Logger的实例之后,接下来将配置Log4j使用环境:
语法表示:
BasicConfigurator.configure():自动快速地使用缺省Log4j环境。
PropertyConfigurator.configure(String configFilename):读取使用Java的特性文件编写的配置文件。
DOMConfigurator.configure(String filename):读取XML形式的配置文件。
实际使用:
PropertyConfigurator.configure("ServerWithLog4j.properties");
3、 插入日志信息
完成了以上连个步骤以后,下面就可以按日志的不同级别插入到你要记录日志的任何地方了。
语法表示:
Logger.debug(Object message);//调试信息
Logger.info(Object message);//一般信息
Logger.warn(Object message);//警告信息
Logger.error(Object message);//错误信息
Logger.fatal(Object message);//致命错误信息
实际使用:logger.info("ServerSocket before accept: " + server);
5. 配置过程
在实际编程时,要使Log4j真正在系统中运行事先还要对配置文件进行定义。定义步骤就是对Logger、Appender及Layout的分别使用。
Log4j支持两种配置文件格式,一种是XML格式的文件,一种是java properties(key=value)【Java特性文件(键=值)】。下面我们介绍使用Java特性文件做为配置文件的方法
具体如下:
1、配置根Logger,其语法为:
log4j.rootLogger = [ level ] , appenderName1, appenderName2, …
level : 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。Log4j建议只使用四个级别,优 先级从高到低分别是ERROR、WARN、INFO、DEBUG。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定 义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。
appenderName:就是指定日志信息输出到哪个地方。您可以同时指定多个输出目的地。
例如:log4j.rootLogger=info,A1,B2,C3
2、配置日志信息输出目的地,其语法为:
log4j.appender.appenderName = fully.qualified.name.of.appender.class //
"fully.qualified.name.of.appender.class" 可以指定下面五个目的地中的一个:
1.org.apache.log4j.ConsoleAppender(控制台)
2.org.apache.log4j.FileAppender(文件)
3.org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
4.org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
5.org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
1.ConsoleAppender选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
Target=System.err:默认情况下是:System.out,指定输出控制台
2.FileAppender 选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt:指定消息输出到mylog.txt文件。
Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
3.DailyRollingFileAppender 选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt:指定消息输出到mylog.txt文件。
Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
DatePattern='.'yyyy-ww:每周滚动一次文件,即每周产生一个新的文件。当然也可以指定按月、周、天、时和分。即对应的格式如下:
1)'.'yyyy-MM: 每月
2)'.'yyyy-ww: 每周
3)'.'yyyy-MM-dd: 每天
4)'.'yyyy-MM-dd-a: 每天两次
5)'.'yyyy-MM-dd-HH: 每小时
6)'.'yyyy-MM-dd-HH-mm: 每分钟
4.RollingFileAppender 选项
Threshold=WARN:指定日志消息的输出最低层次。
ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt:指定消息输出到mylog.txt文件。
Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
MaxFileSize=100KB: 后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。
MaxBackupIndex=2:指定可以产生的滚动文件的最大数。
实际应用:
log4j.appender.A1=org.apache.log4j.ConsoleAppender //这里指定了日志输出的第一个位置A1是控制台ConsoleAppender
3、配置日志信息的格式,其语法为:
A. log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class
"fully.qualified.name.of.layout.class" 可以指定下面4个格式中的一个:
1.org.apache.log4j.HTMLLayout(以HTML表格形式布局),
2.org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
3.org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
4.org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
1.HTMLLayout 选项
LocationInfo=true:默认值是false,输出java文件名称和行号
Title=my app file: 默认值是 Log4J Log Messages.
2.PatternLayout 选项
ConversionPattern=%m%n :指定怎样格式化指定的消息。
3.XMLLayout 选项
LocationInfo=true:默认值是false,输出java文件和行号
实际应用:
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
B. log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n
这里需要说明的就是日志信息格式中几个符号所代表的含义:
-X号: X信息输出时左对齐;
%p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
%d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%r: 输出自应用启动到输出该log信息耗费的毫秒数
%c: 输出日志信息所属的类目,通常就是所在类的全名
%t: 输出产生该日志事件的线程名
%l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
%%: 输出一个"%"字符
%F: 输出日志消息产生时所在的文件名称
%L: 输出代码中的行号
%m: 输出代码中指定的消息,产生的日志具体信息
%n: 输出一个回车换行符,Windows平台为"\r\n",Unix平台为"\n"输出日志信息换行
可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。如:
1)%20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,默认的情况下右对齐。
2)%-20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,"-"号指定左对齐。
3)%.30c:指定输出category的名称,最大的宽度是30,如果category的名称大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
4)%20.30c:如果category的名称小于20就补空格,并且右对齐,如果其名称长于30字符,就从左边交远销出的字符截掉。
这里上面三个步骤是对前面Log4j组件说明的一个简化;下面给出一个具体配置例子,在程序中可以参照执行:
log4j.rootLogger=INFO,A1,B2
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n
根据上面的日志格式,某一个程序的输出结果如下:
0 INFO 2003-06-13 13:23:46968 ClientWithLog4j Client socket: Socket[addr=localhost/127.0.0.1,port=8002,localport=2014]
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server says: 'Java server with log4j, Fri Jun 13 13:23:46 CST 2003'
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j GOOD
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server responds: 'Command 'HELLO' not understood.'
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j HELP
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server responds: 'Vocabulary: HELP QUIT'
16 DEBUG 2003-06-13 13:23:46984 ClientWithLog4j QUIT
4. # 当输出信息于回滚文件时
log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender //指定以文件的方式输出日志
log4j.appender.ROLLING_FILE.Threshold=ERROR
log4j.appender.ROLLING_FILE.File=rolling.log //文件位置,也可以用变量${java.home}、rolling.log
log4j.appender.ROLLING_FILE.Append=true
log4j.appender.ROLLING_FILE.MaxFileSize=10KB //文件最大尺寸
log4j.appender.ROLLING_FILE.MaxBackupIndex=1 //备份数
log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
6. Log4j比较全面的配置
LOG4J的配置之简单使它遍及于越来越多的应用中了:Log4J配置文件实现了输出到控制台、文件、回滚文件、发送日志邮件、输出到数据库日志表、自定义标签等全套功能。择其一二使用就够用了,
log4j.rootLogger=DEBUG,CONSOLE,A1,im
log4j.addivity.org.apache=true
# 应用于控制台
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.Threshold=DEBUG
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
#log4j.appender.CONSOLE.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[thread] n%c[CATEGORY]%n%m[MESSAGE]%n%n
#应用于文件
log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=file.log
log4j.appender.FILE.Append=false
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
# Use this layout for LogFactor 5 analysis
# 应用于文件回滚
log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender
log4j.appender.ROLLING_FILE.Threshold=ERROR
log4j.appender.ROLLING_FILE.File=rolling.log //文件位置,也可以用变量${java.home}、rolling.log
log4j.appender.ROLLING_FILE.Append=true //true:添加 false:覆盖
log4j.appender.ROLLING_FILE.MaxFileSize=10KB //文件最大尺寸
log4j.appender.ROLLING_FILE.MaxBackupIndex=1 //备份数
log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
#应用于socket
log4j.appender.SOCKET=org.apache.log4j.RollingFileAppender
log4j.appender.SOCKET.RemoteHost=localhost
log4j.appender.SOCKET.Port=5001
log4j.appender.SOCKET.LocationInfo=true
# Set up for Log Facter 5
log4j.appender.SOCKET.layout=org.apache.log4j.PatternLayout
log4j.appender.SOCET.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[thread]%n%c[CATEGORY]%n%m[MESSAGE]%n%n
# Log Factor 5 Appender
log4j.appender.LF5_APPENDER=org.apache.log4j.lf5.LF5Appender
log4j.appender.LF5_APPENDER.MaxNumberOfRecords=2000
# 发送日志给邮件
log4j.appender.MAIL=org.apache.log4j.net.SMTPAppender
log4j.appender.MAIL.Threshold=FATAL
log4j.appender.MAIL.BufferSize=10
log4j.appender.MAIL.From=web@www.wuset.com
log4j.appender.MAIL.SMTPHost=www.wusetu.com
log4j.appender.MAIL.Subject=Log4J Message
log4j.appender.MAIL.To=web@www.wusetu.com
log4j.appender.MAIL.layout=org.apache.log4j.PatternLayout
log4j.appender.MAIL.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
# 用于数据库
log4j.appender.DATABASE=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.DATABASE.URL=jdbc:mysql://localhost:3306/test
log4j.appender.DATABASE.driver=com.mysql.jdbc.Driver
log4j.appender.DATABASE.user=root
log4j.appender.DATABASE.password=
log4j.appender.DATABASE.sql=INSERT INTO LOG4J (Message) VALUES ('[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n')
log4j.appender.DATABASE.layout=org.apache.log4j.PatternLayout
log4j.appender.DATABASE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
log4j.appender.A1=org.apache.log4j.DailyRollingFileAppender
log4j.appender.A1.File=SampleMessages.log4j
log4j.appender.A1.DatePattern=yyyyMMdd-HH'.log4j'
log4j.appender.A1.layout=org.apache.log4j.xml.XMLLayout
#自定义Appender
log4j.appender.im = net.cybercorlin.util.logger.appender.IMAppender
log4j.appender.im.host = mail.cybercorlin.net
log4j.appender.im.username = username
log4j.appender.im.password = password
log4j.appender.im.recipient = corlin@cybercorlin.net
log4j.appender.im.layout=org.apache.log4j.PatternLayout
log4j.appender.im.layout.ConversionPattern =[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
try { String file = "fx_log4j.properties"; InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(file); Properties log4jProperties = new Properties(); log4jProperties.load(is); if (log4jProperties != null) PropertyConfigurator.configure(log4jProperties); } catch (Exception exception) { exception.printStackTrace(); System.err.println("Unable to load log4j configuration file. " + exception.getMessage()); }
Log4J将写日志功能抽象成七个核心类/接口:Logger、LoggerRepository、Level、LoggingEvent、Appender、Layout、ObjectRender。其类图如下:
更详细的,实现Log4J主要功能相关的类图:
其实Log4J最核心的也就5个类:Logger用于对日志记录行为的抽象,提供记录不同级别日志的接口;Level对日志级别的抽象;Appender是对记录日志形式的抽象;Layout是对日志行格式的抽象;而LoggingEvent是对一次日志记录过程中所能取到信息的抽象。另外两个LoggerRepository是Logger实例的容器,而ObjectRender是对日志实例的解析接口,它们主要提供了一种扩展支持。
简单的一次记录日志过程的序列图如下:
即获取Logger实例->判断Logger实例对应的日志记录级别是否要比请求的级别低->若是调用forceLog记录日志->创建LoggingEvent实例->将LoggingEvent实例传递给Appender->Appender调用Layout实例格式化日志消息->Appender将格式化后的日志信息写入该Appender对应的日志输出中。
包含Log4J其他模块类的更详细序列图如下:
在简单的介绍了Log4J各个模块类的作用后,以下将详细的介绍各个模块的具体作用以及代码实现。
Logger类
Logger是对记录日志动作的抽象,它提供了记录不同级别日志的接口,日志信息可以包含异常信息也可以不包含:
1 public void debug(Object message) {
2 if(isLevelEnabled(Level.DEBUG)) {
3 forceLog(FQCN, Level.DEBUG, message, null);
4 }
5 }
6 public void debug(Object message, Throwable cause) {
7 if(isLevelEnabled(Level.DEBUG)) {
8 forceLog(FQCN, Level.DEBUG, message, cause);
9 }
10 }
11 protected void forceLog(String fqcn, Level level, Object message, Throwable t) {
12 callAppenders(new LoggingEvent(fqcn, this, level, message, t));
13 }
Logger类包含Level信息 ,如果当前Logger未设置Level值,它也可以中父节点中继承下来,该值可以用来控制该Logger可以记录的日志级别:
1 protected Level level;
2 public Level getEffectiveLevel() {
3 for(Logger logger = this; logger != null; logger = logger.parent) {
4 if(logger.level != null) {
5 return logger.level;
6 }
7 }
8 return null;
9 }
10 public boolean isLevelEnabled(Level level) {
11 return level.isGreaterOrEqual(this.getEffectiveLevel());
12 }
13 public boolean isDebugEnabled() {
14 return isLevelEnabled(Level.DEBUG);
15 }
Logger是一个命名的实体,其名字一般用”.”分割以体现不同Logger的层次关系,其中Level和Appender信息可以从父节点中获取,因而Logger类中还具有name和parent属性。
1 private String name;
2 protected Logger parent;
在某些情况下,我们希望某些Logger只将日志记录到特定的Appender中,而不想记录在父节点中的Appender中,Log4J为这种需求提供了additivity属性,即对当前Logger节点,如果其additivity属性设置为false,则该Logger不会继承父节点的Appender信息,但是其子节点依然会继承该Logger的Appender信息,除非子节点的additivity属性也设置成了false。
1 private boolean additive = true;
2 public void callAppenders(LoggingEvent event) {
3 int writes = 0;
4
5 for(Logger logger = this; logger != null; logger = logger.parent) {
6 synchronized(logger) {
7 if(logger.appenders != null) {
8 writes += logger.appenders.appendLoopOnAppenders(event);
9 }
10 if(!logger.additive) {
11 break;
12 }
13 }
14 }
15
16 if(writes == 0) {
17 System.err.println("No Appender is configed.");
18 }
19 }
最后,为了支持国际化,Log4J还提供了两个l7dlog()方法,通过指定的key,以从资源文件中获取消息内容。为了使用这两个方法,需要设置资源文件。同样,资源文件也是可以从父节点中继承的。
1 private ResourceBundle resourceBundle;
2 public void l7dlog(Level level, String key, Throwable cause) {
3 if(isLevelEnabled(level)) {
4 String message = getResourceBundleString(key);
5 if(message == null) {
6 message = key;
7 }
8 forceLog(FQCN, level, message, cause);
9 }
10 }
11
12 public void l7dlog(Level level, String key, Object[] params, Throwable cause) {
13
14 if(pattern == null) {
15 message = key;
16 } else {
17 message = MessageFormat.format(pattern, params);
18 }
19
20 }
21
22 protected String getResourceBundleString(String key) {
23 ResourceBundle rb = getResourceBundle();
24
25 return rb.getString(key);
26 }
27 public ResourceBundle getResourceBundle() {
28 for(Logger logger = this; logger != null; logger = logger.parent) {
29 if(logger.resourceBundle != null) {
30 return logger.resourceBundle;
31 }
32 }
33 return null;
34 }
另外,在实际开发中经常会遇到要把日志信息同时写到不同地方,如同时写入文件和控制台,因而一个Logger实例中可以包含多个Appender,为了管理多个Appender,Log4J抽象出了AppenderAttachable接口,它定义了几个用于管理多个Appender实例的方法,这些方法由AppenderAttachableImpl类实现,而Logger会实例化AppenderAttachableImpl,并将这些方法代理给该实例:
1 public interface AppenderAttachable {
2 public void addAppender(Appender newAppender);
3 public Enumeration getAllAppenders();
4 public Appender getAppender(String name);
5 public boolean isAttached(Appender appender);
6 void removeAllAppenders();
7 void removeAppender(Appender appender);
8 void removeAppender(String name);
9 }
RootLogger类
在Log4J中,所有Logger实例组成一个单根的树状结构,由于Logger实例的根节点有一点特殊:它的名字为“root”,它没有父节点,它的Level字段必须设值以防止其他Logger实例都没有设置Level值的情况。基于这些考虑,Log4J通过继承Logger类实现了RootLogger类,它用于表达所有Logger实例的根节点:
1 public final class RootLogger extends Logger {
2 public RootLogger(Level level) {
3 super("root");
4 setLevel(level);
5 }
6 public final Level getChainedLevel() {
7 return level;
8 }
9 public final void setLevel(Level level) {
10 if (level == null) {
11 LogLog.error("You have tried to set a null level to root.",
12 new Throwable());
13 } else {
14 this.level = level;
15 }
16 }
17 }
NOPLogger类
有时候,为了测试等其他需求,我们希望Logger本身不做什么事情,Log4J为这种需求提供了NOPLogger类,它继承自Logger,但是基本上的方法都为空。
Level类
Level是对日志级别的抽象,目前Log4J支持的级别有FATAL、ERROR、WARN、INFO、DEBUG、TRACE,从头到尾一次级别递减,另外Log4J还支持两种特殊的级别:ALL和OFF,它们分别表示打开和关闭日志功能。
1 public static final int OFF_INT = Integer.MAX_VALUE;
2 public static final int FATAL_INT = 50000;
3 public static final int ERROR_INT = 40000;
4 public static final int WARN_INT = 30000;
5 public static final int INFO_INT = 20000;
6 public static final int DEBUG_INT = 10000;
7 public static final int TRACE_INT = 5000;
8 public static final int ALL_INT = Integer.MIN_VALUE;
9
10 public static final Level OFF = new Level(OFF_INT, "OFF", 0);
11 public static final Level FATAL = new Level(FATAL_INT, "FATAL", 0);
12 public static final Level ERROR = new Level(ERROR_INT, "ERROR", 3);
13 public static final Level WARN = new Level(WARN_INT, "WARN", 4);
14 public static final Level INFO = new Level(INFO_INT, "INFO", 6);
15 public static final Level DEBUG = new Level(DEBUG_INT, "DEBUG", 7);
16 public static final Level TRACE = new Level(TRACE_INT, "TRACE", 7);
17 public static final Level ALL = new Level(ALL_INT, "ALL", 7);
每个Level实例包含了该Level代表的int值(一般是从级别低到级别高一次增大)、该Level的String表达、该Level和系统Level的对应值。
1 protected transient int level;
2 protected transient String levelStr;
3 protected transient int syslogEquivalent;
4 protected Level(int level, String levelStr, int syslogEquivalent) {
5 this.level = level;
6 this.levelStr = levelStr;
7 this.syslogEquivalent = syslogEquivalent;
8 }
Level类主要提供了判断哪个Level级别更高的方法isGreaterOrEqual()以及将int值或String值转换成Level实例的toLevel()方法:
1 public boolean isGreaterOrEqual(Level level) {
2 return this.level >= level.level;
3 }
4 public static Level toLevel(int level) {
5 return toLevel(level, DEBUG);
6 }
7 public static Level toLevel(int level, Level defaultLevel) {
8 switch(level) {
9 case OFF_INT: return OFF;
10 case FATAL_INT: return FATAL;
11 case ERROR_INT: return ERROR;
12 case WARN_INT: return WARN;
13 case INFO_INT: return INFO;
14 case DEBUG_INT: return DEBUG;
15 case TRACE_INT: return TRACE;
16 case ALL_INT: return ALL;
17 }
18 return defaultLevel;
19 }
另外,由于对相同级别的Level实例来说,它必须是单例的,因而Log4J对序列化和反序列化做了一些处理。即它的三个成员都是transient,真正序列化和反序列化的代码自己写,并且加入readResolve()方法的支持,以保证反序列化出来的相同级别的Level实例是相同的实例。
1 private void readObject(final ObjectInputStream input) throws IOException, ClassNotFoundException {
2 input.defaultReadObject();
3 level = input.readInt();
4 syslogEquivalent = input.readInt();
5 levelStr = input.readUTF();
6 if(levelStr == null) {
7 levelStr = "";
8 }
9 }
10 private void writeObject(final ObjectOutputStream output) throws IOException {
11 output.defaultWriteObject();
12 output.writeInt(level);
13 output.writeInt(syslogEquivalent);
14 output.writeUTF(levelStr);
15 }
16 private Object readResolve() throws ObjectStreamException {
17 if(this.getClass() == Level.class) {
18 return toLevel(level);
19 }
20 return this;
21 }
如果要实现自己的Level类,可以继承自Level,并且实现相应的静态toLevel()方法即可。关于如何实现自己的Level类将会在配置文件相关小节中详细讨论。
LoggerRepository类
LoggerRepository从概念以及字面上来说它就是一个Logger实例的容器:一方面相同名字的Logger实例只需要创建一次,在后面的使用中,只需要从这个容器中取即可;另一方面,Logger容器可以存放从配置文件中解析出来的信息,从而使配置信息可以无缝的应用到Log4J内部系统中;最后Logger容器还为维护Logger的树状层次结构提供了方面,每个Logger只维护父节点的信息,有了Logger容器的存在则可以很容易的找到一个新的Logger实例的父节点;关于Logger容器将在下一节中详细讲解。
LoggingEvent类
LoggingEvent个人感觉用LoggingContext更合适一些,它是对一次日志记录时哪能获取到的数据的封装。它包含了以下信息以提供Layout在format()方法中使用:
1. fqnOfCategoryClass:日志记录接口(默认为Logger)的类全名,该信息主要用于计算日志记录点的源文件、调用方法以及行号等位置信息。
2. locationInfo:通过fqnOfCategoryClass计算位置信息,位置信息的计算由LocationInfo类实现,这些信息可以提供给Layout使用。
3. logger:目前来看主要是通过Logger实例取得LogRepository实例,并通过LogRepository取得注册的ObjectRender实例,如果有的话。
4. loggerName:当前日志记录的Logger名称,提供给Layout使用。
5. threadName:当前线程名,提供给Layout使用。
6. level:当前日志的级别,提供给Layout使用。
7. message:当前日志类,一般是String类型,但是也可以通过注册ObjectRender,然后传入相应的其他对象类型。
8. renderedMessage:经过ObjectRender处理后的日志信息,提供给Layout使用。
9. throwableInfo:异常信息,如果存在的话,提供给Layout使用。
10. timestamp:创建LoggingEvent实例的时间,提供给Layout使用。
11. 其他相对不常用的信息将会在后面小节中讲解。
LoggingEvent只是一个简单的数据对象(DO),因而其实现还是比较简单的,即在创建实例时将数据提供给它,在其他类(Layout等)使用它时通过getXXX()方法取数据。不过还是有几个方法可以简单的讲解一下。
LocationInfo类计算位置信息
LocationInfo所指的位置信息主要包括记录日志所在的源文件名、类名、方法名、所在源文件的行号。
1 transient String lineNumber;
2 transient String fileName;
3 transient String className;
4 transient String methodName;
5 //fully.qualified.classname.of.caller.methodName(Filename.java:line)
6 public String fullInfo;
我们知道在异常栈中每一条记录都包含了方法调用对应的这些信息,Log4J的这些信息正是利用了这个原理,即通过构建一个Throwable实例,而后在该Throwable的栈信息中解析出来的:
1 public LocationInfo getLocationInformation() {
2 if (locationInfo == null) {
3 locationInfo = new LocationInfo(new Throwable(),
4 fqnOfCategoryClass);
5 }
6 return locationInfo;
7 }
以上Throwable一般会产生如下异常栈:
1 java.lang.Throwable
2
3 at org.apache.log4j.PatternLayout.format(PatternLayout.java:413)
4 at org.apache.log4j.FileAppender.doAppend(FileAppender.java:183)
5 at org.apache.log4j.Category.callAppenders(Category.java:131)
6 at org.apache.log4j.Category.log(Category.java:512)
7 at callers.fully.qualified.className.methodName(FileName.java:74)
8
因而我们就可以通过callers.fully.qualified.className信息来找到改行信息,这个className信息即是传入的fqnOfCategoryClass。
如果当前JDK版本是1.4以上,我们就可以通过JDK提供的一些方法来查找:
1 getStackTraceMethod = Throwable.class.getMethod("getStackTrace",
2 noArgs);
3 Class stackTraceElementClass = Class
4 .forName("java.lang.StackTraceElement");
5 getClassNameMethod = stackTraceElementClass.getMethod(
6 "getClassName", noArgs);
7 getMethodNameMethod = stackTraceElementClass.getMethod(
8 "getMethodName", noArgs);
9 getFileNameMethod = stackTraceElementClass.getMethod("getFileName",
10 noArgs);
11 getLineNumberMethod = stackTraceElementClass.getMethod(
12 "getLineNumber", noArgs);
13
14 Object[] noArgs = null;
15 Object[] elements = (Object[]) getStackTraceMethod.invoke(t,
16 noArgs);
17 String prevClass = NA;
18 for (int i = elements.length - 1; i >= 0; i--) {
19 String thisClass = (String) getClassNameMethod.invoke(
20 elements[i], noArgs);
21 if (fqnOfCallingClass.equals(thisClass)) {
22 int caller = i + 1;
23 if (caller < elements.length) {
24 className = prevClass;
25 methodName = (String) getMethodNameMethod.invoke(
26 elements[caller], noArgs);
27 fileName = (String) getFileNameMethod.invoke(
28 elements[caller], noArgs);
29 if (fileName == null) {
30 fileName = NA;
31 }
32 int line = ((Integer) getLineNumberMethod.invoke(
33 elements[caller], noArgs)).intValue();
34 if (line < 0) {
35 lineNumber = NA;
36 } else {
37 lineNumber = String.valueOf(line);
38 }
39 StringBuffer buf = new StringBuffer();
40 buf.append(className);
41 buf.append(".");
42 buf.append(methodName);
43 buf.append("(");
44 buf.append(fileName);
45 buf.append(":");
46 buf.append(lineNumber);
47 buf.append(")");
48 this.fullInfo = buf.toString();
49 }
50 return;
51 }
52 prevClass = thisClass;
53 }
否则,则需要我们通过字符串查找的方式来查找:
1 String s;
2 // Protect against multiple access to sw.
3 synchronized (sw) {
4 t.printStackTrace(pw);
5 s = sw.toString();
6 sw.getBuffer().setLength(0);
7 }
8 int ibegin, iend;
9 ibegin = s.lastIndexOf(fqnOfCallingClass);
10 if (ibegin == -1)
11 return;
12 // See bug 44888.
13 if (ibegin + fqnOfCallingClass.length() < s.length()
14 && s.charAt(ibegin + fqnOfCallingClass.length()) != '.') {
15 int i = s.lastIndexOf(fqnOfCallingClass + ".");
16 if (i != -1) {
17 ibegin = i;
18 }
19 }
20
21 ibegin = s.indexOf(Layout.LINE_SEP, ibegin);
22 if (ibegin == -1)
23 return;
24 ibegin += Layout.LINE_SEP_LEN;
25
26 // determine end of line
27 iend = s.indexOf(Layout.LINE_SEP, ibegin);
28 if (iend == -1)
29 return;
30
31 // VA has a different stack trace format which doesn't
32 // need to skip the inital 'at'
33 if (!inVisualAge) {
34 // back up to first blank character
35 ibegin = s.lastIndexOf("at ", iend);
36 if (ibegin == -1)
37 return;
38 // Add 3 to skip "at ";
39 ibegin += 3;
40 }
41 // everything between is the requested stack item
42 this.fullInfo = s.substring(ibegin, iend);
对于通过字符串查找到的fullInfo值,在获取其他单个值时还需要做相应的字符串解析:
className:
1 // Starting the search from '(' is safer because there is
2 // potentially a dot between the parentheses.
3 int iend = fullInfo.lastIndexOf('(');
4 if (iend == -1)
5 className = NA;
6 else {
7 iend = fullInfo.lastIndexOf('.', iend);
8
9 // This is because a stack trace in VisualAge looks like:
10
11 // java.lang.RuntimeException
12 // java.lang.Throwable()
13 // java.lang.Exception()
14 // java.lang.RuntimeException()
15 // void test.test.B.print()
16 // void test.test.A.printIndirect()
17 // void test.test.Run.main(java.lang.String [])
18 int ibegin = 0;
19 if (inVisualAge) {
20 ibegin = fullInfo.lastIndexOf(' ', iend) + 1;
21 }
22
23 if (iend == -1)
24 className = NA;
25 else
26 className = this.fullInfo.substring(ibegin, iend);
fileName:
1
2 int iend = fullInfo.lastIndexOf(':');
3 if (iend == -1)
4 fileName = NA;
5 else {
6 int ibegin = fullInfo.lastIndexOf('(', iend - 1);
7 fileName = this.fullInfo.substring(ibegin + 1, iend);
8 }
lineNumber:
1 int iend = fullInfo.lastIndexOf(')');
2 int ibegin = fullInfo.lastIndexOf(':', iend - 1);
3 if (ibegin == -1)
4 lineNumber = NA;
5 else
6 lineNumber = this.fullInfo.substring(ibegin + 1, iend);
methodName:
1 int iend = fullInfo.lastIndexOf('(');
2 int ibegin = fullInfo.lastIndexOf('.', iend);
3 if (ibegin == -1)
4 methodName = NA;
5 else
6 methodName = this.fullInfo.substring(ibegin + 1, iend);
ObjectRender接口
Log4J中,对传入的message实例,如果是非String类型,会先使用注册的ObjectRender(在LogRepository中查找注册的ObjectRender信息)处理成String后返回,若没有找到相应的ObjectRender,则使用默认的ObjectRender,它只是调用该消息实例的toString()方法。
1 public Object getMessage() {
2 if (message != null) {
3 return message;
4 } else {
5 return getRenderedMessage();
6 }
7 }
8 public String getRenderedMessage() {
9 if (renderedMessage == null && message != null) {
10 if (message instanceof String)
11 renderedMessage = (String) message;
12 else {
13 LoggerRepository repository = logger.getLoggerRepository();
14
15 if (repository instanceof RendererSupport) {
16 RendererSupport rs = (RendererSupport) repository;
17 renderedMessage = rs.getRendererMap()
18 .findAndRender(message);
19 } else {
20 renderedMessage = message.toString();
21 }
22 }
23 }
24 return renderedMessage;
25 }
ThrowableInformation类
ThrowableInformation类用以处理异常栈信息,即通过Throwable实例获取异常栈字符串数组。同时还支持自定义的ThrowableRender(在LogRepository中设置),默认的ThrowableRender通过系统printStackTrace()方法来获取信息:
1 if (throwable != null) {
2 this.throwableInfo = new ThrowableInformation(throwable, logger);
3 }
4 ThrowableRenderer renderer = null;
5 if (category != null) {
6 LoggerRepository repo = category.getLoggerRepository();
7 if (repo instanceof ThrowableRendererSupport) {
8 renderer = ((ThrowableRendererSupport) repo)
9 .getThrowableRenderer();
10 }
11 }
12 if (renderer == null) {
13 rep = DefaultThrowableRenderer.render(throwable);
14 } else {
15 rep = renderer.doRender(throwable);
16 }
17 public static String[] render(final Throwable throwable) {
18 StringWriter sw = new StringWriter();
19 PrintWriter pw = new PrintWriter(sw);
20 try {
21 throwable.printStackTrace(pw);
22 } catch (RuntimeException ex) {
23 }
24 pw.flush();
25 LineNumberReader reader = new LineNumberReader(new StringReader(
26 sw.toString()));
27 ArrayList lines = new ArrayList();
28 try {
29 String line = reader.readLine();
30 while (line != null) {
31 lines.add(line);
32 line = reader.readLine();
33 }
34 } catch (IOException ex) {
35 if (ex instanceof InterruptedIOException) {
36 Thread.currentThread().interrupt();
37 }
38 lines.add(ex.toString());
39 }
40 String[] tempRep = new String[lines.size()];
41 lines.toArray(tempRep);
42 return tempRep;
43 }
Layout类
Layout负责将LoggingEvent中的信息格式化成一行日志信息。对不同格式的日志可能还需要提供头和尾等信息。另外有些Layout不会处理异常信息,此时ignoresThrowable()方法返回false,并且异常信息需要Appender来处理,如PatternLayout。
1 public abstract class Layout implements OptionHandler {
2 public final static String LINE_SEP = System.getProperty("line.separator");
3 public final static int LINE_SEP_LEN = LINE_SEP.length();
4 abstract public String format(LoggingEvent event);
5 public String getContentType() {
6 return "text/plain";
7 }
8 public String getHeader() {
9 return null;
10 }
11 public String getFooter() {
12 return null;
13 }
14 abstract public boolean ignoresThrowable();
15 }
Layout的实现比较简单,如SimpleLayout对一行日志信息只是打印日志级别信息以及日志信息。
1 public class SimpleLayout extends Layout {
2 StringBuffer sbuf = new StringBuffer(128);
3 public SimpleLayout() {
4 }
5 public void activateOptions() {
6 }
7 public String format(LoggingEvent event) {
8 sbuf.setLength(0);
9 sbuf.append(event.getLevel().toString());
10 sbuf.append(" - ");
11 sbuf.append(event.getRenderedMessage());
12 sbuf.append(LINE_SEP);
13 return sbuf.toString();
14 }
15 public boolean ignoresThrowable() {
16 return true;
17 }
18 }
关于Layout更详细的信息将会在以后小节中介绍。
Appender接口
Appender负责定义日志输出的目的地,它可以是控制台(ConsoleAppender)、文件(FileAppender)、JMS服务器(JmsLogAppender)、以Email的形式发送出去(SMTPAppender)等。Appender是一个命名的实体,另外它还包含了对Layout、ErrorHandler、Filter等引用:
1 public interface Appender {
2 void addFilter(Filter newFilter);
3 public Filter getFilter();
4 public void clearFilters();
5 public void close();
6 public void doAppend(LoggingEvent event);
7 public String getName();
8 public void setErrorHandler(ErrorHandler errorHandler);
9 public ErrorHandler getErrorHandler();
10 public void setLayout(Layout layout);
11 public Layout getLayout();
12 public void setName(String name);
13 public boolean requiresLayout();
14 }
简单的,在配置文件中,Appender会注册到Logger中,Logger在写日志时,通过继承机制遍历所有注册到它本身和其父节点的Appender(在additivity为true的情况下),调用doAppend()方法,实现日志的写入。在doAppend方法中,若当前Appender注册了Filter,则doAppend还会判断当前日志时候通过了Filter的过滤,通过了Filter的过滤后,如果当前Appender继承自SkeletonAppender,还会检查当前日志级别时候要比当前Appender本身的日志级别阀门要打,所有这些都通过后,才会将LoggingEvent实例传递给Layout实例以格式化成一行日志信息,最后写入相应的目的地,在这些操作中,任何出现的错误都由ErrorHandler字段来处理。
SkeletonAppender类
目前Log4J实现的Appender都继承自SkeletonAppender类,该类对Appender接口提供了最基本的实现,并且引入了Threshold的概念,即所有的比当前Appender定义的日志级别阀指要大的日志才会记录下来。
1 public abstract class AppenderSkeleton implements Appender, OptionHandler {
2 protected Layout layout;
3 protected String name;
4 protected Priority threshold;
5 protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();
6 protected Filter headFilter;
7 protected Filter tailFilter;
8 protected boolean closed = false;
9 public AppenderSkeleton() {
10 super();
11 }
12 public void activateOptions() {
13 }
14 abstract protected void append(LoggingEvent event);
15 public boolean isAsSevereAsThreshold(Priority priority) {
16 return ((threshold == null) || priority.isGreaterOrEqual(threshold));
17 }
18 public synchronized void doAppend(LoggingEvent event) {
19 if (closed) {
20 LogLog.error("Attempted to append to closed appender named ["
21 + name + "].");
22 return;
23 }
24 if (!isAsSevereAsThreshold(event.getLevel())) {
25 return;
26 }
27 Filter f = this.headFilter;
28 FILTER_LOOP: while (f != null) {
29 switch (f.decide(event)) {
30 case Filter.DENY:
31 return;
32 case Filter.ACCEPT:
33 break FILTER_LOOP;
34 case Filter.NEUTRAL:
35 f = f.getNext();
36 }
37 }
38 this.append(event);
39 }
40 public void finalize() {
41 if (this.closed)
42 return;
43 LogLog.debug("Finalizing appender named [" + name + "].");
44 close();
45 }
46 }
SkeletonAppender实现了doAppend()方法,它首先检查日志级别是否要比threshold要大;然后如果注册了Filter,则使用Filter对LoggingEvent实例进行过滤,如果Filter返回Filter.DENY则doAppend()退出,否则执行append()方法,该方法由子类实现。
在Log4J中,Filter组成一条链,它定了以decide()方法,由子类实现,若返回DENY则日志不会被记录、NEUTRAL则继续检查下一个Filter实例、ACCEPT则Filter通过,继续执行后面的写日志操作。使用Filter可以为Appender加入一些出了threshold以外的其他逻辑,由于它本身是链状的,而且它的执行是横跨在Appender的doAppend方法中,因而这也是一个典型的AOP的概念。Filter的实现将会在下一小节中讲解。
SkeletonAppender还重写了finalize()方法,这是因为Log4J本身作为一个组件,它可能还是通过其他组件如commons-logging或slf4j组件间接的引入,因而使用它的程序不应该对它存在依赖的,然而在程序退出之前所有的Appender需要调用close()方法以释放它所占据的资源,为了不在使用Log4J的程序手动的close()的方法,以减少Log4J代码的侵入性,因而Log4J将close()的方法调用加入到finalize()方法中,即在垃圾回收器回收Appender实例时就会调用它的close()方法。
WriterAppender类和ConsoleAppender类
WriterAppender将日志写入Java IO中,它继承自SkeletonAppender类。它引入了三个字段:immediateFlush,指定没写完一条日志后,即将日志内容刷新到设备中,虽然这么做会有一点性能上的损失,但是如果不怎么做,则会出现在程序异常终止的时候无法看到部分日志信息,而经常这些丢失的日志信息要用于分析为什么会出现异常终止的情况,因而一般推荐将该值设置为true,即默认值;econding用于定义日志文本的编码方式;qw定义写日志的writer,它可以是文件或是控制台等Java IO支持的流。
在写日志文本前,WriterAppender还会做其他检查,如该Appender不能已经closed、qw和layout必须有值等,而后才可以将layout格式化后的日志行写入设备中。若layout本身不处理异常问题,则有Appender处理异常问题。最后如果每行日志需要刷新,则调用刷新操作。
1 public class WriterAppender extends AppenderSkeleton {
2 protected boolean immediateFlush = true;
3 protected String encoding;
4 protected QuietWriter qw;
5 public WriterAppender() {
6 }
7 public WriterAppender(Layout layout, OutputStream os) {
8 this(layout, new OutputStreamWriter(os));
9 }
10 public WriterAppender(Layout layout, Writer writer) {
11 this.layout = layout;
12 this.setWriter(writer);
13 }
14 public void append(LoggingEvent event) {
15 if (!checkEntryConditions()) {
16 return;
17 }
18 subAppend(event);
19 }
20 protected boolean checkEntryConditions() {
21 if (this.closed) {
22 LogLog.warn("Not allowed to write to a closed appender.");
23 return false;
24 }
25 if (this.qw == null) {
26 errorHandler
27 .error("No output stream or file set for the appender named ["
28 + name + "].");
29 return false;
30 }
31 if (this.layout == null) {
32 errorHandler.error("No layout set for the appender named [" + name
33 + "].");
34 return false;
35 }
36 return true;
37 }
38 protected void subAppend(LoggingEvent event) {
39 this.qw.write(this.layout.format(event));
40 if (layout.ignoresThrowable()) {
41 String[] s = event.getThrowableStrRep();
42 if (s != null) {
43 int len = s.length;
44 for (int i = 0; i < len; i++) {
45 this.qw.write(s[i]);
46 this.qw.write(Layout.LINE_SEP);
47 }
48 }
49 }
50 if (shouldFlush(event)) {
51 this.qw.flush();
52 }
53 }
54 public boolean requiresLayout() {
55 return true;
56 }
57 }
ConsoleAppender继承自WriterAppender,它只是简单的将System.out或System.err实例传递给WriterAppender以构建相应的writer,最后实现将日志写入到控制台中。
Filter类
在Log4J中,Filter组成一条链,它定了以decide()方法,由子类实现,若返回DENY则日志不会被记录、NEUTRAL则继续检查下一个Filter实例、ACCEPT则Filter通过,继续执行后面的写日志操作。使用Filter可以为Appender加入一些出了threshold以外的其他逻辑,由于它本身是链状的,而且它的执行是横跨在Appender的doAppend方法中,因而这也是一个典型的AOP的概念。
1 public abstract class Filter implements OptionHandler {
2 public Filter next;
3 public static final int DENY = -1;
4 public static final int NEUTRAL = 0;
5 public static final int ACCEPT = 1;
6 public void activateOptions() {
7 }
8 abstract public int decide(LoggingEvent event);
9 public void setNext(Filter next) {
10 this.next = next;
11 }
12 public Filter getNext() {
13 return next;
14 }
15 }
Log4J本身提供了四个Filter:DenyAllFilter、LevelMatchFilter、LevelRangeFilter、StringMatchFilter。
DenyAllFilter只是简单的在decide()中返回DENY值,可以将其应用在Filter链尾,实现如果之前的Filter都没有通过,则该LoggingEvent没有通过,类似或的操作:
1 public class DenyAllFilter extends Filter {
2 public int decide(LoggingEvent event) {
3 return Filter.DENY;
4 }
5 }
StringMatchFilter通过日志消息中的字符串来判断Filter后的状态:
1 public class StringMatchFilter extends Filter {
2 boolean acceptOnMatch = true;
3 String stringToMatch;
4 public int decide(LoggingEvent event) {
5 String msg = event.getRenderedMessage();
6 if (msg == null || stringToMatch == null)
7 return Filter.NEUTRAL;
8 if (msg.indexOf(stringToMatch) == -1) {
9 return Filter.NEUTRAL;
10 } else { // we've got a match
11 if (acceptOnMatch) {
12 return Filter.ACCEPT;
13 } else {
14 return Filter.DENY;
15 }
16 }
17 }
18 }
LevelMatchFilter判断日志级别是否和设置的级别匹配以决定Filter后的状态:
1 public class LevelMatchFilter extends Filter {
2 boolean acceptOnMatch = true;
3 Level levelToMatch;
4 public int decide(LoggingEvent event) {
5 if (this.levelToMatch == null) {
6 return Filter.NEUTRAL;
7 }
8 boolean matchOccured = false;
9 if (this.levelToMatch.equals(event.getLevel())) {
10 matchOccured = true;
11 }
12 if (matchOccured) {
13 if (this.acceptOnMatch)
14 return Filter.ACCEPT;
15 else
16 return Filter.DENY;
17 } else {
18 return Filter.NEUTRAL;
19 }
20 }
21 }
LevelRangeFilter判断日志级别是否在设置的级别范围内以决定Filter后的状态:
1 public class LevelRangeFilter extends Filter {
2 boolean acceptOnMatch = false;
3 Level levelMin;
4 Level levelMax;
5 public int decide(LoggingEvent event) {
6 if (this.levelMin != null) {
7 if (event.getLevel().isGreaterOrEqual(levelMin) == false) {
8 return Filter.DENY;
9 }
10 }
11 if (this.levelMax != null) {
12 if (event.getLevel().toInt() > levelMax.toInt()) {
13 return Filter.DENY;
14 }
15 }
16 if (acceptOnMatch) {
17 return Filter.ACCEPT;
18 } else {
19 return Filter.NEUTRAL;
20 }
21 }
22 }
总结
这一系列终于是结束了。本文主要介绍了Log4J核心类的实现和他们之间的交互关系。涉及到各个模块本身的其他详细信息将会在接下来的小节中详细介绍,如LogRepository与配置信息、Appender类结构的详细信息、Layout类结构的详细信息以及部分LoggingEvent提供的高级功能。而像Level、Logger本身,由于内容不多,已经在这一小节中全部介绍完了
转自:http://www.blogjava.net/DLevin/archive/2012/06/28/381667.html。
相关推荐
Log4j只需要引入一个JAR包,即log4j.jar,而Log4j2则需要引入两个核心JAR包,即log4j-core.jar和log4j-api.jar。大家可以发现,Log4j和Log4j2的包路径是不同的,Apache为了区分,包路径都更新了。 文件渲染 Log4j...
Log4j是一个广泛使用的Java日志记录框架,它允许开发者在应用程序中轻松地记录各种级别的日志信息,如DEBUG、INFO、WARN、ERROR等。在2021年底,一个重大的安全漏洞(CVE-2021-44228)被发现在Log4j2的早期版本中,...
针对Log4j 2 远程代码执行漏洞,需要用到的升级资源包,适用于maven资源库,包括log4j,log4j-core,log4j-api,log4j-1.2-api,log4j-jpa等全套2.15.0 maven资源库jar包。如果是maven本地仓库使用,需要将zip包解压...
分别有disruptor-3.3.4.jar(Log4j2异步日志的底层实现)、log4j-api-2.19.0.jar(log4j门面)、log4j-core-2.19.0.jar(log4j实现)、log4j-slf4j-impl-2.19.0.jar(SLF4J与Log4j绑定)、slf4j-api-1.7.30.jar(SLF...
### Log4j2简介 Log4j2是Apache软件基金会推出的日志框架,它是Log4j 1.x的重构版本,旨在提供更为高效且灵活的日志解决方案。与Log4j 1.x相比,Log4j2在设计上进行了重大改进,并解决了Logback等其他日志框架中...
apache-log4j-1.2.15.jar, apache-log4j-extras-1.0.jar, apache-log4j-extras-1.1.jar, apache-log4j.jar, log4j-1.2-api-2.0.2-javadoc.jar, log4j-1.2-api-2.0.2-sources.jar, log4j-1.2-api-2.0.2.jar, log4j-...
总结,SLF4J和Log4j的组合使用让日志管理更加灵活,开发者可以通过SLF4J的简洁API进行日志记录,同时利用Log4j的强大功能,如自定义输出格式和多种输出目的地。通过适当的配置和测试,我们可以确保日志系统按照预期...
Apache log4j2零日漏洞,根据 log4j-2.15.0-rc2 版本编译生成log4j-api-2.15.0.jar 1.解压你的jar jar xvf XXX.jar 2. 删除旧版本jar cd ./BOOT-INF/lib rm -rf log4j-api-*.jar 3. 上传新版本log4j-api-2.15.0....
Log4j、Log4j2和Fastjson的安全性问题在过去曾引起广泛关注,例如Log4j2的CVE-2021-44228(也被称为Log4Shell漏洞),这是一个远程代码执行漏洞,影响了许多使用Log4j2的系统。这个插件可能就是为了检测和利用这些...
此次提及的`log4j-api-2.12.4.jar`和`log4j-core-2.12.4.jar`是Log4j 2框架的两个关键组件,版本号为2.12.4,这个版本主要修复了之前版本中可能存在的安全漏洞。 **log4j-api-2.12.4.jar** 是Log4j 2框架的API模块...
《深入理解Log4j 2.15.0-rc2:日志处理的关键技术解析》 Log4j,作为Java领域广泛使用的日志记录框架,一直以来都是开发者们的重要工具。这次我们关注的是其最新版本——logging-log4j2-log4j-2.15.0-rc2。这个版本...
Log4j和Log4j2是两种广泛使用的Java日志框架,它们提供了灵活的日志配置和高性能的日志处理能力。本文将详细介绍如何在SpringBoot项目中配置Log4j和Log4j2。 ### SpringBoot与Log4j Log4j是Apache的一个开源项目,...
**日志框架Log4j详解** 在Java开发中,日志记录是一项不可或缺的功能,它能够帮助开发者追踪程序运行状态,定位错误,优化性能,并为后期维护提供重要信息。Log4j是Apache组织开发的一个强大的、灵活的日志记录框架...
《log4j-2.18.0:修复重大安全漏洞的紧急更新》 在IT领域,安全性始终是首要关注的问题。近期,一个名为“log4j2”的严重安全漏洞引发了广泛关注,它影响了所有log4j2版本,从2.0开始直到2.18.0版本之前。这个漏洞...
Apache log4j2零日漏洞,根据 log4j-2.15.0-rc2 版本编译生成log4j-api-2.15.0.jar 1.解压你的jar jar xvf XXX.jar 2. 删除旧版本jar cd ./BOOT-INF/lib rm -rf log4j-api-*.jar 3. 上传新版本log4j-api-...
标题提及的是"log4j-API-最新稳定版本log4j-1.2.17",这表明我们关注的是日志框架Log4j的一个特定版本,即1.2.17。Log4j是Apache软件基金会开发的一个用于Java应用程序的日志记录工具,它提供了灵活的日志记录功能,...
总的来说,"logging-log4j2-log4j-2.16.0-rc1.zip"的发布是Log4j团队对"Log4Shell"漏洞的有力回应,通过禁用可能导致安全问题的功能,提高了整体的安全标准。这一事件提醒我们,安全无小事,及时的更新和维护是保障...
这个“log4j示例项目”旨在帮助开发者理解和使用Log4j,通过该项目,我们可以深入学习Log4j的配置、使用方法以及其在实际开发中的应用。 **1. Log4j的组成部分** Log4j主要包括三个核心组件:Logger(日志器)、...
《深入理解log4j-api-2.17.1.jar与log4j-core-2.17.1.jar》 在Java开发中,日志管理是不可或缺的一部分,它帮助我们跟踪程序运行状态、定位错误和调试问题。Log4j作为一款广泛使用的日志框架,历经多次迭代,现在...
Log4j 是一个日志记录框架,Log4j 2 是对 Log4j 的升级,提供了重大改进,超越其前身 Log4j 1.x,并提供许多其它现代功能 ,例如对标记的支持、使用查找的属性替换、lambda 表达式与日志记录时无垃圾等。 Apache ...