Java日志——Log4j的使用及原理

什么是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:判断日志级别是否在设置的级别范围内以决定是否记录日志。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值