任何项目工程,日志的作用都毋庸置疑得重要,监控,问题查找,统计,大数据资源来源等。在阅读spring源码过程中开启spring的日志对于阅读的帮助也是非常大的。
先说说apache自带的log组件 commons-logging
commons-logging提供的是一个日志的接口(interface),并有一个简单的日志实现SimpleLog.class
。它提供给中间件/日志工具开发者一个简单的日志操作抽象,允许程序开发人员使用不同的具体日志实现工具。然后对各种实现的日志工具如Log4j、Avalon LogKit, and JDK(具体实现在java.util.logging
package中)中选择一个。有各种整合方式。
网上看到的一个commons-logging出现的小故事
org.apache.commons.logging.Log 和org.apache.log4j.Logger 这两个类,通过包名我们可以发现它们都是 apache 的项目,既然如此,为何要动如此大的动作搞两个东西(指的是 commons-logging 和 log4j)出来呢?事实上,在 sun 开发 logger 前,apache 项目已经开发了功能强大的 log4j 日志工具,并向 sun 推荐将其纳入到 jdk 的一部分,可是 sun 拒绝了 apache 的提议,sun 后来自己开发了一套记录日志的工具。可是现在的开源项目都使用的是 log4j,log4j 已经成了事实上的标准,但由于又有一部分开发者在使用 sun logger,因此 apache 才推出 commons-logging,使得我们不必关注我们正在使用何种日志工具
在不使用log4j的情况只使用commons-logging 我们也是可以打印日志出来的,下面看下源码:
找到
点击Log进入源代码
Log是个interface
package org.apache.commons.logging;
public interface Log {
void debug(Object message);
void debug(Object message, Throwable t);
void error(Object message);
void error(Object message, Throwable t);
void fatal(Object message);
void fatal(Object message, Throwable t);
void info(Object message);
void info(Object message, Throwable t);
boolean isDebugEnabled();
boolean isErrorEnabled();
boolean isFatalEnabled();
boolean isInfoEnabled();
boolean isTraceEnabled();
boolean isWarnEnabled();
void trace(Object message);
void trace(Object message, Throwable t);
void warn(Object message);
void warn(Object message, Throwable t);
}
LogFactory是个抽象类public abstract class LogFactory
LogSource是以前创建Log的class 现在已被遗弃
LogConfigurationException是 LogFactory
或者 Log
实例创建失败的时候抛出的异常
impl包下的AvalonLogger
Jdk13LumberjackLogger
Log4JLogger
LogKitLogger
NoOpLog
SimpleLog
是Log
接口的实现
ServletContextCleaner是ServletContextListener的实现 在contextDestroyed的时候把创建的LogFactory销毁掉。
public class ServletContextCleaner implements ServletContextListener {
private static final Class[] RELEASE_SIGNATURE = {ClassLoader.class};
public void contextDestroyed(ServletContextEvent sce) {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Object[] params = new Object[1];
params[0] = tccl;
…………………
} catch(InvocationTargetException ex) {
// This is not expected
System.err.println("LogFactory instance release method failed!");
loader = null;
}
}
LogFactory.release(tccl);
其中LogFactory.release(tccl)
在LogFactory中这样是这样实现
public static void release(ClassLoader classLoader) {
…………
factory.release();
factories.remove(classLoader);
}
}
}
}
其中factory是 protected static Hashtable factories = null;
在LogFactory实例化的时候 调用 static {……factories = createFactoryStore();……}
其中createFactoryStore创建了
private static final String WEAK_HASHTABLE_CLASSNAME ="org.apache.commons.logging.impl.WeakHashtable";
属性 即WeakHashtable。存放LogFactory
最后 factories.release的时候相当于清空了WeakHashtable,WeakHashtable其实是基于WeakReference 实现的 这种弱引用在一定情况下会被jvm优先回收掉 所以节约内存。时间久了不用jvm会回收掉后,当再次用到的时候再创建即可。
先说了销毁再看下是怎么创建和选择Log实现类的
private static final Log log = LogFactory.getLog(FactoryBeanTests.class);
接着找到getLog
public static Log getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}
这里面有两个重要的方法getFactory()和getInstance(clazz)
先说getFactory()
getFactory的时候会先取到ClassLoader 用户加载所需要类的class文件
(基本各种判断后获得到的一般是当前类的classLoader classLoader = Thread.currentThread().getContextClassLoader()
)
接着LogFactory factory = getCachedFactory(contextClassLoader);
这是从上面说的WeakHashtable的实例factories中取,第一次肯定是没有的 继续往下会去读取Properties文件
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
public static final String FACTORY_PROPERTIES = "commons-logging.properties";
用户没配置commons-logging.properties的情况下取到props==null
继续往下回去系统启动的属性中去找
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
public static final String FACTORY_PROPERTY = "org.apache.commons.logging.LogFactory";
启动的时候没有设置则factoryClass==null 继续往下
会读取META-INF/services/org.apache.commons.logging.LogFactory这个文件
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
protected static final String SERVICE_ID =
"META-INF/services/org.apache.commons.logging.LogFactory";
没有配置这个文件的情况下继续往下
到了
if (factory == null) {
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
}
public static final String FACTORY_DEFAULT = "org.apache.commons.logging.impl.LogFactoryImpl";
这个地方newFactory 方法去创建FACTORY_DEFAULT = "org.apache.commons.logging.impl.LogFactoryImpl"利用反射机制 默认创建了他的实现类LogFactoryImpl
再来看上面说的
这里面有两个重要的方法getFactory()和getInstance(clazz)
的第二个方法getInstance 此时的getInstance是LogFactoryImpl的
public Log getInstance(Class clazz) throws LogConfigurationException {
return getInstance(clazz.getName());
}
接着往下走调用了newInstance 方法
if (logConstructor == null) {
instance = discoverLogImplementation(name);
}
第一次logConstructor 肯定为null所以继续进去看
private Log discoverLogImplementation(String logCategory)
…………
initConfiguration();
String specifiedLogClassName = findUserSpecifiedLogClassName();
if (specifiedLogClassName != null) {
………………
for(int i=0; i
列出几个的关键点initConfiguration初始化一些配置
findUserSpecifiedLogClassName 是去缓存找缓存的specifiedClass(具体的class)这里会查找这两个,一个是以前的另一个是现在在用的
public static final String LOG_PROPERTY = "org.apache.commons.logging.Log";
protected static final String LOG_PROPERTY_OLD = "org.apache.commons.logging.log";
第一次所以没有的话继续到
for(int i=0; i
这个是最关键的部分
private static final String[] classesToDiscover = {
LOGGING_IMPL_LOG4J_LOGGER,
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
选择默认找这三个
第一个org.apache.commons.logging.impl.Jdk14Logger
impl包就有 遍历的时候判断了
第一次没仔细看纠结了半天 以为三个都创建了 其实值创建了一个
好了肯定是创建了org.apache.commons.logging.impl.Jdk14Logger 这个类
到这个类中看 就是封装了JDK的日志实现
这个类Jdk14Logger 没有找到设置日志级别的方法
继续往jdk实现中找,在
java.util.logging.Logger
找到
private static Logger demandLogger(String name, String resourceBundleName, Class> caller) {
LogManager manager = LogManager.getLogManager();
SecurityManager sm = System.getSecurityManager();
if (sm != null && !SystemLoggerHelper.disableCallerCheck) {
if (caller.getClassLoader() == null) {
return manager.demandSystemLogger(name, resourceBundleName);
}
}
return manager.demandLogger(name, resourceBundleName, caller);
// ends up calling new Logger(name, resourceBundleName, caller)
// iff the logger doesn't exist already
}
其实决定权在LogManager manager = LogManager.getLogManager();
到LogManager 类中getLogManager方法
public static LogManager getLogManager() {
if (manager != null) {
manager.ensureLogManagerInitialized();
}
return manager;
}
继续找到ensureLogManagerInitialized
final void ensureLogManagerInitialized() {
final LogManager owner = this;
if (initializationDone || owner != manager) {
return;
}
………………
synchronized(this) {
assert rootLogger == null;
assert initializedCalled && !initializationDone;
// Read configuration.
owner.readPrimordialConfiguration();
…………………………
if (!owner.rootLogger.isLevelInitialized()) {
owner.rootLogger.setLevel(defaultLevel);
}
…………………………
} finally {
initializationDone = true;
}
}
}
最重要的一句owner.rootLogger.setLevel(defaultLevel);
这个defaultLevel是private final static Level defaultLevel = Level.INFO;
默认是INFO,那么怎么打印DEBUG呢
其实在静态初始化块中还有个重要的方法owner.readPrimordialConfiguration()
其中owner是final LogManager owner = this;
即当前LogManager
继续看readPrimordialConfiguration方法
private void readPrimordialConfiguration() {
……………………
AccessController.doPrivileged(new PrivilegedExceptionAction() {
@Override
public Void run() throws Exception {
readConfiguration();
// Platform loggers begin to delegate to java.util.logging.Logger
sun.util.logging.PlatformLogger.redirectPlatformLoggers();
return null;
……………………
}
}
看到readConfiguration方法,重点就是这个方法,先会尝试去加载 String cname = System.getProperty("java.util.logging.config.class");
java.util.logging.config.class 加载不到的话回去加载配置文件String fname = System.getProperty("java.util.logging.config.file");java.util.logging.config.file 如果还加载不到的话会去java home下面加载logging.properties ,哈哈 所以只要配置logging.properties即可了
源码
public void readConfiguration() throws IOException, SecurityException {
checkPermission();
// if a configuration class is specified, load it and use it.
String cname = System.getProperty("java.util.logging.config.class");
if (cname != null) {
try {
// Instantiate the named class. It is its constructor's
// responsibility to initialize the logging configuration, by
// calling readConfiguration(InputStream) with a suitable stream.
try {
Class> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
clz.newInstance();
return;
} catch (ClassNotFoundException ex) {
Class> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);
clz.newInstance();
return;
}
} catch (Exception ex) {
System.err.println("Logging configuration class \"" + cname + "\" failed");
System.err.println("" + ex);
// keep going and useful config file.
}
}
String fname = System.getProperty("java.util.logging.config.file");
if (fname == null) {
fname = System.getProperty("java.home");
if (fname == null) {
throw new Error("Can't find java.home ??");
}
File f = new File(fname, "lib");
f = new File(f, "logging.properties");
fname = f.getCanonicalPath();
}
try (final InputStream in = new FileInputStream(fname)) {
final BufferedInputStream bin = new BufferedInputStream(in);
readConfiguration(bin);
}
}
那么怎么配置这个logging.properties呢 其实就是在readConfiguration(bin)方法中 进去看
public void readConfiguration(InputStream ins) throws IOException, SecurityException {
checkPermission();
reset();
// Load the properties
props.load(ins);
// Instantiate new configuration objects.
String names[] = parseClassNames("config");
for (int i = 0; i < names.length; i++) {
String word = names[i];
try {
Class> clz = ClassLoader.getSystemClassLoader().loadClass(word);
clz.newInstance();
} catch (Exception ex) {
System.err.println("Can't load config class \"" + word + "\"");
System.err.println("" + ex);
// ex.printStackTrace();
}
}
// Set levels on any pre-existing loggers, based on the new properties.
setLevelsOnExistingLoggers();
// Notify any interested parties that our properties have changed.
// We first take a copy of the listener map so that we aren't holding any
// locks when calling the listeners.
Map
找到setLevelsOnExistingLoggers() 方法进去
synchronized private void setLevelsOnExistingLoggers() {
Enumeration> enum_ = props.propertyNames();
while (enum_.hasMoreElements()) {
String key = (String)enum_.nextElement();
if (!key.endsWith(".level")) {
// Not a level definition.
continue;
}
int ix = key.length() - 6;
String name = key.substring(0, ix);
Level level = getLevelProperty(key, null);
if (level == null) {
System.err.println("Bad level value for property: " + key);
continue;
}
for (LoggerContext cx : contexts()) {
Logger l = cx.findLogger(name);
if (l == null) {
continue;
}
l.setLevel(level);
}
}
}
哈哈加载!key.endsWith(".level") 以.level结尾的key 这样你就知道怎么配置了吧
那么看看simpleConfig这个类吧
其实可以将这两个
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
删除这样就会创建org.apache.commons.logging.impl.SimpleLog
了,我非常非常喜欢这个类 因为够简单
初始化的时候还是会读取classpath下的simplelog.properties文件,这个文件主要读取的是dateTimeFormat 方式 没读取到则采用默认的
看源码
static {
// Add props from the resource simplelog.properties
InputStream in = getResourceAsStream("simplelog.properties");
if(null != in) {
try {
simpleLogProps.load(in);
in.close();
} catch(java.io.IOException e) {
// ignored
}
}
showLogName = getBooleanProperty(systemPrefix + "showlogname", showLogName);
showShortName = getBooleanProperty(systemPrefix + "showShortLogname", showShortName);
showDateTime = getBooleanProperty(systemPrefix + "showdatetime", showDateTime);
if(showDateTime) {
dateTimeFormat = getStringProperty(systemPrefix + "dateTimeFormat",
dateTimeFormat);
try {
dateFormatter = new SimpleDateFormat(dateTimeFormat);
} catch(IllegalArgumentException e) {
// If the format pattern is invalid - use the default format
dateTimeFormat = DEFAULT_DATE_TIME_FORMAT;
dateFormatter = new SimpleDateFormat(dateTimeFormat);
}
}
}
static protected final String DEFAULT_DATE_TIME_FORMAT = "yyyy/MM/dd HH:mm:ss:SSS zzz";
这个类只能用作控制台输出 没有做文件输出 所以够简单 看源码就是
就是调用了
protected void write(StringBuffer buffer) {
System.err.println(buffer.toString());
}
System的东西 太熟悉了 就和自己写的一样简单 哈哈。但是只能用于控制台打印 呵呵吧。
还提供了
public void setLevel(int currentLogLevel) {
this.currentLogLevel = currentLogLevel;
}
设置日志级别的,所以我们可以改变日志输出级别 不过我感觉没有人会愿意使用这个类打印日志。
这样我们发现即使没有配置其他日志组件也是可以打印日志的
我们做个测试
build path将log4j从path中remove掉 不移除掉会创建到Log4JLogger这个类是和log4j的封装类这样这样就没法测试了
找个打印日志的类 我找的是FactoryBeanTests 自己加了条日志
这里有个小技巧
每次判断级别的时候log.isDebugEnabled() 是可以少创建个类 假如你不判断的话debug中
public void debug(Object message) {
getLogger().log(FQCN, Level.DEBUG, message, null);
}
getLogger会多走逻辑和创建对象的。看到基本开源的都是这种写法算是个好习惯吧。
跑一下
看到已经是输出日志的 这印证了我们的说法。
其实只要检测到了log4j便会走Log4j的打印日志的方式 后续还想说说 slfj4 今天是说不玩了。还是说下配置文件怎么配置吧。
主要配置的组件
- Loggers(记录器)
Loggers组件在此系统中被分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的,DEBUG < INFO < WARN < ERROR < FATAL,分别用来指定这条日志信息的重要程度,明白这一点很重要,Log4j有一个规则:只输出级别不低于设定级别的日志信息,假设Loggers级别设定为INFO,则INFO、WARN、ERROR和FATAL级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出。 - Appenders (输出源)
禁用和使用日志请求只是Log4j的基本功能,Log4j日志系统还提供许多强大的功能,比如允许把日志输出到不同的地方,如控制台(Console)、文件(Files)等,可以根据天数或者文件大小产生新的文件,可以以流的形式发送到其它地方等等。常使用的类如下:org.apache.log4j.ConsoleAppender(控制台)org.apache.log4j.FileAppender(文件)org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)配置模式:log4j.appender.appenderName = classNamelog4j.appender.appenderName.Option1 = value1…log4j.appender.appenderName.OptionN = valueN - Layouts(布局)
有时用户希望根据自己的喜好格式化自己的日志输出,Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供四种日志输出样式,如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式。常使用的类如下:org.apache.log4j.HTMLLayout(以HTML表格形式布局)org.apache.log4j.PatternLayout(可以灵活地指定布局模式)org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)配置模式:log4j.appender.appenderName.layout =classNamelog4j.appender.appenderName.layout.Option1 = value1…log4j.appender.appenderName.layout.OptionN = valueN
1、配置根Logger:log4j.rootLogger = [ level ] , appenderName1, appenderName2, …log4j.additivity.org.apache=false:表示Logger不会在父Logger的appender里输出,默认为true。level :设定日志记录的最低级别,可设的值有OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别,Log4j建议只使用中间四个级别。通过在这里设定级别,您可以控制应用程序中相应级别的日志信息的开关,比如在这里设定了INFO级别,则应用程序中所有DEBUG级别的日志信息将不会被打印出来。appenderName:就是指定日志信息要输出到哪里。可以同时指定多个输出目的地,用逗号隔开。例如:log4j.rootLogger=INFO,A1,B2,C32、配置日志信息输出目的地(appender):log4j.appender.appenderName = classNameappenderName:自定义appderName,在log4j.rootLogger设置中使用;className:可设值如下:(1)org.apache.log4j.ConsoleAppender(控制台)(2)org.apache.log4j.FileAppender(文件)(3)org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)(4)org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)(5)org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)(1)ConsoleAppender选项:Threshold=WARN:指定日志信息的最低输出级别,默认为DEBUG。ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认值是true。Target=System.err:默认值是System.out。(2)FileAppender选项:Threshold=WARN:指定日志信息的最低输出级别,默认为DEBUG。ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认值是true。Append=false:true表示消息增加到指定文件中,false则将消息覆盖指定的文件内容,默认值是true。File=D:/logs/logging.log4j:指定消息输出到logging.log4j文件中。(3)DailyRollingFileAppender选项:Threshold=WARN:指定日志信息的最低输出级别,默认为DEBUG。ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认值是true。Append=false:true表示消息增加到指定文件中,false则将消息覆盖指定的文件内容,默认值是true。File=D:/logs/logging.log4j:指定当前消息输出到logging.log4j文件中。DatePattern='.'yyyy-MM:每月滚动一次日志文件,即每月产生一个新的日志文件。当前月的日志文件名为logging.log4j,前一个月的日志文件名为logging.log4j.yyyy-MM。另外,也可以指定按周、天、时、分等来滚动日志文件,对应的格式如下:1)'.'yyyy-MM:每月2)'.'yyyy-ww:每周3)'.'yyyy-MM-dd:每天4)'.'yyyy-MM-dd-a:每天两次5)'.'yyyy-MM-dd-HH:每小时6)'.'yyyy-MM-dd-HH-mm:每分钟(4)RollingFileAppender选项:Threshold=WARN:指定日志信息的最低输出级别,默认为DEBUG。ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认值是true。Append=false:true表示消息增加到指定文件中,false则将消息覆盖指定的文件内容,默认值是true。File=D:/logs/logging.log4j:指定消息输出到logging.log4j文件中。MaxFileSize=100KB:后缀可以是KB, MB 或者GB。在日志文件到达该大小时,将会自动滚动,即将原来的内容移到logging.log4j.1文件中。MaxBackupIndex=2:指定可以产生的滚动文件的最大数,例如,设为2则可以产生logging.log4j.1,logging.log4j.2两个滚动文件和一个logging.log4j文件。3、配置日志信息的输出格式(Layout):log4j.appender.appenderName.layout=classNameclassName:可设值如下:(1)org.apache.log4j.HTMLLayout(以HTML表格形式布局)(2)org.apache.log4j.PatternLayout(可以灵活地指定布局模式)(3)org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)(4)org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)(1)HTMLLayout选项:LocationInfo=true:输出java文件名称和行号,默认值是false。Title=My Logging: 默认值是Log4J Log Messages。(2)PatternLayout选项:ConversionPattern=%m%n:设定以怎样的格式显示消息
附上个简单的demo
# Set root logger level to Debug and its only appender to A1
log4j.rootLogger=INFO, A1,UID
log4j.category.org.springframework = info
# A1 is set to be ConsoleAppender
log4j.appender.A1=org.apache.log4j.ConsoleAppender
# A1 uses PatternLayout
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%t] - %m%n
# A2 is set to be logfile
log4j.appender.A2=org.apache.log4j.RollingFileAppender
# Define the file name
log4j.appender.A2.File=ts.log
# Define the layout
log4j.appender.A2.layout=org.apache.log4j.PatternLayout
log4j.appender.A2.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%t] %x - %m%n
log4j.appender.UID=org.apache.log4j.DailyRollingFileAppender
log4j.appender.UID.File=${catalina.base}/logs/uid.log
log4j.appender.UID.layout=org.apache.log4j.PatternLayout
log4j.appender.UID.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%t] %x - %m%n
log4j.logger.com.holly.wang=DEBUG
#unify log
log4j.logger.sysunifylog=debug, sysunifylog
log4j.appender.sysunifylog=org.apache.log4j.DailyRollingFileAppender
log4j.appender.sysunifylog.DatePattern='.'yyyy-MM-dd-HH
log4j.appender.sysunifylog.File=${catalina.base}/logs/sysunifylog.log
log4j.appender.sysunifylog.layout=org.apache.log4j.PatternLayout
log4j.appender.sysunifylog.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss.SSS}][%p] %m%n
log4j.additivity.sysunifylog = false
这里还要说个问题 看到同事在指定自定义类设置的时候还是用的
log4j.category.org.springframework = info
其实category已经被抛弃了哈哈,以后很有可能被删除 所以不要用了 log4j.logger.com.holly.wang=DEBUG 这样就可以了 就category改成logger既可。还有自定义类设置输出的时候经常遇到重复输出的问题 只需要设置log4j.additivity 为false即可。
回家喽 好晚啦!