Java ログ メカニズムの概要
レベル: JDK では、オフ、重大、警告、情報、構成、細かい、より細かい、最も細かい、すべての 9 つのログ レベルが定義されています。「オフ」を定義します。ログは *** レベル、All は *** レベルです。各ログはレベルに対応している必要があります。レベルの定義は主にログの重要度を分類するために使用されますが、ログを出力するかどうかを制御するためにも使用できます。
LogRecord: 各ログは LogRecord として記録され、クラス名、メソッド名、スレッド ID、出力メッセージなどの情報が保存されます。
ロガー: ログ構造の基本単位。ロガーは、ルート ノードをルートとするツリー構造でメモリに保存されます。 com.test (存在する場合) は、com.test.demo (存在する場合) の親ノードである必要があります。つまり、一致するプレフィックスを持つ既存のロガーがこのロガーの親ノードである必要があります。この親子関係を定義することで、ユーザーはより自由な制御の粒度を得ることができます。子ノードにレベル ハンドラー、フォーマッタなどの処理ルールが定義されていない場合、親ノードのこれらの処理ルールがデフォルトで使用されるためです。
ハンドラー: LogRecord の処理に使用されます。デフォルトのハンドラーをチェーンに接続して、LogRecord を順番に処理できます。
フィルター: ログフィルター。 JDK には実装がありません。
Formatter: 主に LogRecord の出力形式を定義するために使用されます。
#図 1. Java ログ処理フロー
図 1 は、LogRecord の処理フローを示しています。ログが処理フローに入ると、まず通過可能なレベルを定義する Logger に入り、LogRecord の Level が Logger の Level よりも高い場合は、フィルタリングのために Filter (存在する場合) に入ります。レベルが定義されていない場合は、親ロガーのレベルが使用されます。 Handler の処理も同様で、Handler は渡すことができるレベルを定義し、Filter フィルタリングを実行します。背後に他の Handler がある場合は、後続の Handler に直接処理が渡され、そうでない場合は直接処理されます。フォーマッタにバインドされ、指定された場所に出力されます。 ログ キャッシュを実装する前に、まず 2 つの補助クラス Filter と Formatter を紹介します。Filter
Filter は、主に LogRecord をフィルタリングし、LogRecord をさらに処理するかどうかを制御するために使用されるインターフェースであり、Logger または Handler の下にバインドできます。 ブール型 isLoggable (LogRecord) メソッドにフィルタリング ロジックを追加する限り、ログレコードを制御できます。例外が発生したログ レコードのみを記録したい場合は、リスト 1 を使用して実現できます。まず、setFilter(Filter) メソッドまたは構成ファイルを呼び出して、フィルターを対応するロガーまたはハンドラーにバインドする必要があります。リスト 1. フィルター インスタンスの実装
Override public boolean isLoggable(LogRecord record){ if(record.getThrown()!=null){ return true; }else{ return false; } }
フォーマッター
フォーマッターは主にハンドラーによって出力に記録されますlog 出力日付の形式、出力が HTML 形式か XML 形式か、テキスト パラメータの置換などの形式制御。 Formatter は Handler にバインドでき、Handler は自動的に Formatter の String format (LogRecord r) メソッドを呼び出してログ レコードをフォーマットします。このメソッドにはデフォルトの実装があります。カスタム フォーマットを実装する場合は、Formatter クラスを継承してこれをオーバーライドできます。たとえば、リスト 2 では、デフォルトでは、Formatter によるフォーマット後に、{0} と {1} が対応するパラメーターに置き換えられます。リスト 2. ログを記録する
logger.log(Level.WARNING,"this log is for test1: {0} and test2:{1}", new Object[]{newTest1(), new Test2()});
MemoryHandler
MemoryHandler は、Java の 2 つの主要なタイプのハンドラーのうちの 1 つです。ロギング: もう 1 つのタイプは StreamHandler で、どちらも Handler から直接継承し、2 つの異なる設計アイデアを表します。 Java ロギング ハンドラーは抽象クラスであり、それぞれのパブリッシュ、フラッシュ、クローズ、その他のメソッドを実装するには、使用シナリオに従って特定のハンドラーを作成する必要があります。 MemoryHandler は、典型的な「登録 – 通知」オブザーバー パターンを使用します。 MemoryHandler は、まず、関心のあるロガー (logger.addHandler(handler)) に登録し、これらのロガーでログを公開するための API (log()、logp()、logrb() など) を呼び出し、すべてのロガーを走査します。これらの Logger ハンドラの下にバインディングを作成すると、通知によって独自の pub(LogRecord) メソッドの呼び出しがトリガーされ、ログがバッファに書き込まれます。次のログ公開プラットフォームへのダンプ条件が満たされると、ログがダンプされ、バッファが書き込まれます。クリアされました。 ここでのバッファとは、MemoryHandler 自体がカスタマイズ可能な循環バッファ キューを維持し、実行時にトリガーされたすべての例外ログ エントリを保存することです。同時に、ログ エントリ レベルが MemoryHandler によって設定されたプッシュ レベル (この例では SEVERE と定義されている) よりも高いなど、特定のフラッシュ バッファ条件下では、出力を受信するためにコンストラクターでターゲット ハンドラーを指定する必要があります。 、などの場合、ログは引き継がれます。次の出力プラットフォームに移動します。これにより、次のようなログ ダンプ出力チェーンが生成されます。図 2. ログ ダンプ チェーン
在实例中,通过对 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 中国語 Web サイトの他の関連記事を参照してください。