Java日志框架学习--JUL和Log4j--上

  • 引言
    • 日志框架
    • 市面流行的日志框架
    • 日志门面和日志框架的区别
  • JUL
    • JUL简介
    • JUL组件介绍
    • 实际使用
    • Logger之间的父子关系
    • 默认配置文件位置
    • 读取自定义配置文件
    • 追加日志信息
  • Log4j
    • Log4j简介
    • Log4j组件介绍
      • Loggers
      • Appenders
      • Layouts
        • 日志输出格式说明
    • 应用
      • 配置文件加载时机
      • 打开日志输出的详细信息
      • 自定义日志输出格式
      • 日志输出到文件
        • 日志按照文件大小进行拆分
        • 日志按照文件时间进行拆分
    • 日志持久化
    • 自定义Logger

引言

日志框架

1.控制日志输出的内容和格式。

2.控制日志输出的位置。

3.日志文件相关的优化,如异步操作、归档、压缩…

4.日志系统的维护

5.面向接口开发 – 日志的门面


市面流行的日志框架

JUL java util logging

  • Java原生日志框架,亲儿子

Log4j

  • Apache的一个开源项目

Logback

  • 由Log4j之父做的另一个开源项目
  • 业界中称作log4j后浪
  • 一个可靠、通用且灵活的java日志框架

Log4j2

  • Log4j官方的第二个版本,各个方面都是与Logback及其相似
  • 具有插件式结构、配置文件优化等特征
  • Spring Boot1.4版本以后就不再支持log4j,所以第二个版本营运而生

JCL

SLF4j


日志门面和日志框架的区别

日志框架技术 JUL、Logback、Log4j、Log4j2

用来方便有效地记录日志信息

日志门面技术 JCL、SLF4j

为什么要使用日志门面技术:

每一种日志框架都有自己单独的API,要使用对应的框架就要使用对应的API,这就大大的增加了应用程序代码对于日志框架的耦合性。

我们使用了日志门面技术之后,对于应用程序来说,无论底层的日志框架如何改变,应用程序不需要修改任意一行代码,就可以直接上线了。


JUL

JUL简介

JUL全称 Java Util Logging,它是java原生的日志框架,使用时不需要另外引用第三方的类库,相对其他的框架使用方便,学习简单,主要是使用在小型应用中。

JUL组件介绍

Logger:被称为记录器,应用程序通过获取Logger对象,使用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。

Handler:处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。

Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。

Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。

Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。


实际使用

public class JUITest {private final String NOW="当前时间: "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));@Testpublic void normalTest(){//获取Logger--传入当前类的全类名Logger logger = Logger.getLogger(JUITest.class.getName());//info级别输出日志logger.info(NOW);logger.log(Level.INFO,NOW);//占位符传参--0和1必须指定,否则占位符不生效logger.log(Level.INFO,"管理员姓名为: {0}, 年龄为{1}",new Object[]{"大忽悠",18});//输出不同的日志级别--默认为info,因此只会输出比info级别高的日志信息,包括infologger.severe("severe");logger.warning("warning");logger.info("info");//------------默认输出上面三个-------------logger.fine("fine");logger.finer("finer");logger.finest("finest");}@Testpublic void changeLevelTest(){Logger logger = Logger.getLogger(JUITest.class.getName());//将默认的日志打印方式关闭掉,这样日志打印方式就不会按照父logger默认的方式去进行操作logger.setUseParentHandlers(false);//使用控制台处理器ConsoleHandler handler = new ConsoleHandler();//创建日志格式化组件SimpleFormatter formatter = new SimpleFormatter();//设置处理器中日志输出格式handler.setFormatter(formatter);//在记录器中添加处理器logger.addHandler(handler);//设置日志级别//此处必须将日志记录器和处理器的基本进行统一的设置,才会达到日志显示相应级别的效果logger.setLevel(Level.ALL);handler.setLevel(Level.ALL);logger.severe("severe");logger.warning("warning");logger.info("info");logger.fine("fine");logger.finer("finer");logger.finest("finest");}/*** handler可以同时添加多个*/@Testpublic void logToFile() throws IOException {Logger logger = Logger.getLogger(JUITest.class.getName());logger.setUseParentHandlers(false);ConsoleHandler consoleHandler = new ConsoleHandler();FileHandler fileHandler = new FileHandler("test.log");SimpleFormatter formatter = new SimpleFormatter();consoleHandler.setFormatter(formatter);fileHandler.setFormatter(formatter);logger.addHandler(consoleHandler);logger.addHandler(fileHandler);logger.setLevel(Level.ALL);consoleHandler.setLevel(Level.ALL);fileHandler.setLevel(Level.INFO);logger.severe("severe");logger.warning("warning");logger.info("info");logger.fine("fine");logger.finer("finer");logger.finest("finest");}
}


给出的是最后一个测试的结果

真正完成日志记录的源码如下,其实很简单:

    public void log(LogRecord record) {if (!isLoggable(record.getLevel())) {return;}Filter theFilter = filter;if (theFilter != null && !theFilter.isLoggable(record)) {return;}// Post the LogRecord to all our Handlers, and then to// our parents' handlers, all the way up the tree.Logger logger = this;while (logger != null) {final Handler[] loggerHandlers = isSystemLogger? logger.accessCheckedHandlers(): logger.getHandlers();for (Handler handler : loggerHandlers) {//每个handler的publish方法,也会去判断当前日志级别,然后拿到格式化器,最终进行输出handler.publish(record);}final boolean useParentHdls = isSystemLogger? logger.useParentHandlers: logger.getUseParentHandlers();if (!useParentHdls) {break;}logger = isSystemLogger ? logger.parent : logger.getParent();}}

不同handler的相同抽象publish的核心逻辑

    @Overridepublic synchronized void publish(LogRecord record) {if (!isLoggable(record)) {return;}String msg;try {msg = getFormatter().format(record);} catch (Exception ex) {// We don't want to throw an exception here, but we// report the exception to any registered ErrorManager.reportError(null, ex, ErrorManager.FORMAT_FAILURE);return;}try {if (!doneHeader) {writer.write(getFormatter().getHead(this));doneHeader = true;}writer.write(msg);} catch (Exception ex) {// We don't want to throw an exception here, but we// report the exception to any registered ErrorManager.reportError(null, ex, ErrorManager.WRITE_FAILURE);}}

writer可以是输出向控制台,也可以是文件。

    private volatile Writer writer;

Logger之间的父子关系

    @Testpublic void testParentLogger(){//父亲是RootLogger,名称默认是一个空的字符串,RootLogger可以被称之为所有Logger对象的顶层LoggerLogger logger1 = Logger.getLogger("helper.com.logTest");Logger logger2 = Logger.getLogger("helper.com.logTest.JUITest");System.out.println("log1的父log引用为: "+logger1.getParent()+" ; 名称为: "+logger1.getName()+"  父亲的名称为: "+logger1.getParent().getName());System.out.println("log2的父log引用为: "+logger2.getParent()+" ; 名称为: "+logger2.getName()+"  父亲的名称为: "+logger2.getParent().getName());}


父亲的设置会影响到儿子,也可以按照包的角度来理解,我可以设置整个包的日志属性,也可以定制化包下某个类的日志属性

    @Testpublic void testParentLogger(){Logger logger1 = Logger.getLogger("helper.com.logTest");logger1.setUseParentHandlers(false);ConsoleHandler handler = new ConsoleHandler();handler.setLevel(Level.ALL);logger1.addHandler(handler);logger1.setLevel(Level.ALL);Logger logger2 = Logger.getLogger("helper.com.logTest.JUITest");logger2.severe("severe");logger2.warning("warning");logger2.info("info");logger2.fine("fine");logger2.finer("finer");logger2.finest("finest");}

logger2默认为info级别,但是因为我们设置了它的父logger日志级别,所以这里按照父logger属性进行输出

底层通过一个保存的节点树,再创建每个logger的时候,通过节点树,找到其父节点

具体原理,可以参考这篇文章


默认配置文件位置

我们上面都是硬编码方式完成的,但是大部分情况下,都是通过配置文件完成的

Logger.getLogger方法会调用到ensureLogManagerInitialized方法,默认配置文件的加载在该方法中完成

final void ensureLogManagerInitialized() {....// Read configuration.--读取配置文件owner.readPrimordialConfiguration();...}
private void readPrimordialConfiguration() {if (!readPrimordialConfiguration) {synchronized (this) {if (!readPrimordialConfiguration) {// If System.in/out/err are null, it's a good// indication that we're still in the// bootstrapping phaseif (System.out == null) {return;}readPrimordialConfiguration = true;try {AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {@Overridepublic Void run() throws Exception {//继续看readConfiguration();// Platform loggers begin to delegate to java.util.logging.Loggersun.util.logging.PlatformLogger.redirectPlatformLoggers();return null;}});} catch (Exception ex) {assert false : "Exception raised while reading logging configuration: " + ex;}}}}}
public void readConfiguration() throws IOException, SecurityException {checkPermission();// if a configuration class is specified, load it and use it.String cname = System.getProperty("java.util.logging.config.class");if (cname != null) {try {// Instantiate the named class.  It is its constructor's// responsibility to initialize the logging configuration, by// calling readConfiguration(InputStream) with a suitable stream.try {Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);clz.newInstance();return;} catch (ClassNotFoundException ex) {Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);clz.newInstance();return;}} catch (Exception ex) {System.err.println("Logging configuration class \"" + cname + "\" failed");System.err.println("" + ex);// keep going and useful config file.}}//查看是否指定了配置文件位置 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");//默认配置文件为java_home目录下的lib目录下的logging.properties文件fname = f.getCanonicalPath();}try (final InputStream in = new FileInputStream(fname)) {final BufferedInputStream bin = new BufferedInputStream(in);readConfiguration(bin);}}

默认配置文件如下:

#RootLogger使用的处理器
#如果想要添加其他的处理器,可以采用逗号分隔的形式,添加多个处理器
handlers= java.util.logging.ConsoleHandler#默认RootLogger的日志级别
#全局日志级别
.level= INFO#文件处理器属性设置
#输出日志文件路径设置
java.util.logging.FileHandler.pattern = %h/java%u.log
#输出日志文件的限制--字节
java.util.logging.FileHandler.limit = 50000
#输出日志文件的格式
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatterjava.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter#也可以将日志级别设置到具体的某个包下
com.xyz.foo.level = SEVERE

读取自定义配置文件

    @Testpublic void testParentLogger() throws IOException {LogManager logManager = LogManager.getLogManager();logManager.readConfiguration(new FileInputStream("logging.properties"));Logger logger = Logger.getLogger(JUITest.class.getName());logger.severe("severe");logger.warning("warning");logger.info("info");logger.fine("fine");logger.finer("finer");logger.finest("finest");}

自定义配置文件内容:

#RootLogger使用的处理器
#如果想要添加其他的处理器,可以采用逗号分隔的形式,添加多个处理器
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler#默认RootLogger的日志级别
#全局日志级别
.level= ALL#文件处理器属性设置
#输出日志文件路径设置
java.util.logging.FileHandler.pattern = %h/java%u.log
#输出日志文件的限制--字节
java.util.logging.FileHandler.limit = 50000
#输出日志文件的格式
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter#自定义Logger
helper.com.logTest.handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler
#自定义日志等级
helper.com.logTest.level=CONFIG
#屏蔽父Logger的配置
helper.com.logTest.useParentHandlers=falsejava.util.logging.ConsoleHandler.level = CONFIG
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

%h是默认放到用户文件夹下,windows是C盘用户目录下

%u是日志文件数量,默认从0开始,依次累加


追加日志信息

配置文件添加下面这行即可

java.util.logging.FileHandler.append=true

Log4j

Log4j简介

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

官方网站: http://logging.apache.org/log4j/1.2/ Log for java

我们使用log4j技术,主要使用的是其配置文件

Log4j组件介绍

Log4j主要由 Loggers (日志记录器)、Appenders(输出控制器)和 Layout(日志格式化器)组成。其中 Loggers 控制日志的输出以及输出级别(JUL做日志级别Level);Appenders 指定日志的输出方式(输出到控制台、文件等);Layout 控制日志信息的输出格式。

Loggers

日志记录器,负责收集处理日志记录,实例的命名就是类的全限定名,如com.bjpowernode.log4j.XX, Logger的名字大小写敏感,其命名有继承机制:例如:name为com.bjpowernode.log4j的logger会继承 name为com.bjpowernode。 Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接 或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。自log4j 1.2版以来, Logger 类已经取代了 Category 类。对于熟悉早期版本的log4j的人来说, Logger 类可以被视为 Category 类的别名

com.bjpowernode.log4j.XX 儿子
com.bjpowernode.log4j 父亲
com.bjpowernode 爷爷


Root logger
上辈所做的日志属性设置,会直接的影响到子辈

关于日志级别信息,例如DEBUG、INFO、WARN、ERROR…级别是分大小的,DEBUG < INFO < WARN < ERROR,分别用来指定这条日志信息的重要程度,Log4j输出日志的规则是:只输出级别不低于设定级别的日志信息,假设Loggers级别设定为INFO,则INFO、WARN、ERROR级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出。


Appenders

记录日志以及定义日志的级别仅仅是Log4j的基本功能,Log4j日志系统还提供许多强大的功能,比如允许把日志输出到不同的地方,如控制台(Console)、文件(Files)等,可以根据天数或者文件大小产生新的文件,可以以流的形式发送到其它地方等等。

常用Appenders:

ConsoleAppender :将日志输出到控制台

FileAppender : 将日志输出到文件中

DailyRollingFileAppender : 将日志输出到一个日志文件,并且每天输出到一个新的文件

RollingFileAppender : 将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件

JDBCAppender: 把日志信息保存到数据库中


Layouts

有时用户希望根据自己的喜好格式化自己的日志输出,Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供四种日志输出样式,如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式。

常用Layouts:

HTMLLayout :格式化日志输出为HTML表格形式

SimpleLayout: 简单的日志输出格式化,打印的日志格式如默认INFO级别的消息

PatternLayout : 最强大的格式化组件,可以根据自定义格式输出日志,如果没有指定转换格式, 就是用默认的转换格式


日志输出格式说明

使用PatternLayout可以自定义格式输出,是我们最常用的方式

这种格式化输出采用类似于 C 语言的 printf 函数的打印格式格式化日志信息,具体的占位符及其含义如下:

  • %m 输出代码中指定的日志信息
  • %p 输出优先级,及 DEBUG、INFO 等
  • %n 换行符(Windows平台的换行符为 “\n”,Unix 平台为 “\n”)
  • %r 输出自应用启动到输出该 log 信息耗费的毫秒数
  • %c 输出打印语句所属的类的全名
  • %t 输出产生该日志的线程全名
  • %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss}
  • %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10)
  • %F 输出日志消息产生时所在的文件名称
  • %L 输出代码中的行号
  • %% 输出一个 “%” 字符
  • 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式。如:
  • %5c 输出category名称,最小宽度是5,category<5,默认的情况下右对齐
  • %-5c 输出category名称,最小宽度是5,category<5,"-"号指定左对齐,会有空格
  • %.5c 输出category名称,最大宽度是5,category>5,就会将左边多出的字符截掉,<5不会有空格
  • %20.30c category名称<20补空格,并且右对齐,>30字符,就从左边交远销出的字符截掉

应用

添加依赖:

<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version>
</dependency>

test01: 默认使用前需要加载初始化配置

    @Testpublic void test01(){//加载初始化配置BasicConfigurator.configure();Logger logger = Logger.getLogger(Log4jTest.class.getName());logger.fatal("fatal");logger.error("error");logger.warn("warn");logger.info("info");logger.debug("debug");logger.trace("trace");}

该初始化配置负责初始化一个RootLogger和一个控制台输出的appender

    public static void configure() {Logger root = Logger.getRootLogger();root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n")));}

默认提供的八种日志级别:

debug是默认输出级别


配置文件加载时机

LogManager日志管理器中规定了下面这种配置文件格式名:


配置文件的加载是在静态代码块中完成的:

 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) {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 {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.xml");if (url == null) {url = Loader.getResource("log4j.properties");}

最终由下面这行代码解析配置文件:

 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());

selectAndConfigure代码中,最终完成配置文件解析任务,又交给了PropertyConfigurator

            configurator = new PropertyConfigurator();((Configurator)configurator).doConfigure(url, hierarchy);

PropertyConfigurator里面规定了所有可以在配置文件中书写的格式信息:


先来看看log4j.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 {//这里appenderName就是我们自定义的appender的名字String prefix = "log4j.appender." + appenderName;//"log4j.appender." + appenderNam后面再加上.layout,可以配置该appender的日志输出格式String layoutPrefix = prefix + ".layout";...this.parseAppenderFilters(props, appenderName, appender);this.registryPut(appender);return appender;}}}

通过上面这段源码的简单分析,我们可以在配置文件中完成以下配置:

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.SimpleLayout

log4j.rootLogger的解析过程:

    void configureRootCategory(Properties props, LoggerRepository hierarchy) {String effectiveFrefix = "log4j.rootLogger";...this.parseCategory(props, root, effectiveFrefix, "root", value);}
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;}//第一个表示的是RootLogger的日志级别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();//第二个到第N个值,就是RootLogger的多种Appender方式//可以配置根节点多种日志输出方式while(st.hasMoreTokens()) {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=info,console,file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.SimpleLayout

打开日志输出的详细信息

    @Testpublic void test01(){LogLog.setInternalDebugging(true);Logger logger = Logger.getLogger(Log4jTest.class.getName());logger.fatal("fatal");logger.error("error");logger.warn("warn");logger.info("info");logger.debug("debug");logger.trace("trace");}


自定义日志输出格式

public class PatternLayout extends Layout {public static final String DEFAULT_CONVERSION_PATTERN = "%m%n";public static final String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %x - %m%n";protected final int BUF_SIZE;protected final int MAX_CAPACITY;private StringBuffer sbuf;private String pattern;private PatternConverter head;public PatternLayout() {this("%m%n");}public PatternLayout(String pattern) {this.BUF_SIZE = 256;this.MAX_CAPACITY = 1024;this.sbuf = new StringBuffer(256);this.pattern = pattern;this.head = this.createPatternParser(pattern == null ? "%m%n" : pattern).parse();}public void setConversionPattern(String conversionPattern) {this.pattern = conversionPattern;this.head = this.createPatternParser(conversionPattern).parse();}public String getConversionPattern() {return this.pattern;}
...
}

由上面的代码可知,我们可以在配置文件中通过conversionPattern来配置PatternLayout

log4j.rootLogger=info,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n



日志输出到文件

通过查看FileAppender类的源码,我们可以看到他可以设置的四个属性:

public class FileAppender extends WriterAppender {//是否追加日志protected boolean fileAppend;
//文件输出位置    protected String fileName;
//缓存IO    protected boolean bufferedIO;
//缓存区大小    protected int bufferSize;public FileAppender() {this.fileAppend = true;this.fileName = null;this.bufferedIO = false;this.bufferSize = 8192;}public FileAppender(Layout layout, String filename, boolean append, boolean bufferedIO, int bufferSize) throws IOException {this.fileAppend = true;this.fileName = null;this.bufferedIO = false;this.bufferSize = 8192;this.layout = layout;this.setFile(filename, append, bufferedIO, bufferSize);}public FileAppender(Layout layout, String filename, boolean append) throws IOException {this.fileAppend = true;this.fileName = null;this.bufferedIO = false;this.bufferSize = 8192;this.layout = layout;this.setFile(filename, append, false, this.bufferSize);}...

可以看出默认采用日志追加方式,并且默认缓冲区大小为8192


父类WriterAppender还有两个比较重要的属性:

//规定输出日志编码格式protected String encoding;
//每条日志写完后,立马刷新出写入文件,不会在缓冲区等候满了之后,再一次性全部写出
protected boolean immediateFlush = true;

使用演示:

log4j.rootLogger=info,console,filelog4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%nlog4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.file=test.log
log4j.appender.file.encoding=utf-8
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n


日志按照文件大小进行拆分

RollingFileAppender源码康康:

public class RollingFileAppender extends FileAppender {/**The default maximum file size is 10MB.*/protected long maxFileSize = 10*1024*1024;/**There is one backup file by default.*/protected int  maxBackupIndex  = 1;...

RollingFileAppender在FileAppender的基础上扩展了两个属性给我们进行设置

log4j.rootLogger=info,rollingFilelog4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%nlog4j.appender.rollingFile=org.apache.log4j.RollingFileAppender
log4j.appender.rollingFile.file=test.log
log4j.appender.rollingFile.encoding=utf-8
log4j.appender.rollingFile.maxFileSize=1MB
log4j.appender.rollingFile.maxBackupIndex=3
log4j.appender.rollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.rollingFile.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n


maxBackupIndex规定另外生成的文件数量最多为3个,并且最新生产的日志先写入test.log中,旧日志会改名,后缀加上.序号


源码追查:

//默认采用日志追加方式protectedvoid subAppend(LoggingEvent event) {//通过父类方法,将日志信息追加写入到我们规定的文件中super.subAppend(event);if(fileName != null && qw != null) {//判断当前日志文件的字节数是否大于了maxFileSize 和 nextRolloverlong size = ((CountingQuietWriter) qw).getCount();if (size >= maxFileSize && size >= nextRollover) {//如果超过了,就进行文件回滚rollOver();}}}
publi void rollOver() {File target;File file;if (qw != null) {//拿到当前已经写出的字节数long size = ((CountingQuietWriter) qw).getCount();LogLog.debug("rolling over count=" + size);//   if operation fails, do not roll again until//      maxFileSize more bytes are written//下一次回滚时,要求已经写出的字节数要在当前基础上在多出maxFileSize个字节,才会进行写出操作nextRollover = size + maxFileSize;}LogLog.debug("maxBackupIndex="+maxBackupIndex);boolean renameSucceeded = true;// If maxBackups <= 0, then there is no file renaming to be done.//最多可以进行备份的日志文件数必须要大于0//假设这里我们设置为3if(maxBackupIndex > 0) {// Delete the oldest file, to keep Windows happy.//下面是测试test.log.3是否已经存在,已经存在,说明我们备份文件用完了file = new File(fileName + '.' + maxBackupIndex);//用完了怎么办,保留最新的,删除最老的if (file.exists())//把最老版本的test.log.3删除掉renameSucceeded = file.delete();// Map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3, 2}//最老版本的已经删除掉了,那么test.log.2和test.log.1就需要改名为test.log.3和test.log.2for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) {file = new File(fileName + "." + i);if (file.exists()) {target = new File(fileName + '.' + (i + 1));LogLog.debug("Renaming file " + file + " to " + target);renameSucceeded = file.renameTo(target);}}//如果改名成功if(renameSucceeded) {// Rename fileName to fileName.1//将当前写满的test.log改名为test.log.1target = new File(fileName + "." + 1);//关闭拿捏着test.log文件输出句柄的writerthis.closeFile(); // keep windows happy.//改名: 将当前写满的test.log改名为test.log.1file = new File(fileName);LogLog.debug("Renaming file " + file + " to " + target);renameSucceeded = file.renameTo(target);////   if file rename failed, reopen file with append = true////如果改名失败if (!renameSucceeded) {try {//重新打开文件this.setFile(fileName, true, bufferedIO, bufferSize);}catch(IOException e) {if (e instanceof InterruptedIOException) {Thread.currentThread().interrupt();}LogLog.error("setFile("+fileName+", true) call failed.", e);}}}}////   if all renames were successful, then////如果改名成功if (renameSucceeded) {try {// This will also close the file. This is OK since multiple// close operations are safe.//创建一个test.logthis.setFile(fileName, false, bufferedIO, bufferSize);//置为零nextRollover = 0;}catch(IOException e) {if (e instanceof InterruptedIOException) {Thread.currentThread().interrupt();}LogLog.error("setFile("+fileName+", false) call failed.", e);}}}

super.setFile关闭了以前操作该文件的所有句柄,然后重新打开文件,如果文件不存在会创建该文件:

  publicsynchronizedvoid setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)throws IOException {super.setFile(fileName, append, this.bufferedIO, this.bufferSize);if(append) {File f = new File(fileName);//如果文件有内容,那么count的值就不是从0开始算起了((CountingQuietWriter) qw).setCount(f.length());}}


日志按照文件时间进行拆分

DailyRollingFileAppender源码康康:

  private String datePattern = "'.'yyyy-MM-dd";private String scheduledFilename;

两个主要属性,datePattern决定日期拆分策略和按照策略拆分生成的文件名的时间后缀

  public void activateOptions() {super.activateOptions();if(datePattern != null && fileName != null) {now.setTime(System.currentTimeMillis());sdf = new SimpleDateFormat(datePattern);int type = computeCheckPeriod();printPeriodicity(type);rc.setType(type);File file = new File(fileName);//可以看到日志文件名的生成策略scheduledFilename = fileName+sdf.format(new Date(file.lastModified()));} else {LogLog.error("Either File or DatePattern options are not set for appender ["+name+"].");}}

log4j.rootLogger=info,dailyRollingFilelog4j.appender.dailyRollingFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyRollingFile.file=test.log
log4j.appender.dailyRollingFile.encoding=utf-8
log4j.appender.dailyRollingFile.datePattern='.'yyyy-MM-dd HH:mm:ss
log4j.appender.dailyRollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.dailyRollingFile.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n

源码追踪:

  protected void subAppend(LoggingEvent event) {long n = System.currentTimeMillis();//如果当前时间戳大于下一次检查的时间戳if (n >= nextCheck) {//设置下一次检查的时间now.setTime(n);nextCheck = rc.getNextCheckMillis(now);try {//日志回滚rollOver();}catch(IOException ioe) {if (ioe instanceof InterruptedIOException) {Thread.currentThread().interrupt();}LogLog.error("rollOver() failed.", ioe);}}//将日志写入新的日志文件中去super.subAppend(event);}
}

日志回滚

在实际记录之前,此方法将检查是否该进行翻转。如果是,它将安排下一个翻转时间,然后进行翻转。

void rollOver() throws IOException {/* Compute filename, but only if datePattern is specified */if (datePattern == null) {errorHandler.error("Missing DatePattern option in rollOver().");return;}//本次记录的日志名String datedFilename = fileName+sdf.format(now);// It is too early to roll over because we are still within the// bounds of the current interval. Rollover will occur once the// next interval is reached.//判断是否回滚过早if (scheduledFilename.equals(datedFilename)) {return;}// close current file, and rename it to datedFilename//关闭当前文件this.closeFile();//scheduledFilename需要把当前旧的test.log日志改名为scheduledFilename//scheduledFilename会在刚开始创建appender对象的时候被赋值一次// scheduledFilename = fileName+sdf.format(new Date(file.lastModified())); File target  = new File(scheduledFilename);if (target.exists()) {target.delete();}//尝试重命名文件--将test.log  File file = new File(fileName);boolean result = file.renameTo(target);if(result) {LogLog.debug(fileName +" -> "+ scheduledFilename);} else {LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"].");}try {// This will also close the file. This is OK since multiple// close operations are safe.this.setFile(fileName, true, this.bufferedIO, this.bufferSize);}catch(IOException e) {errorHandler.error("setFile("+fileName+", true) call failed.");}//scheduledFilename = datedFilename;}

日志持久化


JDBCAppender中我们需要在配置文件中设置的属性已经标红了

log4j.rootLogger=all,logDBlog4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.logDB.layout=org.apache.log4j.PatternLayout
log4j.appender.logDB.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n
log4j.appender.logDB.Driver=com.mysql.cj.jdbc.Driver
log4j.appender.logDB.uRL=jdbc:mysql://xxx:3306/helper?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
log4j.appender.logDB.user=root
log4j.appender.logDB.password=xxx
log4j.appender.logDB.sql=insert into test_log(name,createTime,level,category,fileName,message) values('pro_log','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%m')

具体建表和测试,这里就不展示了


自定义Logger


自定义Logger或者自定义父Logger的日志级别会覆盖上上层父logger或者顶层rootlogger的日志级别输出。

对于appender的配置来说,父类logger的appender和子类的appender都会生效。

通过自定义logger,我们可以指定某个包下所有类的日志输出级别和日志输出形式,也可以指定某个包下所有子包的下所有类的日志输出级别和日志输出形式

log4j.rootLogger=alllog4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%nlog4j.logger.org.apache=info,console


Java日志框架学习--JUL和Log4j--上相关推荐

  1. Java日志框架学习笔记

    Java日志框架学习笔记 文章目录 0 主流Java日志框架 1 log4j 1.1 理论知识 1.1.1 Loggers日志记录器 1.1.2 Appenders输出端 1.1.3 Layout日志 ...

  2. java日志框架(一)JUL 学习 ,这个是什么,他在代码中如何使用,一篇文章讲清楚

    这里写目录标题 JUL 是什么 JUL组件介绍 代码中如何使用(控制台输出) 日志级别 自定义输出级别 输出日志到文件(磁盘文件中) 日志对象父子关系 配置文件 使用方法总结 JUL 是什么 JUL全 ...

  3. Java日志框架之JUL(java util logging)详解

    定义: JUL全称Java util logging,是java原生的日志框架,使用时不需要另外引入第三方类库,相对于其他框架使用方便,学习简单,能够在小型的应用中灵活使用. 架构: Applicat ...

  4. 日志框架-1 jul与log4j与logback介绍

    背景 日志框架一直没进行过细致的梳理,对其理解仅留在基本使用的程度:周五自测小功能时暴露了技术漏洞

  5. 学习Java日志框架之——搞懂JUL(java.util.logging)

    文章目录 系列文章目录 一.JUL简介 二.JUL组件介绍 三.代码实例 1.入门案例 2.日志级别 (1)默认日志级别源码分析 3.自定义日志级别 4.将日志输出到文件中 5.Logger的父子关系 ...

  6. 学习Java日志框架之——搞懂日志门面(JCL+SLF4J)

    文章目录 系列文章目录 一.什么是日志门面 1.门面模式(外观模式) 2.日志门面 二.了解JCL 1.JCL组件结构 2.JCL案例 (1)JCL默认实现 (2)导入log4j测试原有程序 三.SL ...

  7. Java日志框架日志门面介绍

    文章目录 一.日志 二.常见日志框架 历史 各大框架介绍 JUL Log4j(1999-2015) Logback(2006-?) Log4j2 Logback与Log4j2对比 三.日志门面 什么是 ...

  8. java日志框架JUL、JCL、Slf4j、Log4j、Log4j2、Logback 一网打尽

    为什么程序需要记录日志 我们不可能实时的24小时对系统进行人工监控,那么如果程序出现异常错误时要如何排查呢?并且系统在运行时做了哪些事情我们又从何得知呢?这个时候日志这个概念就出现了,日志的出现对系统 ...

  9. Java日志框架 -- 日志框架介绍、日志门面技术、JUL日志(JUL架构、JUL入门示例、JUL日志级别、JUL日志的配置文件)

    1. 日志的概念 日志文件是用于记录系统操作事件的文件集合,可分为事件日志和消息日志.具有处理历史数据.诊断问题的追踪以及理解系统的活动等重要作用. 2. Java日志框架 问题: 控制日志输出的内容 ...

最新文章

  1. 第五章 Spring进阶-注解方式实现AOP(1)
  2. Asp.Net Web API 2第一课——入门
  3. 根因分析初探:一种报警聚类算法在业务系统的落地实施
  4. java中的关键字:this与super 大详解
  5. C++ Opengl 粒子系统(Particles)源码
  6. java泛型类指定多个泛型_Java泛型中的多态
  7. c语言10个数如何求最大值,C语言,输入10个数怎样输出10个数中最大值,最小值(大一计算机)...
  8. x86 和 ARM 谁能主宰服务器市场?Linux 之父和 Redis 之父有分歧了
  9. 自定义tt文本模板实现MySql指数据库中生成实体类
  10. 移动开发—媒体查询(Media Query)
  11. c++ opencv [ INFO:0] global c:\build\master_winpack-build-win64-vc15\***
  12. Pycharm-SSH连接服务器
  13. python中的系统模块_python中与系统发育相关的模块
  14. 回归创业本质:生意的本质就是赚钱
  15. stm32 USB HID多点触摸屏上报安卓触摸信号
  16. speedoffice使用方法-Word怎么在方框里面打对勾
  17. 12C ORA-错误汇总20
  18. Postman批量执行接口
  19. Map中的keySet方法
  20. SQLite3 dll加载失败问题解决

热门文章

  1. 对数化绘制P值地形图
  2. MySQL之事务 索引 锁_MySql 知识点之事务、索引、锁原理与用法解析
  3. w7设置双显示器_如何在Win7桌面上为两个不同的显示器设置双屏幕? -win7多屏显示设置...
  4. linux命令删除sh文件,Linux下面使用命令如何运行.sh文件的两种解决办法 删除windows.old文件夹的解决...
  5. fedora xface 更改系统语言
  6. linux 查看进程数据段,如何读取Linux进程中的代码段和数据段
  7. Sourceinsight超好看护眼的配色方案!!!每一步都有步骤
  8. Labview字符串的基础讲解
  9. MTK 7628 i2s 语音对讲系统
  10. 【linux】循序渐进学运维-基础篇-分区命令fdisk