Java の基礎となる JDK Logging モジュールを処理する方法

WBOY
リリース: 2023-05-10 14:55:06
転載
1315 人が閲覧しました

サンプルから始める

JDK ロギングの使用は非常に簡単です。次のコードに示すように、まず Logger クラスの静的メソッド getLogger を使用してロガーを取得し、その後、次のコードを使用できます。取得したロガーを任意の場所に配置し、ログを入力します。たとえば、logger.info("Main running.") のような呼び出しです。

package com.bes.logging;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggerTest {
      private static Loggerlogger = Logger.getLogger("com.bes.logging");
      public static void main(String argv[]) {
               // Log a FINEtracing message
               logger.info("Main running.");
               logger.fine("doingstuff");
               try {
                         Thread.currentThread().sleep(1000);// do some work
               } catch(Exception ex) {
                         logger.log(Level.WARNING,"trouble sneezing", ex);
               }
               logger.fine("done");
      }
}
ログイン後にコピー

コードの変更や JDK 構成の変更を行わずに上記の例を実行すると、ログ [Main running.] のみがコンソールに表示されることがわかります。次のような疑問が頭に浮かぶはずです...

1. [Main running.] 以外のログが出力されないのはなぜですか?それらを出現させるにはどうすればよいでしょうか?

2. ログ出力に表示される時間、クラス名、メソッド名などはどこにありますか?

3. ログがコンソールに表示されるのはなぜですか?

4. 大規模なシステムには多数のサブモジュール (単に多数のパッケージ名があると理解できます) が存在する場合がありますが、これらのサブモジュールのログ レベルを個別に制御するにはどうすればよいでしょうか?

5, 拡張: Apache の人気のある log4j プロジェクトは JDK ログに関連していますか? 独自の LoggerManager を実装するにはどうすればよいですか?

これらの質問により、JDK のロギング メカニズムについて学習することに興味があるかもしれません。この章では、この単純なモジュールのメカニズムを分析します。

用語の答え

詳細な分析を行う前に、次の用語をマスターする必要があります

logger: ロガーについては、次のことを理解しておく必要があります。側面

1, Logger は、コードがログを入力する必要がある場合はどこでも使用されます。これはほぼ JDK ログ モジュールのスポークスマンです。取得するには Logger.getLogger("com.aaa.bbb"); をよく使用します。ロガーを選択し、ロガーを使用してログ出力を行います。

2、ロガーは実際には単なる論理管理ユニットであり、その操作のほとんどは他の「ロール」を渡すためのリレーとして機能するだけです。例: Logger.getLogger("xxx") への呼び出しは Relying LogManager クラスでは、ロガーを使用してログ情報を入力するときに、ロガー内のすべてのハンドラーがログを入力するために呼び出されます。

3. ロガーには階層関係があり、一般的にはパッケージ名の親子継承関係として理解できます。各ロガーには通常、Java パッケージ名に基づいて名前が付けられます。子ロガーは通常、ロガー レベル、ハンドラー、ResourceBundle 名 (国際化情報に関連する) などを親ロガーから継承します。

4、JVM 全体に空の名前を持つルート ロガーが存在し、すべての匿名ロガーはルート ロガーを親とみなします

LogManager: すべてJVM 全体内のロガー管理、ロガーの生成、取得、その他の操作 (構成ファイルの読み取りを含む) はすべて、これに依存します。 LogManager には、現在のすべてのロガーを保存するためのハッシュテーブル [プライベート Hashtable> ロガー] が存在します。ロガーを取得する必要がある場合、ハッシュテーブルにすでにロガーが存在する場合は、それが返されます。ハッシュテーブルにロガーがない場合は、新しいロガーを作成し、ハッシュテーブルに保存します。

Handler: ログ出力の制御に使用されます。たとえば、JDK に付属の ConsoleHanlder は、出力ストリームを System.err 出力にリダイレクトします。ハンドラーは、Logger メソッドが実行されるたびに呼び出されます。は出力のために呼び出されます。publish メソッドでは、各ロガーには複数のハンドラーがあります。ハンドラーを使用して、ログをさまざまな場所 (ファイル システムやリモート ソケット接続など) に入力できます。

Formatter: ログは、実際に出力される前にフォーマットする必要があります。たとえば、Do時間を出力したいですか?時間形式?スレッド名を入力しますか?国際化情報等を使用するかどうかはFormatterに依存します。

ログ レベル : 言うまでもなく、これは理解しやすいものであり、ログ出力がさまざまな段階でのログ出力の粒度に対するさまざまなニーズに適応するのに役立つ理由でもあります。開発からデバッグ、導入、オンラインまで。 JDK ログ レベルは高から低まで、OFF(231-1)—>SEVERE(1000)—>WARNING(900)—>INFO(800)—>CONFIG(700)—> ; FINE(500)—>FINER(400)—>FINEST(300)—>ALL(-231). 各レベルは番号に対応します. ログ出力時のレベルの比較は、ログのサイズによって異なります数値を比較してください。ただし、ロガーだけでなくハンドラーにもレベルがあることに注意してください。つまり、ロガー レベルが FINE で、顧客が FINE レベルのログを入力したい場合、ロガーに対応するハンドラー レベルが INFO であれば、 FINEレベルでもログ出力できません。

対応関係の概要

LogManager とロガーは 1 対多の関係にあり、JVM 全体が実行されている場合、LogManager は 1 つだけ存在し、すべてのロガーは LogManager 内にあります。

#ロガーとハンドラーは多対多の関係にあり、ログ出力時にロガーはログ処理のためにすべてのハンドラーを呼び出します。

ハンドラーとフォーマッタは 1 対 1 の関係にあり、ハンドラーには、ログをフォーマットするためのフォーマッタがあります。

明らかに、ロガーとレベルの間には 1 対 1 の関係があり、ハンドラーとレベルの間にも 1 対 1 の関係があります。

ロギング構成:

JDK のデフォルトのロギング構成ファイルは $JAVA_HOME/jre /lib/logging.properties です。システム プロパティ java.util.logging.config.file を使用して、対応する構成を指定できます。ファイルを使用してデフォルトの構成ファイルを上書きします。構成ファイルには通常、次の定義が含まれています:

1, handlers:用逗号分隔每个Handler,这些handler将会被加到root logger中。也就是说即使我们不给其他logger配置handler属性,在输出日志的时候logger会一直找到root logger,从而找到handler进行日志的输入。

2, .level是root logger的日志级别

3, .xxx是配置具体某个handler的属性,比如java.util.logging.ConsoleHandler.formatter便是为ConsoleHandler配置相应的日志Formatter.

4, logger的配置,所有以[.level]结尾的属性皆被认为是对某个logger的级别的定义,如com.bes.server.level=FINE是给名为[com.bes.server]的logger定义级别为FINE。顺便说下,前边提到过logger的继承关系,如果还有com.bes.server.webcontainer这个logger,且在配置文件中没有定义该logger的任何属性,那么其将会从[com.bes.server]这个logger进行属性继承。除了级别之外,还可以为logger定义handler和useParentHandlers(默认是为true)属性,如com.bes.server.handler=com.bes.test.ServerFileHandler(需要是一个extends java.util.logging.Handler的类),com.bes.server.useParentHandlers=false(意味着com.bes.server这个logger进行日志输出时,日志仅仅被处理一次,用自己的handler输出,不会传递到父logger的handler)。以下是JDK配置文件示例

handlers= java.util.logging.FileHandler,java.util.logging.ConsoleHandler
.level= INFO
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter =java.util.logging.XMLFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter =java.util.logging.SimpleFormatter
com.xyz.foo.level = SEVERE
sun.rmi.transport.tcp.logLevel = FINE
ログイン後にコピー

Logging执行原理

Logger的获取

A,首先是调用Logger的如下方法获得一个logger

    public static synchronized Logger getLogger(String name) {
           LogManager manager =LogManager.getLogManager();
        returnmanager.demandLogger(name);
    }
ログイン後にコピー

B,上面的调用会触发java.util.logging.LoggerManager的类初始化工作,LoggerManager有一个静态化初始化块(这是会先于LoggerManager的构造函数调用的~_~):

static {
   AccessController.doPrivileged(newPrivilegedAction<Object>() {
       public Object run() {
           String cname =null;
           try {
               cname =System.getProperty("java.util.logging.manager");
               if (cname !=null) {
                  try {
                       Class clz =ClassLoader.getSystemClassLoader().loadClass(cname);
                       manager= (LogManager) clz.newInstance();
                   } catch(ClassNotFoundException ex) {
               Class clz =Thread.currentThread().getContextClassLoader().loadClass(cname);
                      manager= (LogManager) clz.newInstance();
                   }
               }
           } catch (Exceptionex) {
              System.err.println("Could not load Logmanager \"" + cname+ "\"");
              ex.printStackTrace();
           }
           if (manager ==null) {
               manager = newLogManager();
           }
      
           manager.rootLogger= manager.new RootLogger();
          manager.addLogger(manager.rootLogger);
 
           Logger.global.setLogManager(manager);
          manager.addLogger(Logger.global);
 
           return null;
       }
   });
}
ログイン後にコピー

从静态初始化块中可以看出LoggerManager是可以使用系统属性java.util.logging.manager指定一个继承自java.util.logging.LoggerManager的类进行替换的,比如Tomcat启动脚本中就使用该机制以使用自己的LoggerManager。

不管是JDK默认的java.util.logging.LoggerManager还是自定义的LoggerManager,初始化工作中均会给LoggerManager添加两个logger,一个是名称为””的root logger,且logger级别设置为默认的INFO;另一个是名称为global的全局logger,级别仍然为INFO。

LogManager”类”初始化完成之后就会读取配置文件(默认为$JAVA_HOME/jre/lib/logging.properties),把配置文件的属性名<->属性值这样的键值对保存在内存中,方便之后初始化logger的时候使用。

C,A步骤中Logger类发起的getLogger操作将会调用java.util.logging.LoggerManager的如下方法:

     Logger demandLogger(String name) {
       Logger result =getLogger(name);
       if (result == null) {
           result = newLogger(name, null);
           addLogger(result);
           result =getLogger(name);
       }
       return result;
     }
ログイン後にコピー

可以看出,LoggerManager首先从现有的logger列表中查找,如果找不到的话,会新建一个looger并加入到列表中。当然很重要的是新建looger之后需要对logger进行初始化,这个初始化详见java.util.logging.LoggerManager#addLogger()方法中,改方法会根据配置文件设置logger的级别以及给logger添加handler等操作。

到此为止logger已经获取到了,你同时也需要知道此时你的logger中已经有级别、handler等重要信息,下面将分析输出日志时的逻辑。

日志的输出

首先我们通常会调用Logger类下面的方法,传入日志级别以及日志内容。

    public void log(Levellevel, String msg) {
          if (level.intValue() < levelValue ||levelValue == offValue) {
              return;
          }
          LogRecord lr = new LogRecord(level, msg);
          doLog(lr);
    }
ログイン後にコピー

该方法可以看出,Logger类首先是进行级别的校验,如果级别校验通过,则会新建一个LogRecord对象,LogRecord中除了日志级别,日志内容之外还会包含调用线程信息,日志时刻等;之后调用doLog(LogRecord lr)方法

    private void doLog(LogRecord lr) {
          lr.setLoggerName(name);
          String ebname =getEffectiveResourceBundleName();
          if (ebname != null) {
              lr.setResourceBundleName(ebname);
              lr.setResourceBundle(findResourceBundle(ebname));
          }
          log(lr);
    }
ログイン後にコピー

doLog(LogRecord lr)方法中设置了ResourceBundle信息(这个与国际化有关)之后便直接调用log(LogRecord record) 方法

   public void log(LogRecord record) {
          if (record.getLevel().intValue() <levelValue || levelValue == offValue) {
              return;
          }
          synchronized (this) {
              if (filter != null &&!filter.isLoggable(record)) {
                  return;
              }
          }
          Logger logger = this;
          while (logger != null) {
              Handler targets[] = logger.getHandlers();
              if(targets != null) {
                  for (int i = 0; i < targets.length; i++){
                           targets[i].publish(record);
                       }
              }
              if(!logger.getUseParentHandlers()) {
                       break;
              }
              logger= logger.getParent();
          }
    }
ログイン後にコピー

很清晰,while循环是重中之重,首先从logger中获取handler,然后分别调用handler的publish(LogRecordrecord)方法。while循环证明了前面提到的会一直把日志委托给父logger处理的说法,当然也证明了可以使用logger的useParentHandlers属性控制日志不进行往上层logger传递的说法。到此为止logger对日志的控制差不多算是完成,接下来的工作就是看handler的了,这里我们以java.util.logging.ConsoleHandler为例说明日志的输出。

public class ConsoleHandler extends StreamHandler {
    public ConsoleHandler() {
          sealed = false;
          configure();
          setOutputStream(System.err);
          sealed = true;
    }
ログイン後にコピー

ConsoleHandler构造函数中除了需要调用自身的configure()方法进行级别、filter、formatter等的设置之外,最重要的我们最关心的是setOutputStream(System.err)这一句,把系统错误流作为其输出。而ConsoleHandler的publish(LogRecordrecord)是继承自java.util.logging.StreamHandler的,如下所示:

 public synchronized void publish(LogRecord record) {
       if(!isLoggable(record)) {
           return;
       }
       String msg;
       try {
           msg =getFormatter().format(record);
       } catch (Exception ex){
           // We don&#39;t want tothrow an exception here, but we
           // report theexception to any registered ErrorManager.
           reportError(null,ex, ErrorManager.FORMAT_FAILURE);
           return;
       }
       try {
           if (!doneHeader) {
              writer.write(getFormatter().getHead(this));
               doneHeader =true;
           }
           writer.write(msg);
       } catch (Exception ex){
           // We don&#39;t want tothrow an exception here, but we
           // report theexception to any registered ErrorManager.
           reportError(null,ex, ErrorManager.WRITE_FAILURE);
       }
    }
ログイン後にコピー

方法逻辑也很清晰,首先是调用Formatter对消息进行格式化,说明一下:格式化其实是进行国际化处理的重要契机。然后直接把消息输出到对应的输出流中。需要注意的是handler也会用自己的level和LogRecord中的level进行比较,看是否真正输出日志。

总结

至此,整个日志输出过程已经分析完成。细心的读者应该可以解答如下四个问题了。

1,【Main running.】以外的日志为什么没有输出?怎么让它们也能够出现?

这就是JDK默认的logging.properties文件中配置的handler级别和跟级别均为info导致的,如果希望看到FINE级别日志,需要修改logging.properties文件,同时进行如下两个修改

    java.util.logging.ConsoleHandler.level= FINE//修改
    com.bes.logging.level=FINE//添加
ログイン後にコピー

2,日志中出现的时间、类名、方法名等是从哪里输出的?

请参照[java.util.logging.ConsoleHandler.formatter= java.util.logging.SimpleFormatter]配置中指定的java.util.logging.SimpleFormatter类,其publicsynchronized String format(LogRecord record) 方法说明了一切。

public synchronized String format(LogRecord record) { StringBuffer sb = new StringBuffer(); // Minimize memory allocations here. dat.setTime(record.getMillis()); args[0] = dat; StringBuffer text = new StringBuffer(); if (formatter == null) { formatter = new MessageFormat(format); } formatter.format(args, text, null); sb.append(text); sb.append(" "); if (record.getSourceClassName() != null) { sb.append(record.getSourceClassName()); } else { sb.append(record.getLoggerName()); } if (record.getSourceMethodName() != null) { sb.append(" "); sb.append(record.getSourceMethodName()); } sb.append(lineSeparator); String message = formatMessage(record); sb.append(record.getLevel().getLocalizedName()); sb.append(": "); sb.append(message); sb.append(lineSeparator); if (record.getThrown() != null) { try { StringWriter sw = newStringWriter(); PrintWriter pw = newPrintWriter(sw); record.getThrown().printStackTrace(pw); pw.close(); sb.append(sw.toString()); } catch (Exception ex) { } } return sb.toString();}public synchronized String format(LogRecord record) {
    StringBuffer sb = new StringBuffer();
    // Minimize memory allocations here.
    dat.setTime(record.getMillis());
    args[0] = dat;
    StringBuffer text = new StringBuffer();
    if (formatter == null) {
        formatter = new MessageFormat(format);
    }
    formatter.format(args, text, null);
    sb.append(text);
    sb.append(" ");
    if (record.getSourceClassName() != null) {     
        sb.append(record.getSourceClassName());
    } else {
        sb.append(record.getLoggerName());
    }
    if (record.getSourceMethodName() != null) {
        sb.append(" ");
       sb.append(record.getSourceMethodName());
    }
    sb.append(lineSeparator);
    String message = formatMessage(record);
   sb.append(record.getLevel().getLocalizedName());
    sb.append(": ");
    sb.append(message);
    sb.append(lineSeparator);
    if (record.getThrown() != null) {
        try {
            StringWriter sw = newStringWriter();
            PrintWriter pw = newPrintWriter(sw);
           record.getThrown().printStackTrace(pw);
            pw.close();
             sb.append(sw.toString());
        } catch (Exception ex) {
        }
    }
    return sb.toString();
}
ログイン後にコピー

3,为什么日志就会出现在控制台?

    看到java.util.logging.ConsoleHandler 类构造方法中的[setOutputStream(System.err)]语句,相信你已经明白。

4,大型的系统可能有很多子模块(可简单理解为有很多包名),如何对这些子模块进行单独的日志级别控制?

    在logging.properties文件中分别对各个logger的级别进行定义,且最好使用java.util.logging.config.file属性指定自己的配置文件。

以上がJava の基礎となる JDK Logging モジュールを処理する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:yisu.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート