文章目录
什么是Log4j
Log4J 是 Apache 的一个开源项目,其取名意为Log For Java,相较于JUL,它提供了更多样化的日志服务。
Log4j的日志级别(Level)
Log4J 在 org.apache.log4j.Level 类中定义了OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL七种日志级别:
OFF——最高日志级别,关闭左右日志
FATAL——将会导致应用程序退出的错误
ERROR——发生错误事件,但仍不影响系统的继续运行
WARN——警告,即潜在的错误情形
INFO——粗粒度级别,强调应用程序的运行全程
DEBUG——用于细粒度级别上,对调试应用程序非常有帮助
ALL——最低等级,打开所有日志记录
org.apache.log4j.Priority
我们打开org.apache.log4j.Level类发现,其继承了org.apache.log4j.Priority类,这个类中定义了七个日志级别:
public final static int OFF_INT = Integer.MAX_VALUE;
public final static int FATAL_INT = 50000;
public final static int ERROR_INT = 40000;
public final static int WARN_INT = 30000;
public final static int INFO_INT = 20000;
public final static int DEBUG_INT = 10000;
public final static int ALL_INT = Integer.MIN_VALUE;
可以看出日志的级别是越低,记录的信息就会越多,int值也相应的越来越小。其还提供了一些static final的对象,但已经标注被废弃了。而且这个类的注释上也说了Refrain from using this class directly, use
the {@link Level} class instead(不要直接使用这个类,而是使用Level对象)
org.apache.log4j.Level
Level类在Priority类的基础上又引入了一个级别TRACE,其int值比DEBUG还低,意为更详细的信息。
public static final int TRACE_INT = 5000;
Level类还提供了两个静态方法,分别用于通过字符串和日志级别int值来获取Level对象。
public static Level toLevel(int val, Level defaultLevel) {
switch(val) {
case ALL_INT: return ALL;
case DEBUG_INT: return Level.DEBUG;
case INFO_INT: return Level.INFO;
case WARN_INT: return Level.WARN;
case ERROR_INT: return Level.ERROR;
case FATAL_INT: return Level.FATAL;
case OFF_INT: return OFF;
case TRACE_INT: return Level.TRACE;
default: return defaultLevel;
}
}
public static Level toLevel(String sArg, Level defaultLevel) {
if(sArg == null)
return defaultLevel;
String s = sArg.toUpperCase();
if(s.equals("ALL")) return Level.ALL;
if(s.equals("DEBUG")) return Level.DEBUG;
if(s.equals("INFO")) return Level.INFO;
if(s.equals("WARN")) return Level.WARN;
if(s.equals("ERROR")) return Level.ERROR;
if(s.equals("FATAL")) return Level.FATAL;
if(s.equals("OFF")) return Level.OFF;
if(s.equals("TRACE")) return Level.TRACE;
//
// For Turkish i problem, see bug 40937
//
if(s.equals("\u0130NFO")) return Level.INFO;
return defaultLevel;
}
com.log4j.Logger类
log4j的中心类,除了日志配置,其他的操作基本都靠这个类来完成。Logger类只提供了getLogger、getRootLogger、trace、isTraceEnabled这几个方法,其他的都是靠继承其父类。查看这个类向上还有两层:org.apache.log4j.Category类和org.apache.log4j.spi.AppenderAttachable接口。
org.apache.log4j.spi.AppenderAttachable接口
定义了一些Appender的添加和移除操作。
org.apache.log4j.Category类
通过类注释,我们发现这个类已经被其子类Logger替代了,由于要保持向后兼容性,所以一直存在,但已经废弃了。
在Log4j内部,任何时候生成一个Category的对象,实际上生成的都是其子类Logger的对象。为了保证向后兼容性,之前的方法参数还是Category。
在这里我挑了Category类的几个属性来看
resourceBundle
从名字可以看出这是用来进行资源绑定的,JUL的Logger实现也用到了这个类。在Category中分别提供了setResourceBundle、getResourceBundle、getResourceBundleString三个方法,分别用来设置资源、获取资源和根据key值获取资源。
protected ResourceBundle resourceBundle;
// Set the resource bundle to be used with localized logging methods
// 通过方法注释也说明了resourceBundle:本地化日志记录使用的资源包
public void setResourceBundle(ResourceBundle bundle) {
resourceBundle = bundle;
}
public ResourceBundle getResourceBundle() {
for(Category c = this; c != null; c=c.parent) {
if(c.resourceBundle != null)
return c.resourceBundle;
}
// It might be the case that there is no resource bundle
return null;
}
protected String getResourceBundleString(String key) {
ResourceBundle rb = getResourceBundle();
if(rb == null) {
return null;
} else {
try {
return rb.getString(key);
} catch(MissingResourceException mre) {
error("No resource is associated with key \""+key+"\".");
return null;
}
}
}
在setResourceBundle上游一个@Link到l7dlog这个方法,提示如果要记录本地化信息,使用这个方法会将用户提供的key对应的value值替换为从resourceBundle获取的本地化版本。
public void l7dlog(Priority priority, String key, Throwable t) {
if(repository.isDisabled(priority.level)) {
return;
}
if(priority.isGreaterOrEqual(this.getEffectiveLevel())) {
// 就是这里进行本地化替换的
String msg = getResourceBundleString(key);
if(msg == null) {
msg = key;
}
forcedLog(FQCN, priority, msg, t);
}
}
repository
这是一个LoggerRepository类型的变量,看接口的注释以及接口中的方法,可以推测出这是个接口的作用是创建和检索Logger对象,具体的实现是org.apache.log4j.Hierarchy类。
我们先看一下这个类的构造器,发现是在org.apache.log4j.LogManager中被调用的。Hierarchy实现了exists(String name)、getLogger、setThreshold、isDisabled等方法,我们简单看一下前两者,也能看出Hierarchy管理已创建的Logger是通过将它们放入一个Hashtable中缓存。
public Hierarchy(Logger root) {
// 用于存放key-Logger关系的数据,缓存已创建的Logger
ht = new Hashtable();
listeners = new Vector(1);
this.root = root;
// 设置可被Hierarchy管理的Logger级别,默认为全部
setThreshold(Level.ALL);
this.root.setHierarchy(this);
rendererMap = new RendererMap();
defaultFactory = new DefaultCategoryFactory();
}
public Logger exists(String name) {
// 从Hashtable中拿Logger对象
Object o = ht.get(new CategoryKey(name));
if(o instanceof Logger) {
return (Logger) o;
} else {
return null;
}
}
public Logger getLogger(String name, LoggerFactory factory) {
CategoryKey key = new CategoryKey(name);
Logger logger;
synchronized(ht) {
Object o = ht.get(key);
if(o == null) {
// 如果获取不到指定名称的Logger就创建一个
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateParents(logger);
return logger;
} else if(o instanceof Logger) {
return (Logger) o;
} else if (o instanceof ProvisionNode) {
// 如果获取到的是一个ProvisionNode对象,也创建一个新的Logger
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateChildren((ProvisionNode) o, logger);
updateParents(logger);
return logger;
} else {
return null;
}
}
}
AppenderAttachableImpl
AppenderAttachableImpl是接口AppenderAttachable的直接实现类。由于Category类也实现了AppenderAttachable接口,所以其必须对接口的方法进行实现。持有了AppenderAttachableImpl对象后,AppenderAttachable抽象方法的实现都是在内部调用AppenderAttachableImpl相应的实现。
org.apache.log4j.LogManager类
类的注释上有对它功能的描述:用LogManager类来检索Logger对象或操作当前的LoggerRepository,当LogManager被加载进内存时,默认的初始化过程会被执行。
LogManager的初始化
LogManager类定义了几个常量来获取资源:
/**
* @deprecated This variable is for internal use only. It will
* become package protected in future versions.
* */
static public final String DEFAULT_CONFIGURATION_FILE = "log4j.properties";
static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml";
/**
* @deprecated This variable is for internal use only. It will
* become private in future versions.
* */
static final public String DEFAULT_CONFIGURATION_KEY = "log4j.configuration";
/**
* @deprecated This variable is for internal use only. It will
* become private in future versions.
* */
static final public String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass";
/**
* @deprecated This variable is for internal use only. It will
* become private in future versions.
*/
public static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride";
其中大部分的注释了都表明不推荐使用,可能会在未来的版本将它们私有化,除了DEFAULT_XML_CONFIGURATION_FILE这个常量
static {
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
repositorySelector = new DefaultRepositorySelector(h);
// 先从JVM属性中尝试获取几个名称为常量的属性,看是否能获取到配置
String override =OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY,
null);
if(override == null || "false".equalsIgnoreCase(override)) {
String configurationOptionStr = OptionConverter.getSystemProperty(
DEFAULT_CONFIGURATION_KEY,
null);
String configuratorClassName = OptionConverter.getSystemProperty(
CONFIGURATOR_CLASS_KEY,
null);
URL url = null;
// 如果前面没有获取到,就获取log4j.xml
if(configurationOptionStr == null) {
url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
if(url == null) {
// 如果log4j.xml也没有,就获取log4j.properties
url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
}
} else {
try {
url = new URL(configurationOptionStr);
} catch (MalformedURLException ex) {
// 如果获取的configurationOptionStr不是一个url,那么就从classpath下获取资源
url = Loader.getResource(configurationOptionStr);
}
}
// 如果拿到一个非空的url,将剩下的配置委托给OptionConverter.selectAndConfigure来完成
if(url != null) {
LogLog.debug("Using URL ["+url+"] for automatic log4j configuration.");
try {
OptionConverter.selectAndConfigure(url, configuratorClassName,
LogManager.getLoggerRepository());
} catch (NoClassDefFoundError e) {
LogLog.warn("Error during default initialization", e);
}
} else {
LogLog.debug("Could not find resource: ["+configurationOptionStr+"].");
}
} else {
LogLog.debug("Default initialization of overridden by " +
DEFAULT_INIT_OVERRIDE_KEY + "property.");
}
}
LogManager的其他方法
LoggerManger类还提供了getLogger、exists、resetConfiguration等方法,其内部实现都是调用getLoggerRepository获取LoggerRepository对象后,再调用相应的方法。
static public LoggerRepository getLoggerRepository() {
if (repositorySelector == null) {
repositorySelector = new DefaultRepositorySelector(new NOPLoggerRepository());
guard = null;
Exception ex = new IllegalStateException("Class invariant violation");
String msg = "log4j called after unloading, see http://logging.apache.org/log4j/1.2/faq.html#unload.";
if (isLikelySafeScenario(ex)) {
LogLog.debug(msg, ex);
} else {
LogLog.error(msg, ex);
}
}
return repositorySelector.getLoggerRepository();
}
在getLoggerRepository中可以看到是从一个repositorySelector中拿到的LoggerRepository,repositorySelector是在LoggerManager初始化时赋值的,可以再去看一下static静态块中初始化的内容。
Log4j日志信息格式Layout
Layout的功能就是格式化日志的输出,看其接口,只有一个抽象方法format。format方法接收LoggingEvent类型参数,联想之前分析的JUL日志工具,其实就可以大胆的推测LoggingEvent功能与JUL中的LogRecord是类似的,都是存放日志消息体的。
Log4j提供的layout有以下几种:
- org.apache.log4j.HTMLLayout——以HTML表格形式布局
- org.apache.log4j.PatternLayout——可以灵活地指定布局模式
- org.apache.log4j.SimpleLayout——包含日志信息的级别和信息字符串
- org.apache.log4j.TTCCLayout——包含日志产生的时间、线程、类别等等信息
在实际项目中使用最多的一般是PatternLayout,因为其更灵活,可以通过在配置文件中,配置想要的输出格式。通过在配置文件中设置log4j.appender.console.layout.ConversionPattern这个属性,就可以控制日志输出的格式。常用的配置内容如下:
%m 输出代码中指定的消息;
%M 输出打印该条日志的方法名;
%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL;
%r 输出自应用启动到输出该log信息耗费的毫秒数;
%c 输出所属的类目,通常就是所在类的全名;
%t 输出产生该日志事件的线程名;
%n 输出一个回车换行符,Windows平台为”rn”,Unix平台为”n”;
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2002-10-18 22:10:28,921;
%l 输出日志事件的发生位置,及在代码中的行数
Appender
Log4j的Appender功能等同于JUL中的Handler,是实际进行日志输出的类。具体的日志输出逻辑在org.apache.log4j.AppenderSkeleton类进行doAppend方法的部分实现。
public synchronized void doAppend(LoggingEvent event) {
// 判断Appender是否关闭
if(closed) {
LogLog.error("Attempted to append to closed appender named ["+name+"].");
return;
}
// 是否设置了过滤级别,默认的threshold为null
if(!isAsSevereAsThreshold(event.getLevel())) {
return;
}
// 判断是否能通过过滤器
Filter f = this.headFilter;
FILTER_LOOP:
while(f != null) {
switch(f.decide(event)) {
case Filter.DENY: return;
case Filter.ACCEPT: break FILTER_LOOP;
case Filter.NEUTRAL: f = f.getNext();
}
}
// 这个方法又是一个抽象方法,其实现推迟到具体的子类中去,是真正输出日志的逻辑
this.append(event);
}
Filter
Log4j中的过滤器是一个链式结构,Filter定义了三个常量来代表是否记录日志:
// 不记录日志
public static final int DENY = -1;
// 去过滤器链上的下一个过滤器中判断
public static final int NEUTRAL = 0;
// 记录日志
public static final int ACCEPT = 1;
Log4j本身提供了四个Filter:DenyAllFilter、LevelMatchFilter、LevelRangeFilter、StringMatchFilter。
DenyAllFilter:直接返回是否可以记录日志值,通常用在过滤器链尾。
LevelMatchFilter:判断日志级别是否和Filter设置的级别匹配以决定是否记录日志。
StringMatchFilter:通过日志消息中的字符串来进行判断是否记录日志。
LevelRangeFilter:判断日志级别是否在设置的级别范围内以决定是否记录日志。