01、JUL日志(JDK自带日志框架,包含源码分析)(一)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 01、JUL日志(JDK自带日志框架,包含源码分析)(一)

一、JUL架构介绍


1.1、认识不同组件



Logger与Handler都可设置过滤器Filter。

日志靠几个组件完成:


Loggers(日志记录器):Logger通常是应用程序访问日志系统的入口程序,负责捕捉事件并将其发送给何时的Appender,其关联着一组Handler。

Appenders(输出源):也称为Handlers,负责从Logger中取出日志消息,并使用Layouts(即Formatters)来格式化信息,然后将消息发送出去。一个logger可以有多个Handlers。可通过抽象类Handler下的具体实现类来决定输出位置,可输出多个位置。(控制台、文件或其他日志收集系统)

Layouts(布局器):称为Formatters,负责对日志事件中的数据进行转换和格式化,通过使用抽象类Formatter下的指定实例来决定输出格式。

Filters:过滤器,根据需要来定制哪些日志会被记录或放过。

日志输出大致流程:


首先获取到Logger实例(默认日志等级为INFO),该实例初始化会自动配置一个rootlogger(即它的父logger),这个rootlogger中会有一个Consolehandler(用于将输出到屏幕)。

当使用Logger实例来调用方法输出日志时,首先会比对Logger本身的日志等级以及其过滤器。

接着会依次执行Logger实例中的handlers集合中的handler的publish()方法来输出日志信息,再此之前会与handler的日志等级作比较以及handler的filter过滤器。(若无handlers则跳过)

接着会根据Logger中的useParentHandlers布尔值来判断是否执行rootlogger中的handlers,操作如同3一样。

注意:Logger、Handler都有自己对应的日志level等级,初始默认都为INFO(800)。上面的流程初始阶段了解即可,对于详细的执行流程可见最后总结。



1.2、Logger



Logger:是一个独立的类,它有一个子类为RootLogger(是LogManager类的内部类)


包含方法:


static Logger getLogger(String name):获取一个Logger实例,其有参构造器为私有的。

void log(Level level, String msg):用于打印日志信息,第一个参数为日志等级,第二个参数为日志信息。

void info(String msg):打印Level.INFO级别的日志,实际上也是调用的log()方法。对应类似方法还有void warning(String msg)以及其他几个级别日志方法,更加方便打印日志。

void setUseParentHandlers(boolean useParentHandlers):不使用默认自带的父类的handler。

void setLevel(Level newLevel):设置日志等级。(默认为INFO)

void addHandler(Handler handler):添加指定类型handler到handlers集合中。

void setFilter(Filter newFilter):设置过滤器。


1.3、Handler



Handler:是一个抽象类,其中包含了set/get方法。可设置对应的Formatter、Filter,并自身带有对应的日志等级。




相关实现类:用于输出到不同位置


StreamHandler:将日志通过OutputStream输出。

FileHandler:对应的OutputStream对象是FileOutputStream,能够将日志输出到文件。

ConsoleHandler:对应的OutputStream对象是System.err,会将日志输出到控制台。

SocketHandler:对应的OutputStream对象是Socket.getOutputStream(),会将日志输出到网络套接字。

MemoryHandler:将日志输出到一个内存缓冲区。

相关方法如下:



synchronized void setLevel(Level newLevel):设置Handler的日志等级。

synchronized void setFilter(Filter newFilter):设置对应过滤器。

synchronized void setFormatter(Formatter newFormatter):设置对应的格式化输出器。

synchronized void setEncoding(String encoding):设置输出时的字符编码。

ErrorManager getErrorManager():返回错误处理器,errorManager用来处理日志记录过程中发生的异常信息。

synchronized void publish(LogRecord record):用于输出日志。


二、输出日志信息


JUL:JDK自带的日志实现依赖。


输出Info级别日志的三种方式:


@Test
public void test01(){
    //方式一:通过Logger.getLogger()方法获取日志记录对象
    //有参构造是全限定名
    Logger logger = Logger.getLogger("xyz.changlu.LogTest");
    logger.info("hello jul");
    //方式二:通用方法进行日志输出
    logger.log(Level.INFO,"自定义level为INFO,报错");
    //方式三:通过占位符方式
    String methodName = "test01";
    int msg = 2;
    logger.log(Level.INFO,"方法名{0},次数为{1}",new Object[]{methodName,msg});
}


方式一的logger.info()内部实际上也是调用的log()方法(传INFO参数即可)。

log()方法第一个参数可以指定任意级别,后面msg则是输出到控制台信息的内容;重载方法也可以通过占位符方式填入指定要输出的信息。




第一行信息自带日期时间,包名及方法都是自动获取输出的;第二行的信息:后则是我们输入的msg。


三、自定义日志级别配置


3.1、认识Level类


Level:是一个枚举类(jdk1.5以前自定义枚举类)。


该类中包含七个不同等级的枚举实例,等级从高到低如下:


SEVERE:造成了程序的终止,可以使用该等级来记录。(value:1000)

WARNING:记录程序发生的一些问题,但问题不会造成程序终止。(value:900)

INFO:消息记录,记录数据库的连接信息,IO的传递信息等等。(value:800)

CONFIG:配置信息,加载了配置文件,读取配置的一些参数。(value:700)

下面的三个都是用来Debug日志记录的消息,记录程序运行的状态跟执行的流程,参数的传递信息。这三者value级别的大小不同,FINE低一些,FINEST高一些,这三个开发中拿一个即可

FINE:(value:500)

FINER:(value:400)

FINEST:(value:300)

ALL:所有日志级别都能执行。(value:Integer.MIN_VALUE)

OFF:所有日志都不能执行。(value:Integer.MAX_VALUE)

源码实例:



value是枚举实例在构造时填入的参数,决定了该Level实例的等级。


3.2、输出不同等级日志


上面枚举实例对应的value起到什么作用呢?


用于当前logger实例的日志等级value值与调用log()方法输出指定日志级别的value值对比,有限制日志输出的作用。

我们看下面的例子:同时输出7个不同等级的日志


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu.test01");
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}



可以看到只有severe、warning、info等级能够输出msg信息。

那么为什么只输出了前三个等级的日志呢?我们看Logger源码:


//这里调用的是Logger类的log()
public void log(Level level, String msg) {
    if (!isLoggable(level)) {//首先会判断是否大于当前logger的等级,一旦小于返回isLoggable()返回false,则会执行return结束方法
        return;
    }
    //传入进来的level符合现有等级,那么就会执行下去通过handler进行输出信息
    LogRecord lr = new LogRecord(level, msg);
    doLog(lr);
}
public boolean isLoggable(Level level) {
    //这里levelValue是现有logger实例中的日志等级默认为INFO(800),一旦小于800或等于Integer.MAX_VALUE(Level.OFF实例值)返回false
    if (level.intValue() < levelValue || levelValue == offValue) {
        return false;
    }
    return true;
}



能够看到在调用log()方法会去与自己本身的日志等级比较(初始默认为INFO),所以只会输出INFO等级及以上。


我们可通过使用void setLevel(Level newLevel)来设置当前Logger的日志等级:


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu.test01");
    logger.setLevel(Level.WARNING);
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}



果然生效了,当设置Logger的当前日志等级为WARNING时,有对应效果。

那我们接着设置低级别的呢,如logger.setLevel(Level.CONFIG);,结果却并没有输出config的日志信息。其实原因是因为在进行日志输出时,并不仅仅会比对Logger本身的日志等级还会比其Handler的日志等级(初始默认也为INFO)。


若是我们想要输出所有日志等级的日志信息呢?


将logger实例以及其handler的日志等级都设置为ALL即可。
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu.test01");
    //不执行rootlogger中的handlers
    logger.setUseParentHandlers(false);
    //自定义handler
    ConsoleHandler ch = new ConsoleHandler();
    ch.setFormatter(new SimpleFormatter());
    //将实例logger与对应handler都设置自定义的日志等级
    ch.setLevel(Level.ALL);
    logger.setLevel(Level.ALL);
    //将handler添加到logger的handlers集合中
    logger.addHandler(ch);
    //输出不同等级的日志
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
} 


获得的logger实例中会有一个rootlogger,我们不自定义handler时,使用log()输出到控制台信息就是通过rootlogger中的ConsoleHandler输出的。

第4行:我们使用该方法,传入false表示不执行rootlogger的handlers。

第7行-13行:自定义handler并设置对应的转换器,接着将handler添加到logger中。其中有两个关键操作是设置自定义handler的日志等级以及设置logger实例的日志等级为Level.ALL。ALL对应的值为Integer.MIN_VALUE,所以所有级别的日志都能够输出显示。


3.2、自定义日志级别(console与文件输出)


@Test
public void test01() throws IOException {
    //输出日志到调试窗口
    Logger logger = Logger.getLogger("xyz.changlu.test01");
    //不将记录发送到父类处理器中
    logger.setUseParentHandlers(false);
    //1、定义窗口处理器ConsoleHandler
    ConsoleHandler ch = new ConsoleHandler();
    ch.setFormatter(new SimpleFormatter());//ConsoleHandler设置简单格式化输出器
    //2、定义文件处理器
    FileHandler fh = new FileHandler("jul.log");//指定路径(这里就是工程目录下)
    fh.setFormatter(new SimpleFormatter());
    //logger器中添加两个handler
    logger.addHandler(ch);
    logger.addHandler(fh);
    //输出的所有等级的日志信息到对应handler指定的区域(窗口以及文件)
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}


第4、6行:通过Logger静态方法getLogger()会获取到一个Logger实例,它有个rootlogger,并自带handlers(ConsoleHandler,其使用的SimpleFormatter转换器),由于不使用rootlogger中的handlers,所以我们调用setUseParentHandlers(false)方法传入参数false表示不执行rootlogger的handlers(防止重复打印)。

第9、12行:就是设置不同的handler(输出到窗口、文件),并将对应的转换器放置到hander中。

第15、16行:将多个handler依次放置到logger实例中,一旦执行log()方法就会执行其所有的handler。




四、Logger的子父类关系


4.1、认识根Logger


探究初始创建Logger与根Logger的关系


JUL中的Logger之间存在子父类关系,当我们初始化一个Logger时,会默认创建一个rootlogger(一般情况下,除非下面①)


①直接创建一个rootlogger(name=""就是):


①我们先创建一个顶层Logger(即name=""):


 

@Test
    public void test01(){
        //传入空字符串获取Logger实例
        Logger logger2 = Logger.getLogger("");
        System.out.println(logger2);
        System.out.println(logger2.getParent());//打印其父类Logger
    }



当传入的name为""时,就会默认获取一个RootLogger实例,即为根Logger,因为是最顶层的了,所以就没有父Logger了。


②我们接着传入包名来获取Logger实例


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu");
    System.out.println(logger+",name="+logger.getName());
    System.out.println(logger.getParent()+",name="+logger.getParent().getName());
}



可以看到当我们创建一个name为"xyz.changlu"的Logger实例时,其父类则是根Logger,其name=""。


③创建有层级关系的Logger实例:


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu");
    //传入name为xyz.changlu的上级包名为xyz的Logger实例
    Logger logger2 = Logger.getLogger("xyz");
    System.out.println("logger="+logger+",name="+logger.getName());
    System.out.println("logger2="+logger.getParent()+",name="+logger.getParent().getName());
    System.out.println(logger.getParent() == logger2);
    System.out.println("logger2的根looger="+logger2.getParent()+",name="+logger2.getParent().getName());
}



注意当我们创建了logger的上级Logger实例(name为xyz)时,该实例的上级Logger默认会变为logger2的实例了,此时Logger2的父类则为根Logger了。(可以注意到当定义了有层级相关的Logger时会自动继承相关关系,也与包的层级有关)

总结:


当我们初始化一个Logger实例时(name!=""情况下),会默认自带一个rootlogger实例,可通过getParent()获取到,其name=""。

当我们初始化一个Logger实例时,设置其name=""会直接获得一个rootlogger实例,此时就没有父Logger了。

当我们定义多个Logger实例时他们的name具有层级关系,例如:xyz.changlu、xyz,那么他们会自动建立关联,name=xyz.changlu的Logger实例其父Logger则为刚刚定义的name=xyz的Logger实例,name=xyz的父Logger则为根Logger。


查看rootlogger中的handler、formatter、level等级


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu");
    Logger parent = logger.getParent();//为根Logger实例
    System.out.println("rootLogger:"+parent);
    System.out.println("rootlogger的name:"+parent.getName());
    System.out.println("rootlogger的filter:"+parent.getFilter());
    System.out.println("rootlogger的level:"+parent.getLevel());
    System.out.println("handler:"+parent.getHandlers()[0]);
    System.out.println("handler的Formatter:"+parent.getHandlers()[0].getFormatter());
    System.out.println("handler的level:"+parent.getHandlers()[0].getLevel());
    System.out.println("handler的filter:"+parent.getHandlers()[0].getFilter());
}



获得rootlogger还有一种方式:Logger.getLogger("") 空字符串即可。

可以看到rootlogger的level与该logger的handler的level都为INFO;Handler为ConsoleHandler,Formatter为SimpleFormatter。

此时对于logger算是有一些了解了,不过还不够继续往下看。



4.2、Logger的info(msg)执行流程*


这里默认name=xyz.changlu的logger实例,其父Logger为rootlogger


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu");
    logger.info("报错啦");
}


过程如下:


比较该logger实例的level是否高于INFO。(这里由于logger本身初始化并没有设定级别,所以使用的是rootlogger的日志级别)。

发现>=INFO,则继续,接着调用logger的Filter实例的isLoggable(),若没有添加,就不进行过滤。

接着开始依次调用logger的handlers(即handler集合),由于初始化没有指定handler,就不进行日志输出。

判断useParentHandlersboolean参数是否为true,默认为true,接着调用rootlogger的handler实例的publish()方法。

在rootlogger的handler的publish()方法中判断该handler的级别是否>=INFO,由于默认为INFO,大于继续执行。

接着调用rootlogger的handler的isLoggable()进行过滤,没有则不过滤。

使用rootlogger的ConsoleHandler的getFormatter().format(LogRecord)进行格式化(默认为SimpleFormatter)。

最终调用write()将格式化后数据输出到控制台。

注意注意:在抛到上层的logger时并不会判断该logger的level,仅仅是handlers的level。


针对于Logger与handler规范:


一个Logger对应1个Level,对应0个或1个Filter,对应0个或多个Handler,对应0个或1个ParentLogger 。
一个Handler对应1个Level,对应0个或1个Filter,对应1个Formatter。


通过上面说明,我们来看下面例子,请说出打印的内容有些什么:


public class LogTest {
    private static final Logger logger1 = Logger.getLogger("");
    private static final Logger logger2 = Logger.getLogger("cn");
    private static final Logger logger3 = Logger.getLogger("cn.codecrazy");
    static {
        logger2.addHandler(new ConsoleHandler());
        logger3.addHandler(new ConsoleHandler());
        logger2.setLevel(Level.WARNING);
        logger3.setLevel(Level.INFO);
    }
    public static void main(String[] args) {
        System.out.println(logger2.getLevel());
        System.out.println(logger2.getHandlers()[0].getLevel());
        logger1.info("logger1");
        logger2.info("logger2");
        logger3.info("logger3");
    }
}



若是创建一个ConsoleHandler,其会自动初始化一个INFO的日志等级(剧透一下是因为读取了默认的配置文件设置的等级)。

介绍整个流程:首先定义了三个日志实例,互相包含层级关系,在static代码块中将logger2中的日志等级设置为WARNING,并且添加了一个ConsoleHandler(该handler会自动赋予自己的一个INFO日志等级);接着给logger3中的日志等级设置为INFO,并也添加了一个ConsoleHandler。


开始进入main()方法,之前设置了logger2的等级,第一行则输出WARNING,logger2中的handler就是刚刚的consolehandler,默认等级为INFO。接着开始INFO打印输出,


①首先是第15行,对logger1进行日志输出(其等级为INFO),由于该logger实例是rootlogger其handler默认日志等级都为INFO,所以直接打印。


②接着是16行,首先会比对logger2中的日志等级(由于代码块中设置为WARNING),所以直接方法过程中直接return了,没有打印。


③接着是17行,首先会比对logger3的日志等级,本身为INFO,所以通过,接着会遍历logger3的handlers,其中consolehandler日志等级默认为INFO通过,所以在loggers中打印了第一次;接着会调用执行父logger(即为logger2)中的handlers,比对其handler的日志等级为INFO,所以再次通过打印第二次;最后再次调用父logger(这次为rootlogger)的handlers,由于是consolehandler,所以日志等级还是INFO,所以再次打印。


所以logger1打印了1次,logger3打印了3次。


本部分参考案例来源: Java Logging之JUL系列——Logger Hierarchy



4.3、设置日志等级


logger实例本身设置高的日志等级,logger尝试打印


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu");
    Logger parent = logger.getParent();//rootlogger
    //logger实例本身设置日志等级
    logger.setLevel(Level.WARNING);
    System.out.println(parent.getLevel());
    System.out.println(logger.getLevel());
    logger.info("cl报错啦");
}



该logger在进行日志等级判定时就会判定<INFO,所以无打印。


rootlogger设置WARNING等级,logger来打印日志


@Test
public void test01(){
    Logger logger = Logger.getLogger("xyz.changlu");
    Logger parent = logger.getParent();//rootlogger
    //logger实例本身设置日志等级
    parent.setLevel(Level.WARNING);
    logger.info("cl报错啦");
}



当rootlogger设置等级时则会影响到其子logger的level等级。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
26天前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
165 3
|
9天前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
36 1
|
22天前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
19 1
|
2月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
43 2
|
25天前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
2月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
2月前
|
人工智能 Java 测试技术
JDK11下Mock框架进化:从PowerMockito到Mockito Only
本文探讨了从使用PowerMock的测试环境迁移到仅使用Mockito(Mockito Only)策略的必要性和实践方法。
|
2月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
197 1
|
2月前
|
Java
安装JDK18没有JRE环境的解决办法
安装JDK18没有JRE环境的解决办法
256 3
|
3月前
|
Java 关系型数据库 MySQL
"解锁Java Web传奇之旅:从JDK1.8到Tomcat,再到MariaDB,一场跨越数据库的冒险安装盛宴,挑战你的技术极限!"
【8月更文挑战第19天】在Linux上搭建Java Web应用环境,需安装JDK 1.8、Tomcat及MariaDB。本指南详述了使用apt-get安装OpenJDK 1.8的方法,并验证其版本。接着下载与解压Tomcat至`/usr/local/`目录,并启动服务。最后,通过apt-get安装MariaDB,设置基本安全配置。完成这些步骤后,即可验证各组件的状态,为部署Java Web应用打下基础。
55 1