您的位置:首页 > 博客中心 > 编程语言 >

Java 日志缓存机制的实现--转载

时间:2022-03-23 23:18

概述

日志技术为产品的质量和服务提供了重要的支撑。JDK 在 1.4 版本以后加入了日志机制,为 Java 开发人员提供了便利。但这种日志机制是基于静态日志级别的,也就是在程序运行前就需设定下来要打印的日志级别,这样就会带来一些不便。

在 JDK 提供的日志功能中,日志级别被细化为 9 级,用以区分不同日志的用途,用来记录一个错误,或者记录正常运行的信息,又或是记录详细的调试信息。由于日志级别是静态的,如果日志级别设定过高,低级别的日志难以打印出来,从而导致在错误发生时候,难以去追踪错误的发生原因,目前常常采用的方式是在错误发生的时候,不得不先调整日志级别到相对低的程度,然后再去触发错误,使得问题根源得到显现。但是这种发生问题需要改动产品配置,然后重新触发问题进行调试的方式使得产品用户体验变差,而且有些问题会因为偶发性,环境很复杂等原因很难重新触发。

相反,如果起初就把日志级别调整到比较低,那么日志中间会有大量无用信息,而且当产品比较复杂的时候,会导致产生的日志文件很大,刷新很快,无法及时的记录有效的信息,甚至成为性能瓶颈,从而降低了日志功能对产品的帮助。

本文借助 Java Logging 中的 MemoryHandler 类将所有级别日志缓存起来,在适当时刻输出,来解决这个问题。主要围绕 MemoryHandler 的定义和 logging.properties 文件的处理而展开。

实例依附的场景如下,设想用户需要在产品发生严重错误时,查看先前发生的包含 Exception 的错误信息,以此作为诊断问题缘由的依据。使用 Java 缓冲机制作出的一个解决方案是,将所有产品运行过程中产生的包含 Exception 的日志条目保存在一个可设定大小的循环缓冲队列中,当严重错误(SEVERE)发生时,将缓冲队列中的日志输出到指定平台,供用户查阅。

 

Java 日志机制的介绍

Java 日志机制在很多文章中都有介绍,为了便于后面文章部分的理解,在这里再简单介绍一下本文用到的一些关键字。

Level:JDK 中定义了 Off、Severe、Warning、Info、Config、Fine、Finer、Finest、All 九个日志级别,定义 Off 为日志最高等级,All 为最低等级。每条日志必须对应一个级别。级别的定义主要用来对日志的严重程度进行分类,同时可以用于控制日志是否输出。

LogRecord:每一条日志会被记录为一条 LogRecord, 其中存储了类名、方法名、线程 ID、打印的消息等等一些信息。

Logger:日志结构的基本单元。Logger 是以树形结构存储在内存中的,根节点为 root。com.test(如果存在)一定是 com.test.demo(如果存在)的父节点,即前缀匹配的已存在的 logger 一定是这个 logger 的父节点。这种父子关系的定义,可以为用户提供更为自由的控制粒度。因为子节点中如果没有定义处理规则,如级别 handler、formatter 等,那么默认就会使用父节点中的这些处理规则。

Handler:用来处理 LogRecord,默认 Handler 是可以连接成一个链状,依次对 LogRecord 进行处理。

Filter:日志过滤器。在 JDK 中,没有实现。

Formatter:它主要用于定义一个 LogRecord 的输出格式。

图 1. Java 日志处理流程

gxlsystem.com,布布扣

2. 开发 log 接口过程中处理错误日志

在开发 log 相关接口中调用自身接口打印 log,可能会陷入无限循环。Java.util.logging 中考虑到这类问题,提供了一个 ErrorManager 接口,供 Handler 在记录日志期间报告任何错误,而非直接抛出异常或调用自身的 log 相关接口记录错误或异常。Handler 需实现 setErrorManager() 方法,该方法为此应用程序构造 java.util.logging.ErrorManager 对象,并在错误发生时,通过 reportError 方法调用 ErrorManager 的 error 方法,缺省将错误输出到标准错误流,或依据 Handler 中自定义的实现处理错误流。关闭错误流时,使用 Logger.removeHandler 移除此 Handler 实例。

两种经典使用场景,一种是自定义 MyErrorManager,实现父类相关接口,在记录日志的程序中调用 MyHandler.setErrorManager(new MyEroorManager()); 另一种是在 Handler 中自定义 ErrorManager 相关方法,示例如清单 7:

清单 7
 public class MyHandler extends Handler{ 
 // 在构造方法中实现 setErrorManager 方法
 public MyHandler(){ 
	......
	 setErrorManager (new ErrorManager() { 
		 public void  error (String msg, Exception ex, int code) { 
			 System.err.println("Error reported by MyHandler "
							  + msg + ex.getMessage()); 
		 } 
	 }); 
 } 
 public void publish(LogRecord record){ 
	 if (!isLoggable(record)) return; 
	 try { 
		 // 一些可能会抛出异常的操作
	 } catch(Exception e) { 
		 reportError ("Error occurs in publish ", e, ErrorManager.WRITE_FAILURE); 
	 } 
 } 
 ......
 }
 

logging.properties

logging.properties 文件是 Java 日志的配置文件,每一行以“key=value"的形式描述,可以配置日志的全局信息和特定日志配置信息,清单 8 是我们为测试代码配置的 logging.properties。

清单 8. logging.properties 文件示例
 #Level 等级 OFF > SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST > ALL 
 # 为 FileHandler 指定日志级别
 java.util.logging.FileHandler.level=WARNING 
 # 为 FileHandler 指定 formatter 
 java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter 
 # 为自定义的 TestMemoryHandler 指定日志级别
 com.ibm.test.MemoryHandler.level=INFO 
 # 设置 TestMemoryHandler 最多记录日志条数
 com.ibm.test.TestMemoryHandler.size=1000 
 # 设置 TestMemoryHandler 的自定义域 useParentLevel 
 com.ibm.test.TestMemoryHandler.useParentLevel=WARNING 
 # 设置特定 log 的 handler 为 TestMemoryHandler 
 com.ibm.test.handlers=com.ibm.test.TestMemoryHandler 
 # 指定全局的 Handler 为 FileHandler 
 handlers=java.util.logging.FileHandler

从  中可以看出 logging.properties 文件主要是用来给 logger 指定等级(level),配置 handler 和 formatter 信息。

如何监听 logging.properties

如果一个系统对安全性要求比较高,例如系统需要对更改 logging.properties 文件进行日志记录,记录何时何人更改了哪些记录,那么应该怎么做呢?

这里可以利用 JDK 提供的 PropertyChangeListener 来监听 logging.properties 文件属性的改变。

例如创建一个 LogPropertyListener 类,其实现了 java.benas.PropertyChangeListener 接口,PropertyChangeListener 接口中只包含一个 propertyChange(PropertyChangeEvent)方法,该方法的实现如清 9 所示。

清单 9. propertyChange 方法的实现
 @Override 
 public void propertyChange(PropertyChangeEvent event) { 
    if (event.getSource() instanceof LogManager){ 
        LogManager manager=(LogManager)event.getSource(); 
        update(manager); 
        execute(); 
        reset(); 
    } 
 }

propertyChange(PropertyChangeEvent)方法中首先调用 update(LogManager)方法来找出 logging.properties 文件中更改的,增加的以及删除的项,这部分代码如清单 10 所示;然后调用 execute() 方法来执行具体逻辑,参见 ;最后调用 reset() 方法对相关属性保存以及清空,如  所示。

清单 10. 监听改变的条目
 public void update(LogManager manager){ 
  Properties logProps = null ; 
   // 使用 Java 反射机制获取私有属性
    try { 
      Field f = manager.getClass().getDeclaredField("props"); 
      f.setAccessible(true ); 
      logProps=(Properties)f.get(manager); 
	 }catch (Exception e){ 
	    logger.log(Level.SEVERE,"Get private field error.", e); 
		 return ; 
    } 
    Set<String> logPropsName=logProps.stringPropertyNames(); 
     for (String logPropName:logPropsName){ 
         String newVal=logProps.getProperty(logPropName).trim(); 
        // 记录当前的属性
        newProps.put(logPropName, newVal);   
        // 如果给属性上次已经记录过
        if (oldProps.containsKey(logPropName)){ 
			 String oldVal = oldProps.get(logPropName); 
			 if (newVal== null ?oldVal== null :newVal.equals(oldVal)){ 
            // 属性值没有改变,不做任何操作
		 }else { 
             changedProps.put(logPropName, newVal); 
        } 
        oldProps.remove(logPropName); 
    }else {// 如果上次没有记录过该属性,则其应为新加的属性,记录之
         changedProps.put(logPropName, newVal);               
        } 
     } 
 }

代码中 oldProps、newProps 以及 changedProps 都是 HashMap<String,String> 类型,oldProps 存储修改前 logging.properties 文件内容,newProps 存储修改后 logging.properties 内容,changedProps 主要用来存储增加的或者是修改的部分。

方法首先通过 Java 的反射机制获得 LogManager 中的私有属性 props(存储了 logging.properties 文件中的属性信息),然后通过与 oldProps 比较可以得到增加的以及修改的属性信息,最后 oldProps 中剩下的就是删除的信息了。

清单 11. 具体处理逻辑方法
 private void execute(){ 
  // 处理删除的属性
  for (String prop:oldProps.keySet()){ 
    // 这里可以加入其它处理步骤
    logger.info("‘"+prop+"="+oldProps.get(prop)+"‘has been removed"); 			
  } 
  // 处理改变或者新加的属性
  for (String prop:changedProps.keySet()){ 
      // 这里可以加入其它处理步骤
	  logger.info("‘"+prop+"="+oldProps.get(prop)+"‘has been changed or added"); 
  } 
 }

该方法是主要的处理逻辑,对修改或者删除的属性进行相应的处理,比如记录属性更改日志等。这里也可以获取当前系统的登录者,和当前时间,这样便可以详细记录何人何时更改过哪个日志条目。

清单 12. 重置所有数据结构
private void reset(){ 
oldProps = newProps; 
newProps= new HashMap< String,String>(); 
changedProps.clear(); 
}

reset() 方法主要是用来重置各个属性,以便下一次使用。

当然如果只写一个 PropertyChangeListener 还不能发挥应有的功能,还需要将这个 PropertyChangeListener 实例注册到 LogManager 中,可以通过清单 13 实现。

清单 13. 注册 PropertyChangeListener
 // 为‘logging.properties‘文件注册监听器
 LogPropertyListener listener= new LogPropertyListener(); 
 LogManager.getLogManager().addPropertyChangeListener(listener);

如何实现自定义标签

在 中有一些自定义的条目,比如 com.ibm.test.TestMemoryHandler。

useParentLever=WARNING",表示如果日志等级超过 useParentLever 所定义的等级 WARNING 时,该条日志在 TestMemoryHandler 处理后需要传递到对应 Log 的父 Log 的 Handler 进行处理(例如将发生了 WARNING 及以上等级的日志上下文缓存信息打印到文件中),否则不传递到父 Log 的 Handler 进行处理,这种情况下如果不做任何处理,Java 原有的 Log 机制是不支持这种定义的。那么如何使得 Java Log 支持这种自定义标签呢?这里可以使用 PropertyListener 对自定义标签进行处理来使得 Java Log 支持这种自定义标签,例如对“useParentLever”进行处理可以通过清单 14 实现。

清单 14
	 private void execute(){ 
        // 处理删除的属性
         for (String prop:oldProps.keySet()){ 
             if (prop.endsWith(".useParentLevel")){ 
                String logName=prop.substring(0, prop.lastIndexOf(".")); 
                Logger log=Logger.getLogger(logName); 
                 for (Handler handler:log.getHandlers()){ 
                     if (handler  instanceof TestMemoryHandler){ 
                        ((TestMemoryHandler)handler) 
                            .setUseParentLevel(oldProps.get(prop)); 
                         break ; 
                    } 
                } 
            } 
        } 
        // 处理改变或者新加的属性
         for (String prop:changedProps.keySet()){ 
             if (prop.endsWith(".useParentLevel")){ 
                // 在这里添加逻辑处理步骤
            } 
        } 
 }

在清单 14 处理之后,就可以在自定义的 TestMemoryHandler 中进行判断了,对 log 的等级与其域 useParentLevel 进行比较,决定是否传递到父 Log 的 Handler 进行处理。在自定义 TestMemoryHandler 中保存对应的 Log 信息可以很容易的实现将信息传递到父 Log 的 Handler,而保存对应 Log 信息又可以通过 PropertyListener 来实现,例如清单 15 更改了 中相应代码实现这一功能。

清单 15
 if (handler  instanceof TestMemoryHandler){ 
     ((TestMemoryHandler)handler).setUseParentLevel(oldProps.get(prop)); 
     ((TestMemoryHandler)handler).addLogger(log); 
       break ; 
 }

具体如何处理自定义标签的值那就看程序的需要了,通过这种方法就可以很容易在 logging.properties 添加自定义的标签了。

自定义读取配置文件

如果 logging.properties 文件更改了,需要通过调用 readConfiguration(InputStream)方法使更改生效,但是从 JDK 的源码中可以看到 readConfiguration(InputStream)方法会重置整个 Log 系统,也就是说会把所有的 log 的等级恢复为默认值,将所有 log 的 handler 置为 null 等,这样所有存储的信息就会丢失。

比如,TestMemoryHandler 缓存了 1000 条 logRecord,现在用户更改了 logging.properties 文件,并且调用了 readConfiguration(InputStream) 方法来使之生效,那么由于 JDK 本身的 Log 机制,更改后对应 log 的 TestMemoryHandler 就是新创建的,那么原来存储的 1000 条 logRecord 的 TestMemoryHandler 实例就会丢失。

那么这个问题应该如何解决呢?这里给出三种思路:

1). 由于每个 Handler 都有一个 close() 方法(任何继承于 Handler 的类都需要实现该方法),Java Log 机制在将 handler 置为 null 之前会调用对应 handler 的 close() 方法,那么就可以在 handler(例如 TestMemoryHandler)的 close() 方法中保存下相应的信息。

2). 研究 readConfiguration(InputStream)方法,写一个替代的方法,然后每次调用替代的方法。

3). 继承 LogManager 类,覆盖 readConfiguration(InputStream)方法。

这里第一种方法是保存原有的信息,然后进行恢复,但是这种方法不是很实用和高效;第二和第三种方法其实是一样的,都是写一个替代的方法,例如可以在替代的方法中对 Handler 为 TestMemoryHandler 的不置为 null,然后在读取 logging.properties 文件时发现为 TestMemoryHandler 属性时,找到对应 TestMemoryHandler 的实例,并更改相应的属性值(这个在清单 14 中有所体现),其他不属于 TestMemoryHandler 属性值的可以按照 JDK 原有的处理逻辑进行处理,比如设置 log 的 level 等。

另一方面,由于 JDK1.6 及之前版本不支持文件修改监听功能,每次修改了 logging.properties 文件后需要显式调用 readConfiguration(InputStream)才能使得修改生效,但是自 JDK1.7 开始已经支持对文件修改监听功能了,主要是在 java.nio.file.* 包中提供了相关的 API,这里不再详述。

那么在 JDK1.7 之前,可以使用 apache 的 commons-io 库中的 FileMonitor 类,在此也不再详述。

 

总结

通过对 MemoryHandler 和 logging.properties 进行定义,可以通过 Java 日志实现自定义日志缓存,从而提高 Java 日志的可用性,为产品质量提供更强有力的支持。

原文:http://www.ibm.com/developerworks/cn/java/j-lo-logbuffer/

热门排行

今日推荐

热门手游