log4j看上去像是一种简单的,易配置的日志打印技术。但是实际使用的时候发现,还有各种很相似的日志技术。很多地方的配置一乱就不知道怎么对应了。所以应该把log4j的一切做个简单的分类记录。
(一)java.util.logging.Logger
这个在java的util包里面,不需要任何Maven依赖,是最最基本的日志打印。功能很不完善,基本没有人在大型软件中使用这个。
示例代码:(日志重要级从上往下递增)
1 public class TestLog { 2 public static void main(String[] args) { 3 Logger logger = Logger.getLogger(TestLog.class.toString()); 4 logger.log(Level.ALL, "ALL"); 5 logger.log(Level.FINEST, "FINEST"); 6 logger.log(Level.FINER, "FINER"); 7 logger.log(Level.FINE, "FINE"); 8 logger.log(Level.CONFIG, "CONFIG"); 9 logger.log(Level.INFO, "INFO"); 10 logger.log(Level.WARNING, "WARNING"); 11 logger.log(Level.SEVERE, "SEVERE"); 12 logger.log(Level.OFF, "OFF"); 13 } 14 }
控制台输出:
一月 17, 2017 2:36:23 下午 com.jd.Test.TestLog main 信息: INFO 一月 17, 2017 2:36:23 下午 com.jd.Test.TestLog main 警告: WARNING 一月 17, 2017 2:36:23 下午 com.jd.Test.TestLog main 严重: SEVERE 一月 17, 2017 2:36:23 下午 com.jd.Test.TestLog main
禁用: OFF
那么为什么从info开始输出呢?原因是在很多实际应用中会只在控制台打印某个级别以上的日志信息。否则在调试的时候有可能会面对海量的杂乱日志而不知如何下手。所以这里也用了这样的设计。
想要修改默认的日志输出级别的话,去jre安装目录的lib下面:
1 # Limit the message that are printed on the console to INFO and above. 2 java.util.logging.ConsoleHandler.level = INFO
那么,如何制定日志的输出目录呢?这个可以用java代码指定:
1 public class TestLog { 2 public static void main(String[] args) throws SecurityException, IOException { 3 Logger logger = Logger.getLogger(TestLog.class.toString()); 4 5 FileHandler fileHandler = new FileHandler("/Users/Davie/File/foo.log"); 6 fileHandler.setLevel(Level.ALL); 7 logger.addHandler(fileHandler); 8 9 logger.log(Level.ALL, "ALL"); 10 logger.log(Level.FINEST, "FINEST"); 11 logger.log(Level.FINER, "FINER"); 12 logger.log(Level.FINE, "FINE"); 13 logger.log(Level.CONFIG, "CONFIG"); 14 logger.log(Level.INFO, "INFO"); 15 logger.log(Level.WARNING, "WARNING"); 16 logger.log(Level.SEVERE, "SEVERE"); 17 logger.log(Level.OFF, "OFF"); 18 } 19 }
但是结果是一个xml,比较难debug。
(二)common-logging
首先,它是apache提供的一个通用的日志接口,它的存在的意义是把普遍意义上的日志和真正的日志实现解耦合。那么它本身需要什么配置呢?其实什么都不需要。因为在运行时,它会自动去找实现。这里的实现可能是上文的java.util.logging.Logger这个JDK自带的最基本的日志实现,也可能是使用广泛的Log4j。
首先在Maven加入依赖:
1 <dependency> 2 <groupId>commons-logging</groupId> 3 <artifactId>commons-logging</artifactId> 4 <version>1.1.1</version> 5 </dependency>
然后在java类中写入:
1 class Foo { 2 3 private Log log = LogFactory.getLog(TestLog.class); 4 5 public void method() { 6 // log.debug("debug"); 7 log.info("info"); 8 log.warn("warn"); 9 log.error("error"); 10 // log.fatal("fatal"); 11 } 12 }
注释的这两行去掉注释是不起作用的。原因是目前只在Maven中添加了common-logging,它会使用JDK的实现,而JDK的实现是没有debug和fatal的。那么问题是到底这个common-logging会使用哪一个Log呢?它会按照如下规则寻找其实现类(转载自:http://jiangzhengjun.iteye.com/blog/520733):
1) 首先在classpath下寻找自己的配置文件commons-logging.properties,如果找到,则使用其中定义的Log实现类;
2) 如果找不到commons-logging.properties文件,则在查找是否已定义系统环境变量org.apache.commons.logging.Log,找到则使用其定义的Log实现类;
如果在Tomact中可以建立一个叫 :CATALINA_OPTS 的环境变量
给 他的 值 : - Dorg.apache.commons.logging.Log = org.apache.commons.logging.impl.SimpleLog - Dorg.apache.commons.logging.simplelog.defaultlog = warn
3) 否则,查看classpath中是否有Log4j的包,如果发现,则自动使用Log4j作为日志实现类;
4) 否则,使用JDK自身的日志实现类(JDK1.4以后才有日志实现类);
5) 否则,使用commons-logging自己提供的一个简单的日志实现类SimpleLog;
这样呢,往往就可以保证按照用户的意愿实现log。
另外,以上的Maven依赖中如果加入了log4j相关的的依赖,反倒会在运行时报错,原因是common-logging发现了log4j,所以会使用log4j进行输出,但是这个时候log4j还没有配置于是会输出:
log4j:WARN No appenders could be found for logger (com.jd.logTest.Foo).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
(三)SLF4j
这个其实也是一种接口。它自己不实现任何真正的日志打印,而是通过再引入别的包来转换。它的最特别的好处有两个,第一是可以通过占位符来打印日志,第二是配置具体实现简单且解耦合。
对于第一点,在日志系统的使用中,往往不只是要打印出:“**异常”,还要打印出具体的异常,和异常产生时的参数环境。最简单的例子如下:
1 public void say(String word) { 2 if (word == null) { 3 log.error("输入参数异常,word:" + word); 4 return ; 5 } 6 System.out.println(word); 7 }
这样的问题是,当要打印的参数多时,要产生多个String对象。其次是代码很不直观。
对于第二点,如果没有使用这个接口,而是直接耦合了log4j。那么如果你在代码中引入了一个模块,而这个模块使用了别的日志系统,那么你就必须在你的代码里面引入那一个日志系统并且配置好,维护好。这很明显引入了不必要的工作。
那么你要怎么才能使用一个SLF4j呢?官网上有一张图:
这张图体现了SLF4j的核心思想:你只需要把正确的包引入你的工程,日志系统就能正常的工作。
那么具体如何使用SLF4j呢?首先,你需要一个最基本的SLF4j的API包:
1 <dependency> 2 <groupId>org.slf4j</groupId> 3 <artifactId>slf4j-api</artifactId> 4 <version>${slf4j.version}</version> 5 </dependency>
这个包提供了SLF4j的基本API。接着,你需要一个“转换的包”。说起转换,当然就能联想到适配器设计模式。这个地方可以理解为一个适配器。这个地方的理解是指宏观的理解:你引入了如果所示的对应的包,就把SLF4j对应的实现包引入,SLF4j就会自动去找这个包的实现。
同时这个地方记录一下SLF4j和common-logging的实现方式的不同:
common-logging是通过动态查找的机制,在程序运行时自动找出真正使用的日志库。
SLF4j在编译时静态绑定真正的Log库。
(四)log4j
这个可以说是最享有大名的一个日志系统。
说起这个日志系统,里面的坑其实不少。我一次上线测试的时候,出了bug。但是比出bug更可怕的是:本地是可以打出日志的,而在线上的服务器就无法打出日志。那个错误直接导致我无法正常debug。
要使用这个日志工具,首先添加依赖:
1 <dependency> 2 <groupId>log4j</groupId> 3 <artifactId>log4j</artifactId> 4 <version>${log4j.version}</version> 5 </dependency>
然后再添加配置文件。这里的具体的配置有用log4j.xml和log4j.properties两种方式,这里使用xml来配置。把xml加入编译路径中:src/main/resources:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd"> 3 <log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'> 4 5 <appender name="ALL" class="org.apache.log4j.DailyRollingFileAppender"> 6 <param name="file" value="/Users/Davie/File/export/Logs/TestProject/ALL.log" /> 7 <param name="DatePattern" value="'.'yyyy-MM-dd" /> 8 <layout class="org.apache.log4j.PatternLayout"> 9 <param name="ConversionPattern" value="%d [%7r] %6p - %30.30c - %m " /> 10 </layout> 11 </appender> 12 13 <appender name="FILE_MONITORFOLDER" class="org.apache.log4j.DailyRollingFileAppender"> 14 <param name="file" 15 value="/Users/Davie/File/export/Logs/TestProject/FILE_MONITORFOLDER.log" /> 16 <param name="DatePattern" value="'.'yyyy-MM-dd" /> 17 <layout class="org.apache.log4j.PatternLayout"> 18 <param name="ConversionPattern" value="%d [%7r] %6p - %30.30c - %m " /> 19 </layout> 20 </appender> 21 22 <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> 23 <layout class="org.apache.log4j.PatternLayout"> 24 <param name="ConversionPattern" value="%d [%7r] %6p - %30.30c - %m " /> 25 </layout> 26 </appender> 27 28 <logger name="FILE_MONITORFOLDER" additivity="false"> 29 <level value="${task.log.level}" /> 30 <appender-ref ref="FILE_MONITORFOLDER" /> 31 </logger> 32 33 <logger name="CONSOLE" additivity="false"> 34 <level value="${task.log.level}" /> 35 <appender-ref ref="CONSOLE" /> 36 </logger> 37 38 <root> 39 <priority value="${task.log.level}" /> 40 <appender-ref ref="ALL" /> 41 ${log4j.log.appender.console} 42 </root> 43 </log4j:configuration>
在实际的使用中,往往由一些意想不到的点。
比如说,在dev时(可以理解为Profile处于dev),我们应当允许其不单单向日志文件写日志,还可以同时向控制台打印日志。这样在工程师调试的时候可以说是极其方便的。但是一旦到了线上的环境,恐怕就没有控制台这个东西了,日志全部写入到了日志文件,一般是通过SHELL语句去跟踪log日志文件的打印情况来实现的。这也就是为什么我一直没找到我的日志。在上面的配置文件中,最后的:${log4j.log.appender.console}到底是什么呢?实际上,在log4j中,如果你没有配置<logger>或者<category>这种标签,日志就会去找<root>来实现打印。那么<root>这里的这句话其实是一句控制是否往控制台打印日志的语句。
在dev的profile中,配置是:
1 <log4j.log.appender.console><![CDATA[<appender-ref ref="CONSOLE"/>]]></log4j.log.appender.console>
在其他的profile中,配置是:
1 <log4j.log.appender.console></log4j.log.appender.console>
当然也就是不往控制台打印日志的意思了。那么当时我的日志没有打印出来的逻辑也就是:
1,使用了:private Logger log = Logger.getLogger(Foo.class);
2,配置文件里面没有配置这个logger,于是自动去找<root>,
3,本地的dev的profile配置了要打印到控制台,于是正常,而线上的test profile则根本没有配置打印到控制台(当然实际上打印了也是没有意义的,单纯浪费效率罢了)。
然后,往往在调试的时候会有很多调试时的日志,也就是一些局部的过程,用:log.debug打印出来。很明显,如果任由这种日志挤成一大堆,那么首先是性能的开销,其次是一旦出了问题,在浩如烟海的杂乱日志中恐怕要很费力气才能得到自己想要的信息。所以在不同的Maven的Profile中,一般会配置一个不同的量来控制当前环境下的日志打印级别。比如上文中的:${task.log.level}
在dev的profile中:
1 <task.log.level>DEBUG</task.log.level>
在test的profile中:
1 <task.log.level>INFO</task.log.level>
那么这样就可以很细节的控制打印日志的颗粒度。
第三个问题是这样的,上文已经说过形如:
1 private Logger log = Logger.getLogger(“foo”);
这样写,结果是你配置了foo这个logger,就会去找这个logger,否则会去root里面。那么在一个多module的工程中,就不能像写小玩意一样随便选择当前的类作为日志了。一般在这样的大工程中,都会为某个module,或者某个模块写一个logger:
1 <logger name="FILE_MONITORFOLDER" additivity="false"> 2 <level value="${task.log.level}" /> 3 <appender-ref ref="FILE_MONITORFOLDER" /> 4 </logger> 5 <appender name="FILE_MONITORFOLDER" class="org.apache.log4j.DailyRollingFileAppender"> 6 <param name="file" 7 value="/Users/Davie/File/export/Logs/TestProject/FILE_MONITORFOLDER.log" /> 8 <param name="DatePattern" value="'.'yyyy-MM-dd" /> 9 <layout class="org.apache.log4j.PatternLayout"> 10 <param name="ConversionPattern" value="%d [%7r] %6p - %30.30c - %m " /> 11 </layout> 12 </appender>
这样在MONITORFOLDER这个模块中,就这么用:
1 private Logger log = Logger.getLogger(“FILE_MONITORFOLDER”);
这样,在线上的时候,就可以去对应的日志文件中看这个模块独有的日志信息了。
以上都是描述这些东西单独怎么用,但实际上很多时候会结合这些东西来用。
比如你想用SLF4J+log4j,那么Maven里面你这么配置:
1 <dependency> 2 <groupId>log4j</groupId> 3 <artifactId>log4j</artifactId> 4 <version>${log4j.version}</version> 5 </dependency> 6 <dependency> 7 <groupId>org.slf4j</groupId> 8 <artifactId>slf4j-api</artifactId> 9 <version>${slf4j.version}</version> 10 </dependency> 11 <dependency> 12 <groupId>org.slf4j</groupId> 13 <artifactId>slf4j-log4j12</artifactId> 14 <version>${slf4j.version}</version> 15 </dependency>
JAVA代码代码里面这么写:
1 private Logger log = LoggerFactory.getLogger("FILE_MONITORFOLDER");
那么实际运行的时候,效果和上面直接使用log4j一样,因为SLF4J通过依赖包找到了其底层实现是log4j。
如果情况更极端一点:你最开始的代码使用了common-logging,那么你需要先把和这个转化为SLF4J,再按照SLF4J的规则打印日志。那么是否意味着你要改动代码?不需要,你只需要添加一个依赖:
1 <dependency> 2 <groupId>org.slf4j</groupId> 3 <artifactId>jcl-over-slf4j</artifactId> 4 <version>${slf4j.version}</version> 5 </dependency>
这样的话,common-logging就会通过SLF4J去打印日志了。