Tomcat 8 源码学习五之Tomcat日志系统
Contents
juli
默认情况下,Tomcat使用自身的juli
作为Tomcat内部的日志处理系统.它的源码,在Tomcat源码结构下的org.apache.juli
包下.
juli与JDK logger的关系
juli是继承自JDK的logger的.通过源码可以确认这一点:
public class ClassLoaderLogManager extends LogManager {
}
如果想要开启juli日志自身的调试,可以设置一个JVM系统属性org.apache.juli.ClassLoaderLogManager.debug=true
即可。
juli日志构架
ClassLoaderLogManager
JULI的核心类,负责加载配置文件,以及初始化配置文件里的Handlers
, Logger
等信息.
可以看到,它的构造函数:
public ClassLoaderLogManager() {
super();
try {
Runtime.getRuntime().addShutdownHook(new Cleaner());
} catch (IllegalStateException ise) {
// We are probably already being shutdown. Ignore this error.
}
}
在JVM退出时,添加了一个shutdownHook
,来进行日志框架自身的清理工作.它调用了LogManager.shutdown()
方法.
它的公共方法有如下:
ClassLoaderLogManager # 构造函数
isUseShutdownHook # 判断是否使用shutdown勾子
setUseShutdownHook # 设置是否使用shutdown勾子
addLogger # 添加一个Logger
getLogger # 获取一个Logger
getLoggerNames # 获取所有的Logger名字
getProperty # 获取属性
readConfiguration # 读取配置文件 logging.properties,并初始化Logger
readConfiguration # 读取配置文件 logging.properties,并初始化Logger
reset # 重置所有logger
shutdown # 清理所有logger
readConfiguration(ClassLoder)
它读取logging.properties
,并创建一个RootLogger
(名字为”“,即空字符串,不是NULL哈).并创建一个ClassLoaderLogInfo
对象(与指定的ClassLoader匹配)
创建完成后,它再调用readConfiguration(InputStream is, ClassLoader classLoader)f
方法,将logging.properties
作为InputStream
,再传入classLoader
参数来调用继续初始化其他的Logger.
readConfiguration(InputStream is, ClassLoader classLoader)
获取根据参数(classLoader)来获取ClassLoaderLogInfo
对象,看它的数据结构可知,它是整个ClassLoder的所有Loger结构对象.
protected static final class ClassLoaderLogInfo {
final LogNode rootNode;
final Map<String, Logger> loggers = new ConcurrentHashMap<>();
final Map<String, Handler> handlers = new HashMap<>();
final Properties props = new Properties();
ClassLoaderLogInfo(final LogNode rootNode) {
this.rootNode = rootNode;
}
}
包含RootLogger
(它保存在rootNode节点中),以及其他Loggers,Handlers,以及logging.properties
的pros
对象.
经过上面的处理,我们再来看下之后的逻辑:
- 根据
logging.properties
获取.handlers
属性的值来获取所有的handlers
对象.以默认的Tomcat的logging.properties
为例,它的配置如下:
.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
.handlers
:它是RootLogger
默认的Handler
- 根据
logging.properties
获取handlers
属性的值来获取非RootLogger
的handlers
对象.Tomcat中默认有以下几个:
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
- 如果
handlers
的值为空(null),则将handlers
的字符串进行处理.分别获取以下的值(这里以1catalina.org.apache.juli.AsyncFileHandler
为例子,其他类似)
handlerName
:1catalina.org.apache.juli.AsyncFileHandler
handlerClassName
:catalina.org.apache.juli.AsyncFileHandler
(它会删除handlerName数字开头的部分.剩下的就是handlerClassName)
prefix
:1catalina.
然后创建一个指定的handlerClassName
的Handler
,然后保存到ClassLoaderLogInfo
对象中的handlers
字段里,handlers
属性是一个Map<String,Hander>
,它的Key
就是handlerName
(这里的值为1catalina.org.apache.juli.AsyncFileHandler
,value就是根据handlerClassName
创建出来的Handler
对象.
其他的handler以此类推.
到这里,根据配置文件初始化RootLogger
以及配置文件里指定的Handler
都已经初始化完毕了.
LogFactory
private static final Log log = LogFactory.getLog(Bootstrap.class);
我们来看看这Log到底是如何实现的,上面这一行代码,获取了一个org.apache.juli.logging.Log
对象。它是通过org.apache.juli.logging.LogFactory
来获取一个实例的.整个调用栈为:
LogFactory.getLog(name) -> LogFactory.getInstance(name) -> DirectJDKLog.getInstance(name) -> new DirectJDKLog(name)
然后,在DirectJDKLog
对象里,使用了JDK中的Logger
来进行日志打印.
调用JDK的Logger时,会触发初始化juli的ClassLoaderLogManager
(主要加载配置文件).(因为Tomcat启动时,会指定两个日志参数:
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=/ihome/java/tomcat/apache-tomcat-8.0.36-src/conf/logging.properties
Log Level
java.util.logging.Level
类记录了每个Log Level以及它们对应的int值.比较Leve时,就是通过比较它们的intValue来比较大小的.
org.apache.juli.logging.Log
它实质上是通过持有JDKjava.util.logging.Logger
来实现日志打印的.所以,我们来看看这个JDK的Logger对象. 化繁为简,它主要有以下这几个(这里只是列出主要部分):
private volatile LogManager manager; # 所属的LogManager, 因为JDK里,允许自定义LogManager,即Tomcat上面启动时指定的参数, -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
private String name; # Logger的名字
private final CopyOnWriteArrayList<Handler> handlers = new CopyOnWriteArrayList<>(); # 这个就是Logger的Handler, 日志就是通过它来控制输出的,比如输出到文件,终端,或者Socket等.这个在初始化 ClassLoaderLogManager 时加载.
更新
2016-7-7
今天再次调试了一下, 发现org.apache.juli.ClassLoaderLogManager.readConfiguration(ClassLoader classLoader)
这个方法, 发现它加载的次数为:
AppClassLoader.即加载以下以这些类:
--> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/charsets.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/deploy.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/cldrdata.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/dnsns.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/jaccess.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/jfxrt.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/localedata.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/nashorn.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/sunec.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/sunjce_provider.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/sunpkcs11.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/ext/zipfs.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/javaws.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/jce.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/jfr.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/jfxswt.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/jsse.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/management-agent.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/plugin.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/resources.jar --> url --> file:/ihome/java/jdk/jdk1.8.0_60/jre/lib/rt.jar --> url --> file:/ihome/java/tomcat/apache-tomcat-7.0.70-src/target/classes/ --> url --> file:/home/sky/.m2/repository/org/apache/ant/ant/1.7.0/ant-1.7.0.jar --> url --> file:/home/sky/.m2/repository/org/apache/ant/ant-launcher/1.7.0/ant-launcher-1.7.0.jar --> url --> file:/home/sky/.m2/repository/wsdl4j/wsdl4j/1.6.2/wsdl4j-1.6.2.jar --> url --> file:/home/sky/.m2/repository/javax/xml/jaxrpc-api/1.1/jaxrpc-api-1.1.jar --> url --> file:/home/sky/.m2/repository/org/eclipse/jdt/core/compiler/ecj/4.5/ecj-4.5.jar --> url --> file:/ihome/java/ide/idea-IU-145.258.11/lib/idea_rt.jar
这一次,一般会找不到logging.properties
, 然后它会进入到以下判断中:
if ((is == null) && (classLoader == ClassLoader.getSystemClassLoader())) {
String configFileStr = System.getProperty("java.util.logging.config.file");
if (configFileStr != null) {
try {
is = new FileInputStream(replace(configFileStr));
} catch (IOException e) {
System.err.println("Configuration error");
e.printStackTrace();
}
}
// Try the default JVM configuration
if (is == null) {
File defaultFile = new File(new File(System.getProperty("java.home"), "lib"),
"logging.properties");
try {
is = new FileInputStream(defaultFile);
} catch (IOException e) {
System.err.println("Configuration error");
e.printStackTrace();
}
}
}
这个就是加载Tomcat默认的logging.properties
日志配置文件.
URLClassLoader. 这个没有加载任何资源
WebappClassLoader
这个classloder会加载webapps
目录下每一个应用, 即每一个应用,都会加载一次,并且读取该应用下的classpath中的logging.properties
日志配置文件.它加载的资源为:
webapps/appname/WEB-INF/classes
目录和
webapps/appname/WEB-INF/lib
目录
Tomcat启动时报错
SEVERE: Error listenerStart 或者 SEVERE: Error filterStart 等
可以利用上面的说明, 在出现问题的应用上,假设你的应用叫demo
, 那么,就可以在目录:webapps/demo/WEB-INF/classes
下添加一个名为logging.properties
的文件, 文件的内容为:
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
org.apache.juli.FileHandler.level = FINE
org.apache.juli.FileHandler.directory = ${catalina.base}/logs
org.apache.juli.FileHandler.prefix = error-debug.
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
这时,Tomcat就会在控制台里打印详细的报错信息了.