在使用Commons Logging时,经常在服务器部署中会遇到ClassLoader的问题,这也是经常被很多人所诟病的地方,特别是在和Log4J一起使用的时候。常见的如,由于Common Logging使用非常广泛,因而很多Web容器(WebSphere)在内也会使用它作为日志处理系统而将其jar包引入到容器本身中,此时LogFactory是使用Web容器本身的ClassLoader装载的,即使Log4J中使用了ContextClassLoader来查找配置文件,此时的Thread依然在容器中,因而它使用的ClassLoader还是容器本身的ClassLoader实例,此时需要把Log4J的配置文件放到共享目录下,该配置文件才能被正常识别(以我的理解,容器在启动的时候,它根本无法获得Web应用程序中的jar包,所以也需要将Log4J的jar包放到共享目录中才可以,不过我木有用过WebSphere,也没法测试,所以只能猜测~)。在WebSphere还可以通过设置类的加载顺序为PARENT_LAST的方法来解决。而在Jboss中则只能将自己的配置加到其conf下的Log4J配置文件中,因为Jboss默认导入Log4J包。具体可以参考我转载的一篇文章,Log4j/common log和各种服务器集成的问题(木有经验,只能用别人的文章了。。。还很可惜的木有机会测试。。。):http://www.blogjava.net/DLevin/archive/2012/11/02/390639.html,另外还找到一篇更加详细的描述Commons Logging中存在的ClassLoader问题的文章:http://articles.qos.ch/classloader.html
Commons Logging的具体实现:
在使用Commons Logging时,一般是通过LogFactory获取Log实例,然后调用Log接口中相应的方法。因而Commons Logging的实现可以分成以下几个步骤:
1. LogFactory类初始化
a. 缓存加载LogFactory的ClassLoader(thisClassLoader字段),出于性能考虑。因为getClassLoader()方法可能会使用AccessController(虽然目前并没有使用),因而缓存起来以提升性能。
b. 初始化诊断流。读取系统属性org.apache.commons.logging.diagnostics.dest,若该属性的值为STDOUT、STDERR、文件名。则初始化诊断流字段(diagnosticStream),并初始化诊断消息的前缀(diagnosticPrefix),其格式为:”[LogFactory from <ClassLoaderName@HashCode>] “, 该前缀用于处理在同一个应用程序中可能会有多个ClassLoader加载LogFactory实例的问题。
c. 如果配置了诊断流,则打印当前环境信息:java.ext.dir、java.class.path、ClassLoader以及ClassLoader层级关系信息。
d. 初始化factories实例(Hashtable),用于缓存LogFactory(context-classloader –-> LogFactory instance)。如果系统属性org.apache.commons.logging.LogFactory.HashtableImpl存在,则使用该属性定义的Class作为factories Hashtable的实现类,否则,使用Common Logging实现的WeakHashtable。若初始化没有成功,则使用Hashtable类本身。使用WeakHashtable是为了处理在webapp中,当webapp被卸载是引起的内存泄露问题,即当webapp被卸载时,其ClassLoader的引用还存在,该ClassLoader不会被回收而引起内存泄露。因而当不支持WeakHashtable时,需要卸载webapp时,调用LogFactory.relase()方法。
e. 最后,如果需要打印诊断信息,则打印“BOOTSTRAP COMPLETED”信息
2. 查找LogFactory类实现,并实例化。
当调用LogFactory.getLog()方法时,它首先会创建LogFactory实例(getFactory()),然后创建相应的Log实例。getFactory()方法不支持线程同步,因而多个线程可能会创建多个相同的LogFactory实例,由于创建多个LogFactory实例对系统并没有影响,因而可以不用实现同步机制。
a. 获取context-classloader实例。
b. 从factories Hashtable(缓存)中获取LogFactory实例。
c. 读取commons-logging.properties配置文件(如果存在的话,如果存在多个,则可以定义priority属性值,取所有commons-logging.properties文件中priority数值最大的文件),如果设置use_tccl属性为false,则在类的加载过程中使用初始化cache的thisClassLoader字段,而不用context ClassLoader。
d. 查找系统属性中是否存在org.apache.commons.logging.LogFactory值,若有,则使用该值作为LogFactory的实现类,并实例化该LogFactory实例。
e. 使用service provider方法查找LogFactory的实现类,并实例化。对应Service ID是:META-INF/services/org.apache.commons.logging.LogFactory
f. 查找commons-logging.properties文件中是否定义了LogFactory的实现类:org.apache.commons.logging.LogFactory,是则用该类实例化一个出LogFactory
g. 否则,使用默认的LogFactory实现:LogFactoryImpl类。
h. 缓存新创建的LogFactory实例,并将commons-logging.properties配置文件中所有的键值对加到LogFactory的属性集合中。
3. 通过LogFactory实例查找Log实例(LogFactoryImpl实现)
使用LogFactory实例调用getInstance()方法取得Log实例。
a. 如果缓存(instances字段,Hashtable)存在,则使用缓存中的值。
b. 查找用户自定义的Log实例,即从先从commons-logging.properties配置文件中配置的org.apache.commons.logging.Log(org.apache.commons.logging.log,旧版本)类,若不存在,查找系统属性中配置的org.apache.commons.logging.Log(org.apache.commons.logging.log,旧版本)类。如果找到,实例化Log实例
c. 遍历classesToDiscover数组,尝试创建该数组中定义的Log实例,并缓存Log类的Constructor实例,在下次创建Log实例是就不需要重新计算。在创建Log实例时,如果use_tccl属性设为false,则使用当前ClassLoader(加载当前LogFactory类的ClassLoader),否则尽量使用Context ClassLoader,一般来说Context ClassLoader和当前ClassLoader相同或者是当前ClassLoader的下层ClassLoader,然而在很多自定义ClassLoader系统中并没有设置正确的Context ClassLoader导致当前ClassLoader成了Context ClassLoader的下层,LogFactoryImpl默认处理这种情况,即使用当前ClassLoader。用户可以通过设置org.apache.commons.logging.Log.allowFlawedContext配置作为这个特性的开关。
d. 如果Log类定义setLogFactory()方法,则调用该方法,将当前LogFactory实例传入。
e. 将新创建的Log实例存入缓存中。
4. 调用Log实例中相应的方法
Log接口比较简单,并且Log4J、JDK相应的实现类也都直接代理给各自框架,因而实现比较简单,不在详述。关于SimpleLog,可以类似的参考http://www.blogjava.net/DLevin/archive/2012/06/12/380647.html。 不过还是有必要对Jdk14Logger的实现吐槽一下,每次调用log方法时都会创建新的Throwable实例,然后去计算ClassName和Method,这会引起严重的性能问题,因为创建一个Throwable实例,意味着需要停止当前的运行,dump出一个调用栈快照。它为什么不像Jdk13LumberjackLogger的实现一样,把这两个字段缓存起来呢??