您的当前位置:首页正文

日志框架(1) : 日志介绍、JUL框架

2024-11-24 来源:个人技术集锦


1.第一章 日志简介

1.1日志概述

  • 只要程序员投身在实际的学习和生产环境中,就会对日志的重要性有着充分的认知,尤其是对于Web以及更高级的应用。在很多情况下,日志可能是我们了解应用如何执行的唯一方式
  • 但是现实是很多程序员对于日志的记录的认知比较肤浅,认为日志的记录输出是一件很简单而且会自动发生的事情,所以会经常忽略和日志相关的问题。
  • Java语言的强大之处就是因为它强大而且成熟的生态体系。其中包括日志框架,就有很多成熟的开源资源可以直接使用。

1.2日志文件

  • 日志文件是用于记录应用系统操作事件的文件集合。
  • 日志文件它具有处理历史数据诊断问题的追踪以及理解系统的活动等重要的作用。

1.2.1调试日志

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

    远程DEBUG来调试程序

1.2.2系统日志

  • 系统日志是用来记录系统中硬件、软件和系统相关问题的信息。同时还可以监视系统中发生的事件。用户可以通过它来检查错误发生的原因,或者寻找收到攻击是留下的痕迹。
    系统日志包括系统日志、应用日志、RPC调用日志、中间件日志、网关日志、安全日志这几种分类。

1.3日志框架

1.3.1日志框架的作用

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

1.3.2日志框架的价值

  • 减少输出形式与程序的耦合。
  • System.out只能输出到控制台,而log4j之类的日志实现可以配置输出目标,输出等级。输出等级大致有 debug/info/warn/error,有些信息是调试相关的,在正式运行时并不想看到,就直接把输出等级调到info或更高即可,不用改代码。
  • 输出目标在调试的时候可以输出到控制台,然而正式运行时看不到控制台,所以可以输出到文本文件,网页文件,甚至是发送邮件。这些修改也只需要改一个配置即可。

1.3.3市面流行的日志框架

日志框架

  • JUL — java util logging
    • Java原生日志框架
  • Log4j
    • Apache的一个开源项目
  • Logback
    • 由Log4j之父做的另一个开源项目
    • 业界中称作log4j后浪
    • 一个可靠、通用且灵活的java日志框架
  • Log4j2
    • Log4j官方的第二个版本,各个方面都是与Logback及其相似
    • 具有插件式结构、配置文件优化等特征
    • Spring Boot1.4版本以后就不再支持log4j,所以第二个版本营运而生

日志门面

  • JCL
  • SLF4j

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

  • 目前功能最强大的组合 : Slf4j + Log4j2
  • 日志框架技术 JUL、Log4j、Logback、Log4j2
    • 用来方便有效地记录日志信息
  • 日志门面技术 JCL、SLF4j

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

  • 每一种日志框架都有自己单独的API,要使用对应的框架就要使用对应的API,这就大大的增加了应用程序代码对于日志框架的耦合性。
  • 们使用了日志门面技术之后,对于应用程序来说,无论底层的日志框架如何改变,应用程序不需要修改任意一行代码,就可以直接上线了。

    使用slf4j日志门面的api, 底层可以动态的选择我们提供的日志框架实现。当需要更改日志框架实现的时候, 修改即可; 代码无需改动, 即可更改底层日志实现。

2.第二章 JUL

2.1JUL简介

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

2.2JUL组件介绍

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

实战

拓展: 在类上标注Lombok的@Log注解, 可以自动生成JUL的Logger对象, 我们就不需要手动写: Logger logger = Logger.getLogger(“com.bjpowernode.jul.test.JULTest”); 代码了

package com.bjpowernode.jul.test;

import org.junit.Test;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.*;

public class JULTest {

    @Test
    public void test01(){

        //入门案例
        /*
            日志入口程序
            java.util.logging.Logger
         */
         
        //Logger对象的创建方式,不能直接new对象
        //取得对象的方法参数,需要引入当前类的全路径字符串(当前我们先这么用,以后根据包结构有Logger父子关系,以后详细介绍)
        Logger logger = Logger.getLogger("com.bjpowernode.jul.test.JULTest");

        /*
            对于日志的输出,有两种方式

            第一种方式:
                直接调用日志级别相关的方法,方法中传递日志输出信息
                假设现在我们要输出info级别的日志信息
         */
        //logger.info("输出info信息1");
        
        /*
            第二种方式:
                调用通用的log方法,然后在里面通过Level类型来定义日志的级别参数,以及搭配日志输出信息的参数
         */
        //logger.log(Level.INFO,"输出info信息2");


        /*
            输出学生信息
                姓名
                年龄
         */
        /*
        String name = "zs";
        int age = 23;
        logger.log(Level.INFO,"学生的姓名为:"+name+";年龄为:"+age);*/
        /*
            对于输出消息中,字符串的拼接弊端很多
            1.麻烦
            2.程序效率低
            3.可读性不强
            4.维护成本高

            我们应该使用动态生成数据的方式,生产日志
            我们使用的就是占位符的方式来进行操作
         */

        String name = "zs";
        int age = 23;
        logger.log(Level.INFO,"学生的姓名:{0},年龄:{1}",new Object[]{name,age});

    }

    @Test
    public void test02(){

        /*
            日志的级别(通过源码查看,非常简单)

              SEVERE : 错误 --- 最高级的日志级别
              WARNING : 警告
              INFO : (默认级别)消息
              CONFIG : 配置
              FINE : 详细信息(少)
              FINER : 详细信息(中)
              FINEST : 详细信息 (多) --- 最低级的日志级别

            两个特殊的级别
               OFF 可用来关闭日志记录
               ALL 启用所有消息的日志记录

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

            SEVERE 1000
            WARNING 900

            ...
            ...

            FINEST 300


            ALL Integer.MIN_VALUE 整型最小值

            这个数值的意义在于,如果我们设置的日志的级别是INFO -- 800
            那么最终展现的日志信息,必须是数值大于800的所有的日志信息
            最终展现的就是
            SEVERE
            WARNING
            INFO
         */

        Logger logger = Logger.getLogger("com.bjpowernode.jul.test.JULTest");

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

        /*
            如果仅仅只是通过以下形式来设置日志级别
            那么不能够起到效果
            将来需要搭配处理器handler共同设置才会生效
         */
        logger.setLevel(Level.CONFIG); // 设置日志级别

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

    @Test
    public void test03(){

        /*
            自定义日志的级别
         */
        //日志记录器
        Logger logger = Logger.getLogger("com.bjpowernode.jul.test.JULTest");

		// 将默认的日志打印方式(控制台)关掉
        // 参数设置为false, 我们打印日志的方式就不会按照父logger默认方式去操作
        logger.setUseParentHandlers(false);

        //处理器Handler
        //在此我们使用的是控制台日志处理器,取得处理器对象
        ConsoleHandler handler = new ConsoleHandler();
        //创建日志格式化组件对象(采用简单类型的格式化)
        Formatter formatter = new SimpleFormatter();
        // Formatter formatter = new XMLFormatter();

        //在处理器中设置输出格式
        handler.setFormatter(formatter);
        //在记录器中添加处理器
        logger.addHandler(handler);

        //设置日志的打印级别
        //此处必须将日志记录器和处理器的级别进行统一的设置,才会达到日志显示相应级别的效果
        //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信息");
    }

    @Test
    public void test04() throws IOException {

        /*
            将日志输出到具体的磁盘文件中
            这样做相当于是做了日志的持久化操作
         */
        Logger logger = Logger.getLogger("com.bjpowernode.jul.test.JULTest");
        logger.setUseParentHandlers(false);

        //文件日志处理器(下面代码表示,把日志输出到指定路径的log文件,按照Simple格式)
        FileHandler handler = new FileHandler("D:\\test\\myLogTest.log");
        SimpleFormatter formatter = new SimpleFormatter();
        handler.setFormatter(formatter);
        logger.addHandler(handler);

        //也可以同时在控制台和文件中进行打印
        ConsoleHandler handler2 = new ConsoleHandler();
        handler2.setFormatter(formatter);
        //可以在记录器(logger)中同时添加多个处理器,此时在文件和控制台都会打印日志
        logger.addHandler(handler2); 

        logger.setLevel(Level.ALL);
        handler.setLevel(Level.ALL);
        handler2.setLevel(Level.CONFIG);

        logger.severe("severe信息");
        logger.warning("warning信息");
        logger.info("info信息");
        logger.config("config信息");
        logger.fine("fine信息");
        logger.finer("finer信息");
        logger.finest("finest信息");
        
        /*
            总结:
                用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler
                (日志的记录使用的是Logger,日志的输出使用的是Handler)
                添加了哪些handler对象,就相当于需要根据所添加的handler
                将日志输出到指定的位置上,例如控制台、文件..
         */
    }

    @Test
    public void test05(){

        /*
            Logger之间的父子关系
                JUL中Logger之间是存在"父子"关系的
                值得注意的是,这种父子关系不是我们普遍认为的类之间的继承关系
                关系是通过树状结构存储的

                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关系

                private LogNode               node; //节点
         */

        /*
            从下面创建的两个logger对象看来
            我们可以认为logger1是logger2的父亲
         */

        //父亲是RootLogger,名称默认是一个空的字符串
        //RootLogger可以被称之为所有logger对象的顶层logger
        Logger logger1 = Logger.getLogger("com.bjpowernode.jul.test");

        Logger logger2 = Logger.getLogger("com.bjpowernode.jul.test.JULTest");

        //System.out.println(logger2.getParent()==logger1); //true

        System.out.println("logger1的父Logger引用为:"
                +logger1.getParent()+"; 名称为"+logger1.getName()+"; 父亲的名称为"+logger1.getParent().getName());


        System.out.println("logger2的父Logger引用为:"
                +logger2.getParent()+"; 名称为"+logger2.getName()+"; 父亲的名称为"+logger2.getParent().getName());


        /*
            父亲所做的设置,也能够同时作用于儿子
            对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印
         */
        //父亲做设置
        logger1.setUseParentHandlers(false);
        ConsoleHandler handler = new ConsoleHandler();
        SimpleFormatter formatter = new SimpleFormatter();
        handler.setFormatter(formatter);
        logger1.addHandler(handler);
        handler.setLevel(Level.ALL);
        logger1.setLevel(Level.ALL);

        //儿子做打印
        logger2.severe("severe信息");
        logger2.warning("warning信息");
        logger2.info("info信息");
        logger2.config("config信息");
        logger2.fine("fine信息");
        logger2.finer("finer信息");
        logger2.finest("finest信息");
    }

    @Test
    public void test06() throws Exception {

        /*
            以上所有的配置相关的操作,都是以java硬编码的形式进行的
            我们可以使用更加清晰,更加专业的一种做法,就是使用配置文件
            如果我们没有自己添加配置文件,则会使用系统默认的配置文件
            这个配置文件:
                owner.readPrimordialConfiguration();
                readConfiguration();
                java.home --> 找到jre文件夹 --> lib --> logging.properties

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

        InputStream input = new FileInputStream("D:\\test\\logging.properties");

        //取得日志管理器对象
        LogManager logManager = LogManager.getLogManager();

        //读取自定义的配置文件
        logManager.readConfiguration(input);

        Logger logger = Logger.getLogger("com.bjpowernode.jul.test.JULTest");

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

以上所有的配置相关的操作,都是以java硬编码的形式进行的
我们可以使用更加清晰,更加专业的一种做法,就是使用配置文件
如果我们没有自己添加配置文件,则会使用系统默认的配置文件
这个配置文件:
owner.readPrimordialConfiguration();
readConfiguration();
java.home --> 找到jre文件夹 --> lib --> logging.properties
做文件日志打印,新日志会覆盖掉原来的日志
但是我们现在的需求不是覆盖,而是追加

JUL系统默认配置文件解读

# 这里设置的是RootLogger使用的处理器,在获取RootLogger对象时读取该配置,并进行设置
# 默认情况下, 下面配置的是控制器处理器, 只能往控制台上进行输出操作;这也证明了为什么一开始我们写的示例程序打印到控制台中
# 如果想添加其他的处理器, 在当前处理器类后面通过逗号的形式进行分割,可以添加多个处理器(一个Logger记录器可以添加多个处理器)
handlers= java.util.logging.ConsoleHandler 

# RootLogger的日志级别, 默认的情况下, 这是全局的日志级别, 如果不手动配置其他的日志级别
# 默认输出下面配置的级别和比它更高的级别
.level= INFO

# 文件处理器相关属性设置
# 将Logger记录到的日志,输出到哪个路径下的日志文件
java.util.logging.FileHandler.pattern = /Users/test/Desktop/learn/java%u.log
# 输出日志文件的限制(50000字节)
java.util.logging.FileHandler.limit = 50000
# 设置日志文件的数量
java.util.logging.FileHandler.count = 1
# 输出日志的格式; 默认是以XML的方式进行的输出
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

JUL自定义Logger配置文件输出


# 这里设置的是RootLogger使用的处理器,在获取RootLogger对象时读取该配置,并进行设置
# 默认情况下, 下面配置的是控制器处理器, 只能往控制台上进行输出操作;这也证明了为什么一开始我们写的示例程序打印到控制台中
# 如果想添加其他的处理器, 在当前处理器类后面通过逗号的形式进行分割,可以添加多个处理器(一个Logger记录器可以添加多个处理器)
handlers= java.util.logging.ConsoleHandler 
# RootLogger的日志级别, 默认的情况下, 这是全局的日志级别, 如果不手动配置其他的日志级别
# 默认输出下面配置的级别和比它更高的级别
.level= INFO

# ---------------------------------------自定义的------------------------------------------------------
# 自定义该路径下的Logger(最顶部是给RootLogger设置的处理器, 这里我们自定义自己的Logger,并给其设置处理器)
com.lucky.handlers = java.util.logging.FileHandler
# 自定义Logger的日志等级
com.lucky.level=CONFIG
#  屏蔽父Logger的日志设置
com.lucky.useParentHandlers=false
# ---------------------------------------------------------------------------------------------

# 文件处理器相关属性设置
# 将Logger记录到的日志,输出到哪个路径下的日志文件
java.util.logging.FileHandler.pattern = /Users/test/Desktop/learn/java%u.log
# 输出日志文件的限制(50000字节)
java.util.logging.FileHandler.limit = 50000
# 设置日志文件的数量
java.util.logging.FileHandler.count = 1
# 输出日志的格式; 默认是以XML的方式进行的输出
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

# 输出日志文件, 是否是追加 (设置追加)
java.util.logging.FileHandler.append = true
@Test
public void test06() throws Exception{
    InputStream input = new FileInputStream("/Users/testyang/Desktop/learn/logging.properties");

    //取得日志管理器对象
    LogManager logManager = LogManager.getLogManager();

    //读取自定义的配置文件
    logManager.readConfiguration(input);

    Logger logger = Logger.getLogger("com.lucky.jul.JULTest");

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

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

#输出日志文件, 是否是追加
java.util.logging.FileHandler.append = true

3. JUL总结

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

1.初始化LogManager (先有日志处理器,才有日志记录器Logger)
    LogManager加载logging.properties配置文件
    添加LoggerLogManager
2.从单例的LogManager获取Logger
3.设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord4.Filter作为过滤器提供了日志级别之外更细粒度的控制
5.Handler日志处理器,决定日志的输出位置,例如控制台、文件...
6.Formatter是用来格式化输出的
显示全文