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

x33g5p2x  于2022-05-16 转载在 Java  
字(27.5k)|赞(0)|评价(0)|浏览(293)

引言

日志框架

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"));
    @Test
    public 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级别高的日志信息,包括info
         logger.severe("severe");
         logger.warning("warning");
         logger.info("info");
         //------------默认输出上面三个-------------
         logger.fine("fine");
         logger.finer("finer");
         logger.finest("finest");
    }

    @Test
    public 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可以同时添加多个
     */
    @Test
    public 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的核心逻辑

@Override
    public 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之间的父子关系

@Test
    public void testParentLogger(){
        //父亲是RootLogger,名称默认是一个空的字符串,RootLogger可以被称之为所有Logger对象的顶层Logger
        Logger 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());
    }

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

@Test
    public 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 phase
                    if (System.out == null) {
                        return;
                    }
                    readPrimordialConfiguration = true;

                    try {
                        AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                                @Override
                                public Void run() throws Exception {
                                   //继续看
                                    readConfiguration();

                                    // Platform loggers begin to delegate to java.util.logging.Logger
                                    sun.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.XMLFormatter

java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

#也可以将日志级别设置到具体的某个包下
com.xyz.foo.level = SEVERE

读取自定义配置文件

@Test
    public 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=false

java.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: 默认使用前需要加载初始化配置

@Test
    public 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

打开日志输出的详细信息

@Test
    public 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,file

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

log4j.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,rollingFile

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

log4j.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中,旧日志会改名,后缀加上.序号

源码追查:

//默认采用日志追加方式
  protected
  void subAppend(LoggingEvent event) {
   //通过父类方法,将日志信息追加写入到我们规定的文件中
    super.subAppend(event);
    if(fileName != null && qw != null) {
    //判断当前日志文件的字节数是否大于了maxFileSize 和 nextRollover
        long 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
    //假设这里我们设置为3
    if(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.2
      for (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.1
      target = new File(fileName + "." + 1);
      //关闭拿捏着test.log文件输出句柄的writer
      this.closeFile(); // keep windows happy.

       //改名: 将当前写满的test.log改名为test.log.1
      file = 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.log
      this.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关闭了以前操作该文件的所有句柄,然后重新打开文件,如果文件不存在会创建该文件:

public
  synchronized
  void 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,dailyRollingFile

log4j.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,logDB

log4j.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=all

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


log4j.logger.org.apache=info,console

相关文章