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 日誌處理流程
#圖 1 展示了一個 LogRecord 的處理流程。一個日誌進入處理流程首先是 Logger,其中定義了可通過的 Level,如果 LogRecord 的 Level 高於Logger 的等級,則進入 Filter(如果有)過濾。如果沒有定義 Level,則使用父親 Logger 的 Level。 Handler 中過程類似,其中 Handler 也定義了可通過 Level,然後進行 Filter 過濾,透過如果後面還有其他 Handler,則直接交由後面的 Handler 進行處理,否則會直接綁定到 formatter 上面輸出到指定位置。
在實作日誌快取之前,先對 Filter 和 Formatter 兩個輔助類別進行介紹。
Filter
Filter 是一個接口,主要是對 LogRecord 進行過濾,控制是否對 LogRecord 進行進一步處理,其可以綁定在 Logger 下或 Handler 下。
只要在boolean isLoggable(LogRecord)方法中加上過濾邏輯就可以實現對logrecord 進行控制,如果只想對發生了Exception 的那些log 記錄進行記錄,那麼可以透過清單1 來實現,當然首先需要將該Filter 透過呼叫setFilter(Filter)方法或設定檔方式綁定到對應的Logger 或Handler。
清單1. 一個Filter 實例的實作
Override public boolean isLoggable(LogRecord record){ if(record.getThrown()!=null){ return true; }else{ return false; } }
Formatter
Formatter 主要是對Handler 在輸出log 記錄的格式進行控制,例如輸出日期的格式,輸出為HTML 還是XML 格式,文字參數替換等。 Formatter 可以綁定到Handler 上,Handler 會自動呼叫Formatter 的String format(LogRecord r) 方法對日誌記錄進行格式化,該方法具有預設的實現,如果想實現自訂格式可以繼承Formater 類別並重寫該方法,預設情況下例如清單2 在經過Formatter 格式化後,會將{0} 和{1} 替換成對應的參數。
清單2. 記錄一筆log
logger.log(Level.WARNING,"this log is for test1: {0} and test2:{1}", new Object[]{newTest1(), new Test2()});
MemoryHandler
MemoryHandler 是Java Logging 中兩大類別Handler 之一,另一類是 StreamHandler,二者直接繼承於Handler,代表了兩種不同的設計想法。 Java Logging Handler 是抽象類別,需要根據使用場景創建特定 Handler,實現各自的 publish、flush 以及 close 等方法。
MemoryHandler 使用了典型的「註冊 – 通知」的觀察者模式。 MemoryHandler 先註冊到對自己感興趣的Logger 中(logger.addHandler(handler)),在這些Logger 呼叫發布日誌的 API:log()、logp()、logrb() 等,遍歷這些Logger 呼叫發布日誌的 API:log()、logp()、logrb() 等,遍歷這些Logger 下綁定的所有Handlers 時,通知觸發自身 publish(LogRecord)方法的調用,將日誌寫入buffer,當轉儲到下一個日誌發布平台的條件成立,轉儲日誌並清空buffer。
這裡的 buffer 是 MemoryHandler 本身維護一個可自訂大小的循環緩衝佇列,來保存所有執行時間觸發的 Exception 日誌條目。同時在建構函式中要求指定一個Target Handler,用於承接輸出;在滿足特定flush buffer 的條件下,如日誌條目等級高於MemoryHandler 設定的push level 等級(實例中定義為 SEVERE)等,將日誌移交至下一步輸出平台。從而形成如下日誌轉儲輸出鏈:
圖 2. Log 轉儲鏈
在实例中,通过对 MemoryHandler 配置项 .push 的 Level 进行判断,决定是否将日志推向下一个 Handler,通常在 publish() 方法内实现。代码清单如下:
清单 3
// 只纪录有异常并且高于 pushLevel 的 logRecord final Level level = record.getLevel(); final Throwable thrown = record.getThrown(); If(level >= pushLevel){ push(); }
MemoryHandler.push 方法的触发条件
Push 方法会导致 MemoryHandler 转储日志到下一 handler,清空 buffer。触发条件可以是但不局限于以下几种,实例中使用的是默认的***种:
日志条目的 Level 大于或等于当前 MemoryHandler 中默认定义或用户配置的 pushLevel;
外部程序调用 MemoryHandler 的 push 方法;
MemoryHandler 子类可以重载 log 方法或自定义触发方法,在方法中逐一扫描日志条目,满足自定义规则则触发转储日志和清空 buffer 的操作。MemoryHanadler 的可配置属性
表 1.MemoryHandler 可配置属性
属性名 | 描述 | 缺省值 | |
---|---|---|---|
继承属性 | MemoryHandler.level | MemoryHandler 接受的输入到 buffer 的日志等级 | Level.INFO |
MemoryHandler.filter | 在输入到 buffer 之前,可在 filter 中自定义除日志等级外的其他过滤条件 | (Undefined) | |
MemoryHandler.formatter | 指定输入至 buffer 的日志格式 | (Undefined) | |
MemoryHandler.encoding | 指定输入至 buffer 的日志编码,在 MemoryHandler 中应用甚少 | (Undefined) | |
私有属性 | MemoryHandler.size | 以日志条目为单位定义循环 buffer 的大小 | 1,000 |
MemoryHandler.push | 定义将 buffer 中的日志条目发送至下一个 Handler 的*** Level(包含) | Level.SEVERE | |
MemoryHandler.target | 在构造函数中指定下一步承接日志的 Handler | (Undefined) |
使用方式:
以上是记录产品 Exception 错误日志,以及如何转储的 MemoryHandler 处理的内部细节;接下来给出 MemoryHandler 的一些使用方式。
1. 直接使用 java.util.logging 中的 MemoryHandler
清单4
// 在 buffer 中维护 5 条日志信息 // 仅记录 Level 大于等于 Warning 的日志条目并 // 刷新 buffer 中的日志条目到 fileHandler 中处理 int bufferSize = 5; f = new FileHandler("testMemoryHandler.log"); m = new MemoryHandler(f, bufferSize, Level.WARNING); … myLogger = Logger.getLogger("com.ibm.test"); myLogger.addHandler(m); myLogger.log(Level.WARNING, “this is a WARNING log”);
. 自定义
1)反射
思考自定义 MyHandler 继承自 MemoryHandler 的场景,由于无法直接使用作为父类私有属性的 size、buffer 及 buffer 中的 cursor,如果在 MyHandler 中有获取和改变这些属性的需求,一个途径是使用反射。清单 5 展示了使用反射读取用户配置并设置私有属性。
清单5
int m_size; String sizeString = manager.getProperty(loggerName + ".size"); if (null != sizeString) { try { m_size = Integer.parseInt(sizeString); if (m_size <p><strong>2)重写</strong></p><p>直接使用反射方便快捷,适用于对父类私有属性无频繁访问的场景。思考这样一种场景,默认环形队列无法满足我们存储需求,此时不妨令自定义的 MyMemoryHandler 直接继承 Handler,直接对存储结构进行操作,可以通过清单 6 实现。</p><p><strong>清单 6</strong></p><pre class="brush:php;toolbar:false">public class MyMemoryHandler extends Handler{ // 默认存储 LogRecord 的缓冲区容量 private static final int DEFAULT_SIZE = 1000; // 设置缓冲区大小 private int size = DEFAULT_SIZE; // 设置缓冲区 private LogRecord[] buffer; // 参考 java.util.logging.MemoryHandler 实现其它部分 ... }
使用 MemoryHandler 时需关注的几个问题
了解了使用 MemoryHandler 实现的 Java 日志缓冲机制的内部细节和外部应用之后,来着眼于两处具体实现过程中遇到的问题:Logger/Handler/LogRecord Level 的传递影响,以及如何在开发 MemoryHandler 过程中处理错误日志。
1. Level 的传递影响
Java.util.logging 中有三种类型的 Level,分别是 Logger 的 Level,Handler 的 Level 和 LogRecord 的 Level. 前两者可以通过配置文件设置。之后将日志的 Level 分别与 Logger 和 Handler 的 Level 进行比较,过滤无须记录的日志。在使用 Java Log 时需关注 Level 之间相互影响的问题,尤其在遍历 Logger 绑定了多个 Handlers 时。如图 3 所示:
图 3. Java Log 中 Level 的传递影响
Java.util.logging.Logger 提供的 setUseParentHandlers 方法,也可能会影响到最终输出终端的日志显示。这个方法允许用户将自身的日志条目打印一份到 Parent Logger 的输出终端中。缺省会打印到 Parent Logger 终端。此时,如果 Parent Logger Level 相关的设置与自身 Logger 不同,则打印到 Parent Logger 和自身中的日志条目也会有所不同。如图 4 所示:
图 4. 子类日志需打印到父类输出终端
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 文件是 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
从 清单 8 中可以看出 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() 方法来执行具体逻辑,参见 清单 11;***调用 reset() 方法对相关属性保存以及清空,如 清单 12 所示。
清单 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); } } }</string>
代码中 oldProps、newProps 以及 changedProps 都是 HashMap类型,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(); changedProps.clear(); }
eset() 方法主要是用来重置各个属性,以便下一次使用。
当然如果只写一个 PropertyChangeListener 还不能发挥应有的功能,还需要将这个 PropertyChangeListener 实例注册到 LogManager 中,可以通过清单 13 实现。
清单 13. 注册 PropertyChangeListener
// 为'logging.properties'文件注册监听器 LogPropertyListener listener= new LogPropertyListener(); LogManager.getLogManager().addPropertyChangeListener(listener);
在 清单 8中有一些自定义的条目,比如 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 更改了 清单 13中相应代码实现这一功能。
清单 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)方法。
以上是Java如何實作日誌快取機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!