日志设施与通用实现
简介: 本文较详尽地介绍了jakarta项目之一commons-logging(日志通用实现)。为了使大家对日志通用实现有个较清楚和全面的了解,本文主要分两大部分讲述:第一部分讲述了日志设施的愿景、用户价值和功能特性,第二部分讲述通用实现。在第二部分中,首先讲述如何使用通用实现(读者可以从这里得到直观感受),接着讲述了通用实现的内部设计,然后在遗留问题一节中提出了通用实现相对完整的日志设施来讲的不完善之处。第一部分提出问题,第二部分解决问题。最后是总结收尾,主要是对读者的使用建议和设计建议。
本文的标签: _unassigned
本文较祥尽地介绍了jakarta项目之一 commons-logging
(日志通用实现)。为了使大家对日志通用实现有个较清楚和全面的了解,本文主要分两大部分讲述:第一部分讲述了日志设施的愿景、用户价值和功能特性,第二部分讲述通用实现。在第二部分中,首先讲述如何使用通用实现(读者可以从这里得到直观感受),接着讲述了通用实现的内部设计,然后在遗留问题一节中提出了通用实现相对完整的日志设施来讲的不完善之处。第一部分提出问题,第二部分解决问题。最后是总结收尾,主要是对读者的使用建议和设计建议。
本文是开源项目系列文章的第一篇,作者将以下面的文章思路统一组织这一系列,并且称这一思路为"开源项目描述框架":
- 前言-用于描述文章的目的
- 项目愿景-用于描述作者本人理解的项目的远大目标;
- 用户问题-用于描述项目可能产生的用户价值;
- 简单分析-用于描述在用户问题驱动下,项目产品应具有的特性;
- 使用界面-用于描述项目产品的使用方法,一般给出一些简单的例子从应用编程接口和静态配置界面两方面进行描述;
- 内部剖析-用于描述项目产品源代码使用的模式和最佳设计实践;
- 遗留问题-用于讲述项目产品未来的可能发展方向;
- 总结主要-用于讲述对读者的使用建议和设计建议。
为软件开发提供一个现成的、定义良好的、可扩展的日志设施。所谓"现成的"意思为软件开发可以即刻使用,包括API文档、使用实例和库;"定义良好的"表示项目提供良好的使用接口和具有优秀的内部设计;可扩展的意味用户可以进一步扩展功能。
关心软件日志的主要有三类用户:开发人员、系统管理人员和系统运行单位。三类用户各有各的日志需求:
- 开发人员在写代码的时候经常要输出程序的内部状态,目的可以是开发时的调试,或运行时的维护。
- 系统管理人员需要获取软件的状态数据以便进一步配置系统使其正常和高效运行。
- 系统运行单位需要软件保存操作日志以便例行检查或秋后算账。
虽然三类用户各有各的需求,对于日志设施来说可以归结为以下功能特性:
- 日志操作
日志功能是指基本的日志登记操作,是软件系统和日志设施之间的简单接口。软件系统一般只向这些功能传递应用日志信息。 - 级别
级别是指软件系统可以分级别地进行日志登记操作。日志设施的级别特性表现为日志操作和设施配置两部分。日志操作的级别表现为软件系统可以指定某次日志登记的级别,设施配置的级别规定了有效的日志操作的最低级别。如果软件系统中某个日志操作的级别低于配置指定级别,这个日志操作是无效的,既不会发生日志登记行为。
日志设施的级别性对于开发人员来说非常有用,它一方面有助于开发人员调式系统时了解详尽的系统状态信息,另一方面有利于开发人员对运行时软件系统故障的诊断和问题解决。而且系统从开发状态到运行状态转变时,开发人员插入到软件中的调式日志代码不需要删除,只需要提高日志的配置级别,并且最终使得程序员对System.out.println的嗜好已成为过去。
- 日志目标多样性
日志目标的多样性指日志可以被登记到多个日至设备,比如文件、控制台、数据库、邮件系统等。
日志目标多样性使得软件系统可以按照某种标准把日志输出到不同的设备上,比如调试用的日志一般可以输出到控制台,例行检查的日志可以保存到数据库中,系统出错的日志可以发通过邮件系统发到管理员或维护员邮箱。
- 日志格式
作为一种设施,除了登记软件系统指定的应用日志信息之外,日志设施往往还提供一些额外的系统日志信息,比如系统时间,日志发生的上下文等,而且能对所有的日志信息进行格式编排。
日志格式一般在日志设施的配置文件中设置,有助于节省软件系统调用日志操作接口时的编程负担,降低接口的复杂度。
值得注意的是软件系统到底往日志设备中记录什么东西,也就是说应用日志信息的具体内容由运用日志设施的软件系统决定,与日志设施没有直接关系。
使用界面分为编程接口和配置界面,编程接口谈软件系统中如何使用日志设施进行日志登记,配置界面规划日志设施的运行。
Common-logging的日志级别分为6种,从低到高分别为trace,debug,info,warn,error,fatal。它的级别一部分在编程接口中体现,一部分在配置界面中体现。
Common-logging为common的原因在于它是一个通用日志封装,被封装的可以是log4j,logkit,以及jsdk 1.4中的log等具体日志系统。在运行当中到底和那种绑定主要依赖配置界面和Common-logging的绑定搜索策略。
日志格式,日志目标多样性特性主要依赖具体日志系统的能力和配置情况。
Common-logging的应用程序编程接口主要在org.apache.commons.logging.log接口中定义,这个接口主要定义了两类操作:
- 一类是级别判断,用于减少不必要的日志操作的参数计算从而提高性能,函数名和参数如下所示:
log.isFatalEnabled(); log.isErrorEnabled(); log.isWarnEnabled(); log.isInfoEnabled(); log.isDebugEnabled(); log.isTraceEnabled();
下面的代码可以很好地解释这点:
if (log.isDebugEnabled()) { ... 一些高代价操作 ... log.debug(theResult); }
如果日志设施的级别定义高于debug,这些高代价操作可以避免运行。
- 另一类是日志登记,按照级别登记日志信息,函数名和参数如下所示:
log.fatal(Object message); log.fatal(Object message, Throwable t); log.error(Object message); log.error(Object message, Throwable t); log.warn(Object message); log.warn(Object message, Throwable t); log.info(Object message); log.info(Object message, Throwable t); log.debug(Object message); log.debug(Object message, Throwable t); log.trace(Object message); log.trace(Object message, Throwable t);
日志登记操作分又为两小类:一个参数的日志信息登记操作和两个参数的日志信息登记操作。前者对三类用户都适用,后者用于打印日志登记处的出错堆栈信息,所以更适用于开发人员调式与维护使用。
commons-logging的配置可在系统属性中设置,但是这是一个不好的习惯,系统属性会影响同一jvm下的所有类,而且不好控制,所以配置commons-logging的最好位置在commons-logging.properties文件中。使用属性文件配置commons-logging时一定要把其放到软件系统的classpath下。
commons-logging.properties中可以对下面两个属性项进行设置:
- org.apache.commons.logging.Log
commons-logging的缺省 LogFactory 按照这个项的值来实例化实现应用编程接口log的实例。如果这个项没被设置,LogFactory在软件系统的classpath下按下面的顺序搜索实现log接口的类:
- Log4J
- JSDK 1.4
- JCL SimpleLog
这个规则我们把它称为搜索策略。
- org.apache.commons.logging.LogFactory这个项覆盖缺省的LogFactory实现,用来满足应用系统的特定需求,如重新定义搜索策略。
5.4.1 源代码
/* * Created on 2003-6-5 * * To change the template for this generated file go to * Window>Preferences>Java>Code Generation>Code and Comments */ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * @author gongys * * To change the template for this generated type comment go to * Window>Preferences>Java>Code Generation>Code and Comments */ public class Foo { private static Log log = LogFactory.getLog(Foo.class); private String _name; private int _length; public static void main(String[] args) { Foo foo = new Foo("foo",2); //看看正常的长度会产生什么样的日志信息(日志操作一) foo.set_length(12); //看看不合格的名字会产生什么样的日志信息(日志操作二) foo.set_name("Jacky Bush"); //看看不合格的长度会产生什么样的日志信息(日志操作三) foo.set_length(-1); //看看合格的名字会产生什么样的日志信息(日志操作四) foo.set_name("Jacky Bush junior"); //瞧瞧具体的日志系统是什么(日志操作五) log.info("the log is instance of " + log.getClass().getName()); } public Foo(String name,int length){ this._name = name; this._length = length; } /** * @return */ public int get_length() { return _length; } /** * @return */ public String get_name() { return _name; } /**主要显示两类日志登记操作的使用 * @param length */ public void set_length(int length) { StringBuffer loginfo = new StringBuffer(); if(length <= 0){ loginfo.append("length must be more than zero! this changing is ingored!"); log.error(loginfo.toString(),new Throwable("invalid length setting(" + length +")")); }else{ loginfo.append("length is changed into "); loginfo.append(length); log.debug(loginfo.toString()); _length = length; } } /**主要用来显示级别判断的使用 * @param name */ public void set_name(String name) { StringBuffer loginfo = new StringBuffer(); if (name.length()< get_length() && log.isWarnEnabled()){ loginfo.append("name's length must be more than "); loginfo.append(get_length()); loginfo.append(". so my name is not changed!"); log.warn(loginfo.toString(), new Throwable("invalid name setting("+name + ")") ); return; } if (log.isInfoEnabled()){ loginfo.append("oww! my name is changed into "); loginfo.append(name); loginfo.append("!"); log.info(loginfo.toString()); } _name = name; } } |
5.4.2 ant 脚本和编译
ant 脚本build.xml文件内容如下:
<project name="loggingwork" default="compile" basedir="." > <target name="init"> <property name="lib.dir" location="libs" ></property> <property name="src.dir" location="src" ></property> <property name="bin.dir" location="bin" ></property> <path id="classpath"> <fileset dir="${lib.dir}"> <include name="*.jar" /> </fileset> </path> </target> <target name="compile" depends="init" > <javac srcdir="${src.dir}" destdir="${bin.dir}" > <classpath refid="classpath"/> </javac> </target> <!-- <target name="run" depends="compile" > <java classname="Foo" > <classpath> <pathelement location="bin"/> <path refid="classpath"/> </classpath> </java> </target> --> </project> |
执行命令编译源代码:ant compile
5.4.3 运行脚本和运行
在windows环境下执行脚本又两个文件组成。
setlocalpath.bat文件:
@echo off set LOCALPATH=%LOCALPATH%;%1 run.bat文件: set LOCALPATH= for %%l IN (libs/*.jar) DO call setlocalpath %%l java -cp "bin;%LOCALPATH%" Foo |
在libs下有commons-logging.jar文件。
执行命令 ./run.bat 运行代码
5.4.4 运行结果与分析
下面是代码运行结果:
1 2003-6-5 20:20:27 Foo set_name 2 警告: name's length must be more than 12. so my name is not changed! 3 java.lang.Throwable: invalid name setting(Jacky Bush) 4 at Foo.set_name(Foo.java:76) 5 at Foo.main(Foo.java:23) 6 2003-6-5 20:20:27 Foo set_length 7 严重: length must be more than zero! this changing is ingored! 8 java.lang.Throwable: invalid length setting(-1) 9 at Foo.set_length(Foo.java:55) 10 at Foo.main(Foo.java:24) 11 2003-6-5 20:20:27 Foo set_name 12 信息: oww! my name is changed into Jacky Bush junior! 13 2003-6-5 20:20:27 Foo main 14 信息: the log is instance of org.apache.commons.logging.impl.Jdk14Logger |
为了分析方便,我在每行前加入了行号。日志操作一使用debug级别,但是被封装的日志系统缺省的日志级别比debug高,所以没有输出信息。1-5行为日志操作二的输出信息;6-10行为日志操作三的输出信息;11-13行为日志操作四的输出信息;14行为日志操作五的输出信息,表明被封装的具体日志系统为jdk14,这是由前面所说的搜索策略决定的。
commons-logging的主题在于日志设施,要达到"设施"要求除了有良好的使用界面外,还必须有优秀的内部设计。整个commons-logging.jar中主要有6个类,类关系如《Commons-logging设计》图所示,图中灰色部分表示外部类。
Log接口代表应用编程接口;LogFactory体现配置界面,它读取配置项并实现搜索策略。
commons-logging是个日志设施通用实现,虽然提供了对应用编程接口的缺省实现(SimpleLog),但是主要意图还是希望封装强大的日志系统。明白了这一点,我们就面临这样的场景:一边有现成的日志系统,如Log4j,Jdk14;另一边有易用的使用界面。我们需要一种设计能使这两边协调工作,设计模式-适配模式是我们的理想选择。为了加强本文的可读性,我在下表引用了适配模式的描述:
名称 | Adapter |
结构 | |
意图 | 将一个类的接口转换成客户希望的另外一个接口。A d a p t e r 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 |
下面是《Commons-logging设计》图中各实体与上表适配模式中各元素的对照表:
序号 | 类图中实体 | 模式中元素 |
1 | Log | Target |
2 | Jdk14Logger | Adapter |
3 | Log4jLogger | Adapter |
4 | Logger (java.util.logging) | Adaptee |
5 | Logger (org.apache.log4j) | Adaptee |
《Commons-logging设计》图中两次利用了适配模式:第一次是由上表中的(1/2/4) 组成的通用实现和Jdk14日志的适配,第二次是由上表中的(1/3/5) 组成的通用实现和Log4j日志的适配。
既然commons-logging是一个通用接口,它的实现就不能和某个具体的日志系统绑死。我们需要一种能在代码外实现这种绑定的设计。一般地我们用gang of four creational模式类中的一种模式来创建实现某个接口的类的实例,commons-logging采用了工厂方法模式来选择具体的日志实现。下表引入工厂方法模式描述。
名称 | Factory Method |
结构 | |
意图 | 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。 |
下面是《Commons-logging设计》图中各实体与上表工厂方法模式中各元素的对照表:
序号 | 类图中实体 | 模式中元素 |
1 | Log | Product |
2 | Jdk14Logger | ConcreteProduct |
3 | Log4jLogger | ConcreteProduct |
4 | SimpleLog | ConcreteProduct |
5 | LogFactory | Creator |
6 | LogFactoryImpl | ConcreteCreator |
LogFactory是个抽象类,除了提供创建日志操作接口Log的实例外,还负责搜索扩展LogFactory 的类,比如LogFactoryImpl,后者负责Log的实例创建。
LogFactoryImpl扩展LogFactory,除了创建Log的实例外,主要用于搜索实现日志操作接口Log的类,比如Log4jLogger。
所以我们就有两个搜索策略:LogFactory的具体类搜索策略和Log的具体类搜索策略。就是这两个搜索策略提供了整个体系的灵活性和可扩展性。
下面的代码是LogFactory的类方法:
public static Log getLog(Class clazz) throws LogConfigurationException { return (getFactory().getInstance(clazz)); } |
为了返回实现Log的具体类,getLog 方法首先调用getFactory方法获得LogFactory的具体类的实例,接着调用这个实例的getInstance方法。getFactory方法实现LogFactory的具体类搜索策略;getInstance方法实现Log的具体类搜索策略。
6.2.1 LogFactory的具体类搜索策略
LogFactory会按下面的顺序搜索实现LogFactory的具体类的类名:
- 利用系统属性。如果运行软件系统时设置了虚拟机的系统属性org.apache.commons.logging.LogFactory,则返回这个属性的值作为实现LogFactory的具体类的类名。
- JDK1.3 jar 服务提供者发现机制。如果classloader在classpath中能找到META-INF/services/org.apache.commons.logging.LogFactory文件,从这个文件中的第一行读出的值就为实现LogFactory的具体类的类名。
- 属性配置文件。如果classloader在classpath中能找到commons-logging.properties文件,这个属性文件中的org.apache.commons.logging.LogFactory属性值就为实现LogFactory的具体类的类名。
- 最后底牌LogFactoryImpl。如果上面的方法都找不到实现LogFactory的具体类的类名,返回Commons-logging自己提供的类:org.apache.commons.logging.impl.LogFactoryImpl。
6.2.2 Log的具体类搜索策略
LogFactoryImpl会按下面的顺序搜索实现Log的具体类的类名:
- 属性配置文件。如果classloader在classpath中能找到commons-logging.properties文件,这个属性文件中的org.apache.commons.logging.Log属性值就为实现Log的具体类的类名。
- 利用系统属性。如果运行软件系统时设置了虚拟机的系统属性org.apache.commons.logging.Log,则返回这个属性的值作为实现Log的具体类的类名。
- 检查Log4J。如果在classpath中能找到org.apache.log4j.Logger和org.apache.commons.logging.impl.Log4JLogger,返回org.apache.commons.logging.impl. Log4JLogger作为实现Log的具体类的类名。
- 检查Jdk14。如果在classpath中能找到java.util.logging.Logger和org.apache.commons.logging.impl.Jdk14Logger,返回org.apache.commons.logging.impl.Jdk14Logger作为实现Log的具体类的类名。
- 最后底牌SimpleLog。如果上面的方法都找不到实现Log的具体类的类名,返回Commons-logging自己提供的类:org.apache.commons.logging.impl.SimpleLog";
commons-logging是一个通用接口,具有非常简单的使用界面,这是优点,但是缺少对日志目标设备定义、日志格式支持和完备的级别支持能力。需要这些日至设施特性的软件系统必须寻求别的日志设施,比如log4j,jdk14log等。
commons-logging可以根据代码以外的配置界面获得灵活性,但是缺少(至少可以说没有完善的)程序运行时动态配置界面。
commons-logging是一套轻量级、易使用的日志设施,它可以和复杂的日志设施一起使用,例如log4j,jdk14log等。commons-logging除了是一个封套、提供一个简单的使用界面外,还附带了简单的实现-SimpleLog。SimpleLog把所有符合级别的日志信息简单地输出到System.err.日志目标上,它是commons-logging搜索策略的底牌,这样使得即使在找不到log4j和jdk14log的软件系统运行环境中,commons-logging日志设施也能正常工作。如果要完整功能的日志设施,只使用commons-logging不能满足要求。这种情况下软件系统可以同时使用commons-logging和别的日志设施,但这样就得对付两套使用界面,所以对于有强大日志需求的软件系统来说,在目前的commons-logging现状下最好单独使用log4j或jdk14log。
commons-logging内部设计中值得一提的是实现与接口的绑定设计,它利用工厂方法模式创建接口实例,同时使用了两种策略分别寻找具体工厂类和日志操作接口实现类。对于两种策略的实现它使用了类继承的方式,读者自己可以使用重构技术把它实现为gang of four的strategy模式。