Logger:被称为记录器,应用程序通过获取Logger对象,抵用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。
Handler:处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。
Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。
Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。
Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。
public class JULTest { @Test public void test01(){ /* 日志入口程序 java.util.logging.Logger */ // Logger对象的创建方式,不能直接new对象 // 取得对象的方法参数,需要引入当前类的全路径字符串(当前我们先这么用,以后根据包结构有Logger父子关系,以后详细介绍) Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 对于日志的输出,有两种方式 第一种方式: 直接调用日志级别相关的方法,方法中传递日志输出信息 假设现在我们要输出info级别的日志信息 */ //logger.info("输出info信息1"); /* 第二种方式: 调用通用的log方法,然后在里面通过Level类型来定义日志的级别参数,以及搭配日志输出信息的参数 */ //logger.log(Level.INFO,"输出info信息2"); /* 输出学生信息 姓名 年龄 */ /*String name = "zs"; int age = 23; logger.log(Level.INFO,"学生的姓名为:"+name+";年龄为:"+age);*/ /* 对于输出消息中,字符串的拼接弊端很多 1.麻烦 2.程序效率低 3.可读性不强 4.维护成本高 我们应该使用动态生成数据的方式,生产日志 我们使用的就是占位符的方式来进行操作 */ String name = "zs"; int age = 23; logger.log(Level.INFO,"学生的姓名:{0},年龄:{1}",new Object[]{name,age}); } }
@Test public void test02(){ /* 日志的级别(通过源码查看,非常简单) SEVERE : 错误 --- 最高级的日志级别 WARNING : 警告 INFO : (默认级别)消息 源码:Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右有一个 defaultlevel属性 CONFIG : 配置 FINE : 详细信息(少) FINER : 详细信息(中) FINEST : 详细信息 (多) --- 最低级的日志级别 两个特殊的级别 OFF 可用来关闭日志记录 ALL 启用所有消息的日志记录 对于日志的级别,重点关注的是new对象的时候的第二个参数,是一个数值(源码中有) OFF Integer.MAX_VALUE 整型最大值 SEVERE 1000 WARNING 900 ... ... FINEST 300 ALL Integer.MIN_VALUE 整型最小值 这个数值的意义在于,如果设置的日志的级别是INFO -- 800 那么最终展现的日志信息,必须是数值大于800的所有的日志信息 最终展现的就是 SEVERE WARNING INFO */ Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 通过打印结果,我们看到了仅仅只是输出了info级别以及比info级别高的日志信息 比info级别低的日志信息没有输出出来 证明了info级别的日志信息,它是系统默认的日志级别 在默认日志级别info的基础上,打印比它级别高的信息 */ /* 如果仅仅只是通过以下形式来设置日志级别 那么不能够起到效果 将来需要搭配处理器handler共同设置才会生效 */ logger.setLevel(Level.CONFIG); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
@Test public void test03(){ /* 自定义日志的级别 */ // 日志记录器 Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 将默认的日志打印方式关闭掉 参数设置为false,打印日志的方式就不会按照父logger默认的方式去进行操作 */ logger.setUseParentHandlers(false); /* 处理器Handler 这里使用的是控制台日志处理器,取得处理器对象 */ ConsoleHandler handler = new ConsoleHandler(); // 创建日志格式化组件对象 SimpleFormatter formatter = new SimpleFormatter(); // 在处理器中设置输出格式 handler.setFormatter(formatter); // 在记录器中添加处理器 logger.addHandler(handler); /* 设置日志的打印级别 此处必须将日志记录器和处理器的级别进行统一的设置,才会达到日志显示相应级别的效果 logger.setLevel(Level.CONFIG); handler.setLevel(Level.CONFIG); */ // 设置如下的all级别之后,那么下面所有的输出信息都会打印到控制台,在需要的时候改成对应的日志级别即可 logger.setLevel(Level.ALL); handler.setLevel(Level.ALL); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
@Test public void test04() throws IOException { /* 将日志输出到具体的磁盘文件中 这样做相当于是做了日志的持久化操作 */ Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); logger.setUseParentHandlers(false); // 文件日志处理器 FileHandler handler = new FileHandler("D:\\test\\" + this.getClass().getSimpleName() + ".log"); SimpleFormatter formatter = new SimpleFormatter(); handler.setFormatter(formatter); logger.addHandler(handler); // 也可以同时在控制台和文件中进行打印 ConsoleHandler handler2 = new ConsoleHandler(); handler2.setFormatter(formatter); // 可以在记录器中同时添加多个处理器,这样磁盘和控制台中都可以输出对应级别的日志信息了 logger.addHandler(handler2); logger.setLevel(Level.ALL); handler.setLevel(Level.ALL); handler2.setLevel(Level.CONFIG); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); /* 总结: 用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler (日志的记录使用的是Logger,日志的输出使用的是Handler) 添加了哪些handler对象,就相当于需要根据所添加的handler 将日志输出到指定的位置上,例如控制台、文件.. */ }
@Test public void test05(){ /* Logger之间的父子关系 JUL中Logger之间是存在"父子"关系的 值得注意的是,这种父子关系不是我们普遍认为的类之间的继承关系 关系是通过树状结构存储的 */ /* 从下面创建的两个logger对象看来 我们可以认为logger1是logger2的父亲 */ /* 父亲是RootLogger,名称默认是一个空的字符串 RootLogger可以被称之为所有logger对象的顶层logger */ // 这就是父 Logger logger1 = Logger.getLogger("cn.zixieqing.jul.test"); // 这是子,甚至cn.zixieqing.jul也是这个logger2的父 Logger logger2 = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); //System.out.println(logger2.getParent()==logger1); //true System.out.println("logger1的父Logger引用为:" +logger1.getParent()+"; 名称为"+logger1.getName()+"; 父亲的名称为"+logger1.getParent().getName()); System.out.println("logger2的父Logger引用为:" +logger2.getParent()+"; 名称为"+logger2.getName()+"; 父亲的名称为"+logger2.getParent().getName()); /* 父亲所做的设置,也能够同时作用于儿子 对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印 */ // 父亲做设置 logger1.setUseParentHandlers(false); ConsoleHandler handler = new ConsoleHandler(); SimpleFormatter formatter = new SimpleFormatter(); handler.setFormatter(formatter); logger1.addHandler(handler); handler.setLevel(Level.ALL); logger1.setLevel(Level.ALL); // 儿子做打印 - 结果就是:儿子logger2没做配置,但是父亲logger1做了,所以儿子logger2的输出级别就是父亲的级别 logger2.severe("severe信息"); logger2.warning("warning信息"); logger2.info("info信息"); logger2.config("config信息"); logger2.fine("fine信息"); logger2.finer("finer信息"); logger2.finest("finest信息"); }
小结:看源码的结果
// JUL在初始化时会创建一个顶层RootLogger作为所有Logger的父Logger,java.util.logging.LogManager$RootLogger,默认的名称为空串 // 查看源码Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右: owner.rootLogger = owner.new RootLogger(); RootLogger是LogManager的内部类 /* 以上的RootLogger对象作为树状结构的根节点存在的 将来自定义的父子关系通过路径来进行关联 父子关系,同时也是节点之间的挂载关系 */ // 350行左右 owner.addLogger(owner.rootLogger); addLogger ----> LoggerContext cx = getUserContext(); /* LoggerContext一种用来保存节点的Map关系,WeakHashMap
Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->345行左右: 有这么一行代码:owner.readPrimordialConfiguration(); 点击readPrimordialConfiguration(),在340行左右有一句readConfiguration(); 点击readConfiguration();在1290行左右,有如下的代码 String fname = System.getProperty("java.util.logging.config.file"); if (fname == null) { fname = System.getProperty("java.home"); if (fname == null) { throw new Error("Can't find java.home ??"); } File f = new File(fname, "lib"); f = new File(f, "logging.properties"); fname = f.getCanonicalPath(); }
结论:默认配置文件所在地
分析一下上述目录中的logging.properties文件,找打这个目录下的此文件打开
# RootManager默认使用的处理器 # 若是想要配置多个处理器,可以使用逗号进行分开,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandler handlers= java.util.logging.ConsoleHandler # RootManager的默认日志级别,这是全局的日志级别 # 若不手动进行级别配置,那么默认使用INFO及更高的级别进行输出 .level= INFO # 文件处理器设置 # 日志文件的路径 # %h/java%u.log h指的是用户目录 - 不分window还是linux # java%u.log是生成的文件名,其中:u相当于是自增,从0开始的 # 如:java0.log、java1.log、java2.log......这里生成多少份由java.util.logging.FileHandler.count = 1这个配置决定 java.util.logging.FileHandler.pattern = %h/java%u.log # 日志文件的限制 - 默认50000字节 java.util.logging.FileHandler.limit = 50000 # 日志文件的数量 - 默认1份 java.util.logging.FileHandler.count = 1 # 日志文件的格式,默认XML格式,也可以采用SimpleFormatter java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter # 控制台处理器设置 # 控制台默认级别 java.util.logging.ConsoleHandler.level = INFO # 控制台默认输出格式 java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # 这是这个源文档中举的一个例子,意思是像下面这样,可将日志级别设定到具体的某个包下 # com.xyz.foo.level = SEVERE
# 自定义Logger cn.zixieqing.handlers=java.util.logging.FileHandler # 自定义Logger日志级别 cn.zixieqing.level=SERVER # 屏蔽父logger的日志设置,相当于前面玩的logger.setUseParentHandlers(false); cn.zixieqing.useParentHandlers=false # RootManager默认使用的处理器 # 若是想要配置多个处理器,可以使用逗号进行分开,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandler handlers= java.util.logging.ConsoleHandler # RootManager的默认日志级别,这是全局的日志级别 .level= SERVER # 文件处理器设置 # 日志文件的路径 # %h/java%u.log h指的是用户目录 - 不分window还是linux # java%u.log是生成的文件名,其中:u相当于是自增,从0开始的 # 如:java0.log、java1.log、java2.log......这里生成多少份由java.util.logging.FileHandler.count = 1这个配置决定 java.util.logging.FileHandler.pattern = %h/java%u.log # 日志文件的限制 - 50000字节 java.util.logging.FileHandler.limit = 50000 # 日志文件的数量 java.util.logging.FileHandler.count = 1 # 日志文件的格式 java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter # 控制台处理器设置 # 控制台默认级别 java.util.logging.ConsoleHandler.level = SERVER # 控制台默认输出格式 java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # 下一次生成的日志文件默认是覆盖了上一此的文件 - 改为在原日志上进行追加 java.util.logging.FileHandler.append=true
@Test public void test06() throws Exception { // 加载自定义的配置文件 InputStream input = new FileInputStream("D:\\test\\logging.properties"); // 取得日志管理器对象 LogManager logManager = LogManager.getLogManager(); // 读取自定义的配置文件 logManager.readConfiguration(input); Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1、初始化LogManager
2、从单例的LogManager获取Logger,即:LogManager.getLogManager();
3、设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord类,源码找寻如下:
1、点击logger.severe("severe信息");中的severe,当然点击其他warning、info、config也是可以进去的,之后会看到如下的代码 public void severe(String msg) { log(Level.SEVERE, msg); } 2、点击上述的log(),看到如下的代码 public void log(Level level, String msg) { if (!isLoggable(level)) { return; } // 这里就是目的地 LogRecord lr = new LogRecord(level, msg); doLog(lr); }
4、Filter作为过滤器提供了日志级别之外更细粒度的控制
5、Handler日志处理器,决定日志的输出位置,例如控制台、文件...
6、Formatter是用来格式化输出的
cn.zixieqing.test
,同时:Logger的名字大小写敏感,其命名有继承机制( 和JUL中的父子关系一样,以包路径来区分的 );另外:root logger是所有logger的根 - 上辈所做的日志属性设置,会直接的影响到子辈// root logger的获取 Logger.getRootLogger();
日志级别
log4j log4j 1.2.17
public class Log4jTest01 { @Test public void test01(){ // 加载初始化配置 BasicConfigurator.configure(); // 注意:这个logger是apache下的,前面玩的JUL中的是java。util.logging包下的 Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
BasicConfigurator.configure();
,不加这一句代码就报错 - 没有添加Appenders输出控制器,加上不报错是因为:源码中有这样一句代码 rootManager.addAppenders( XxxxAppender( PatternLayout layout ) )
,configure源码如下:public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
LOG4J日志级别
BasicConfigurator.configure(); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息");
分析 BasicConfigurator.configure();点击configure()
public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
从中可以得到几个信息:
Logger root = Logger.getRootLogger(); ConsoleAppender对象(表示默认打印到控制台,自定义的格式化输出PatternLayout)
那么想要自定义配置文件来实现上述源代码的功能呢?通过上面这个源代码分析,我们需要具备如下的条件:
分析Logger logger = Logger.getLogger(Log4jTest01.class);点击getLogger()
public static Logger getLogger(Class clazz) { return LogManager.getLogger(clazz.getName()); }
LogManager.getLogger(clazz.getName());
,其中: LogManager
j就是日志管理器查看LogManager
public static final String DEFAULT_CONFIGURATION_FILE = "log4j.properties"; static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml"; /** @deprecated */ public static final String DEFAULT_CONFIGURATION_KEY = "log4j.configuration"; /** @deprecated */ public static final String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass"; /** @deprecated */ public static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride"; private static Object guard = null; private static RepositorySelector repositorySelector;
log4j.properties属性
使我们最常使用的,因为它语法简单、使用方便log4j.properties的加载时机
加载 - 那就是static
观察LogManager中的代码,找到其中的静态代码块static
static { Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG)); repositorySelector = new DefaultRepositorySelector(h); String override = OptionConverter.getSystemProperty("log4j.defaultInitOverride", (String)null); if (override != null && !"false".equalsIgnoreCase(override)) { LogLog.debug("Default initialization of overridden by log4j.defaultInitOverrideproperty."); } else { String configurationOptionStr = OptionConverter.getSystemProperty("log4j.configuration", (String)null); String configuratorClassName = OptionConverter.getSystemProperty("log4j.configuratorClass", (String)null); URL url = null; if (configurationOptionStr == null) { url = Loader.getResource("log4j.xml"); if (url == null) { // 前面就是文件格式的一堆判断,这里才是log4j.propertie格式做的事情 url = Loader.getResource("log4j.properties"); } } else { try { url = new URL(configurationOptionStr); } catch (MalformedURLException var7) { url = Loader.getResource(configurationOptionStr); } } if (url != null) { LogLog.debug("Using URL [" + url + "] for automatic log4j configuration."); try { // 这里又是一个信息:selectAndConfigure()翻译就是选择配置文件 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository()); } catch (NoClassDefFoundError var6) { LogLog.warn("Error during default initialization", var6); } } else { LogLog.debug("Could not find resource: [" + configurationOptionStr + "]."); } } }
从源码中,发现 url = Loader.getResource("log4j.properties");
log4j.properties
,而若是maven工程,那么:就应该在resources路径下去找同时在上面的源码中发现 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());
查看selectAndConfigure()
public static void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) { Configurator configurator = null; String filename = url.getFile(); if (clazz == null && filename != null && filename.endsWith(".xml")) { clazz = "org.apache.log4j.xml.DOMConfigurator"; } if (clazz != null) { LogLog.debug("Preferred configurator class: " + clazz); configurator = (Configurator)instantiateByClassName(clazz, Configurator.class, (Object)null); if (configurator == null) { LogLog.error("Could not instantiate configurator [" + clazz + "]."); return; } } else { // 有用信息在这里,即 new PropertyConfigurator();创建了一个properties配置对象 configurator = new PropertyConfigurator(); } ((Configurator)configurator).doConfigure(url, hierarchy); }
查看PropertyConfigurator类
static final String CATEGORY_PREFIX = "log4j.category."; static final String LOGGER_PREFIX = "log4j.logger."; static final String FACTORY_PREFIX = "log4j.factory"; static final String ADDITIVITY_PREFIX = "log4j.additivity."; static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory"; // 这是一个重要信息 static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"; // 这也是一个重要信息 static final String APPENDER_PREFIX = "log4j.appender."; static final String RENDERER_PREFIX = "log4j.renderer."; static final String THRESHOLD_PREFIX = "log4j.threshold"; private static final String THROWABLE_RENDERER_PREFIX = "log4j.throwableRenderer"; private static final String LOGGER_REF = "logger-ref"; private static final String ROOT_REF = "root-ref"; private static final String APPENDER_REF_TAG = "appender-ref"; public static final String LOGGER_FACTORY_KEY = "log4j.loggerFactory"; private static final String RESET_KEY = "log4j.reset"; private static final String INTERNAL_ROOT_NAME = "root";
static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"; static final String APPENDER_PREFIX = "log4j.appender.";
找寻static final String APPENDER_PREFIX = "log4j.appender."中的appender配置方式
直接在当前源码页面搜索appender
Appender parseAppender(Properties props, String appenderName) { Appender appender = this.registryGet(appenderName); if (appender != null) { LogLog.debug("Appender \"" + appenderName + "\" was already parsed."); return appender; } else { // 重要信息就在这里,这里告知了一件事:上面找到的log4j.appender.配置方式为如下的方式 String prefix = "log4j.appender." + appenderName; // 这也是重要信息,layout日志格式化的配置方式 String layoutPrefix = prefix + ".layout"; appender = (Appender)OptionConverter.instantiateByKey(props, prefix, Appender.class, (Object)null); if (appender == null) { LogLog.error("Could not instantiate appender named \"" + appenderName + "\"."); return null; } else { appender.setName(appenderName); if (appender instanceof OptionHandler) { if (appender.requiresLayout()) { Layout layout = (Layout)OptionConverter.instantiateByKey(props, layoutPrefix, Layout.class, (Object)null); if (layout != null) { appender.setLayout(layout); LogLog.debug("Parsing layout options for \"" + appenderName + "\"."); PropertySetter.setProperties(layout, props, layoutPrefix + "."); LogLog.debug("End of parsing for \"" + appenderName + "\"."); } } String errorHandlerPrefix = prefix + ".errorhandler"; String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props); if (errorHandlerClass != null) { ErrorHandler eh = (ErrorHandler)OptionConverter.instantiateByKey(props, errorHandlerPrefix, ErrorHandler.class, (Object)null); if (eh != null) { appender.setErrorHandler(eh); LogLog.debug("Parsing errorhandler options for \"" + appenderName + "\"."); this.parseErrorHandler(eh, errorHandlerPrefix, props, this.repository); Properties edited = new Properties(); String[] keys = new String[]{errorHandlerPrefix + "." + "root-ref", errorHandlerPrefix + "." + "logger-ref", errorHandlerPrefix + "." + "appender-ref"}; Iterator iter = props.entrySet().iterator(); while(true) { if (!iter.hasNext()) { PropertySetter.setProperties(eh, edited, errorHandlerPrefix + "."); LogLog.debug("End of errorhandler parsing for \"" + appenderName + "\"."); break; } Entry entry = (Entry)iter.next(); int i; for(i = 0; i < keys.length && !keys[i].equals(entry.getKey()); ++i) { } if (i == keys.length) { edited.put(entry.getKey(), entry.getValue()); } } } } PropertySetter.setProperties(appender, props, prefix + "."); LogLog.debug("Parsed \"" + appenderName + "\" options."); } this.parseAppenderFilters(props, appenderName, appender); this.registryPut(appender); return appender; } } }
通过上述的源码,发现配置 log4j.appender.
的方式: log4j.appender.+appenderName
继而:推导出 log4j.properties
配置文件中的一个配置项appender输出方式为: log4j.appender.+appenderName=某一种输出控制器名字
Log4j.properties
的appender输出方式配置方式举例就是 log4j.appender.console=org.apache.log4j.ConsoleAppender
String layoutPrefix = prefix + ".layout";
,也就知道了layout输出格式的配置方式log4j.appender.console.layout=org.log4j.SimpleLayout
小小总结一波:log4j.properties配置文件中的appender输出控制器 和 layout日志输出格式的配置方式
log4j.appender,console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.log4j.SimpleLayout
继续找第二个配置static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"中的rootLogger配置方式
通过log4j.rootLogge进行搜索
void configureRootCategory(Properties props, LoggerRepository hierarchy) { String effectiveFrefix = "log4j.rootLogger"; String value = OptionConverter.findAndSubst("log4j.rootLogger", props); if (value == null) { value = OptionConverter.findAndSubst("log4j.rootCategory", props); effectiveFrefix = "log4j.rootCategory"; } if (value == null) { LogLog.debug("Could not find root logger information. Is this OK?"); } else { Logger root = hierarchy.getRootLogger(); synchronized(root) { // 这里面执行了这个方式 this.parseCategory(props, root, effectiveFrefix, "root", value); } } }
查看parseCategory()
void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) { LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "]."); // 配置方式就在这里,这个操作的意思就是:表示要以逗号的方式来切割字符串,证明了log4j.rootLogger的取值,可以有多个值,使用逗号进行分隔 StringTokenizer st = new StringTokenizer(value, ","); if (!value.startsWith(",") && !value.equals("")) { if (!st.hasMoreTokens()) { return; } // 把字符串通过逗号切割之后,第一个值的用途就在这里 - levelStr、level,即:切割后的第一个值是日志的级别 String levelStr = st.nextToken(); LogLog.debug("Level token is [" + levelStr + "]."); if (!"inherited".equalsIgnoreCase(levelStr) && !"null".equalsIgnoreCase(levelStr)) { logger.setLevel(OptionConverter.toLevel(levelStr, Level.DEBUG)); } else if (loggerName.equals("root")) { LogLog.warn("The root logger cannot be set to null."); } else { logger.setLevel((Level)null); } LogLog.debug("Category " + loggerName + " set to " + logger.getLevel()); } logger.removeAllAppenders(); // 字符串切割之后的第一个值是level日志级别,而剩下的值的用途就在这里 while(st.hasMoreTokens()) { // 通过这句代码得知:第2 - 第n个值,就是我们配置的其他信息,这个信息就是appenderName String appenderName = st.nextToken().trim(); if (appenderName != null && !appenderName.equals(",")) { LogLog.debug("Parsing appender named \"" + appenderName + "\"."); Appender appender = this.parseAppender(props, appenderName); if (appender != null) { logger.addAppender(appender); } } } }
log4j.rootLogger
的配置方式为: log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
BasicConfigurator.configure();
替代品的properties配置如下:# rootLogger所有logger的根配置 - log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3.... # 这里的例子没用日志输出路径,这个日志输出路径后续再加 log4j.rootLogger=debug,console # appender输出控制器配置 log4j.appender.+appenderName=某一种输出类型 - 采用Console控制台的方式举例 log4j.appender.console=org.apache.log4j.ConsoleAppender # 输出的格式配置log4j.appender.+appenderName+layout=某种layout格式类型 log4j.appender.console.layout=org.apache.log4j.SimpleLayout
测试
// 注掉这一句就可以了,这句代码的配置由上面的自定义配置进行代替了 //BasicConfigurator.configure(); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息");
@Test public void test03(){ /* 通过Logger中的开关 打开日志输出的详细信息 查看LogManager类中的方法getLoggerRepository() 找到代码LogLog.debug(msg, ex); LogLog会使用debug级别的输出为我们展现日志输出详细信息 Logger是记录系统的日志,那么LogLog就是用来记录Logger的日志 进入到LogLog.debug(msg, ex);方法中 通过代码:if (debugEnabled && !quietMode) 观察到if判断中的这两个开关都必须开启才行 !quietMode是已经启动的状态,不需要我们去管 debugEnabled默认是关闭的 所以我们只需要设置debugEnabled为true就可以了 */ // 开启 debugEnabled LogLog.setInternalDebugging(true); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests - fatal信息 1 [main] ERROR cn.zixieqing.HotelJavaApplicationTests - error信息 1 [main] WARN cn.zixieqing.HotelJavaApplicationTests - warn信息 1 [main] INFO cn.zixieqing.HotelJavaApplicationTests - info信息 1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests - debug信息
log4j: Trying to find [log4j.xml] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2. log4j: Trying to find [log4j.xml] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader. log4j: Trying to find [log4j.xml] using ClassLoader.getSystemResource(). log4j: Trying to find [log4j.properties] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2. log4j: Trying to find [log4j.properties] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader. log4j: Trying to find [log4j.properties] using ClassLoader.getSystemResource(). log4j: Could not find resource: [null]. 0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests - fatal信息 0 [main] ERROR cn.zixieqing.HotelJavaApplicationTests - error信息 1 [main] WARN cn.zixieqing.HotelJavaApplicationTests - warn信息 1 [main] INFO cn.zixieqing.HotelJavaApplicationTests - info信息 1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests - debug信息
Layout
而已,而自定义就是玩的 PatternLayout
,这个类有一个 setConversionPattern()
方法,查看这个方法的源码public void setConversionPattern(String conversionPattern) { this.pattern = conversionPattern; this.head = this.createPatternParser(conversionPattern).parse(); }
String conversionPattern
,因此:在 log4j.properties
配置文件中添加上 conversionPattern
属性配置即可,当然:这个属性配置遵循一定的写法,写法如下:%m 输出代码中指定的日志信息 %p 输出优先级,及 DEBUG、INFO 等 %n 换行符(Windows平台的换行符为 "\n",Unix 平台为 "\n") %r 输出自应用启动到输出该 log 信息耗费的毫秒数 %c 输出打印语句所属的类的全名 %t 输出产生该日志的线程全名 %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss} %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10) %F 输出日志消息产生时所在的文件名称 %L 输出代码中的行号 %% 输出一个 "%" 字符 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式 [%10p]:[]中必须有10个字符,由空格来进行补齐,信息右对齐 [%-10p]:[]中必须有10个字符,由空格来进行补齐,信息左对齐,应用较广泛 上述举例:[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
log4j.properties
的配置如下( 做的修改就是最后两个配置项 )# rootLogger所有logger的根配置 - log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3.... log4j.rootLogger=debug,console # appender输出控制器配置 log4j.appender.+appenderName=某一种输出类型 - 采用Console控制台的方式举例 log4j.appender.console=org.apache.log4j.ConsoleAppender # 输出的格式配置log4j.appender.+appenderName+layout=某种layout格式类型 - 注意:这里类型改了,是PatternLayout,即自定义 log4j.appender.console.layout=org.apache.log4j.PatternLayout # 编写自定义输出格式( 直接把上面举例的拿过来 ) - 注意:加上了刚刚说的conversionPattern属性 log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
public void test04(){ LogLog.setInternalDebugging(true); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
2.2.5.1、改造log4j.properties文件
# rootLogger所有logger的根配置 - 这里再加一个file log4j.rootLogger=debug,console,file # 控制台的appender输出控制器配置 log4j.appender.console=org.apache.log4j.ConsoleAppender # 采用自定义控制台输出的格式 log4j.appender.console.layout=org.apache.log4j.PatternLayout # 编写控制台自定义输出格式 log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n # 再来一份,变为file的配置 - 把console改为file即可 log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
2.2.5.2、指定文件输出位置 及 字符编码设置
FileAppender
源码,首先看到的是四个属性// 表示日志文件是否采用内容追加的方式 - 源码中有一个构造方法,这个的默认值是true protected boolean fileAppend; protected String fileName; protected boolean bufferedIO; // 日志文件的大小 - 源码的构造方法中默认值是8192 protected int bufferSize; // 构造方法源码 public FileAppender() { this.fileAppend = true; this.fileName = null; this.bufferedIO = false; this.bufferSize = 8192; }
FlieAppender
中还有一个 setFile()
的方法,得知这个就是设置文件的方法 ,也就是文件日志文件存放路径位置public void setFile(String file) { String val = file.trim(); this.fileName = val; }
MyBatis
的知识就知道 log4j.properties
配置中的这个对应属性就是file了( 截掉set,得到属性名 )# rootLogger所有logger的根配置 - 这里再加一个file log4j.rootLogger=debug,console,file # 控制台的appender输出控制器配置 log4j.appender.console=org.apache.log4j.ConsoleAppender # 采用自定义控制台输出的格式 log4j.appender.console.layout=org.apache.log4j.PatternLayout # 编写控制台自定义输出格式 log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n # 再来一份,变为file的配置 - 把console改为file即可 log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n # 日志文件保存路径 - 注:前一个file为自定义的appenderName;后一个file是日志文件输出路径的属性,要是怕看错眼,可以把后者换为File大写也没错 log4j.appender.file.file=D:\log4j\zixieqing\log4j.log
FileAppender
的父类 WriterAppender
,去看字符编码设置protected boolean immediateFlush; // 这个属性就是编码设置 protected String encoding; protected QuietWriter qw;
log4.properties
配置为:# 控制台日志输出设置 log4j.rootLogger=debug,console,file log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n # 日志文件输出设置 log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n log4j.appender.file.file=D:\log4j\zixieqing\log4j.log log4j.appender.file.encoding=UTF-8
2.2.5.3、拆分日志文件
一份日志不可能用于一直记录下去,那样的话,日志文件体积就太大了
继续查看 FileAppender
源码,看实现类
2.2.5.3.1、RollingFileAppender实现类
// 达到多大文件时进行日志拆分 protected long maxFileSize = 10485760L; // 一共能够拆分出多少份日志文件 protected int maxBackupIndex = 1;
log4j.properties
配置文件修改一下即可实现日志根据文件大小进行拆分# 注意:记得在log4j.rootLoger=debug,console,file这一句中加上下面的这个文件大小进行拆分的配置 # 如:log4j.rootLoger=debug,console,rollingFile log4j.appender.rollingFile=org.apache.log4j.RollingFileAppender log4j.appender.rollingFile.layout=org.apache.log4j.PatternLayout log4j.appender.rollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n log4j.appender.rollingFile.file=D:\log4j\zixieqing\log4j.log # 文件多大时进行日志拆分 log4j.appender.rollingFile.maxFileSize=10MB # 最多可以拆分多少份 log4j.appender.rollingFile.maxBackupIndex=50
2.2.5.3.2、DailyRollingFileAppender实现类 - 建议用
// 时间格式,默认值就是如下的天 private String datePattern = "'.'yyyy-MM-dd";
log4j.properties
配置文件中修改成如下的配置即可使用# 一样的,要使用这种方式:记得在log4j.rootLogger=debug,console,file这一句中加上下面的这个文件大小进行拆分的配置 # 如:log4j.rootLogger=debug,console,dateRollingFile log4j.appender.dateRollingFile=org.apache.log4j.DailyRollingFileAppender log4j.appender.dateRollingFile.layout=org.apache.log4j.PatternLayout log4j.appender.dateRollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n log4j.appender.dateRollingFile.file=D:\log4j\zixieqing\log4j.log # 加上时间格式 - 下面的值根据实际情况即可 log4j.appender.dateRollingFile.datePattern='.'yyyy-MM-dd
表基础字段
CREATE TABLE tbl_log( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) DEFAULT NULL COMMENT '项目名称', createTime varchar(100) DEFAULT NULL COMMENT '创建时间', level varchar(10) DEFAULT NULL COMMENT '日志级别', category varchar(100) DEFAULT NULL COMMENT '所在类的全路径', fileName varchar(100) DEFAULT NULL COMMENT '文件名称', message varchar(255) DEFAULT NULL COMMENT '日志消息', PRIMARY KEY(id) )
项目MySQL驱动
mysql mysql-connector-java ${mysql.version} runtime
log4.properties配置
# 配置appender输出方式 输出到数据库表 - 注意:在rootManager中加上logDB,如:log4j.rootLogger=debug,console,logDB log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender log4j.appender.logDB.layout=org.apache.log4j.PatternLayout log4j.appender.logDB.Driver=com.mysql.jdbc.Driver log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/test log4j.appender.logDB.User=root log4j.appender.logDB.Password=072413 log4j.appender.logDB.Sql=INSERT INTO tbl_log(name,createTime,level,category,fileName,message) values('project_log','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%m')
测试代码
@Test public void test07(){ Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
rootManager
的logger,接下来就自定义一个logger,看 PropertyConfigurator
类的源码,它里面有一个属性// 自定义logger配置的写法 - 这后面拼接的就是自定义的logger名字 static final String LOGGER_PREFIX = "log4j.logger.";
如:cn.zixieqing.log4j.test.Log4jTest01 它的父logger就是上层的路径或者是更上层的路径 例如: cn.zixieqing.log4j.test cn.zixieqing.log4j ... cn
修改log4j.properties
# 根logger,输出级别是trace,在console控制台进行输出 log4j.rootLogger=trace,console # 自定义logger,级别为info,在file文件中输出 log4j.logger.cn.zixieqing.log4j.test=info,file # 自定义logger,是apache的,级别为error log4j.logger.org.apache=error
自定义logger的注意点
log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./1og/zixieqing.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]‰m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sq1=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
全称为Jakarta Commons Logging,是Apache提供的一个通用日志AP
注意:这个玩意儿本身没有记录日志的实现功能,而是相当于一个门面。我们可以自由选择第三方的日志组件( log4j、JUL )作为具体实现,common-logging会通过动态查找的机制,在程序运行时自动找出真正使用的日志库
JCL的组成
依赖
commons-logging commons-logging 1.2
测试
@Test void jclQuickStartTest() { // LogFactory是org.apache.commons.logging.LogFactory Log log = LogFactory.getLog(JCLTest01.class); log.info("info信息"); }
log4j log4j 1.2.17
log4j.properties
配置文件JCL源码分析
Log接口的4个实现类 JDk13 JDK14 正常java.util.logging Log4j 我们集成的log4j Simple JCL自带实现类 (1)查看Jdk14Logger证明里面使用的是JUL日志框架 - 看import引入的就可以得知 (2)查看Log4JLogger证明里面使用的是Log4j日志框架 - 看import引入的就可以得知 (3)观察LogFactory,看看如何加载的Logger对象 这是一个抽象类,无法实例化 需要观察其实现类LogFactoryImpl (4)观察LogFactoryImpl 真正加载日志实现使用的就是这个实现类LogFactoryImpl (5)进入getLog - 采用打断点debug能够更清晰地看清楚 进入getInstance 找到instance = this.newInstance(name);,继续进入 找到instance = this.discoverLogImplementation(name); 表示发现一个日志的实现 for(int i = 0; i < classesToDiscover.length && result == null; ++i) { result = this.createLogFromClass(classesToDiscover[i], logCategory, true); } 遍历我们拥有的日志实现框架 遍历的是一个数组,这个数组是按照 log4j jdk14 jdk13 SimpleLogger 的顺序依次遍历 表示的是,第一个要遍历的就是log4j,如果有log4j则执行该日志框架 如果没有,则遍历出来第二个,使用jdk14的JUL日志框架 以此类推 result = this.createLogFromClass(classesToDiscover[i], logCategory, true); 表示帮我们创建Logger对象 在这个方法中,我们看到了 c = Class.forName(logAdapterClassName, true, currentCL); 是取得该类型的反射类型对象 使用反射的形式帮我们创建logger对象 constructor = c.getConstructor(this.logConstructorSignature);
常见的日志门面和日志实现
常见的日志实现:JUL、log4j、logback、log4j2
常见的日志门面 :JCL、slf4j
出现顺序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2
了解SLF4J
4.1.1、快速上手
准备知识
trace、debug、info、warn、error五个级别 trace:日志追踪信息 debug:日志详细信息 info:日志的关键信息 默认打印级别 warn:日志警告信息 error:日志错误信息
org.slf4j slf4j-simple 1.7.25
入门
org.slf4j slf4j-api 1.7.25 org.slf4j slf4j-simple 1.7.25
@Test public void test01(){ // 是slf4j包下的 Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); logger.trace("trace信息"); logger.debug("debug信息"); logger.info("info信息"); logger.warn("warn信息"); logger.error("error信息"); }
4.1.2、动态信息输出
@Test public void test02(){ Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); String name = "zs"; int age = 23; /* 字符串拼接的形式:logger.info("学生信息-姓名:"+name+";年龄:"+age); 使用JCL的形式:logger.info("学生信息-姓名:{},年龄:{}",new Object[]{name,age}); 这上面两者虽然都可以做到相应的输出,但是:麻烦 */ // 使用SLF4J的形式 logger.info("学生信息-姓名:{},年龄:{}",name,age); }
4.2.3、输出异常信息
@Test public void test03(){ /* 日志对于异常信息的处理 一般情况下,我们在开发中的异常信息,都是记录在控制台上(我们开发环境的一种日志打印方式) 我们会根据异常信息提取出有用的线索,来调试bug 但是在真实生产环境中(项目上线),对于服务器或者是系统相关的问题 在控制台上其实也会提供相应的异常或者错误信息的输出 但是这种错误输出方式(输出的时间,位置,格式...)都是服务器系统默认的 我们可以通过日志技术,选择将异常以日志打印的方式,进行输出查看 输出的时间,位置(控制台,文件),格式,完全由我们自己去进行定义 */ Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { // e.printStackTrace(); logger.info("XXX类中的XXX方法出现了异常,请及时关注信息"); // e是引用类型对象,不能根前面的{}做有效的字符串拼接 // logger.info("具体错误是:{}",e); // 我们不用加{},直接后面加上异常对象e即可 - 这是利用了重载方法info(String message, Throwable throwable ) logger.info("具体错误是:",e); } }
4.2.4、SLF4J与日志绑定
图中分为了三部分
4.2.5、绑定logback
依赖
org.slf4j slf4j-api 1.7.25 ch.qos.logback logback-classic 1.2.11
测试
public class SLF4JTest { @Test public void bingingLogTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { logger.info("具体错误是:",e); } } }
结果
10:50:15.391 [main] INFO com.zixieqing.SLF4JTest - 具体错误是: java.lang.ClassNotFoundException: aaa at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) .......
slf4j-simple
4.2.6、slf4j-nop禁止日志打印
依赖
junit junit 4.13.2 test org.slf4j slf4j-api 1.7.25 org.slf4j slf4j-nop 1.7.30 ch.qos.logback logback-classic 1.2.11
测试
@Test public void slf4jNopTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { logger.info("具体错误是:",e); } }
结果
SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/org/slf4j/slf4j-nop/1.7.30/slf4j-nop-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. SLF4J: Actual binding is of type [org.slf4j.helpers.NOPLoggerFactory]
4.2.7、绑定log4j
slf4j-log4j12
的适配器玩玩绑定log4j
依赖
junit junit 4.13.2 test org.slf4j slf4j-api 1.7.25 log4j log4j 1.2.17 org.slf4j slf4j-log4j12 1.7.30
测试
@Test public void slf4jBindingLog4jTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); logger.info("info信息"); }
结果
# 虽然没有输出消息,但是有如下这个话就足够了No appenders could be found for logger # 这表示没有appender嘛,加上log4j.properties配置文件就可以使用了 log4j:WARN No appenders could be found for logger (com.zixieqing.SLF4JTest). log4j:WARN Please initialize the log4j system properly. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
其他日志框架的绑定就看官网的那张图,在那张图中对绑定谁时导入哪一个依赖都有备注
4.2.8、slf4j源码分析流程
进入到getLogger 看到Logger logger = getLogger(clazz.getName()); 进入重载的getLogger ILoggerFactory iLoggerFactory = getILoggerFactory(); 用来取得Logger工厂实现的方法 进入getILoggerFactory() 看到以双重检查锁的方式去做判断 执行performInitialization(); 工厂的初始化方法 进入performInitialization() bind()就是用来绑定具体日志实现的方法 进入bind() 看到Set集合SetstaticLoggerBinderPathSet = null; 因为当前有可能会有N多个日志框架的实现 看到staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); 进入findPossibleStaticLoggerBinderPathSet() 看到创建了一个有序不可重复的集合对象 LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet(); 声明了枚举类的路径,经过if else判断,以获取系统中都有哪些日志实现 看到Enumeration paths; if (loggerFactoryClassLoader == null) { paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); } else { paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); } 我们主要观察常量STATIC_LOGGER_BINDER_PATH 通过常量我们会找到类StaticLoggerBinder 这个类是以静态的方式绑定Logger实现的类 来自slf4j-JDK14的适配器 进入StaticLoggerBinder 看到new JDK14LoggerFactory(); 进入JDK14LoggerFactory类的无参构造方法 看到java.util.logging.Logger.getLogger(""); 使用的就是jul的Logger 接着观察findPossibleStaticLoggerBinderPathSet 看到以下代码,表示如果还有其他的日志实现 while(paths.hasMoreElements()) { URL path = (URL)paths.nextElement(); 将路径添加进入 staticLoggerBinderPathSet.add(path); } 回到bind方法 表示对于绑定多实现的处理 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); 如果出现多日志实现的情况 则会打印 Util.report("Class path contains multiple SLF4J bindings.");
通过源码总结:
在真实生产环境中,slf4j只绑定一个日志实现框架就可以了
绑定多个,默认使用导入依赖的第一个,而且会产生没有必要的警告信息
4.2.9、slf4j日志重构
有这么一个情况:项目原本使用的是log4j日志,但是随着技术的迭代,需要使用另外的日志框架,如:slf4j+logback,因此:此时不可能说去该源码,把所有使用log4j的地方都改成slf4j
4.2.9.1、玩一下桥接器
演示bug:先保证项目没有其他任何的干扰,如:另外的依赖、另外日志的代码
依赖
junit junit 4.13.2 test log4j log4j 1.2.17
log4j.properties配置文件
log4j.rootLogger=debug,console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.lo4j.SimpleLayout
测试代码
package com.zixieqing; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.junit.Test; @Test public void bridgingTest() { // 假设项目用的是log4j Logger logger = LogManager.getLogger(SLF4JTest.class); logger.info("inf信息"); }
此时,项目升级,改用slf4j+logback来当日志
1、去掉log4j的依赖
2、添加slf4j的桥接组件,所以现在的依赖就变成如下的样子
junit junit 4.13.2 test org.slf4j slf4j-api 1.7.25 org.slf4j log4j-over-slf4j 1.7.25 ch.qos.logback logback-classic 1.2.11
要引入什么桥接组件,看要怎么重构,然后根据官网的说明来弄就行
测试
package com.zixieqing; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.junit.Test; @Test public void bridgingTest() { // 假设项目用的是log4j Logger logger = LogManager.getLogger(SLF4JTest.class); logger.info("inf信息"); }
结果
14:45:09.901 [main] INFO com.zixieqing.SLF4JTest - inf信息
logback的组成
logback配置文件的类型
logback的日志输出格式
日志输出格式: %-10level 级别 案例为设置10个字符,左对齐 %d{yyyy-MM-dd HH:mm:ss.SSS} 日期 %c 当前类全限定名 %M 当前执行日志的方法 %L 行号 %thread 线程名称 %m或者%msg 信息 %n 换行
error > warn > info > debug > trace 其中:logback的默认日志级别就是debug
依赖
ch.qos.logback logback-classic 1.2.11 org.slf4j slf4j-api 1.7.25 junit junit 4.13.2 test
测试
public class LogBackTest { @Test public void quickStartTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
结果
15:44:27.032 [main] ERROR com.zixieqing.LogBackTest - error信息 15:44:27.033 [main] WARN com.zixieqing.LogBackTest - warn信息 15:44:27.033 [main] INFO com.zixieqing.LogBackTest - info信息 15:44:27.033 [main] DEBUG com.zixieqing.LogBackTest - debug信息
system.err ${pattern}
中 vlaue
的参数配置参考
日志输出格式: %-10level 级别 案例为设置10个字符,左对齐 %d{yyyy-MM-dd HH:mm:ss.SSS} 日期 %c 当前类全限定名 %M 当前执行日志的方法 %L 行号 %thread 线程名称 %m或者%msg 信息 %n 换行
测试
public class LogBackTest { @Test public void quickStartTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
在 logback.xml
中加入如下的配置即可
${filePath}/logback.log ${pattern}
测试代码
@Test public void fileAppenderTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
通过结果观察会发现:logback的日志文件输出默认是以 追加 的形式添加新日志内容
logback.xml
配置文件编写
${filePath}\logback.log ${pattern}
测试
@Test public void htmlFileAppenderTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
${pattern} ${logDir}/roll_logback.log ${logDir}/roll.%d{yyyy-MM-dd}.log%i.gz 1KB
标签必须有,源码中有说明在TimeBasedRollingPolicy类中有一个常量 static final String FNP_NOT_SET ="The FileNamePattern option must be set before using TimeBasedRollingPolicy. "; 这个常量值告知的结果就是上面的注意点
@Test public void rollFileAppenderTest() { for (int i = 0; i < 1000; i++) { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
System.out ${pattern} ERROR ACCEPT DENY
另外:在上面的 1、配置异步日志
里面还有两个配置属性
下面这个配置的是一个阈值,当队列的剩余容量小于这个阈值的时候,当前日志的级别 trace、debug、info这3个级别的日志将被丢弃 设置为0,说明永远都不会丢弃trace、debug、info这3个级别的日志0 配置队列的深度,这个值会影响记录日志的性能,默认值就是256256 关于这两个属性,一般情况下,使用默认值即可 这两个属性不要乱配置,会影响系统性能,了解其功能即可
@Test public void asyncAppenderTest(){ Logger logger = LoggerFactory.getLogger(LOGBACKTest01.class); // 日志打印操作 for (int i = 0; i < 100; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } // 系统本身业务相关的其他操作 System.out.println("系统本身业务相关的操作1"); System.out.println("系统本身业务相关的操作2"); System.out.println("系统本身业务相关的操作3"); System.out.println("系统本身业务相关的操作4"); }
自定义logger配置
log4j2的特征
性能提升
自动重新加载配置
高级过滤
插件架构
无垃圾机制
log4j2的最佳搭配
log4j2 + slf4j
的方式依赖
org.apache.logging.log4j log4j-api 2.17.1 org.apache.logging.log4j log4j-core 2.17.1 junit junit 4.13.2 test
测试
public class Log4j2Test { @Test public void quickStartTest() { // 是org.apache.logging.log4j包下的 Logger logger = LogManager.getLogger(Log4j2Test.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); } }
结果
21:18:43.909 [main] FATAL com.zixieqing.Log4j2Test - fatal信息 21:18:43.911 [main] ERROR com.zixieqing.Log4j2Test - error信息
简单的log4j2.xml配置文件
测试代码
@Test public void log4j2XmlTest() { // 是org.apache.logging.log4j包下的 Logger logger = LogManager.getLogger(Log4j2Test.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); }
结果
直接输出如下内容: fatal信息 error信息 warn信息 info信息
依赖
org.slf4j slf4j-api 1.7.25 org.apache.logging.log4j log4j-slf4j-impl 2.17.1 org.apache.logging.log4j log4j-api 2.17.1 org.apache.logging.log4j log4j-core 2.17.1
log4j2.xml配置文件
测试
import
导入是 org.slf4j
门面的@Test public void log4j2AndSlf4jTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
结果
error信息 warn信息 info信息 debug信息
log4j2.xml的配置文件
D:\test
@Test public void fileAppenderTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
log4j2.xml配置文件
D:\test
测试
@Test public void rollingLogTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
6.1.6.1、AsyncAppender方式 - 了解
标签当有日志事件到达时,会开启另外一个线程来处理它们。 需要注意的是,如果在Appender的时候出现异常,对应用来说是无法感知的。 AsyncAppender应该在它引用的Appender之后配置,默认使用 java.util.concurrent.ArrayBlockingQueue实现而不需要其它外部的类库。 当使用此Appender的时候,在多线程的环境下需要注意,阻塞队列容易受到锁争用的影响,这可能会对性能产生影响。这时候,我们应该考虑使用无锁的异步记录器(AsyncLogger)
步骤
1、添加异步日志依赖
com.lmax disruptor 3.4.2
2、在Appenders标签中,对于异步进行配置
使用Async标签
3、rootlogger引用Async
完整的log4j2.xml配置
D:\test
测试
@Test public void asyncAppenderTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2、AsyncLogger方式 - 必须会
6.1.6.2.1、全局异步 - 了解
所有的日志都异步的记录,在配置文件上不用做任何改动,只需要在jvm启动的时候增加一个参数即可实现
设置方式
只需要 在类路径resources下添加一个properties属性文件 ,做一步配置即可,**文件名要求必须是: log4j2.component.properties**
,这个properties配置文件内容如下:
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
操作一波
在 resources
目录中新建 log4j2.component.properties
文件,并配置如下内容:
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
log4j2.xml
文件配置( 不配置任何的async异步设置 )
D:\test
测试
@Test public void globalAsync() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2.2、混合异步 - 必须会
AsyncLogger
标签来配置log4j2.xml配置
appender
标签中石油``AsyncLogger`标签,然后在这里面引用要异步输出的appender即可cn.zixieqing
包下的日志输出到文件进行异步操作,而 root logger
日志是同步操作D:\test
测试
@Test public void blendAsync() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2.3、AsyncAppender、AsyncLogge两种方式的建议