JDK Logging의 사용법은 매우 간단합니다. 다음 코드와 같이 먼저 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. 왜 [메인실행] 외의 로그는 출력되지 않나요? 어떻게 나타나게 만들 수 있나요?
2. 로그 출력에 나타나는 시간, 클래스 이름, 메서드 이름 등은 어디에 있나요?
3. 콘솔에 로그가 나타나는 이유는 무엇인가요?
4. 대규모 시스템에는 여러 개의 하위 모듈이 있을 수 있습니다(간단히 패키지 이름이 많다고 이해될 수 있음). 이러한 하위 모듈의 로그 수준을 개별적으로 제어하는 방법은 무엇입니까?
5. 확장: Apache의 인기 있는 log4j 프로젝트가 JDK의 로깅과 관련되어 있습니까? 자체 LoggerManager를 구현하는 방법은 무엇입니까?
이러한 질문을 통해 JDK의 로깅 메커니즘에 대해 더 관심을 가질 수 있습니다. 이 장에서는 이 간단한 모듈의 메커니즘을 분석합니다.
깊이 분석하기 전에 다음 용어를 숙지해야 합니다
logger
: 로거의 경우 다음 측면을 알아야 합니다logger
:对于logger,需要知道其下几个方面
1,代码需要输入日志的地方都会用到Logger,这几乎是一个JDK logging模块的代言人,我们常常用Logger.getLogger("com.aaa.bbb");获得一个logger,然后使用logger做日志的输出。
2,logger其实只是一个逻辑管理单元,其多数操作都只是作为一个中继者传递别的<角色>,比如说:Logger.getLogger(“xxx”)的调用将会依赖于LogManager类,使用logger输入日志信息的时候会调用logger中的所有handler进行日志的输入。
3,logger是有层次关系的,我们可一般性的理解为包名之间的父子继承关系。每个logger通常以java包名为其名称。子logger通常会从父logger继承logger级别、handler、ResourceBundle名(与国际化信息有关)等。
4,整个JVM会存在一个名称为空的root logger,所有匿名的logger都会把root logger作为其父
LogManager
:整个JVM内部所有logger的管理,logger的生成、获取等操作都依赖于它,也包括配置文件的读取。LogManager中会有一个Hashtable【private Hashtable
Handler
:用来控制日志输出的,比如JDK自带的ConsoleHanlder把输出流重定向到System.err输出,每次调用Logger的方法进行输出时都会调用Handler的publish方法,每个logger有多个handler。我们可以利用handler来把日志输入到不同的地方(比如文件系统或者是远程Socket连接).
Formatter
:日志在真正输出前需要进行一定的格式话:比如是否输出时间?时间格式?是否输入线程名?是否使用国际化信息等都依赖于Formatter。
Log Level
LogManager
: 전체 JVM 내의 모든 로거를 관리합니다. 로거 생성, 구성 파일 읽기를 포함하여 수집과 같은 작업이 이에 따라 달라집니다. 모든 현재 로거를 저장하기 위해 LogManager에 Hashtable [private HashtableHandler
: 로그 출력을 제어하는 데 사용됩니다. 예를 들어 JDK와 함께 제공되는 ConsoleHanlder는 출력을 위해 Logger 메서드가 호출될 때마다 출력 스트림을 System.err 출력으로 리디렉션합니다. 메서드가 호출되면 각 로거에는 여러 개의 핸들러가 있습니다. 핸들러를 사용하여 다양한 위치(예: 파일 시스템 또는 원격 소켓 연결)에 로그를 입력할 수 있습니다. 포맷터
: 로그는 실제로 출력되기 전에 형식을 지정해야 합니다. 시간 ? 시간 형식? 스레드 이름을 입력하시겠습니까? 국제화 정보 등의 사용 여부는 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와 로거는 일대다 관계에 있습니다. 전체 JVM이 실행 중일 때 LogManager는 하나만 있고 모든 로거는 LogManager에 있습니다. 다대다 관계 로거가 로그를 출력할 때 로그 처리를 위해 모든 핸들러가 호출됩니다🎜🎜핸들러와 포맷터는 일대일 관계를 갖습니다. 각 핸들러에는 로그 포맷을 위한 포맷터가 있습니다🎜🎜분명합니다. logger와 level은 일대일 관계이고, hanlder와 level도 일대일 관계입니다. 🎜🎜로깅 구성: 🎜🎜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,
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
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'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'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 로깅 모듈을 처리하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!