概要
工作原理和日志处理流程
代码解读
LogManager的初始化
Logger的构造
Logger记录日志
Handler日志处理
StreamHandler IO流方式的父类
ConsoleHandler 控制台输出
FileHandler 文件记录
SocketHandler 报文传输
MemoryHandler 缓冲处理
Formatter日志对象格式化
Logging框架的外交官
ErrorManager自身预警器
设计模式分析
创建模式
LogManager
Logger
LoggingMXBean
创建模式小结
结构模式
适配器模式
装饰模式
享元模式
合成模式
代理模式
结构模式小节
行为模式
责任链模式
观察者模式
概要
Logging,就是记录日志,我们的应用程序在开发,维护过程中都需要用到,一个系统的日志作用非常多,但其主要的作用是为了在系统出现问题的时候,可以把日志输出的信息作为分析的依据,分析问题所在。那么作为一个基础应用框架,其应该具有以下几个特点:
1. 可以在项目不同的阶段,可以输出不同的细粒度信息
2. 可以在项目中以各种概念划分,比如数据访问层如何记录日志,或者是项目中的具体某个模块应如何记录日志
3. 高性能,线程安全
4. 框架本身应该有自己的预警措施,不能在系统运行过程中发生错误而影响到正常的系统业务运行。
5. 有对自身框架管理的功能,使得应用系统在运行时对日志框架各种属性进行配置。
在开源社区的大家庭里,有很多成熟的日志工具,比如最常用的LOG4J,但是我们今天研究的是JDK中自带的日志框架,java.util.logging,我一直都认为开源代码中,养分最高的当属JDK,很多时候,JDK中的一小段代码,都会有种眼前一亮,回味无穷的感觉。
好的,跟我一起来吧。
工作原理和日志处理流程
java.util.logging框架对应用系统提供了一个记录日志的对象Logger,应用系统在开发过程中,只需要通过Logger对象的调用,就可以实现记录日志的功能,在调用过程中,日志的级别以调用的方法参数传入,其作用是可以区分开,在某个具体的阶段,哪些日志信息可以输出,哪些信息不应该输出。
在介绍工作原理之前,我们先看下java.util.logging这个框架里面几个重要的类,以及其作用。
1. Logger:对外发布的日志记录器,应用系统可以通过该对象完成日志记录的功能。
2. Level: 日志的记录级别,使得在系统运行的时候.
3. LoggingMXBean:接口对象,对外发布的日志管理器
4. LogRecord:日志信息描述对象
5. LoggerManager:日志管理器
6. Filter:日志过滤器,接口对象,在日志被Handler处理前,起过滤作用
7. Handler:日志处理器,接口对象,决定日志的输出方式
8. Formatter:日志格式转化器,接口对象,决定日志的输出格式。
工作原理:
首先通过LoggerManager进行日志框架的初始化,生成Logger的根节点RootLogger.
这里需要注意的是,LoggerManager的初始化工作,并没有将构建配置文件中所有的日志对
象,而仅仅是构建了根节点,这种方式就是我们多例模式中经常用到的懒加载,对象只有在真正被时候的时候,再进行构建。
通过Logger.getLogger(String name) 获取一个已有的Logger对象或者是新建一个Logger对象。Logger,日志记录器,这就是在应用程序中需要调用的对象了,通过Logger对象的一系列log方法,我们就可以方便的实现日志记录的功能。
Logger的大致处理流程如下:
收到应用程序的记录请求,将参数中的日志信息和运行时的信息构建出LogRecord对象,而后通过Logger对象本身设置的记录级别和调用者传递进来的日志级别,如果传递进来的日志级别低于Logger对象本身设置的记录级别(从语义上的理解,而实际上语义级别越高的级别其内部用数字表示的标志的数值越小),那么Logger对象将直接返回,因为他认为这条日志信息,在当前运行环境中,没有必要记录。
而满足以上条件的日志信息,将会通过Logger对象的filter元素的过滤校验,filter是动态的,在运行时是可以随意设置的,如果有filter对象,那么将调用filter对象,对日志对象LogRecord进行校验,只有校验通过的LogRecord对象,才会继续往下执行。
通过filter校验后,Logger对象将依次调用其配置的处理器,通过处理器来真正实现日志的记录功能,一个Logger对象可以配置多个处理器handler,所以一条日志记录可以被多个处理器处理,同时Logger对象的实现是树形结构,如果Logger对象设置其可以继承其父节点的处理器(默认),一条日志记录还会被其父节点的Logger对象处理。
而handler的处理方式就会是形形色色了,但是归根节点,会有以下几个大的步骤:
1. 级别的判定和比较,决定某条具体的日志记录是否应该继续处理
2. 将日志记录做格式化处理,以达到输出的日志在格式上统一,美观,可读性高。
3. 资源的释放,不管是以何种方式记录日志,总是会消耗一些方面的资源,所以会涉及到资源的释放问题。比如以文件方式记录的日志的,在一定的时候需要做文件关闭操作,以报文方式发送日志的,在和远程通话的过程中,也需要涉及到网络IO的关闭操作,或者是存储在数据库等等,资源释放在程序开发过程中,是个不变的主题。
代码解读
上面介绍了日志的大致工作原理和日志处理流程,让大家有个大致的概念,这节也将按照上面所说的处理流程以此解读其中的代码。
LogManager的初始化
LoggerManager其主要的作用是对框架进行初始化设置,以及提供一系列访问和修改初始化数据的接口。
其初始化过程可分为三部分:类静态块初始化,构造函数初始化,首次访问初始化。下面将对这三部分分别进行说明。
类静态块初始化
在分析这部分之前,请看下图:
方式进行扩展呢?显然,如果需要扩展,我们可以通过继承,新增功能或者是修改原有的功能,或者我们通过聚合和包装,完成我们需要的功能,但是通过聚合和包装的方式显然不是我们想要的,因为包装和聚合,只能满足应用程序的需求,而不能对自身框架的进行扩展,对不对。
好了,我们现在想想继承,假如我们需要对这个类进行扩展,自定义类A,然后我们在应用程序中,构造并初始化A,如果是这样子,那么我们将违反了面向对象的设计原则,依赖反转原则,我们面向了实现编程,由此,我们想到通过工厂方式或者是抽象工厂对A进行构造,貌似解决了这个问题,但是我们现在还是忽视了一个问题,框架本身如何去获取和构造这个A对象列?
所以LoggerManager在设计上需要满足以下两点:
1. 为了方便管理和节省空间,需要是单例
2. 必须可以扩展,因为默认的初始化方式似乎不能满足应用程序中形形色色的需求。
3. 扩展的类必须能被框架自身访问。
那么,该如何做呢?
首先,我们可以明确一点,这么所有的设计应该在LoggerManager内部完成,因为框架本身对LoggerManager是耦合的。先别往下看,大家一起思考五分钟。
好了,或者大家已经想出来了,或者大家都已经知道了这种做法了,请看代码:
在构造LogManager时,进行了构造函数初始化。相关内容请看下面的第二小节,构造函数的初始化,构造函数初始化后,按照一般的思路,我们会对配置文件进行解析,将所有的初始化工作一次性完成,是不是?这是我们一般的常规做法。可是logging框架并没有这么做,框架只是创建了一个根节点的Logger对象,而且这个Logger对象并没有和配置文件发生任何关系,仅仅是创建而已,其代码如下:
构造函数初始化
Logger就提供了一个默认的构造函数,那在构造函数中,做了些什么呢?
不知道大家对HOOK有没有概念,也就是我们经常说到的钩子程序,钩子程序的主要目的就是为了在JVM退出之前,应用程序需要做的一些工作,显然,这对于一个日志框架而言,是有着非常重要的意义。假设应用程序在运行时崩溃,那么造成系统崩溃的相关的日志将会给我们分析问题和解决问题有很大的帮助,在往下的分析中,我们会发现,在我们现在所描述的这种情况下,相关的日志可能还没有输出,系统就已经崩溃了,那么我们在分析问题和解决问题就失去了很多的依据,所以很有必要确保在JVM退出之前,一定要保证系统崩溃的所有日志都已经输出。而这个钩子程序Cleaner巧妙的解决了这个问题。Cleaner的职责在于在JVM崩溃前,将所有Logger对象的Handler对象关闭,这里的关闭其实是讲handler中所有未输出的日志,或者IO的缓冲区输出,从而确保日志的完完全全的输出。
这里的首次访问是指下面这个方法第一次调用的时候
大家会说,用日志框架,如果要用到LogManager对象,那么,getLogManager方法势必是第一被调用的方法,因为这个方式是获取单例LogManager的唯一方法,第一次调用这个方式时,就会先调用静态块,是不是很有道理?
这些思维逻辑是非常合理的,但是,在软件这行业,往往这种逻辑缜密的答案是错误的。这也是一个成熟框架的代码魅力,看完之后,会让我们有眼前一亮的感觉。既然是框架,那么,就要考虑各种情况的出现。如果在没有调用getLogManager之前,我们就在一个代码块中写了这么一句,LogManager.getLoggingMXBean();那么请问,静态块和getLogManager还会在一块执行吗?虽然这种情况基本上不太可能出现,特别是已经在运营的项目了,但是这个框架的应用不仅仅是这些情况,项目开发初期