Java日志框架(1/2)

1.日志简介

1.1.日志概述

  • 只要程序员投身在实际的学习和生产环境中,就会对日志的重要性有着充分的认知,尤其是对于Web以及更高级的应用。在很多情况下,日志可能是我们了解应用如何执行的唯一方式。

  • 但是现实是很多程序员对于日志的记录的认知比较肤浅,认为日志的记录输出是一件很简单而且会自动发生的事情,所以会经常忽略和日志相关的问题。

  • 所以本课程主要就是针对于对于日志概念以及日志的框架不太熟悉的这类开发人群,更加详细且真实的体会日志为我们在开发和生产环境当中所带来的好处。

  • Java语言因为它强大而且成熟的生态体系。其中包括日志框架,就有很多成熟的开源资源可以直接使用。

1.2.日志文件

日志文件是用于记录系统操作事件的文件集合。

日志文件它具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要的作用。

1.2.1.调试日志

​ 在软件开发中,经常的调试程序,或者做一些状态的输出(例如System.out.println),便于我们查看程序的运行状况。为了让我们能够更加灵活且方便的控制这些调试信息,我们需要更加专业的日志技术。我们平时在调试程序的过程中所使用的肯定就是专业开发工具自带的debug功能,可以实时查看程序运行情况,但是不能够有效保存运行情况的信息。

调试日志是能够更加方便的去“重现”这些问题。

1.2.2.系统日志

系统日志是用来记录系统中硬件、软件和系统相关问题的信息。同时还可以监视系统中发生的事件。用户可以通过它来检查错误发生的原因,或者寻找收到攻击是留下的痕迹。

系统日志包括系统日志、应用日志和安全日志这几种分类。

1.3.日志框架

1.3.1.日志框架的作用

  1. 控制日志输出的内容和格式。
  2. 控制日志输出的位置。
  3. 日志文件相关的优化,如异步操作、归档、压缩…
  4. 日志系统的维护
  5. 面向接口开发 – 日志的门面

1.3.2.日志框架的价值

  • 因为软件系统发展到了今天非常的复杂,特别是服务器的软件,涉及到的知识和内容问题非常的多。对于日志记录来讲,在某些方面使用别人研发好的成熟的框架,这就相当于让别人帮你完成一些基础的工作。开发者只需要集中精力去完成业务逻辑就可以了。

  • 比如事务处理,日志记录等一些安全性的问题,我们使用框架去做,不会影响业务的开发效率。

  • 同时框架也是在不断升级的,我们可以不断的享受框架为我们带来的好处。

1.3.3.市面流行的日志框架

1.3.3.1.JUL java util logging

JUL (java util logging)

Java原生日志框架,亲儿子

1.3.3.2.Log4j

Apache的一个开源项目

1.3.3.3.Logback

由Log4j之父做的另一个开源项目

业界中称作log4j后浪

一个可靠、通用且灵活的java日志框架

1.3.3.4.Log4j2

Log4j官方的第二个版本,各个方面都是与Logback及其相似

具有插件式结构、配置文件优化等特征

Spring Boot1.4版本以后就不再支持log4j,所以第二个版本应运而生。

1.3.4.日志门面技术

1.3.4.1.JCL
1.3.4.2.SLF4j

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

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

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

日志门面技术 JCL、SLF4j

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

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

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

原理:

  1. 项目1使用日志框架1,项目2使用项目框架2。如果此时想让项目2改用日志框架1,因此在项目2要把所有的日志代码改变。
    比如日志框架1使用A()方法记录日志,日志框架2使用B()方法记录日志。因为每个日志框架都有单独的API。

  2. 面向门面技术是:既不直接使用志框架1的A()方法,也不直接使用日志框架2的B()方法记录日志。使用日志门面技术,将记录日志的功能抽取出来形成接口(记录日志),例如叫方法C()。从此这个方法在项目1和项目2中通用,二者调用的都是方法C()。对项目1,调用C()方法,其实内部还是调用的方法A()。对项目2也是如此。

2.JUL

2.1.JUL简介

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

2.2.JUL 组件介绍

  • Logger:日志记录器,应用程序获取Logger对象,调用用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。
  • Handler:日志处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。
  • Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。
  • Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。
  • Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。

2.3.JUL日志入口程序

2.3.1.java.util.logging.Logger

日志入口程序,java.util.logging.Logger

Logger对象的创建方式,不能直接new对象。取得对象的方法,需要引入当前类的全路径字符串(当前我们先这么用,以后根据包结构有Logger父子关系,以后详细介绍)

package stonebridge;

import org.junit.Test;
import java.util.logging.Logger;

public class JULTest {
    @Test
    public void test01() {
        Logger logger = Logger.getLogger("stonebridge.JULTest");
    }
}

2.3.2.日志输出

@Test
public void test01() {
    Logger logger = Logger.getLogger("stonebridge.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.维护成本高
        我们应该使用动态生成数据的方式,生产日志
     */
    logger.log(Level.INFO, "学生的姓名:{0},年龄:{1}", new Object[]{name, age});
}

应该使用占位符的方式来进行输出复杂日志的操作

2.4.日志的级别

不同的Java日志框架,他们呢所划分的日志级别是不相同的。

2.4.1.日志的级别种类

对JUL来说日志的级别(通过源码查看)分为如下几种:

  1. SEVERE : 错误 — 最高级的日志级别
  2. WARNING : 警告
  3. INFO : (默认级别)消息
  4. CONFIG : 配置
  5. FINE : 详细信息(少)
  6. FINER : 详细信息(中)
  7. FINEST : 详细信息 (多) — 最低级的日志级别

2.4.2.两个特殊的级别

  • OFF 可用来关闭日志记录
  • ALL 启用所有消息的日志记录

2.4.3.日志级别对应的数值

对于日志的级别,第一个数值OFF,重点关注的是new对象的时候的第二个参数Integer.MAX_VALUE 整型最大值

public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);
日志级别对应数值
OFFInteger.MAX_VALUE(整型最大值
SEVERE1000
WARNING900
INFO800
CONFIG700
FINE500
FINER400
FINEST300
ALLInteger.MIN_VALUE (整型最小值

数值的意义在于:如果我们设置的日志的级别是INFO – 800。那么最终展现的日志信息,必须是数值大于等于800的所有的日志信息。最终展现的就是SEVERE、WARNING、INFO。

测试:

@Test
public void test02() {

    Logger logger = Logger.getLogger("stonebridge.JULTest");
    
    logger.setLevel(Level.FINE);

    logger.severe("severe信息");
    logger.warning("warning信息");
    logger.info("info信息");
    logger.config("config信息");
    logger.fine("fine信息");
    logger.finer("finer信息");
    logger.finest("finest信息");
}

通过打印结果,我们看到了仅仅只是输出了info级别以及比info级别高的日志信息。比info级别低的日志信息没有输出出来,证明了info级别是系统默认的日志级别。只会打印比它级别高的信息。

通过

logger.setLevel(Level.FINE);

设置日志级别,并不能够起到效果,将来需要搭配处理器handler共同设置才会生效。

2.4.4.自定义日志的级别

@Test
public void test03() {
    /*
      自定义日志的级别
    */
    //1.创建日志记录器对象
    Logger logger = Logger.getLogger("stonebridge.JULTest");
    //2.参数设置为false将默认的日志打印方式关闭掉。如果不进行设置,默认走父处理器的方式去进行操作
    logger.setUseParentHandlers(false);
    //3.处理器Handler。在此我们使用的是控制台日志处理器,取得处理器对象。默认就会把日志打印在控制台。
    ConsoleHandler handler = new ConsoleHandler();
    //4.创建日志格式化组件对象
    SimpleFormatter formatter = new SimpleFormatter();
    //5.在处理器中设置输出格式
    handler.setFormatter(formatter);
    //6.在记录器中添加处理器
    logger.addHandler(handler);

    //7.设置日志的打印级别
    //此处必须将日志记录器和处理器的设置为相同的级别,才会达到日志显示相应级别的效果
    //logger.setLevel(Level.CONFIG);
    //handler.setLevel(Level.CONFIG);

    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信息");
}

2.4.5.自定义日志输出

将日志输出到指定的位置上,例如控制台、保存到磁盘文件。根据需要输出日志。

用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler。(日志的记录使用的是Logger,日志的输出使用的是Handler)添加了哪些handler对象,就相当于需要根据所添加的handler。 将日志输出到指定的位置上,例如控制台、保存到磁盘文件…

@Test
public void test04() throws IOException {
    /*
        将日志输出到具体的磁盘文件中
        这样做相当于是做了日志的持久化操作
     */
    Logger logger = Logger.getLogger("stonebridge.JULTest");
    logger.setUseParentHandlers(false);
    SimpleFormatter formatter = new SimpleFormatter();

    //1.定义文件日志持久化日志处理器,将日志保存到具体的文件中
    FileHandler fileHandler = new FileHandler("D:\\logs\\myLogTest.log");
    fileHandler.setFormatter(formatter);
    logger.addHandler(fileHandler);

    //2.同时在控制台输出日志处理器,在控制台输出
    ConsoleHandler consoleHandler = new ConsoleHandler();
    consoleHandler.setFormatter(formatter);
    logger.addHandler(consoleHandler); //可以在记录器中同时添加多个处理器

    //3.1.设置日志记录器对象的输出级别,此时可以输出所有级别
    logger.setLevel(Level.ALL);
    //3.2.设置持久化日志处理器设输出级别,日志记录器对象设置级别是Level.ALL、持久化日志处理器设置的级别是Level.ALL。输出交集级别
    fileHandler.setLevel(Level.CONFIG);
 	//3.3.设置控制台输出日志处理器设输出级别,日志记录器对象设置级别是Level.ALL、控制台输出日志处理器设置的级别是Level.FINE。输出交集级别
    consoleHandler.setLevel(Level.FINE);

    logger.severe("severe信息");
    logger.warning("warning信息");
    logger.info("info信息");
    logger.config("config信息");
    logger.fine("fine信息");
    logger.finer("finer信息");
    logger.finest("finest信息");
}

持久化日志处理器输出:

控制台输出日志处理器输出:

2.5.JULLogger父子层级关系

Logger之间的父子关系:JUL中Logger之间是存在"父子"关系的。这种父子关系不是我们普遍认为的类之间的继承关系。而是通过树状结构存储的,即与类与包,包与包之间的层级关系。

JUL在初始化时会创建一个顶层RootLogger作为所有Logger的父Logger
查看源码:

owner.rootLogger = owner.new RootLogger();

RootLogger是LogManager的内部类,java.util.logging.LogManager$RootLogger,默认的名称为空串。

以上的RootLogger对象作为树状结构的根节点存在的
将来自定义的父子关系通过路径来进行关联父子关系,同时也是节点之间的挂载关系

owner.addLogger(owner.rootLogger);
LoggerContext cx = getUserContext(); //LoggerContext一种用来保存节点的Map关系

LoggerContext一种用来保存节点的Map关系

用private LogNode node; //节点

示例:

@Test
public void test05() {
    /*
        从下面创建的两个logger对象看来
        我们可以认为logger1是logger2的父亲
     */
    //父亲是RootLogger,名称默认是一个空的字符串;RootLogger可以被称之为所有logger对象的顶层logger
    Logger logger1 = Logger.getLogger("stonebridge");
    Logger logger2 = Logger.getLogger("stonebridge.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.severe("severe信息");
    logger2.warning("warning信息");
    logger2.info("info信息");
    logger2.config("config信息");
    logger2.fine("fine信息");
    logger2.finer("finer信息");
    logger2.finest("finest信息");
}

父Logger所做的设置,也能够同时作用于子Logger。对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印。

2.6.JUL配置文件

2.6.1.JUL默认配置文件

上述所有的配置相关的操作,都是以java硬编码的形式进行的。如果我们没有自己添加配置文件,则会使用系统默认的配置文件。

这个配置文件:

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();
}

这个配置文件:
owner.readPrimordialConfiguration();
readConfiguration();
java.home --> 找到jre文件夹 --> lib --> logging.properties

做文件日志打印,新日志会覆盖掉原来的日志,但是我们现在的需求不是覆盖,而是追加。

2.6.2.JUL自定义配置文件

# RootLogger使用的处理器,在获取RootLogger对象以及其他Logger对象时进行的设置
# 默认的配置的处理器是控制台的处理器,只能在控制台进行输出操作
# 如果要添加其他的日志处理器,在当前处理器后面以逗号的形式进行分割、可以添加多个处理器
handlers= java.util.logging.ConsoleHandler


# RootLogger的日志级别
# 默认情况下这是全局的日志级别,如果不手动配置其他的日志级别则默认该级别的日志以及更高的级别
.level= CONFIG

# 文件日志处理器的设置
# 输出日志文件的路径
java.util.logging.FileHandler.pattern = %h/java%u.log
# 输出日志的文件的限制,默认情况下输出50000字节
java.util.logging.FileHandler.limit = 50000
# 设置日子文件的数量为1
java.util.logging.FileHandler.count = 1
# 输出日志的格式
# 默认是以xml格式方式进行输出
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

# 自定义Logger
stonebridge.JULTest.handlers = java.util.logging.FileHandler
# 设置自定义Logger的等级
stonebridge.JULTest.level = ALL
# 忽略父Logger的日志设置
stonebridge.JULTest.useParentHandlers = false


# 控制台日志处理器的属性设置
# 控制台日志处理器输出的默认级别
java.util.logging.ConsoleHandler.level = CONFIG
# 控制默认输出的格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

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

# 输出日志文件,是否追加
java.util.logging.FileHandler.append = true
@Test
public void test06() throws Exception {
    /*
        以上所有的配置相关的操作,都是以java硬编码的形式进行的
        我们可以使用更加清晰,更加专业的一种做法,就是使用配置文件
        如果我们没有自己添加配置文件,则会使用系统默认的配置文件
        这个配置文件:
            owner.readPrimordialConfiguration();
            readConfiguration();
            java.home --> 找到jre文件夹 --> lib --> logging.properties
        做文件日志打印,新日志会覆盖掉原来的日志
        但是我们现在的需求不是覆盖,而是追加
     */
    InputStream input = new FileInputStream("C:\\Users\\stone\\Desktop\\日志\\logging.properties");
    //取得日志管理器对象
    LogManager logManager = LogManager.getLogManager();
    //读取自定义的配置文件
    logManager.readConfiguration(input);
    Logger logger = Logger.getLogger("stonebridge.JULTest");

    logger.severe("severe信息");
    logger.warning("warning信息");
    logger.info("info信息");
    logger.config("config信息");
    logger.fine("fine信息");
    logger.finer("finer信息");
    logger.finest("finest信息");
}

JUL日志框架使用方式总结(原理解析)

  1. 初始化LogManager

    LogManager加载logging.properties配置文件

    添加Logger到LogManager

  2. 从单例的LogManager获取Logger

  3. 设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord类

  4. Filter作为过滤器提供了日志级别之外更细粒度的控制

  5. Handler日志处理器,决定日志的输出位置,例如控制台、文件…

  6. Formatter是用来格式化输出的

3.Log4j

3.1.Log4j简介

​ Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录

器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴

趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

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

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

3.2.Log4j组件介绍

Log4j主要由Loggers (日志记录器)、Appenders(输出控制器)和 Layout(日志格式化器)组成。

  1. Loggers 控制日志的输出以及输出级别(JUL做日志级别Level);
  2. Appenders 指定日志的输出方式(输出到控制台、文件等);
  3. Layout 控制日志信息的输出格式;

3.2.1.Loggers

日志记录器,负责收集处理日志记录,实例的命名就是类的全限定名,如com.stonebridge.log4j.XX, Logger的名字大小写敏感,其命名有继承机制:例如:name为com.stonebridge.log4j的logger会继承 name为com.stonebridge。

Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。

自log4j 1.2版以来, Logger类已经取代了Category类。对于熟悉早期版本的log4j的人来说, Logger 可以被视为Category 类的别名。

​ com.stonebridge.log4j.XX 儿子

​ com.stonebridge.log4j 父亲

​ com.stonebridge爷爷

​ …

​ …

​ Root logger

父类(父包)所做的日志属性设置,会直接的影响到子类(子包)。子类(子包)也可以继承父类(父包)设置,直接用。

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

3.2.2.Appenders

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

常用Appenders:

  • ConsoleAppender

    将日志输出到控制台

  • FileAppender

    将日志输出到文件中

  • DailyRollingFileAppender

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

  • RollingFileAppender

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

  • JDBCAppender

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

3.2.3.Layouts

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

常用Layouts:

  1. HTMLLayout

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

  2. SimpleLayout

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

  3. PatternLayout

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

3.2.4.日志输出格式说明(针对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字符,就从左边交远销出的字符截掉

3.2.5.添加log4j依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

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

3.3.log4j日志级别说明

Log4j提供了8个级别的日志输出,分别为:

  1. ALL 最低等级 用于打开所有级别的日志记录

  2. TRACE 程序推进下的追踪信息,这个追踪信息的日志级别非常低,一般情况下是不会使用的

  3. DEBUG 指出细粒度信息事件对调试应用程序是非常有帮助的,主要是配合开发,在开发过程中打印一些重要的运行信息

  4. INFO 消息的粗粒度级别运行信息

  5. WARN 表示警告,程序在运行过程中会出现的有可能会发生的隐形的错误。

    注意,有些信息不是错误,但是这个级别的输出目的就是为了给程序员以提示

  6. ERROR 系统的错误信息,发生的错误不影响系统的运行

    一般情况下,如果不想输出太多的日志,则使用该级别即可

  7. FATAL 表示严重错误,它是那种一旦发生系统就不可能继续运行的严重错误

    如果这种级别的错误出现了,表示程序可以停止运行了

  8. OFF 最高等级的级别,用户关闭所有的日志记录

DEBUG是我们在没有进行设置的情况下,默认的日志输出级别

Log4j入门案例

注意加载初始化信息:BasicConfigurator.configure();

public void test01() {
    //加载初始化配置
    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信息");
}

3.4.日志管理器

3.4.1.BasicConfigurator.configure()源码解析

  1. 观察源码BasicConfigurator.configure();加载Logger、Appender、Layout

    可以得到两条信息:

    1. 创建了根节点的对象,Logger root = Logger.getRootLogger();
    2. 根节点添加了ConsoleAppender对象(表示默认日志默认打印到控制台,自定义的格式化输出)
  2. 不使用BasicConfigurator.configure();加载Logger、Appender、Layout

    通过我们对于以上第一点的分析

    我们的配置文件需要提供Logger、Appender、Layout这3个组件信息(通过配置来代替以上的代码)

    分析:

    Logger logger = Logger.getLogger(Log4jTest01.class);
    

    进入到getLogger方法,会看到代码:

    public static Logger getLogger(Class clazz) {
    return LogManager.getLogger(clazz.getName());
    }
    

    LogManager:日志管理器

    点击LogManager,看看这个日志管理器中都实现了什么

    看到很多常量信息,他们代表的就是不同形式(后缀名不同)的配置文件

    我们最常使用到的肯定是log4j.properties属性文件(语法简单,使用方便),我们真正要掌握就是属性文件的配置。

    public class LogManager {
        /** @deprecated */
        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;
    
        public LogManager() {
        }
        ……
    }
    
  3. 问题1:log4j.properties的加载时机

    一般情况下,加载过程就在静态代码块static中完成的。

    在static代码块中,我们找到

    Loader.getResource("log4j.properties");
    

    这行代码给我们最大的一个提示信息就是:系统默认要从当前的类路径下找到log4j.properties;对于我们当前的项目是maven工程,那么理应在resources路径下去找。

  4. 问题2:加载完毕后我们来观察配置文件是如何读取的?

    继续观察LogManager

    找到

    try {
    	OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());
    } catch (NoClassDefFoundError var6) {
    	LogLog.warn("Error during default initialization", var6);
    }
    

    作为属性文件的加载,执行相应的properties配置对象:

    Configurator configurator = null;
    ……
    configurator = new PropertyConfigurator();
    

    进入到PropertyConfigurator类中,观察到里面的常量信息。这些常量信息就是我们在properties属性文件中的各种属性配置项。

    public class PropertyConfigurator implements Configurator {
        protected Hashtable registry = new Hashtable(11);
        private LoggerRepository repository;
        protected LoggerFactory loggerFactory = new DefaultCategoryFactory();
        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";
    
        public PropertyConfigurator() {
        }
        ……
    }
    

    其中,我们看到了如下两项信息,这两项信息是必须要进行配置的。

    static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";
    static final String APPENDER_PREFIX = "log4j.appender.";
    
  5. 配置appender

    通过代码:

    String prefix = "log4j.appender." + appenderName;
    

    我们需要自定义一个appendername,我们起名叫做console。

    (起名字也需要见名知意,console那么我们到时候的配置应该配置的就是控制台输出)

    例如:配置log4j.properties:

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

    log4j.appender.console取值就是log4j中为我们提供的appender类

    在appender输出的过程中,还可以同时指定输出的格式

  6. 配置layout

    通过代码:

    String layoutPrefix = prefix + ".layout";
    

    例如:配置log4j.properties:

    log4j.appender.console.layout=org.apache.log4j.SimpleLayout
    
  7. 配置rootLogger(重难点)

    通过log4j.rootLogger继续在PropertyConfigurator类中搜索

    找到void configureRootCategory方法

    在这个方法中执行了this.parseCategory方法

    观察该方法:

    找到代码StringTokenizer st = new StringTokenizer(value, “,”);

    表示要以逗号的方式来切割字符串,证明了log4j.rootLogger的取值,其中可以有多个值,使用逗号进行分隔

    void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) {
        LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "].");
        StringTokenizer st = new StringTokenizer(value, ",");
        if (!value.startsWith(",") && !value.equals("")) {
            if (!st.hasMoreTokens()) {
                return;
            }
            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();
        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);
                }
            }
        }
    }
    

    通过代码:

    String levelStr = st.nextToken();

    表示切割后的第一个值是日志的级别

    通过代码:

    while(st.hasMoreTokens())
    表示接下来的值,是可以通过while循环遍历得到的
    第2~第n个值,就是我们配置的其他的信息,这个信息就是appenderName
    证明了我们配置的方式
    log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3…
    表示可以同时在根节点上配置多个日志输出的途径,比如控制台、文件中

    通过我们自己的配置文件,就可以将原有的加载代码去除掉了

    去除BasicConfigurator.configure();

3.4.2.配置文件log4j.properties测试

# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=trace,console
# 配置appender输出方式
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 配置输出的格式
log4j.appender.console.layout=org.apache.log4j.SimpleLayout
public void test02() {
    //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信息");
}

3.5.日志输出详细信息开关

通过Logger中的开关打开日志输出的详细信息

查看LogManager类中的方法

public static Logger getLogger(Class clazz) {
    return LogManager.getLogger(clazz.getName());
}
public static Logger getLogger(String name) {
    return getLoggerRepository().getLogger(name);
}

查看LogManager类中的getLoggerRepository()方法

找到代码LogLog.debug(msg, ex);表示LogLog会使用debug级别的输出为我们展现日志输出详细信息

Logger是记录系统的日志,那么LogLog是用来记录Logger的日志

public static LoggerRepository getLoggerRepository(){
	……
	LogLog.debug(msg, ex);
	……
}

进入到LogLog.debug(msg, ex);方法中

通过代码:if (debugEnabled && !quietMode) {
观察到if判断中的这两个开关都必须开启才行

public static void debug(String msg, Throwable t) {
    if (debugEnabled && !quietMode) {
        System.out.println("log4j: " + msg);
        if (t != null) {
            t.printStackTrace(System.out);
        }
    }
}

通过代码:if (debugEnabled && !quietMode) {
观察到if判断中的这两个开关都必须开启才行

  • !quietMode是已经启动的状态,不需要我们去管
  • debugEnabled默认是关闭的

所以我们只需要通过 LogLog.setInternalDebugging(true);设置debugEnabled为true就可以了

示例:

  1. 配置文件log4j.properties

    # log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
    log4j.rootLogger=trace,console
    # 配置appender输出方式
    log4j.appender.console=org.apache.log4j.ConsoleAppender
    # 配置输出的格式
    log4j.appender.console.layout=org.apache.log4j.SimpleLayout
    
  2. java代码

    public void test03() {
        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信息");
    }
    
  3. 测试结果

3.6.Log4j自定义日志格式(layout属性配置)

3.6.1.HTMLLayout

# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=trace,html
# 配置appender输出方式,输出日志到html文件
log4j.appender.html=org.apache.log4j.FileAppender
# 配置输出的格式,格式化日志输出为HTML表格形式 
log4j.appender.html.layout=org.apache.log4j.HTMLLayout
# 配置输出到文件的日志格式
log4j.appender.html.layout.conversionPattern=[%-10p] %r %c %t %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
# 日志文件要保存的路径
log4j.appender.html.file=C://Users//stone//Desktop//test.html

测试结果:

3.6.1.PatternLayout(重点)

其中PatternLayout是日常使用最多的方式

查看其源码

setConversionPattern这个方法就是该PatternLayout的核心方法

public void setConversionPattern(String conversionPattern) {
    this.pattern = conversionPattern;
    this.head = this.createPatternParser(conversionPattern).parse();
}

通过配置conversionPattern

在log4j.properties中将layout设置为PatternLayout,我们主要配置的是conversionPattern属性

  1. %m 输出代码中指定的日志信息
  2. %p 输出优先级,及 DEBUG、INFO 等
  3. %n 换行符(Windows平台的换行符为 “\n”,Unix 平台为 “\n”)
  4. %r 输出自应用启动到输出该 log 信息耗费的毫秒数
  5. %c 输出打印语句所属的类的全名
  6. %t 输出产生该日志的线程全名
  7. %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss}
  8. %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10)
  9. %F 输出日志消息产生时所在的文件名称
  10. %L 输出代码中的行号
  11. %% 输出一个 “%” 字符

示例1:

# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=trace,console
# 配置appender输出方式
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 配置日志信息的输出格式PatternLayout 
log4j.appender.console.layout=org.apache.log4j.PatternLayout
# 通过PatternLayout设置具体的日志输出格式 
log4j.appender.console.layout.conversionPattern=[%p]%r %c %t %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
public void test04() {
    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:

可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式

log4j.appender.console.layout.conversionPattern=[%10p]%r %c %t %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

示例3:

[%10p]:[]中必须有10个字符,由空格来进行补齐,信息右对齐

log4j.appender.console.layout.conversionPattern=%10p %r %c %t %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

示例4:

[%-10p]:[]中必须有10个字符,由空格来进行补齐,信息左对齐,应用较广泛

log4j.appender.console.layout.conversionPattern=[%-10p] %r %c %t %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

3.7.Log4j将日志输出到文件的配置

将日志信息输出到文件中(以后所有的练习统一使用配置文件(不硬编码))
配置log4j.appender.console=org.apache.log4j.ConsoleAppender配置的是输出到控制台相关的配置
一般情况下做多方向的输出。控制台的配置保留,另外再配置一套关于输出到文件的配置,将日志保存到文件中。

日志文件要保存到哪个磁盘相关的配置

查看FileAppender的源码,看到属性信息

public class FileAppender extends WriterAppender {
    protected boolean fileAppend;
    protected String fileName;
    protected boolean bufferedIO;
    protected int bufferSize;

    public FileAppender() {
        this.fileAppend = true;
        this.fileName = null;
        this.bufferedIO = false;
        this.bufferSize = 8192;
    }
    ……
}

protected boolean fileAppend; 表示是否追加信息,通过构造方法赋值为true
protected int bufferSize; 缓冲区的大小,通过构造方法赋值为8192

继续观察,找到setFile方法,这个方法就是用来指定文件位置的方法

public void setFile(String file) {
    String val = file.trim();
    this.fileName = val;
}

通过ognl,可以推断setFile方法操作的属性就是file

示例:

  1. java

    public void test05() {
        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. log4j.properties

    配置了ConsoleAppender和FileAppender两种日志输出方式

    # log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
    log4j.rootLogger=trace,console,file
    
    
    # 配置appender输出方式,输出到控制台
    log4j.appender.console=org.apache.log4j.ConsoleAppender
    # 配置输出到控制台的layout
    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
    
    
    # 配置appender输出方式,输出到文件
    log4j.appender.file=org.apache.log4j.FileAppender
    # 配置输出到文件的layout
    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是用来指定文件位置的属性)
    log4j.appender.file.file=D://BaiduNetdiskDownload//log4j.log
    #配置输出字符编码
    log4j.appender.file.encoding=UTF-8
    
  3. 测试效果

    默认日志是追加的

3.8.Log4j按照文件大小进行拆分

将日志输出到文件中,日志太多了,不方便管理和维护怎么办?
FileAppender为我们提供了好用的子类来进一步的对于文件输出进行处理

3.8.1.RollingFileAppender

3.8.1.1.概念解析

这个类是按照文件大小进行拆分,在配置文件进行RollingFileAppender相关配置。

如何进行拆分?观察RollingFileAppender的源码

public class RollingFileAppender extends FileAppender {
  /**
     The default maximum file size is 10MB.
     表示拆分文件的大小,默认最大文件大小为10MB。
  */
  protected long maxFileSize = 10*1024*1024;
  /**
     There is one backup file by default.
     默认情况下有一个备份文件。
   */
  protected int  maxBackupIndex  = 1;
  private long nextRollover = 0;
  ……
}

配置具体说明:

#指定日志文件内容大小
log4j.appender.rollingFile.maxFileSize=1MB
#指定日志文件的数量
log4j.appender.rollingFile.maxBackupIndex=5

只要文件超过1MB,那么则生成另外一个文件,文件的数量最多是5个
文件1 记录日志 1MB
文件2 记录日志 1MB


文件5 1MB

如果5个文件不够怎么办,作为日志管理来讲,也不可能让日志无休止的继续增长下去。
所以,覆盖文件的策略是,按照时间来进行覆盖,原则就是保留新的,覆盖旧的。

3.8.1.2.测试
public void test06() {
        Logger logger = Logger.getLogger(Log4jTest01.class);
        for (int i = 0; i < 10000; i++) {
            logger.fatal("fatal信息");
            logger.error("error信息");
            logger.warn("warn信息");
            logger.info("info信息");
            logger.debug("debug信息");
            logger.trace("trace信息");
        }
}
# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=trace,rollingFile,console

# 配置RollingFileAppender输出方式,输出到文件
log4j.appender.rollingFile=org.apache.log4j.RollingFileAppender
# 配置输出到文件的layout
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
# 日志文件要保存的路径(第一个file是我们自己命名的appenderName,第二个file是用来指定文件位置的属性)
log4j.appender.rollingFile.file=D://BaiduNetdiskDownload//log4j.log
#配置输出字符编码
log4j.appender.rollingFile.encoding=UTF-8
#表示拆分文件的大小
log4j.appender.rollingFile.maxFileSize=1MB
#备份文件的数量
log4j.appender.rollingFile.maxBackupIndex=5

测试结果

3.8.2.DailyRollingFileAppender

3.8.2.1.概念解析

按照时间来进行文件的拆分
查看源码属性:

默认是按照天来进行拆分的

public class DailyRollingFileAppender extends FileAppender {

  /**
     The date pattern. By default, the pattern is set to
     "'.'yyyy-MM-dd" meaning daily rollover.
   */
  private String datePattern = "'.'yyyy-MM-dd";
  ……
}

注意:
我们在练习的时候,可以根据秒来制定拆分策略
但是实际生产环境中,根据秒生成日志文件是绝对不可能的
如果是大型的项目,可以根据天进行拆分
或者如果是小型的项目,可以根据周,月进行拆分

3.8.2.2.测试
# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=trace,console,dailyRollingFile

# 配置DailyRollingFileAppender输出方式,输出到文件
log4j.appender.dailyRollingFile=org.apache.log4j.DailyRollingFileAppender
# 配置输出到文件的layout
log4j.appender.dailyRollingFile.layout=org.apache.log4j.PatternLayout
# 配置输出到文件的日志格式
log4j.appender.dailyRollingFile.layout.conversionPattern=[%-10p] %r %c %t %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
# 日志文件要保存的路径(第一个file是我们自己命名的appenderName,第二个file是用来指定文件位置的属性)
log4j.appender.dailyRollingFile.file=D://BaiduNetdiskDownload//log4j.log
#配置输出字符编码
log4j.appender.dailyRollingFile.encoding=UTF-8
#按照什么时间进行拆分
log4j.appender.dailyRollingFile.datePattern='.'yyyy-MM-dd HH-mm-ss
public void test06() {
    Logger logger = Logger.getLogger(Log4jTest01.class);
    for (int i = 0; i < 10000; i++) {
        try {
            Thread.sleep(100);
        }catch (Exception exception){
            exception.printStackTrace();
        }

        logger.fatal("fatal信息");
        logger.error("error信息");
        logger.warn("warn信息");
        logger.info("info信息");
        logger.debug("debug信息");
        logger.trace("trace信息");
    }
}

3.9.日志持久化到数据库表中

创建表结构:(字段的制定可以根据需求进行调整)

CREATE TABLE tbl_log(
  id int(11) NOT NULL AUTO_INCREMENT, 
  name varchar(255) DEFAULT NULL COMMENT '项目名称', 
  createTime varchar(255) DEFAULT NULL COMMENT '创建时间', 
  level varchar(255) DEFAULT NULL COMMENT '日志级别', 
  category varchar(255) DEFAULT NULL COMMENT '所在类的全路径', 
  fileName varchar(255) DEFAULT NULL COMMENT '文件名称', 
  message varchar(255) DEFAULT NULL COMMENT '日志消息', 
  PRIMARY KEY(id)
)

对于数据库表的日志输出进行相应配置。项目中要引入数据库驱动

# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=trace,console,logDB

# 配置JDBCAppender输出方式,保存日志到数据库
log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender
# 配置样式
log4j.appender.logDB.layout=org.apache.log4j.PatternLayout
# 配置数据库驱动
log4j.appender.logDB.Driver=com.mysql.jdbc.Driver
# 配置数据库url
log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&amp;useSSL=false
# 配置数据库用户名
log4j.appender.logDB.User=root
# 配置数据库密码
log4j.appender.logDB.Password=123456
# 配置插入日志的sql
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')
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信息");
}

3.10.Log4j的自定义logger

3.10.1.Log4j的自定义logger的配置

以前所创建出来的Logger对象,默认都是继承rootLogger的。

log4j.rootLogger=trace,console

我们也可以自定义logger,让其他logger来继承这个自定义logger。

这种继承关系就是按照包结构的关系来进行指定的

例如我们一直使用的Logger,这个Logger指定的类路径为com.stonebridge.log4j.test.Log4jTest01

Logger logger = Logger.getLogger(Log4jTest01.class);

它的父logger就是上层的路径或者是更上层的路径

例如:
com.stonebridge.log4j.test
com.stonebridge.log4j

com

参照logger是如何加载配置文件的

查看PropertyConfigurator的源码,得到信息log4j.logger。

public class PropertyConfigurator implements Configurator {
    ……
    static final String LOGGER_PREFIX = "log4j.logger.";
    ……
}

这个属性值log4j.logger.,就是我们在配置文件中对于自定义logger的配置属性

在log4j.properties中进行配置:

#配置根节点logger
log4j.rootLogger=trace,console
#配置自定义logger
log4j.logger.com.stonebridge.log4j.test = info,file
public void test08() {
    Logger logger = Logger.getLogger(Log4jTest01.class);
    logger.fatal("fatal信息");
    logger.error("error信息");
    logger.warn("warn信息");
    logger.info("info信息");
    logger.debug("debug信息");
    logger.trace("trace信息");
}

console打印:

file输出:

观察结果:
从输出位置来看,控制台通过console输出了信息,日志文件通过file也输出了信息。

所以可以得出结论:

  1. 如果根节点的logger和自定义logger配置的输出位置是不同的(即Appender不同),配置的位置都会进行输出操作。
  2. 如果二者配置的日志级别不同,以按照我们自定的logger的级别输出为准。

3.10.2.自定义logger的应用场景

我们之所以要自定义logger,就是为了针对不同系统信息做更加灵活的输出操作。

我们可以在原有案例的基础之上,加上一个apache的日志输出

#配置根节点logger
log4j.rootLogger=trace,console
#配置自定义logger
log4j.logger.com.stonebridge.log4j.test=info,file
#配置apache的logger
log4j.logger.org.apache=error,console
public void test09() {
     /*
        当前的类Log4jTest01.class路径com.stonebridge.log4j.test.Log4jTest01.class时
        在配置文件中所找到的能够作用的自定义logger和rootLogger
        log4j.rootLogger=trace,console
        com.stonebridge.log4j.test=info,file
        我们刚才配置的log4j.logger.org.apache的路径和我们的com.stonebridge.log4j.test不相符。
        不构成父子关系,所以一下的代码不会执行apache相关的配置。
     */
    Logger logger = Logger.getLogger(Log4jTest01.class);
    logger.fatal("fatal信息");
    logger.error("error信息");
    logger.warn("warn信息");
    logger.info("info信息");
    logger.debug("debug信息");
    logger.trace("trace信息");
    /*
        Logger.class的路径是org.apache.log4j.Logger,此时是log4j.logger.org.apache的子logger
        在根节点中进行了配置,在apache中也进行了配置。
        由于输出的位置appender取的是并集
        所以,既然都配置了,那么就输出了两次
    */
    Logger logger1 = Logger.getLogger(Logger.class);
    logger1.fatal("fatal信息 --");
    logger1.error("error信息 --");
    logger1.warn("warn信息 --");
    logger1.info("info信息 --");
    logger1.debug("debug信息 --");
    logger1.trace("trace信息 --");
}

File输出:

如果要将console不重复打印的日志,只需要去除log4j.logger.org.apache的appender,只通过rootLogger打印。

#配置根节点logger
log4j.rootLogger=trace,console
#配置自定义logger
log4j.logger.com.stonebridge.log4j.test=info,file
#配置apache的logger
log4j.logger.org.apache=error

4.JCL

4.1.JCL简介

全称为Jakarta Commons Logging,是Apache提供的一个通用日志API

用户可以自由选择第三方的日志组件作为具体实现,像log4j,或者jdk自带的jul, common-logging会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。

当然,common-logging内部有一个Simple logger的简单实现,但是功能很弱。所以使用common-logging,通常都是配合着log4j以及其他日志框架来使用。

使用它的好处就是,代码依赖是common-logging而非log4j的API, 避免了和具体的日志API直接耦合,在有必要时,可以更改日志实现的第三方库。

我们学习不同的日志框架,他们的API是不同的,这样难以进行有效的记忆。

在我们的生产环境下,如果我们已经选择使用了一款日志框架,如果根据需求的改变而选择使用了另一种,那么我们用来操作日志的源码都需要发生变化。

JCL 有两个基本的抽象类:

Log:日志记录器

LogFactory:日志工厂(负责创建Log实例)

4.2.JCL案例

4.2.1.引入依赖

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

4.2.2.入门案例

默认的情况下,会使用JUL日志框架做日志的记录操作

JCL使用原则:

  1. 如果有log4j,优先使用log4j
  2. 如果没有任何第三方日志框架的时候,我们使用的就是JUL
public void test01() {
    Log log = LogFactory.getLog(JclTest.class);
    log.info("info信息");
}

4.2.3.Log4j案例

4.2.3.1.引入log4j的依赖
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
4.2.3.2.打印日志
public void test02() {
    Log log = LogFactory.getLog(JclTest.class);
    log.info("info信息");
}
4.2.3.3.运行测试

此时还未配置log4j.properties

4.2.3.4.配置log4j.properties
# log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
#配置根节点logger
log4j.rootLogger=trace,console

# 配置appender输出方式,输出到控制台
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 配置输出到控制台的layout
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

4.2.4.总结

在入门案例中,使用的是JUL,但是在集成了log4j环境后,使用的又是log4j。

通过测试观察,虽然日志框架发生了变化,但是代码完全没有改变

日志门面技术的好处:

  • 门面技术是面向接口的开发,不再依赖具体的实现类,减少代码的耦合性
  • 可以根据实际需求,灵活的切换日志框架
  • 统一的API,方便开发者学习和使用
  • 统一的配置管理便于项目日志的维护工作

4.2.5.源码阅读

4.2.5.1.Log接口的4个实现类
  1. JDk13
  2. JDK14 正常java.util.logging
  3. Log4j 我们集成的log4j
  4. Simple JCL自带实现类
4.2.5.2.加载的Logger对象

观察LogFactory,看看如何加载的Logger对象

这是一个抽象类,无法实例化。需要观察其实现类LogFactoryImpl

观察LogFactoryImpl

真正加载日志实现使用的就是这个实现类LogFactoryImpl

  1. 进入LogFactory.getLog()

    public static Log getLog(Class clazz) throws LogConfigurationException {
        return getFactory().getInstance(clazz);
    }
    
  2. 进入getInstance

    public Log getInstance(Class clazz) throws LogConfigurationException {
        return getInstance(clazz.getName());
    }
    
    public Log getInstance(String name) throws LogConfigurationException {
        Log instance = (Log) instances.get(name);
        if (instance == null) {
            instance = newInstance(name);
            instances.put(name, instance);
        }
        return instance;
    }
    
  3. 找到instance = this.newInstance(name);,继续进入

    protected Log newInstance(String name) throws LogConfigurationException {
        Log instance;
        try {
            if (logConstructor == null) {
                instance = discoverLogImplementation(name);
            }
            else {
                Object params[] = { name };
                instance = (Log) logConstructor.newInstance(params);
            }
    
            if (logMethod != null) {
                Object params[] = { this };
                logMethod.invoke(instance, params);
            }
            return instance;
        } catch (LogConfigurationException lce) {
            throw lce;
        } catch (InvocationTargetException e) {
            Throwable c = e.getTargetException();
            throw new LogConfigurationException(c == null ? e : c);
        } catch (Throwable t) {
            handleThrowable(t); 
            throw new LogConfigurationException(t);
        }
    }
    
  4. 找到instance = this.discoverLogImplementation(name); 表示发现一个日志的实现

    private Log discoverLogImplementation(String logCategory){
        for(int i=0; i<classesToDiscover.length && result == null; ++i) {
            result = createLogFromClass(classesToDiscover[i], logCategory, true);
        }
    }
    

    遍历我们拥有的日志实现框架
    遍历的是一个数组,这个数组是按照
    log4j
    jdk14
    jdk13
    SimpleLogger
    的顺序依次遍历
    表示的是,第一个要遍历的就是log4j,如果有log4j则执行该日志框架
    如果没有,则遍历出来第二个,使用jdk14的JUL日志框架
    以此类推

  5. 创建Logger对象

    result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
    

    表示帮我们创建Logger对象
    在这个方法中,我们看到了
    c = Class.forName(logAdapterClassName, true, currentCL);
    是取得该类型的反射类型对象

  6. 使用反射的形式帮我们创建logger对象

    constructor = c.getConstructor(this.logConstructorSignature);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
框架说白了就是JAVA工作者多年以来总结出的一些开发标准。让我们可以以成功的经验模式来开发我们自已的系统,一般使用框架的好处是 ·在好的框架下,开发者只需要写一些必须的代码;他们不需要直接接触底层的API。 这一点很重要。 ·经过良好设计的框架可以为程序提供清晰的结构并且提高程序的内聚性。好清晰的结构使得其他人可以更容易加入项目。 ·一个容易使用的框架可以通过一些例子和文档为用户提供最佳实践。 ·采用成功的框架的代码比自己的代码容易测试 J2EE本身提供了一些框架。比如, Enterprise Java-Beans (EJB) container或者 Servlet engine 而这些框架一般在中小工程中我们都不会使用,会让我们把大量的时间浪费在开发框架上。 而现在比较流行开源框架,主要是struts,hibernate,spring等 比如struts是在原有mvc基础上实现在代码分离等功能,非常好用。 而hibernate可以把我们的关系型数据库转换成我们在JAVA中的面像对像来使用。从而让我们在开发时不需要直接写SQL语句,比如database.getName();就可以直接把数据库中的用户名取出来。 Spring J2EE框架被大规模地运用到项目中,而项目总要负责这些框架以及自己业务代码的连接,使之真正融合到一起。Spring就是专注于这个问题的,它和Hibernate融合的很好。 这三种框架在一起并不冲突,所以现在最常用的框架就是 struts+hibernate+spring就像我们盖房子一样,先把框架搭好,我们在在上面写代码就很规范。 Struts框架介绍 : Struts只是一个MVC框架(Framework),用于快速开发Java Web应用。Struts实现的重点在C(Controller),包括ActionServlet/RequestProcessor和我们定制的 Action,也为V(View)提供了一系列定制标签(Custom Tag)。但Struts几乎没有涉及M(Model),所以Struts可以采用JAVA实现的任何形式的商业逻辑。 Spring是一个轻型容器(light-weight container),其核心是Bean工厂(Bean Factory),用以构造我们所需要的M(Model)。在此基础之上,Spring提供了AOP(Aspect-Oriented Programming, 面向层面的编程)的实现,用它来提供非管理环境下申明方式的事务、安全等服务;对Bean工厂的扩展ApplicationContext更加方便我们实 现J2EE的应用;DAO/ORM的实现方便我们进行数据库的开发;Web MVC和Spring Web提供了Java Web应用的框架或与其他流行的Web框架进行集成。 就是说可将两者一起使用,达到将两者自身的特点进行互补。 spring 框架介绍 : 它关注的领域是其他许多流行的Framework未曾关注的。Spring要提供的是一种管理你的业务对象的方法。 Spring既是全面的又是模块化的。Spring有分层的体系结构,这意味着你能选择仅仅使用它任何一个独立的部分,而它的架构又是内部一致。 因此你能从你的学习中,得到最大的价值。例如,你可能选择仅仅使用Spring来简单化JDBC的使用,或用来管理所有的业务对象。 它的设计从一开始就是要帮助你编写易于测试的代码。Spring是使用测试驱动开发的工程的理想框架。 Spring不会给你的工程添加对其他的框架依赖。Spring也许称得上是个一站式解决方案,提供了一个典型应用所需要的大部分基础架构。它还涉及到了其他framework没有考虑到的内容。 尽管它仅仅是一个从2003年2月才开始的开源项目,但Spring有深厚的历史根基。 Spring架构上的好处 在我们进入细节之前,让我们来看看Spring能够给工程带来的种种好处: Spring能有效地组织你的中间层对象,不管你是否选择使用了EJB。如果你仅仅使用了Struts或其他为J2EE的 API特制的framework,Spring致力于解决剩下的问题。 Spring能消除在许多工程中常见的对Singleton的过多使用。根据我的经验,这是一个很大的问题,它降低了系统的可测试性和面向对象的程度。 通过一种在不同应用程序和项目间一致的方法来处理配置文件,Spring能消除各种各样自定义格式的属性文件的需要。曾经对某个类要寻找的是哪个 魔法般的属性项或系统属性感到不解,为此不得不去读Javadoc甚至源编码?有了Spring,你仅仅需要看看类的JavaBean属性

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值