之前在项目中用的日志记录器都是log4j的日志记录器,可是到了新公司发现都是slf4j,于是想着研究一下slf4j的用法。
注意:每次引入Logger的时候注意引入的jar包,因为有Logger的包太多了。。。。。。
Logger必须作为类的静态变量使用。原因如下:
1 使用static修饰的属性是归这个类使用的
2 也就是说不论这个类实例化多少个,大家用的都是同一个static属性
3 log4j记录的是当前类的日志,不是每个实例的日志
4 所以只要有一个记录就可以了
创建日志记录器方法:(最好声明加final关键字)
//private static final Logger logger = LoggerFactory.getLogger(Slf4jTest.class);// slf4j日志记录器 private static final Logger logger = LoggerFactory.getLogger(Slf4jTest.class.getName());// slf4j日志记录器
简要记录一下日志级别:
每个Logger都被了一个日志级别(log level),用来控制日志信息的输出。日志级别从高到低分为:
A:off 最高等级,用于关闭所有日志记录。
B:fatal 指出每个严重的错误事件将会导致应用程序的退出。
C:error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
D:warm 表明会出现潜在的错误情形。
E:info 一般和在粗粒度级别上,强调应用程序的运行全程。
F:debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
G:all 最低等级,用于打开所有日志记录。
其实对于不同的版本有不同的级别,不过最常用的就是debuginfowarnerror.下面是摘自log4j-1.2.17.jar中的级别:
可以看出:allTRACEdebug同级别。off与fatal同级别。
package org.apache.log4j; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; public class Level extends Priority implements Serializable { /** * TRACE level integer value. * @since 1.2.12 */ public static final int TRACE_INT = 5000;
final static public Level OFF = new Level(OFF_INT, "OFF", 0); final static public Level FATAL = new Level(FATAL_INT, "FATAL", 0); final static public Level ERROR = new Level(ERROR_INT, "ERROR", 3); final static public Level WARN = new Level(WARN_INT, "WARN", 4); final static public Level INFO = new Level(INFO_INT, "INFO", 6); final static public Level DEBUG = new Level(DEBUG_INT, "DEBUG", 7); public static final Level TRACE = new Level(TRACE_INT, "TRACE", 7); final static public Level ALL = new Level(ALL_INT, "ALL", 7); static final long serialVersionUID = 3491141966387921974L; protected Level(int level, String levelStr, int syslogEquivalent) { super(level, levelStr, syslogEquivalent); } public static Level toLevel(int val, Level defaultLevel) { switch(val) { case ALL_INT: return ALL; case DEBUG_INT: return Level.DEBUG; case INFO_INT: return Level.INFO; case WARN_INT: return Level.WARN; case ERROR_INT: return Level.ERROR; case FATAL_INT: return Level.FATAL; case OFF_INT: return OFF; case TRACE_INT: return Level.TRACE; default: return defaultLevel; } } 。。。。。
0.依赖的Jar包
<!-- slf4j 依赖包 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.0-rc1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.0-rc1</version> </dependency>
或者手动导入下面的包:
介绍:slf4j-api提供接口,slf4j-log4j提供具体的实现。也就是 slf4j只是一个日志标准,并不是日志系统的具体实现,如果项目只有slf4j的包是没有办法实现日志功能的。
1 基本介绍
SLF4J不同于其他日志类库,与其它日志类库有很大的不同。SLF4J(Simple logging Facade for Java)不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志类库。如果是在编写供内外部都可以使用的API或者通用类库,那么你真不会希望使用你类库的客户端必须使用你选择的日志类库。
如果一个项目已经使用了log4j,而你加载了一个类库,比方说 Apache Active MQ——它依赖于于另外一个日志类库logback,那么你就需要把它也加载进去。但如果Apache Active MQ使用了SLF4J,你可以继续使用你的日志类库而无需忍受加载和维护一个新的日志框架的痛苦。
总的来说,SLF4J使你的代码独立于任意一个特定的日志API,这是对于API开发者的很好的思想。虽然抽象日志类库的思想已经不是新鲜的事物,而且Apache commons logging也已经在使用这种思想了,但SLF4J正迅速成为Java世界的日志标准。让我们再看几个使用SLF4J而不是log4j、logback或者java.util.logging的理由。
2 SLF4J对比Log4J,logback和java.util.Logging的优势
正如我之前说的,在你的代码中使用SLF4J写日志语句的主要出发点是使得你的程序独立于任何特定的日志类库,依赖于特定类库可能需要使用不同于你已有的配置,并且导致更多维护的麻烦。除此之外,还有一个SLF4J API的特性是使得我坚持使用SLF4J而抛弃我长期间钟爱的Log4j的理由,是被称为占位符(place holder),在代码中表示为“{}
”的特性。占位符是一个非常类似于在String
的format()
方法中的%s
,因为它会在运行时被某个提供的实际字符串所替换。这不仅降低了你代码中字符串连接次数,而且还节省了新建的String对象。通过使用SLF4J,你可以在运行时延迟字符串的建立,这意味着只有需要的String对象才被建立。而如果你已经使用log4j,那么你已经对于在if条件中使用debug语句这种变通方案十分熟悉了,但SLF4J的占位符就比这个好用得多。
3.slf4j的简单用法
1.简单使用
1.配置文件(log4j.properties)
log4j.rootLogger=debug, C
log4j.appender.A=org.apache.log4j.ConsoleAppender
log4j.appender.A.layout=org.apache.log4j.PatternLayout
log4j.appender.A.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
log4j.appender.B=org.apache.log4j.FileAppender
log4j.appender.B.File=E:\log.log
log4j.appender.B.layout=org.apache.log4j.SimpleLayout
log4j.appender.C=org.apache.log4j.RollingFileAppender
log4j.appender.C.File=E:\log.html
log4j.appender.C.MaxFileSize=1000KB
log4j.appender.C.MaxBackupIndex=10
log4j.appender.C.layout=org.apache.log4j.HTMLLayout
log4j.appender.C.encoding=gbk
log4j.appender.D=org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File=E:\log.log
log4j.appender.D.layout=org.apache.log4j.TTCCLayout
注意:
1.如果需要输出到多个位置的时候可以逗号隔开,比如: log4j.rootLogger=info, A, B
2. log4j.appender.C.encoding=gbk 的配置是为了解决中文乱码,有时候也可以设置为UTF-8试试
关于pattern的设置如下:
%p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss,SSS},SSS为毫秒数(也可以写为SS,只不过SSS如果不足三位会补0),输出类似:2011-10-18 22:10:28,021
%r: 输出自应用启动到输出该日志耗费的毫秒数
%t: 输出产生日志的线程名称
%l: 输出日志事件的位置,相当于%c.%M(%F:L)的组合,包括类全名、方法、文件名以及在代码中行数。例如:cn.xm.test.PlainTest.main(PlanTest.java:12)
%c: 输出日志信息所属的类目,通常就是所在类的全名。可写为%c{num},表示取完整类名的层数,从后向前取,比如%c{2}取 "cn.qlq.exam"类为"qlq.exam"。
%M: 输出产生日志信息的方法名
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中 %%: 输出一个"%"字符 %F: 输出日志消息产生时所在的文件名称 %L: 输出代码中的行号 %m: 输出代码中指定的消息,产生的日志具体信息 %n: 输出一个回车换行符,Windows平台为" ",Unix平台为" "输出日志信息换行
测试代码如下:
package cn.xm.exam.test; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PlainTest { private static final Logger LOGGER = LoggerFactory.getLogger(PlainTest.class); public static void main(String[] args) throws IOException, InterruptedException { LOGGER.debug("线程开始"); Thread.sleep(1 * 1000); test1(); } public static void test1() { LOGGER.debug("test1方法"); } }
(1)配置1
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n
结果:
2019-08-21 22:13:50 [DEBUG] 线程开始
2019-08-21 22:13:51 [DEBUG] test1方法
(2) %t 线程名
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] [%t] %m%n
结果(线程名为main):
2019-08-21 22:16:09 [DEBUG] [main] 线程开始
2019-08-21 22:16:10 [DEBUG] [main] test1方法
(3) %l
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%l] %m%n
结果:
2019-08-21 22:19:53 [cn.xm.exam.test.PlainTest.main(PlainTest.java:12)] 线程开始
2019-08-21 22:19:54 [cn.xm.exam.test.PlainTest.test1(PlainTest.java:19)] test1方法
(4)%r 产生日志距离应用启动的毫秒数
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%l] [%r] %m%n
结果:
2019-08-21 22:34:27 [cn.xm.exam.test.PlainTest.main(PlainTest.java:12)] [0] 线程开始
2019-08-21 22:34:28 [cn.xm.exam.test.PlainTest.test1(PlainTest.java:19)] [1002] test1方法
2.测试代码:
package cn.xm.exam.test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Slf4jTest { private static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);// slf4j日志记录器 public static void main(String[] args) { // 普通的日志记录 logger.debug("普通的日志记录"); // {}占位符记录日志 for (int i = 0; i < 3; i++) { logger.debug("这是第{}条记录", i); } // 用转义{} logger.debug("Set \{} differs from {}", "3"); // output:Set {} differs // from 3 // 两个参数 logger.debug("两个占位符,可以传两个参数{}----{}", 1, 2); // 多个参数(可变参数) logger.debug("debug:多个占位符,{},{},{},{}", 1, 2, 3, 4); // 多个参数(可变参数) logger.info("info:多个占位符,{},{},{},{}", 1, 2, 3, 4); // 多个参数(可变参数) logger.error("error:多个占位符,{},{},{},{}", 1, 2, 3, 4); } }
结果:
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] 普通的日志记录
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] 这是第0条记录
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] 这是第1条记录
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] 这是第2条记录
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] Set {} differs from 3
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] 两个占位符,可以传两个参数1----2
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[DEBUG] debug:多个占位符,1,2,3,4
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[INFO] info:多个占位符,1,2,3,4
2018-08-18 10:58:38 [cn.xm.exam.test.Slf4jTest]-[ERROR] error:多个占位符,1,2,3,4
注意:debug,info,error,等各个级别的方法都可以传入多个可变参数:
至于详细的日志级别的介绍参考我的另一篇博客:http://www.cnblogs.com/qlqwjy/p/7192947.html
2.关于正式开发中的日记记录的方法:(重要)
项目中日志记录还是有很多说法的,比如保存日志级别,日志应该打印的信息,日志参数的设置等:
1.一般是将捕捉到的Exception对象作为日志记录的最后一个参数(会显示具体的出错信息以及出错位置),而且要放在{}可以格式化的参数之外,防止被{}转为e.toString()
例如一个标准的日记记录的方法:
package cn.xm.exam.test; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Slf4jTest { private static Logger log = LoggerFactory.getLogger(Slf4jTest.class); public static void main(String[] args) { openFile("xxxxxx"); } public static void openFile(String filePath) { File file = new File(filePath); try { InputStream in = new FileInputStream(file); } catch (FileNotFoundException e) { log.error("can found file [{}]", filePath, e); } } }
结果:
2018-08-18 10:50:03 [cn.xm.exam.test.Slf4jTest]-[ERROR] can found file [xxxxxx] java.io.FileNotFoundException: xxxxxx (系统找不到指定的文件。) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:146) at cn.xm.exam.test.Slf4jTest.openFile(Slf4jTest.java:22) at cn.xm.exam.test.Slf4jTest.main(Slf4jTest.java:16)
注意: {}的作用是调用对应参数的toString()方法格式化日志,如果exception放在对应参数的位置上也会被格式化。所以,e要放在{}参数之外,而且只能放在最后一个。如果放在中间也不会被打印错误信息:
例如:
- 只要放到{}之外的最后一个参数可以打印错误信息(包括信息和位置)
public static void openFile(String filePath) { File file = new File(filePath); try { InputStream in = new FileInputStream(file); } catch (FileNotFoundException e) { log.error("error:can found file [{}]", filePath, 3, e); } }
结果:
2018-08-18 11:11:18 [cn.xm.exam.test.Slf4jTest]-[ERROR] error:can found file [xxxxxx] java.io.FileNotFoundException: xxxxxx (系统找不到指定的文件。) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:146) at cn.xm.exam.test.Slf4jTest.openFile(Slf4jTest.java:22) at cn.xm.exam.test.Slf4jTest.main(Slf4jTest.java:16)
- 放到{}之外的非最后一个参数不会打印错误信息
public static void openFile(String filePath) { File file = new File(filePath); try { InputStream in = new FileInputStream(file); } catch (FileNotFoundException e) { log.error("error:can found file [{}]", filePath,e,3); } }
结果:
2018-08-18 11:10:38 [cn.xm.exam.test.Slf4jTest]-[ERROR] error:can found file [xxxxxx]
补充: 低版本的slf4j用 log.error("执行SQL出错,sql -> {}", sql, e); 打印不出来e异常堆栈信息,高版本支持这种写法,所以建议用高版本。我上面的1.7.5支持这种写法,1.5.10就不支持。
2.尽量不使用e.getMessage(),因为有的异常不一定有message,可以使用e.toString只会显示信息,不会显示出错的位置信息(不建议这种)
例如:
package cn.xm.exam.test; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Slf4jTest { private static Logger log = LoggerFactory.getLogger(Slf4jTest.class); public static void main(String[] args) { openFile("xxxxxx"); } public static void openFile(String filePath) { File file = new File(filePath); try { InputStream in = new FileInputStream(file); } catch (FileNotFoundException e) { //下面两句的效果一样 log.error("can found file [{}],cause:{}", filePath, e.toString()); log.error("can found file [{}],cause:{}", filePath, e); } } }
结果:
2018-08-18 10:53:31 [cn.xm.exam.test.Slf4jTest]-[ERROR] can found file [xxxxxx],cause:java.io.FileNotFoundException: xxxxxx (系统找不到指定的文件。)
2018-08-18 10:53:31 [cn.xm.exam.test.Slf4jTest]-[ERROR] can found file [xxxxxx],cause:java.io.FileNotFoundException: xxxxxx (系统找不到指定的文件。)
补充:一份上线系统使用的log4j.properties配置:
log4j.rootLogger=info,B
log4j.appender.A=org.apache.log4j.ConsoleAppender
log4j.appender.A.layout=org.apache.log4j.PatternLayout
log4j.appender.A.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
log4j.appender.B=org.apache.log4j.RollingFileAppender
log4j.appender.B.File=E:\test.log
log4j.appender.B.MaxFileSize=10MB
log4j.appender.B.MaxBackupIndex=5
log4j.appender.B.layout=org.apache.log4j.PatternLayout
log4j.appender.B.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
%d{yyyy-MM-dd HH:mm:ss} 也可以改为%d{yyyy-MM-dd HH:mm:ss,SSS} SSS代表至少输出3位数字的毫秒。如果4个S的话不足位补0。
解释:org.apache.log4j.RollingFileAppender是log4j的一个类,下面的配置都是它的属性的配置:(其中还有好多属性来自其父类,需要的时候我们可以去查阅)
log4j.appender.B.File 指定输出的日志的文件名称以及路径
log4j.appender.B.MaxFileSize 指定每个文件的最大大小
log4j.appender.B.MaxBackupIndex 指定文件达到文件大小之后最多重命名的文件数量
log4j.appender.B.layout 指定采用的样式
log4j.appender.B.layout.ConversionPattern 指定样式的格式(值一般固定)
测试代码:
package cn.qlq.slf4jTest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Slf4jTest { private static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);// slf4j日志记录器 public static void main(String[] args) { while (true) { // 普通的日志记录 logger.error("普通的日志记录"); // {}占位符记录日志 for (int i = 0; i < 3; i++) { logger.info("这是第{}条记录", i); } // 用转义{} logger.info("Set \{} differs from {}", "3"); // output:Set {} differs from 3 // 两个参数 logger.info("两个占位符,可以传两个参数{}----{}", 1, 2); } } }
结果:(会产生对应的文件,test.log永远保存最新的日志,达到10M后会重命名文件为test.log.1,并将原来test.log.1命名为test.log.2....)
log内容如下:
补充:Threshold还可以指定输出的日志级别: (如果设置不起作用查看项目是不是用的log4j的包,有可能有多个log4j包,造成冲突)
有时候我们需要把一些报错ERROR日志单独存到指定文件 ,这时候,Threshold属性就派上用场了,比如:
log4j.rootLogger=info,B,C log4j.appender.B=org.apache.log4j.RollingFileAppender log4j.appender.B.Threshold=info log4j.appender.B.File=E:\info.log log4j.appender.B.MaxFileSize=10MB log4j.appender.B.MaxBackupIndex=5 log4j.appender.B.layout=org.apache.log4j.PatternLayout log4j.appender.B.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n log4j.appender.C=org.apache.log4j.RollingFileAppender log4j.appender.C.Threshold=error log4j.appender.C.File=E:\error.log log4j.appender.C.MaxFileSize=10MB log4j.appender.C.MaxBackupIndex=5 log4j.appender.C.layout=org.apache.log4j.PatternLayout log4j.appender.C.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
上面B格式的就是info级别以上的日志,包括error信息
C格式的就是error级别以上的信息。
当然这里有个提前 rootLogger里配置的level必须小于Threshold等级,否则无效 还是按照总的rootLogger里的level来输出,一般我们实际实用的话 rootLogger里配置DEBUG,然后某个文件专门存储ERRO日志,就配置下Threshold为ERROR
代码还是上面的测试代码:
结果:
error.log内容:全是error级别以上的日志
info.log内容(info级别以上,包括error)
补充:log4j也可以对不同的包进行不同的配置,也就是针对不同的包采用不同的日志级别与日志控制器
有时候我们也希望对不同的包采用不通的日志记录级别以及不同的日志记录方式。log4j完全可以做到这点,例如下面
默认包采用rootLogger的配置,info级别、在控制台与文件中进行显示;同时又修改了cn下面的a、b、c包的日志级别:
cn.a只降低了级别为debug,输出方式还是console与file两种。(一般我们采用这种方式修改级别即可,如果再设置输出方式会在原来的基础上增加方式)
cn.b级别设为info,方式设为console,实际是加了第三种方式
cn.c级别设置为error,方式设为file,实际也是在rootLogger基础上增加第三种方式
级别会以log4j.logger.XX级别为准,不管rootLogger级别高于对具体包的设置还是低于具体包的设置;输出方式会在rootLogger的基础上增加新的方式,如果没有额外的方式采用rootLogger的方式。
例如:(注意标红地方)
log4j.rootLogger=info,console,file
log4j.logger.cn.a=debug
log4j.logger.cn.b=info,console
log4j.logger.cn.c=error,file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=E:\test.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
测试:
包结构:
Root
package cn; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RootClass { private static final Logger LOGGER = LoggerFactory.getLogger(RootClass.class); public static void main(String[] args) { LOGGER.debug("RootClass"); LOGGER.info("RootClass"); LOGGER.error("RootClass"); } }
运行结果:
2018-10-23 19:38:04 [cn.RootClass]-[INFO] RootClass
2018-10-23 19:38:04 [cn.RootClass]-[ERROR] RootClass
AC1
package cn.a; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AC1 { private static final Logger LOGGER = LoggerFactory.getLogger(AC1.class); public static void main(String[] args) { LOGGER.debug("AC1"); LOGGER.info("AC1"); LOGGER.error("AC1"); } }
运行结果:
2018-10-23 19:38:31 [cn.a.AC1]-[DEBUG] AC1
2018-10-23 19:38:31 [cn.a.AC1]-[INFO] AC1
2018-10-23 19:38:31 [cn.a.AC1]-[ERROR] AC1
BC1:
package cn.b; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BC1 { private static final Logger LOGGER = LoggerFactory.getLogger(BC1.class); public static void main(String[] args) { LOGGER.debug("BC1"); LOGGER.info("BC1"); LOGGER.error("BC1"); } }
运行结果:
2018-10-23 19:39:04 [cn.b.BC1]-[INFO] BC1
2018-10-23 19:39:04 [cn.b.BC1]-[INFO] BC1
2018-10-23 19:39:04 [cn.b.BC1]-[ERROR] BC1
2018-10-23 19:39:04 [cn.b.BC1]-[ERROR] BC1
CC1:
package cn.c; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CC1 { private static final Logger LOGGER = LoggerFactory.getLogger(CC1.class); public static void main(String[] args) { LOGGER.debug("CC1"); LOGGER.info("CC1"); LOGGER.error("CC1"); } }
运行结果:
2018-10-23 19:39:28 [cn.c.CC1]-[ERROR] CC1
最后查看E:/test.log
2018-10-23 19:38:04 [cn.RootClass]-[INFO] RootClass
2018-10-23 19:38:04 [cn.RootClass]-[ERROR] RootClass
2018-10-23 19:38:31 [cn.a.AC1]-[DEBUG] AC1
2018-10-23 19:38:31 [cn.a.AC1]-[INFO] AC1
2018-10-23 19:38:31 [cn.a.AC1]-[ERROR] AC1
2018-10-23 19:39:04 [cn.b.BC1]-[INFO] BC1
2018-10-23 19:39:04 [cn.b.BC1]-[ERROR] BC1
2018-10-23 19:39:28 [cn.c.CC1]-[ERROR] CC1
2018-10-23 19:39:28 [cn.c.CC1]-[ERROR] CC1
补充:对父包设置日志级别,如果子包没有设置默认采用父包的设置,如果子包设置了会采用单独的设置
配置修改cn包设置以及对b包单独进行设置:
log4j.logger.cn=error
log4j.logger.cn.b=info,console
测试cn.d包默认采用cn包的error级别:
package cn.d; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DC1 { private static final Logger LOGGER = LoggerFactory.getLogger(DC1.class); public static void main(String[] args) { LOGGER.debug("D"); LOGGER.info("D"); LOGGER.error("D"); } }
结果:
2018-12-29 13:30:09 [cn.d.DC1]-[ERROR] D
测试cn.b采用单独对cn.b包的配置
package cn.b; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BC1 { private static final Logger LOGGER = LoggerFactory.getLogger(BC1.class); public static void main(String[] args) { LOGGER.debug("BC1"); LOGGER.info("BC1"); LOGGER.error("BC1"); } }
结果:
2018-12-29 13:31:27 [cn.b.BC1]-[INFO] BC1
2018-12-29 13:31:27 [cn.b.BC1]-[INFO] BC1
2018-12-29 13:31:27 [cn.b.BC1]-[ERROR] BC1
2018-12-29 13:31:27 [cn.b.BC1]-[ERROR] BC1
补充:上面的对包的具体设置日志级别虽然不受总的log4j.rootLogger的日志级别的限制,但是却受特殊的日志的Threshold属性的限制,也就是对具体包的设置必须高于其记录器的Threshold属性,否则以其记录器的Threshold属性为准
例如:修改上面的日志配置(每个日志记录器增加Threshold属性)
log4j.rootLogger=info,console,file
log4j.logger.cn.a=debug
log4j.logger.cn.b=info,console
log4j.logger.cn.c=error,file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Threshold=info
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.Threshold=error
log4j.appender.file.File=E:\test.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
运行RootClass:
2018-11-12 23:02:44 [cn.RootClass]-[INFO] RootClass
2018-11-12 23:02:44 [cn.RootClass]-[ERROR] RootClass
运行AC1:
2018-11-12 23:03:08 [cn.a.AC1]-[INFO] AC1
2018-11-12 23:03:08 [cn.a.AC1]-[ERROR] AC1
运行BC1:
2018-11-12 23:03:33 [cn.b.BC1]-[INFO] BC1
2018-11-12 23:03:33 [cn.b.BC1]-[INFO] BC1
2018-11-12 23:03:33 [cn.b.BC1]-[ERROR] BC1
2018-11-12 23:03:33 [cn.b.BC1]-[ERROR] BC1
运行CC1:
2018-11-12 23:04:25 [cn.c.CC1]-[ERROR] CC1
查看test.log:
2018-11-12 23:02:44 [cn.RootClass]-[ERROR] RootClass
2018-11-12 23:03:08 [cn.a.AC1]-[ERROR] AC1
2018-11-12 23:03:33 [cn.b.BC1]-[ERROR] BC1
2018-11-12 23:04:25 [cn.c.CC1]-[ERROR] CC1
2018-11-12 23:04:25 [cn.c.CC1]-[ERROR] CC1
所以总结上面的日记记录级别的设置优先级可以总结为:Threshold > 具体包的设置 > rootLogger的全局配置
补充:关于NDC和MDC的使用
NDC(Nested Diagnostic Context)和MDC(Mapped Diagnostic Context)是log4j种非常有用的两个类,它们用于存储应用程序的上下文信息(context infomation),从而便于在log中使用这些上下文信息。
(1)NDC的使用:
NDC采用了一个类似栈的机制来push和pop上下文信息,每一个线程都独立地储存上下文信息。
当使用的时候,我们要尽可能确保在进入一个context的时候,把相关的信息使用NDC.push(message);在离开这个context的时候使用NDC.pop()将信息删除。另外由于设计上的一些问题,还需要保证在当前thread结束的时候使用NDC.remove()清除内存,否则会产生内存泄漏的问题。
例如:假设我们又API接口,我们想在访问API的时候记录访问IP的地址。如下:
package cn.xm.exam.test; import org.apache.log4j.NDC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PlainTest { private static final Logger LOGGER = LoggerFactory.getLogger(PlainTest.class); public static void main(String[] args) { LOGGER.debug("main函数开始运行"); // 模拟开启线程访问API接口 new Thread(new Runnable() { @Override public void run() { getUser(); } }).start(); LOGGER.debug("main函数结束运行"); } /** * 假设这是一个API的处理(进入此环境,我们向NDC存储一个访问的IP) */ public static void getUser() { NDC.push("127.0.0.1"); LOGGER.debug("访问接口"); LOGGER.debug("NDC.get(): {} ", NDC.get() ); // 移除 // NDC.pop(); // 适当位置清空 NDC.remove(); LOGGER.debug("remove之后 NDC.get(): {} ", NDC.get() ); } }
配置:(”%x”来输出存储的上下文信息)
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] [%M] %x %m%n
结果:
2019-12-03 14:13:37 [cn.xm.exam.test.PlainTest]-[DEBUG] [main] main函数开始运行
2019-12-03 14:13:37 [cn.xm.exam.test.PlainTest]-[DEBUG] [main] main函数结束运行
2019-12-03 14:13:37 [cn.xm.exam.test.PlainTest]-[DEBUG] [getUser] 127.0.0.1 访问接口
2019-12-03 14:13:37 [cn.xm.exam.test.PlainTest]-[DEBUG] [getUser] 127.0.0.1 NDC.get(): 127.0.0.1
2019-12-03 14:13:37 [cn.xm.exam.test.PlainTest]-[DEBUG] [getUser] remove之后 NDC.get(): null
(2)MDC的使用:(常用)
MDC内部维护一个ThreadLocal(而且是可继承的InheritableThreadLocal)。我们put的时候实际是存放到了InheritableThreadLocal中。MDC可以存放多个值,在配置中也可以获取多个。
常用场景是:我们需要记录当前登录的用户身份信息、或者加上操作的IP。如下:
package cn.xm.exam.test; import org.apache.log4j.MDC; import org.apache.log4j.NDC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PlainTest { private static final Logger LOGGER = LoggerFactory.getLogger(PlainTest.class); public static void main(String[] args) { MDC.put("user", "zhangsan"); LOGGER.debug("main函数开始运行"); // 模拟开启线程访问API接口 new Thread(new Runnable() { @Override public void run() { getUser(); } }).start(); try { Thread.sleep(1 * 100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } MDC.remove("user"); LOGGER.debug("main函数结束运行"); } /** * 假设这是一个API的处理(进入此环境,我们向NDC存储一个访问的IP) */ public static void getUser() { NDC.push("127.0.0.1"); LOGGER.debug("访问接口"); LOGGER.debug("NDC.get(): {} ", NDC.get()); // 移除 // NDC.pop(); // 适当位置清空 NDC.remove(); LOGGER.debug("remove之后 NDC.get(): {} ", NDC.get()); } }
配置:(使用%X{user}获取信息)
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] [%M] %x %X{user} %m%n
结果:
2019-12-03 14:51:38 [cn.xm.exam.test.PlainTest]-[DEBUG] [main] zhangsan main函数开始运行
2019-12-03 14:51:38 [cn.xm.exam.test.PlainTest]-[DEBUG] [getUser] 127.0.0.1 zhangsan 访问接口
2019-12-03 14:51:38 [cn.xm.exam.test.PlainTest]-[DEBUG] [getUser] 127.0.0.1 zhangsan NDC.get(): 127.0.0.1
2019-12-03 14:51:38 [cn.xm.exam.test.PlainTest]-[DEBUG] [getUser] zhangsan remove之后 NDC.get(): null
2019-12-03 14:51:39 [cn.xm.exam.test.PlainTest]-[DEBUG] [main] main函数结束运行
在实际使用的时候可以使用一个过滤器进行处理,过滤器开始的时候进行设置值,过滤器结束的时候清空对应的值。实际MDC也可以作为一个工具类使使用,可以用做ThreadLocal的替代。