您的位置:首页 > 编程语言 > Java开发

java.util.logging源码解析

2012-12-16 17:20 253 查看
目录

概要

工作原理和日志处理流程

代码解读

LogManager的初始化

Logger的构造

Logger记录日志

Handler日志处理

StreamHandler IO流方式的父类

ConsoleHandler 控制台输出

FileHandler 文件记录

SocketHandler 报文传输

MemoryHandler 缓冲处理

Formatter日志对象格式化

Logging框架的外交官

ErrorManager自身预警器

设计模式分析

创建模式

LogManager

Logger

LoggingMXBean

创建模式小结

结构模式

适配器模式

装饰模式

享元模式

合成模式

代理模式

结构模式小节

行为模式

责任链模式

观察者模式

java.util.logging源码解析

概要

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对象并没有和配置文件发生任何关系,仅仅是创建而已,其代码如下:



从后面的内容中我们会知道,此时此刻,我们的配置文件尚未被加载(当然,我们现在这里讨论的是LogManager本身这个对象,并没有在讨论自身拓展的LogManager),那么这是为什么呢?其实道理很简单,因为配置文件中很多的配置属性并不是全局的,很多的配置都是针对某个Logger对象配置的,按照按需分配的原则来讲,需要时再配置会显得更合适,假如配置文件中的配置的五个Logger对象,其中只有一个在系统中用到,那么,我一开始就创建5个Logger对象干嘛呢?哈哈,按需分配,你学到了吗?

 构造函数初始化

Logger就提供了一个默认的构造函数,那在构造函数中,做了些什么呢?



不知道大家对HOOK有没有概念,也就是我们经常说到的钩子程序,钩子程序的主要目的就是为了在JVM退出之前,应用程序需要做的一些工作,显然,这对于一个日志框架而言,是有着非常重要的意义。假设应用程序在运行时崩溃,那么造成系统崩溃的相关的日志将会给我们分析问题和解决问题有很大的帮助,在往下的分析中,我们会发现,在我们现在所描述的这种情况下,相关的日志可能还没有输出,系统就已经崩溃了,那么我们在分析问题和解决问题就失去了很多的依据,所以很有必要确保在JVM退出之前,一定要保证系统崩溃的所有日志都已经输出。而这个钩子程序Cleaner巧妙的解决了这个问题。Cleaner的职责在于在JVM崩溃前,将所有Logger对象的Handler对象关闭,这里的关闭其实是讲handler中所有未输出的日志,或者IO的缓冲区输出,从而确保日志的完完全全的输出。



 首次访问初始化

这里的首次访问是指下面这个方法第一次调用的时候



看到这里,大家或许会说,这部分的初始化工作其实和上面所有的静态类初始化和构造函数初始化是同时进行的,所以所有的初始化配置应该不是上面所说的按需分配,而是统一分配,是不是?

大家会说,用日志框架,如果要用到LogManager对象,那么,getLogManager方法势必是第一被调用的方法,因为这个方式是获取单例LogManager的唯一方法,第一次调用这个方式时,就会先调用静态块,是不是很有道理?

这些思维逻辑是非常合理的,但是,在软件这行业,往往这种逻辑缜密的答案是错误的。这也是一个成熟框架的代码魅力,看完之后,会让我们有眼前一亮的感觉。既然是框架,那么,就要考虑各种情况的出现。如果在没有调用getLogManager之前,我们就在一个代码块中写了这么一句,LogManager.getLoggingMXBean();那么请问,静态块和getLogManager还会在一块执行吗?虽然这种情况基本上不太可能出现,特别是已经在运营的项目了,但是这个框架的应用不仅仅是这些情况,项目开发初期,或者是随便的在IDE上做个小小的实验,什么情况都会发生,对不对?

好了,我们接下来看看这个初始化过程到底做了些什么?



很抱歉,界面的的技术比较差,努力了很久,最终还是有个括号没有截下来。看到这里,我们是不是又看到一点构造LogManager的方法影子?呵呵,是的,首先我们需要支持扩展,对扩展开放,对修改关闭,那么在这里,又是对扩展什么开放,对什么修改关闭呢,首先,解析配置文件的目的在于把配置文件中的内容以KEY-VALUE的形式存储在Properties中,我们可以把最后的解析结果,将Properties中填充完配置信息这视作一个不变的因素,因为这种结构在程序的其他的很多地方都需要用到,然后,我们绝大部分的人都会认为,有Properties的地方就会存在现有的.properties文件,其实这是错误的,假如,一个系统为了在系统中更加方便的更新修改配置信息,把所有的配置信息存储在数据库中,这样做比一个配置文件横行天下会好很多,倘若是这种情况,又该如何呢?这样就很有必要将数据库中配置信息按照.properties的格式以流的形式传递到这个配置解析程序中来。所以这里的配置解析程序提供了一个可扩展的解析方式,应用程序可以根据实际情况,将配置信息组织成一个以.properties的格式流,需要说明的是,这个自定义的类需要在其无参构造函数中完成相关的任务,(也就是要在构造函数中调用LogManager.
public void readConfiguration(InputStream ins)).写到这里,我不得不拜服写这个框架的这位老兄面向对象的能力,将每一个可能遭受变化的因素都封装得这么得体。我真想这时候给他热烈的掌声………….

接下来,我们再来看看readConfiguration(InputStream ins)这个方法,看看他又完成了什么样的任务。代码如下:

我们可以看到,这种支持可扩展的处理方式又一次的呈现在我们面前了,而后,我们看到并没有对所有的配置信息进行解析,而只是对已经存在的日志对象进行级别设置。同时,这里使用了PropertyChangeSupport对属性文件的变动来做处理,这种处理方式的作用我们等下放到设计模式分析里进行讨论。

到现在为止,LogManager的初始化我们已经讨论完毕,从整个过程看,我们已经清楚,LogManager 的初始化其实只做了一些很简单的事情,我们总结一下:

1. 构造LogManager实例对象

2. 如果是用默认的LogManager,定义了其钩子程序

3. 构造了Logger树结构,并创建了根节点

4. 加载了配置文件,并对RootLogger设置了日志级别

那我们在讨论整个初始化的过程中,我们学到了什么?

1. 单例和工厂的结合,使得工厂模式做到了对扩展开放,对修改关闭

2. 延迟加载可以使得我们可以对资源按需分配

3. 适当的钩子程序使得我们的程序更加容易调试,更加容易追踪问题

4. 将流程按逻辑的角度分离,可以让我们的程序更容易适应变化。

Logger的构造

构造Logger对象的方式只有一种,那就是其本身提供的工厂方法,getLogger(String name),在LogManager的初始化的时候,我们知道,初始化的过程并没有将配置文件中配置的Logger对象初始化,我们说那是懒加载,按需分配,那么很必然的,在这个工厂方法上,必然需要对Logger对初始化的工作。那么很必然的,在LogMananger中,必然有对Logger做初始化的功能方法,对,这个就是addLogger(),我们看下面的代码:



从这里我们看出,所有的Logger对象一旦创建后,就一直存在于内存中,这块内存区域由LogManager进行分配和管理。那么这里将存在一个问题,什么问题呢?那就是整个系统中Logger对象数量的问题,因为Logger对象一旦构造,在系统的整个生命周期中一直存在的,所以数量越多,占用的系统资源就越多,而且一直占用着,我经常在代码中看到这样的写法:Logger.getLogger(XXXClass.class.getName());如此的写法来构造和获取一个Logger对

象,试想一下,如果一个大型的系统有5W个类,每个类都有一个Logger对象用来记录日志,那么这个内存占用量是不容忽视的,同时,我们知道,Logger是树形继承结构,大部分构造出来的Logger对象,真正做日志输出的都是他的父节点在起作用,所以这种声明方式是完全没有必要的,是对系统资源很大的浪费。所以Logger日志对象的构建在具体的系统中一定要有一个合适的策略,来避免这种对象大爆炸的现象发生。

好了,下面我们来看看,LogManager是如何对Logger对象进行初始化设置的.我们先看第一小段。



从这里我们可以看出,首先进行设置的Logger的级别,如果在配置文件中有级别的配置,那么就进行设置,如果没有,那么将使用默认的配置,这段很好理解吧。

那我们再来看第二小段。



从这里我们可以看出,进行设置的是Logger的处理器handlers,这些代码应该是很好理解。接着我们来看第三段,第三段的代码和第二段的代码差不多,为了减少空间,我不再粘贴代码,请看代码391--446行(JDK版本1.6)

第三段主要是初始化Logger的父节点,而且初始化的顺序是从祖先节点开始,一级一级往下初始化,这里面涉及到递归,如果发现一个父节点没有被初始化,那么将递归调用getLogger()方法初始化。这些代码很直接,很容易读懂,我不再一一解释。

第三段是构造Logger节点树,代码如下:



我们首先要明确的一点,这一段的主要作用是设置一个日志对象的父对象,在这里,我们看到一个新的类型,LogNode,从名字上我们可以大致的明白,这个类,应该是表示Logger之间关系的一个对象,Node,节点,自然在类里表述节点与节点之间的关系。

我们看下这个findNode:

由上面的代码我们可以确认的一点,这种节点关系是按包名来区分的,如A.B.C这个Logger,在LogNode中的关系应该是root--A--B-C,而A.B.C这个Logger的名字是A.B.C,而在LogNode中的名字是C。由此,我们可以分析出,不是每个LogNode对象都有对应的Logger,就拿A.B.C这个例子来说,如果我们没有A.B这个Logger对象,那么在LogNode中B这个LogNode是不是没有Logger对象?

所以需要想办法解决这种父对象为空,但是子对象又需要继承的问题,我们只能设置一个默认的根对象,如果LogNode的直接父对象Logger为空,那么我们再往上查找,直到查找根节点为止。

这个时候,你可能会有个问题,如果我先创建了一个A.B.C.D的Logger对象,而且在这之前并没有任何A.B.C之类的对象创建,也就是说A.B.C.D创建后的父类就是root,但是在创建A.B.C.D后,我们又创建了A.B.C这个对象,那么这个对象创建后,A.B.C.D的父类是A.B.C还是root呢?

答案肯定是前者,在上面的代码的最后几行,就是处理这个问题的。walkAndSetParent就是为了解决这个问题,这个方法也采用递归调用的方式,会查找他所有的子节点,如果子节点的logger对象不为空,也就是说这个子节点有实例化的Logger对象存在,那么将这个logger对象的父对象设为walkAndSetParent这个方法传递进来的logger对象。

好了,Logger的构造已经讲述完毕,在Logger的构造中,我们总结了一下几点:

1. 每个不同名字的Logger对象只有一个。

2. 系统中所有的Logger对象和配置文件中配置的Logger数不一样的,我们可以说配置文件中配置的Logger是系统中所有Logger对象的类型汇总,同时,我们在开发系统时,应该要控制Logger对象的数量。

3. Logger对象通过其useParentHandlers设置其日志是否需要用父类的处理器来处理,如果所有对象的这个属性都为真的话,那么一条日志将以递归的方式在所有的父类处理器中做处理

好了,到现在为止,我们已经分析完了logging框架的初始化和Logger的构造过程。这个部分我们称之为对象的创建过程,下面我们将分析这个框架的行为---日志记录。

Logger记录日志

Logger的所有方法大致分为以下几个部分:

1. Logger的工厂方法,用以获取和构造一个Logger对象,其中这工厂方法也包括匿名Logger对象的获取和构造。

2. Logger的各种属性或者和获取的方法,我们成它们为JAVABEAN方法

3. Logger记录日志的方法,这些都是根据log方法重写的一类方法,我们称为重写方法,主要是为了方便记录各种日志级别的方法,用于直接调用。

4. 为了满足不同的系统在记录日志的时候,更好的满足应用系统的继续封装,以方便应用程序代码的更方便的调用而提供的方法,我们成只为间接调用方法。具体的方法就是logp(),logrb().

上述的第一条我们已经详细的研究过了,第二条也很简单,但是有一个比较容易忽略的属性是useParentHandlers,注意,其默认值为真,也就是说,如果我们在配置的时候,如果没有设置这个属性,那么默认是为真的,也就是说,这种情况下,这个日志对象对应的所有的父日志对象都会处理这个日志对象需要处理的每条日志。

好了,我们现在来看下第三条,如何记录日志。

不管是调用哪个记录日志的方法,处理流程都是如此:

1. 比较日志对象和要记录的日志的级别,如果不满足记录条件,那么立即返回。

2. 根据传递的参数创建日志记录对象LogRecode.

3. 如果日志对象配有过滤器,调用过滤器,如果日志记录被过滤掉了,那么立即返回。

4. 依次调用logger的每个handler处理日志,处理完成之后,根据logger的属性判断是否需要父Logger对象进行处理,如果是,采用递归的方式调用父Logger处理。

这个处理流程是非常简单,也非常的容易理解,下面我想要说的是,我们如何利用这个Logging框架,如何更加有效率的在应用系统中应用。

假如现在我们的应用代码中,有如下一段代码

logger.info(“………………………“+var+”…………….”);

这么一行代码,用来打印应用程序运行时的上下文的一些信息,同时我们觉得这行信息并不是很重要,我们采用比较低的级别输出这句日志。以便真正应用程序投产后,我们通过配置屏蔽这句话的输出。

这似乎很合理,但是我们忽略了一个问题,那就是,不管日志级别如何设置,这个语句都会执行,特别是,这个参数如果是一个大字符串,或者是开发过程中,采用了很长的字符串拼接方式,这句日志输出是很消耗系统资源和性能的,既然有些日志信息可能输出也可能不输出,为什么不能在最外面就控制他呢?于是有如下写法:

If(logger.getLevel()>=Level.INFO){

logger.info(“…………………………………….”) ;

}

大家是不是觉得这样写很麻烦,而且很没必要,就这么一句话,对系统能造成什么影响呢?呵呵,是的,确实是很微小的影响,但是系统性能问题很多时候都是由很多的微小的影响组成的,大家同意这个观点吗?

Handler日志处理

Handler的作用就是把最终把日志输出,输出的过程中,又提出一个格式化formatter的概念,格式化就是把日志记录按特定的格式输出。

Logging框架已经提供了几种流形式的处理方式,如控制台日志输出,文件方式输出,SOCKET报文的方式输出,这几种处理方式都是基于流形式的。同时还提供了一个缓冲的输出概念,接下来,我们来分析分析这几个类,看看他们是如何实现的,他们这样实现为什么值得我们学习。

我们将按照如下的顺序来讲解:

1. StreamHandler,这是以流方式处理的父类,同时也是抽象类

2. ConsoleHandler以控制台输出的方式的处理类

3. FileHandler以文件存储的方式的处理类

4. SocketHandler以SOCKET报文的方式的处理类

5. MemoryHandler 以缓冲的方式的处理类。

在讲这些之前,我们要清楚地一点,这些处理类也有初始化的过程,而且这些初始化过程都是在其默认的构造函数中完成的,大部分的初始化都是通过读取配置中的相关信息来完成初始化工作的。这些代码我们很简单,我们不再进行逐一分析。

StreamHandler IO流方式的父类

由handler这个类我们知道,其子类需要实现的三个方法有publish(),close(),flush(),其作用分别是,如何处理一条日志记录,如何关闭相关的资源,如何刷新相关的缓冲区。那么很显然,对于流处理方式的处理器来说,分别就对应要做如下是四件事:

1. 获取流对象,(包装流对象,让其以缓冲的方式输出)

2. 将日志格式化后通过流对象输出

3. 刷新缓冲区

4. 关闭流对象

对应于上面的四点,我们就想到了这个类是如何写的了:

1. 获取流对象,自然有setOutputStream()方法,让子类在初始化的过程中进行调用,然后根据这个流对象包装成一个缓冲流对象。

2. 那么publish这个方法的实现自然就是格式化日志记录,然后将格式化后的信息通过缓冲流对象输出

3. 刷新缓冲区,关闭对象流,更加是我们再熟悉不过的了。

ConsoleHandler 控制台输出

由上面对StreamHandler介绍可以知道,ConsoleHandler其实只需要做好初始化工作,并且将System.err作为参数传递给setOutputStream(),所有的工作就圆满结束了。对不对?

FileHandler 文件记录

把日志记录到文件的方式应该是最复杂的一种记录方式了,因为用文件记录,涉及到的问题有很多,下面所述几条便是用文件记录所需要考虑的:

1. 要避免多个日志文件同时向一个文件输出的情况。

2. 必须要考虑到日志文件的大小,不能把所有的记录一直都记录在一个文件中。

3. 必须是线程安全的,因为一个Logger对象将在多线程环境下运行。

根据我们上面说的三点中第一点,在这个类里面是怎么做到的呢?在FileHandler的初始化的时候,已经对这点控制了,我们看下openFiles()方法的下面片段:





首先我们看到这段代码有一个循环,以及一个缓存MAP,那么意思就很明朗了:首先根据文件配置的命名规则和循环的次数生成一个文件名,这个次数号我们真正的意义是,处于同一命名规则的handler的顺序号,如果生成的文件名+”.lrk”已经被使用,那么将跳过继续,同时循环的次数递增,同时,这里有三段校验:

1. 校验文件是否在已经使用过的文件锁中

2. 校验文件是否能打开

3. 检验文件是否能锁定

只有这三点都通过了,生成的这个文件名才是有效的,同时,这个有效的文件名需要添加到缓存MAP中,以方便以后的文件的校验。这个校验的机制已经介绍完毕,那么,我们再看看generate这个方法,其三个参数分别表示的意思是:

1. 文件生成的规则表达式

2. 表示表达式中%g的数值

3. 表示表达式中%u的数值

看到这里,我想起了一个问题,如果我们的表达式中没有这个%g或者是%u这两个标识符时,这个类处理会不会碰到问题?先别往下看答案,自己先自信想想。

答案会肯定的,肯定会碰到问题。如果规则表达式中,没有%u这个参数,那么在文件名校验时,循环到底都只能生成一个文件名,因为generate这个方法的三个参数已经失效,那么,如果想框架正常运行,那么每个handler的表达式必须不一样。如果没有%g这个参数,那么在创建文件(File)对象时,不管怎么循环,生成的都是一个文件,因为generate这个方法的第二个参数已经失效,那么这种情况只能将日志文件的个数设置为1时,才不会有问题。

想到这里,我觉得这个generate和文件生成的规则表达式的这种处理方式还是存在些问题,很容易出问题,为了更加安全的更有效率的应用这个框架,我们只能在规则的表达式中这个两个参数都用上,才会减少问题的发生。

当然,更好的办法是,对于每个handler对象,我们给他们设置成完全不同的规则表达式。

还有,在这个循环进行校验的过程中,还定义了最大的循环次数100,这也就是说,一个带有%u参数的规则表达式,在同一定的时间范围内,最多只允许有100个handler,是不是这样呢?既然是这样,那么,我们是不是需要一个锁释放的过程?答案是肯定的,继续往下看,答案就在后面。

完成校验,我们是不是需要去创建日志输出流了呢?



我们根据配置中的参数count来决定到底生成几个日志输出文件,同时,从循环的方式上我们可以看出,这个文件数字的排列顺序是从小到大的,并且,从open这个方法的参数来分析,我们可以看出,顺序号最小的日志文件中存放的是最近输出的日志。接下来,我们看看这个框架是怎么打开一个文件的?



这个open过程其实就是做了一件事情,根据指定的文件对象创建文件流,并根据参数设置文件流是以追加的方式还是覆盖的方式输出日志到文件中,当然这里,我们看到了一个缓冲流的包装类,MeteredStream,这个类有两个参数,其主要的作用是为了输出到文件流的字节总数,最后,通过调用父类的setOutputStream来完成open这个动作,从StreamHandler的分析中我们知道,其子类其实只要在初始化的过程中,调用这个setOutputStream方法就算完成了任务了。

但是,在FileHandler中,有些特殊情况需要自己处理:在日志文件达到一定的容量是,我们需要将输出切换到另一个输出文件中。所以,我们需要覆盖父类的publish方法来处理这个问题。



从这里我们可以看出,当日志文件达到一定的容量是,会调用rotate这个方法来解决我们上面说的那个问题。Rotate的意思就是切换,转换。我们先来看下他是如何实现的。



首先,这是个同步方法,因为这个切换方法涉及到资源的释放和管理,所以必须要求是线程安全的。在这个for循环中,我们明白了切换的做法,那就是把日志文件通过重命名的方法依次往后移动,如果输出的日志文件已经达到最大的上限,也就是说输出的日志文件已经有count个,这时,最后的日志文件将会被覆盖,对不对,只保留最新的count个文件,然后以覆盖的方式打开第一个日志文件,open方法已经分析过。在整个切换的过程中,框架先把日志的级别设置成OFF,也就是说在rotate方法调用之后未完成之前,应用系统需要输出的日志将会被过滤掉,是不是这样?

答案是否定的,是不是不可思议?在我们刚才说的publish方法中,其实已经把这个问题解决掉了,我们再去看下这个publish方法,这个方法是不是也是一个同步的方法?而在这个同步方法中调用了rotate方法,也就是说,如果调用了rotate方法并且没有执行完之前,任何其他的线程再来调用publish方法时,会一直处于锁等待状态,线程是阻塞的,对不对?直到rotate这个方法执行完毕。

呵呵,即使是这样,我们同样还是有疑问的,既然在这个publish和rotate都处于同步,线程安全的状态,那么为什么还要用setLevel(Level.OFF)来进行设置呢?这是不是有点多此一举呢?

说实话,这个地方我也有点疑惑了,到现在为止,我还是意识不到如果没有这么设置会产生什么问题,也许,这也算是JDK多虑了吧,呵呵,给自己一点信心。

在这个rorate方法中,其中有一行,super.close(),刚开始很纳闷,为什么要写成super.close呢?我是这么想的?如果自身没有覆盖这个方法的话,直接写成close就行,对不对,可想而知,这个close方法应该是覆盖了这个方法,那么为什么自己覆盖的方法不用,而要用父类的方法呢?

让我们回忆一下,我们在什么地方看到过handler.close()方法呢?想起来没有,我们在

讲LogManager的初始化的时候,在讲其钩子程序的时候,那钩子程序是不是会把所有的Logger对象的所有handlers都close了呢?想起来了吧。

好的,还有在FileHandler的初始化过程中,我们为了校验日志文件的有效性,生成了一个.lck文件并用来做日志文件锁?想起这些后,我们自然就知道原因了,父类的close方法只是为了将日志文件流的缓冲区刷新,然后关闭文件流,但是FileHandler的close既要关闭日志文件流,是不是又要解除锁定,删除锁文件呢?因为这些文件只是框架的内部机制,从使用者的角度来说,这些他们是不关心的,对吗?自然,FileHandler的close方法,我们已经知道应该要完成以下四件事:

1. 要关闭日志文件流

2. 关闭日志锁文件流

3. 删除缓存中的锁记录

4. 删除日志锁文件

好了,我们现在总结下FileHandler的关键点:

1. 通过锁机制保证了日志文件的唯一性

2. 通过文件滚动的方式保留最新的日志信息

3. 通过同步的方式保证了日志文件在切换过程中日志信息不遗漏

4. 钩子程序保证了日志信息的完整性。

SocketHandler 报文传输

相对于FileHandler来说,SocketHandler是很简单的,其主要的目的是将日志信息通过报文流的方式传输出去,那么对于一个SocketHandler应该要注意哪些问题呢?

1. 既然是以报文的方式传递,那么,报文的格式显得非常的重要,也就是说,SocketHandler很重要的一部分是如何格式化日志。

2. 报文流传输的目的地,也就是说如何连接远程的服务器

其中的第一个问题,可以通过配置自定义的Formatter对象就可以解决这个问题,而对于第二个问题,SocketHandler默认采用了Socket长连接的方式来连接服务器,因为长连接的方式在传输过程中效率高很多。只是这个长连接的处理方式不是太严谨,比如说如果服务器连接断开了,该如何做?SocketHandler并没有这种自动重练的机制,只是在发生错误的时候,通过内部的报警机制进行了处理,而默认的报警处理,其实就是用System.err打印出相关的错误消息。这个内部的预警机制,我们等下再详细介绍。MemoryHandler

MemoryHandler 缓冲处理

上面的ConsoleHandler,FileHandler,SocketHandler,都是应用程序调用一次,他们就处理一次,这种同步的处理方式对应用程序的处理是很受影响的,特别是在IO处理问题上,这种影响特别明显,为了解决这种性能问题,我们可以做缓冲处理,也就是在处理过程中,我们建立一个缓冲池,应用程序发送过来的日志对象,我们先存储在缓冲池中,等到某个条件满足时,我们再从缓冲池中取出这些日志对象,一次性写入,通过这种办法,可以在一定程度上提高系统的性能。

那么在这种处理方式中,如何建立一个缓冲池成为了最关键的部分,我们来看看它是如何实现的。



从上面我们可以看出,这个缓冲池就是一个带有游标变量和一个记录当前日志总条数的变量的数组,而输出这个缓冲区的条件就是当一条日志记录的日志级别高于预先设置的推送级别时,就将缓冲区中的日志全部输出,然后清空缓冲区。

看到%运算符,我立马想起了日志信息被覆盖的情况,那么这种情况会不会经常发生呢?答案是肯定的,比如这个缓冲区能存储100个日志信息,我们先用INFO记录了100条记录,而pushLevel是ERROR,假设第101才记录才是一条ERROR级别的日志信息,根据这个算法,这条ERROR信息将存储于buffer[0]上,他已经覆盖掉了第一条级别为INFO的日志对象,根据清空日志信息的算法,先冲buffer[1]开始一直到buffer[99],最后输出buffer[0]。在这个处理过程中,是不是第一条INFO级别信息的日志被覆盖掉了?

好了,到现在为止,handler的源码分析算是到一阶段了,接下来,我们要分析的是日志的格式化问题。

Formatter日志对象格式化

我们看到Formatter定义了一个抽闲方法:public abstract String format(LogRecord record);从这个方法我们得知,被格式化的对象就是LogRecord,格式化的结果就是一个字符串,此外在Formatter中,还有一个formatMessage的具体方法,这个类是用来做什么的呢?我们IDE查找他的调用者,立马可以知道,这个方法是对应用程序传递过来的message字符串进行的处理,我们可能在想,传递进去的message需要进行格式化吗?

答案是肯定的,比如我们应用系统对所有的日志输出消息做了映射,在应用程序中,在调用logger.log()是,传递的字符串仅仅是映射的键值,真正有意义的字符描述是可以通过映射的键值获取,而且,这个真正有意义的字符串是可以是动态的,比如我们在消息映射中定义了这么一条:

IO_EXCEPTIOIN:找不到文件:{0}

其中的{0}就表示一个动态的变量,在运行时,我们通过参数传递这个动态变量,例子如下:

Logger.log(Level.ERROR,”IO_EXCEPTION”,”application.log”)

这样,这个方法最终返回的结果就是 “找不到文件:application.log”.

这样做有两个好处,一是能减少应用程序的硬编码,比如,哪天我们觉得这个消息描述得不够恰当,我们不再需要去修改代码,只需要在我们指定的资源文件中修改对应的消息描述就好了,二是能保证同种类型的消息的格式的一致性,在一定程度上,能规范我们的日志

输出,更加有利于我们的调试和运维工作,比如对一种异常消息的处理,我们预先这么定义:SOMETHING_EXCEPION:xxx{0}xxx{1},但是在开发过程中,某位同时觉得这个消息里的这两个参数并不能很好的说明出现问题的原因,而如果再加上另一个参数变量,调试将变得更加的方便和快捷,于是我们在这个映射中,添加了另外一个参数{2},同时邮件告诉其他的同时,对于这个错误,应该这样处理,这样在一定程度上是不是起到了经验的共享呢?

当然,就算是加上了这么个机制,我们的框架还是要能对logger.info(“xxxxxxxxxxxx”) ;做出正常的处理,其实也就说,一个logger对象,如果没有设置资源绑定,没有消息映射,没有动态变量,那么,我们就一成不变的输出应用程序传递过来的消息,对不对?

Formatter在框架中有两个子类,SimpleFormatter和XMLFormatter,其中SimpleFormatter的format()方法是同步的,因为在SimpleFormatter内部有有date,formatter两个成员变量,这两个成员变量是非线程安全的,我们回顾上面所说的handler的publish方法,在其具体的实现类中,都是同步方法,而format这个过程包括在publish中,所以在一定程度上讲,这个format同不同步意义并不大,大家觉得我这样考虑对吗?

Logging框架的外交官

从初始化的过程中,我们已经了解到,LogManager管理了框架中所有的对象,比如将Logger对象添加到Logger树中,重新加载配置,为配置添加监听器,所有的这些功能应该属于框架本身内部的管理功能。

而对于一个访问这来说,对一个框架的内部管理并不关心,他们只关心在某个运行时刻,能更改某个日志对象的输出级别,以方便解决应用系统的问题。他们关心的问题是:

1. 系统到底有多少个Logger对象,每个日志对象的在应用系统中的作用

2. 每个Logger对象的当前运行级别

3. 如果要追踪问题,应该修改哪个Logger对象,并判断将该Logger对象的级别修改成什么合理的值,才有助于系统的调试。

4. Logger对象之间的关系,也就是Logger节点树,通过节点数,可以更加清晰的知道一个Logger对象的最终会输出在那几个地方。

好了,通过这么一分析,大致的意思我们已经非常明了了,上述的这些功能,我们可以通过LogManager得到相应的信息。由于LogManager是可以扩展的,不同的LogManager可能实现的方式不一样,所以这个Logging应该和LogManager形成依赖,以便在LogManager变化时,Logging不需要随之变化。。

因此,这个外交官功能的结构自然就出来了:

1. LoggingMXBean 定义这些功能接口

2. Logging 具体的实现类,也是和LogManager通信的桥梁

3. 通过LogManager获取相关信息,同时Logging对象也是框架本身的一种资源,也由LogManager创建和管理。

ErrorManager自身预警器

在最开始的时候我们讨论过,作为一个日志记录的框架,不能因为自己的异常而影响到正常业务的运行,也就是说,这个框架产生的错误,不能往应用程序抛,所以必须有自己的异常预警机制。ErrorManager就是为实现自身的异常预警而设计的。这种默认的预警机制将框架的第一个异常信息通过System.err输出到控制台。

当然ErrorManager并不是单例,在每个Handler实例中都有一个默认的ErrorManager,同时也支持自定义的预警机制。像FileHandler就用到了自己定义的预警处理器。它这个自定义的预警处理器是在初始化的时候用到,他将记录FileHandler初始化的最后一个异常,如果初始化过程中有异常发生,那么他把最后的一个异常抛出,标志初始化失败。

设计模式分析

设计模式分析将按照模式的分类分为三部分进行分析

1. 创建模式

2. 结构模式

3. 行为模式

创建模式

我们先来回忆一下设计模式中有哪几种创建模式:

1. 抽象工厂模式

2. 简单工厂模式

3. 工厂方法模式

4. 建造模式

5. 原型模式

6. 单例模式

那么在logging框架中,使用了哪几种创建模式呢?

首先我们回顾一下, 在logging框架中我们创建了哪些类型的对象,然后按照顺序一个一个来讲创建模式。

1. LogManager

2. Logger

3. LoggingMXBean

LogManager

首先,LogManager在logging框架的整个生命周期,有且仅有一个对象实例存在。那么我们在某种意义上说他应用了单例模式。

LogManager是支持扩展的,可以通过配置实例化其一个子类,从整个角度上来讲,LogManager又属于简单工厂模式,其通过调用静态的getLogManager()得到的对象不一定是LogManager对象,可以是LogManager的子类。

同时,LogManager对象管理着logging框架中所有的Logger对象的创建和属性监听器的设置,从这个角度来看,LogManager的完整创建其实分为几部分:

1. 本身的实例的创建

2. Logger对象的创建和管理

3. 属性监听器的设置

也就是说,logger对象和属性监听器都是属于LogManager的一部分,从这个角度上讲,我们可以把它看成是创建模式(Builder).虽然根据标准的类图上来分析,我们看不到创建模式的

导演者,其实LogManager 自身充当了这个导演者,这种处理方法在JDK中有很多的应用,比如StringBuilder,StringBuffer就是很鲜明的例子。

Logger

在这个类中有个静态方法getLogger(String name),通过这个方法,可以获取一个Logger对象。通过上一章的源码分析,我们知道,所有的Logger对象都是通过LogManager管理以及初始化的,其中包括其成员属性filter,handlers的初始化,那么在这个过程中,包括哪些创建模式呢?

1. 多例模式

多例模式是单例模式的变体,单例模式是只针对一个类只有一个对象与其对应,而多例也是针对一个类,根据其分类,每种分类对应一个实例对象,当然,这个分类的方法和方式各种各样,形形色色而已。

Logger根据其属性名name进行分裂,每个不同的name对应一个实例。

2. 创建模式

Logger的创建模式要比LogManager表现出来的创建模式要明显,为了构建一个Logger对象,需要依次对其成员变量filter,handlers等进行构造,也就是说Logger对象的结构是很复杂的,需要将这个复杂的过程细细分解,如如何构造filter对象,如何构造handlers这些对象等,创建模式就是为了简化一个复杂的对象的创建,这个简化的意思其实就是,这个部分太复杂了,我自身不负责简化,我只需要简化后的结果,在这个Logger中就是说Logger中的handler的构造太复杂了,我不想管,要他们自己去管理吧,
于是,所有的构造细节Logger不再操心,他负责的就是命令Handler去构造,并将构造的结果反馈给他。当然,那么我们从Builder模式的类图来看,LoggerManager就是导演着(director),filter 和handler就是builder,每个具体的filter和handler就是concreteBuilder.他们共同合作要完成的事情就是创建一个Logger对象。

LoggingMXBean

在源码分析章节中,我们讲过,LoggingMXBean是对外发布的管理接口。在整个logging框架中,只有在LogManager中有一个getLoggingMXBean()的方法。一看代码,我们就知道他是单例模式,但是除了单例模式外,还包含其他的模式在里面吗?

有的,那就是工厂方法模式,是不是有点牵强呢?没有,一点都不牵强?我们先来看看工厂方法模式的类图:



首先,LoggingMXBean是一个接口,在框架中有一个Logging默认的实现类,这两者是不是对应类图左边的产品接口和具体产品类呢?

那么,现在的问题就是去找到Creator和ConcreteCreator.LogManager是一个可实例化具体的类,也就是说,我们还缺少一个Creator接口,是不是?

但是,在分析源码的时候,我们知道LogManager是可以扩展,可继承的,也就是说LogManager其实也可以充当Creator的角色,只是自己定义了一个默认的处理方式,对不对?

如果我们自定义类继承LogManager,不就完全满足了上面的类图结构了吗?

创建模式小结

总上所述,在Logging框架中,我们学习到了如下几种模式

1. 简单工厂模式

2. 工厂方法模式

3. 创建模式

4. 单例模式

5. 多例模式

而且我们知道,上述的这几个模式并非是单独存在的,往往是几个模式结合在一起使用,使得可以集成多个模式的优点,对某个模式的缺点用另外一个模式的优点来互补,以达到最佳的构造效果。

比如在LogManager中,用单例模式和简单工厂模式使得在这个框架中,通过单例模式达到了有且只有一个LogManager对象的效果,同时通过使用简单工厂的模式,使得LogManager具有了良好的扩展性,良好的扩展性刚好弥补了单例模式的不足。

又比如在Logger中,通过使用多例模式和创建模式,使得管理所有的实例对象变得非常的方便,同时,通过使用创建模式,使得在Logger的创建可以支持高细粒度的变化,而不至于牵一发而动全身,如果需要修改filter就修改filter,如果要修改handler就只需要对handler进行修改,而不会因为一个Logger中的某个handler需要修改,导致需要修改Logger.

又如在LoggingMXBean中,通过使用单例模式与工厂方法模式的结合,其用法和LogManager通过使用单例模式和简单工厂模式有异曲同工之妙。

结构模式

与创建模式一样,我们先来回顾一下有哪些结构模式

1. 适配器模式

2. 桥模式

3. 装饰模式

4. 门面模式

5. 享元模式

6. 合成模式

7. 代理模式

那么在logging框架中,用到了哪些结构模式呢?在这一小节中,我们将按照上面模式的顺序,依次来讲解,如果框架中没有涉及到模式,我们将直接掉过。

适配器模式

在源码分析章节中,我们已经知道LoggingMXBean这个借口是对外发布的管理接口,目的是为了给应用系统一个管理logging框架的接口。

同时我们还知道,所有框架的对象在框架内部都是通过LogManager进行管理,想到这里,我们自然就可以想出默认的Logging实现方式了,继承LoggingMXBean, 并通过对调用LogManager相关的方法对其接口进行实现。很显然,这就是一个标准的适配器模式(对象适配器),适配器模式在应用中会起到代码重用,无需重写代码的功能,同时,通过重用,可以提高我们程序的健壮性和稳定性。下面,我们来看看对象适配器的类图:



根据类图,我们可以知道,Target表示框架中的LoggingMXBean,Adapter表示框架中的Logging,Adaptee自然就是表示框架中的LogManager了。

装饰模式

在FileHandler中,定义了一个内部类MeteredStream,其目的是为了计算每个日志文件的大小,通过在内部定义变量written表示已经写入的字节数。MeteredStream继承自OutputStream,并通过和一个具体的OutputStream对象聚合,如此的结构,使得MeteredStream不仅具有了OutputStream输出的功能,同时通过添加了written变量,从而

有了统计日志大小的功能,也就是说,这个MeteredStream为OutputStream增加了新的功能----统计输出量,看到这里,我们大家就应该自然而然的想起了包装模式,包装模式就是为了给一个对象动态的增加额外的行为。请看装饰模式的类图:

这最上面的接口Component在这里就是对应OutputStream,ConreteComponent就是对应FileHandler中的FileOutputStream,当然,这里的Decorator和ConcreteDecoratorA和ConcreteDecoratorB都对应着MeteredStream,这让大家觉得这并非是个标准的装饰模式,其实不然,MeteredStream本身也是可以扩展的,如果它扩展了,和上面的结构不就完全的一致了吗?

享元模式

在讲这个模式之前,我们首先要纠正下很多人对这个模式存在的误区,很多人认为这个享元模式属于创建模式,因为从它的作用上看,他具备有效的存储大量的小对象的功能,一看到对象,大家便认为他是创建模式,其实不然,我们将创建模式,其对象的创建都在运行期,而并非编译期,如果创建过程在运行期,我们称这种模式为多例更加合适。

我们知道,Level这个类主要是用来表示日志的级别,哪些该输出,哪些不该输出。而且这些类型一旦定义好了,基本上不需要再去定义,就算是需要重新定义,其数量也不会很多,如果多了,那么这种级别也就失去了意义,因为这种日志级别还需要认为的去设定,设想下,如果我们的系统定义了上百个日志级别,我估计,我们谁都不知道在什么时候该使用什么级别了,也就是说,这个类在运行期间,创建很多级别相同的对象是毫无意义的,一种类型的级别定义一个就好。

我们先看看我在一些资料上找到的关于享元模式的类图:



看到这个图,我才想起,为什么很多人觉得享元模式是一种创建模式,从上图看,我们甚至可以说这个类图表示的是一个支持多例的简单工厂。而并非是享元模式的本质。享元模式的本质是在编译器就对需要用到的对象进行控制。

如框架中的Level,框架在编译器就定义好我们需要经常用到的对象

Level.INFO,Level.ERROR等。使得我们在进行应用系统开始的时候,可以直接调用。

同样的用法在JDK中数不胜数,如Integer就预先定义好了-128-127这个区间的数值对应的Integer对象,因为JDK认为,这个区间的数字用到的频率很高,没有必要每次都创建。

再者,享元模式中类的还有一个小小的特点,那就是绝大部分这样的类都是不可变类,也就是说其内部属性一旦初始化后是不允许更改的,这个特点其实也是应享元模式需要的,如果享元模式中的类是可以随意更改的话,那么在运行期的不同时段,我们得到的值就不一样,这种方式会导致很多的问题,一定要切记。

合成模式

在分析LogManager源码时,我们提到了LogNode,其目的就是为了明确应用系统中各个Logger对象的继承关系,这是一个简化的合成模式,我们首先来看看标准的合成模式的类图:



咋一看,感觉LogNode和合成模式一点关系都没有,那这个LogNode是如何表现出这种模式的呢?合成模式主要是为了表示对象间整体与部分的层次结构。一个Logger对象表示日志框架在运行期所有Logger对象的一部分,他可以有一个父节点,同时可以有N个子节点,讲到这里,似乎已经和合成模式有点关系了。自然而然,我们就可以理解把Logger认为是一个组件,一个Component,同时我们还可以认为他是一个默认的Leaf,而一个LogNode就表示一个Logger在整个日志框架的运行期的关系,是否有子节点,有几个子节点,是否是根节点。

看到这里,大家可能都会问,合成模式要解决的是一个什么样的问题?也就是合成模式的用途在哪里?从上面的源代码分析章节,我们知道,Logger具有继承其父节点的特点,那么试想下,如何才能更快更有效的找到他的父节点,我们以前学过数据结构,一看到这种问题就想到了树,是的,树结构能快速的解决整个问题,而且从面向对象的角度来看,一个Logger节点可以看成是一个节点,也可以看成是一棵子树,如果Logger是根节点,那么他还代表了整棵树,对不对,上面说的这些,都是在讲树的查找算法,还有一个问题就是,树的构造,如何建立这棵树。

通过代码分析,我们知道,默认的Logger节点关系是通过“.”来控制的,比如一个以A.B.C.D为名的Logger对象,在他的构造过程中,表示整棵树的LogNode必然会构造这么以下这几个节点:””ABCD,其中””表示根节点RootLogger,但是在构造A.B.C.D这个Logger对象时,A.B.C这个Logger对象未必已经创建了,甚至在应用程序中就没有这个对象,可是A.B.C.D这个Logger对象不能因为没有A.B.C这个Logger对象而不能往上继承了,或许已经有了A.B Logger对象或者是A
Logger对象,至少是有了RootLogger对象。因此如果没有直接父节点那么就需要依次往上找到一个最接近的祖先节点来做继承。

想到这里,我们很自然的想起这个LogNode应该具有能快速找到父节点和子节点两个功能,快速的找到父节点,可以更加有效的进行继承,快速的查找子节点,可以更加快速的插入一个新的节点,也就是当插入一个新节点时,如果在其所有的子节点中都找不到,那么就把这个新节点作为一个新的子节点插入。

由此,我们可以看出,合成模式很好的解决了对象间整体和局部的关系,LogNode就是一个很好的例子,说他好,主要是因为他就是正好解决了Logger之间关系,的正是他很好的处理了这种整体和局部之间的关系,使得很多的复杂问题变得非常容易解决,我们可以将一个复杂的问题分成若干个小问题,化整为零,逐一的解决。这种模式在JAVA GUI中表现得非常多,有时间大家可以去细细的研究下。

代理模式

代理模式主要解决的问题是:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。如下图:



我个人觉得代理模式是最好理解了,假设我们在项目开发过程中发现了一个很难解决的问题,不知如何处理为好,于是先找到项目经理,和项目经理讨论和分析后,项目经理展示了他卓越的项目管理才能,他将这件任务指派给项目组一个资深的设计师老王,要他来完成,同时通过需求分析,将这个问题约定一套满足需求的接口,并告诉了你和资深的设计师,项目经理一边和资深设计师老王说,你尽快将的实现类发布出来,然后做好实现和测试,然后一起连调测试。同时一边吩咐你说,你先对这接口做个默认的实现,在这个默认的实现调用老王公布出来的类和方法,然后继续你的后续工作,等老王的实现完成,单元测试通过后,

你们进行连调测试。其实这个过程就是代理模式最好的例子和解释了。

说得更加通俗一点就是代理模式就是把自己必须做但是不愿意做或者是自己做不来的事情交代给别人做。

Handler就用到了这个模式,Handler有个子类MemoryHandler.MemoryHandler的作用是对日志输出做缓冲处理,他并不关心真正的日志输出方式,日志输出的动作由其指定的成员变量target进行处理。至于target如何做,MemoryHandler并不关心,他只关心自己如何做好缓冲处理。

结构模式小节

综上所述,在logging框架中,至少是用了五种结构模式,通过适配器模式有效的利用了现有的代码,同时降低了新代码和已有代码的耦合。通过装饰模式可以使得在原有的功能基础上动态的添加新的行为和功能。通过享元模式节省了应用在运行时的资源,通过合成模式使得在处理整体和部分关系的逻辑结构时变得非常的简单,通过代理模式使得应用程序模块之间的划分变得更加清晰,有效的降低了模块之间的耦合度。

行为模式

我们先来看看有哪些行为模式:

1. 责任链模式

2. 命令模式

3. 解析器模式

4. 迭代器模式

5. 中介模式

6. 备忘录模式

7. 观察者模式

8. 状态模式

9. 策略模式

10. 模板方法模式

11. 访问者模式

而在logging框架中,我们用到了以下几个模式

1. 责任链模式

2. 观察者模式

责任链模式

该模式的意图是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象能处理请求为止。

我们记得,Logger对象是可以继承的,也就是说一个日志对象的处理是一条链路,先是Logger对象本身处理,然后交给其父类处理,父类处理完毕后,再交给他的父类处理,如此循环。这正好是责任链模式的特征。

有很多朋友说,你说的这个Logger的这个责任链模式更像是迭代器模式,因为一个Logger对象对应很多的Handler,在处理的时候,会循环调用每个Handler进行处理,从这个角度上讲,他应该属于迭代器模式?事实并不是这样的

这里的责任链模式并不是指Logger中的Handler的行为,而是Logger自身的行为,Logger于其父类之间的行为。请大家一定要好好的理解这一点。

观察者模式

观察者模式想必大家一定很熟悉了,如果不熟悉的朋友,可以参考JDK中的java.util. Observer,我们甚至可以直接用这些相关类来实现我们应用程序中的监听功能。同时,我也相应,大家都已经知道这个观察者模式在logging框架中的什么地方使用了。在这里,我想说的是,其实观察者模式存在的形式多种多样,形式各异。比如JDK中的事件监听模型就属于观察者模式,同时很多朋友在讨论模式的时候,经常会把观察者模式说成是监听模式,其实都是一个意思。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: