The LoggerRepository and Configurator

LoggerRepository of the Log4J source code are literally understood. It is a Logger container that creates and caches Logger instances so that Logger instances with the same name are not created multiple times to improve performance. This feature is somewhat similar to Spring's IOC concept. Log4J supports two configuration files: properties file and xml file. The Configurator parses the configuration file and adds the parsed information to the LoggerRepository. LogManager eventually integrates LoggerRepository and Configurator.

LoggerRepository interface

LoggerRepository is a Logger container, which is responsible for creating and caching Logger instances. It also maintains the relationship between Loggers. Because in Log4J, all Loggers are assembled with roots of RootLogger. A tree, the level of the tree is determined by the Logger's Name, which is separated by '.'.

In addition to being a Logger container, it also has a Threshold attribute that filters all logs below the Threshold level. And other methods and properties related to Logger operations. The

LoggerRepository interface is defined as follows:

 1 public interface LoggerRepository {  2     public void addHierarchyEventListener(HierarchyEventListener listener);  3     boolean isDisabled(int level);  4     public void setThreshold(Level level);  5     public void setThreshold(String val);  6     public void emitNoAppenderWarning(Category cat);  7     public Level getThreshold();  8     public Logger getLogger(String name);  9     public Logger getLogger(String name, LoggerFactory factory); 10     public Logger getRootLogger(); 11     public abstract Logger exists(String name); 12     public abstract void shutdown(); 13     public Enumeration getCurrentLoggers(); 14     public abstract void fireAddAppenderEvent(Category logger, Appender appender); 15     public abstract void resetConfiguration(); 16 }

Hierarchy Class

Hierarchy is the default implementation of LoggerRepository in Log4J. It is used to express its internal Logger stored in a hierarchical structure. In the implementation of the LoggerRepository interface, the getLogger() method is its core implementation, so start with this method first.

Hierarchy uses a Hashtable to store all Logger instances. It uses the CategoryKey as the key and the Logger as the value. The CategoryKey is the encapsulation of the Name string in the Logger. The reason for introducing this class is for performance reasons. It caches the hash code of the Name string so that it can be retrieved directly during the lookup process without calculating the hash code. The

 1 class CategoryKey {  2     String name;  3     int hashCache;  4   5     CategoryKey(String name) {  6         this.name = name;  7         hashCache = name.hashCode();  8     }  9     final public int hashCode() { 10         return hashCache; 11     } 12     final public boolean equals(Object rArg) { 13         if (this == rArg) 14             return true; 15         if (rArg != null && CategoryKey.class == rArg.getClass()) 16             return name.equals(((CategoryKey) rArg).name); 17         else 18             return false; 19     } 20 }

getLogger() method has an overloaded function that provides the LoggerFactory interface. It is used to create a corresponding Logger instance when the Logger instance is not found in the LoggerRepository. The default implementation directly creates a Logger instance, which can be implemented by a custom LoggerFactory. Create your own Logger instance. The

1 public interface LoggerFactory { 2     public Logger makeNewLoggerInstance(String name); 3 } 4 class DefaultCategoryFactory implements LoggerFactory { 5     public Logger makeNewLoggerInstance(String name) { 6         return new Logger(name); 7     } 8 }

getLogger() method first creates a CategoryKey instance based on the incoming name, and then looks up from the cached ht field:

1. If the corresponding Logger instance is found, it returns directly to the instance.

2. If no instance is found, use LoggerFactory to create a new Logger instance, cache the instance in the ht collection, and update the parent property of the newly created Logger instance. The easiest way to update the parent attribute is to use the '.' as a delimiter to intercept the string from the back. Use the truncated string to find out if there is a Logger instance from the ht collection. If it exists, the parent of the newly created Logger instance. That is, the found instance, if the corresponding parent instance is not found during the entire traversal process, its parent instance is root. However, if an "xyzw" Logger is initially set to root and then the "xyz" Logger instance appears, then the "xyzw" Logger's parent needs to be updated to the "xyz" Logger instance. At this point, you will find a way to find the collection. The problem with the "xyz" Logger instance child node already exists. Of course, a simple approach is to iterate through all the instances in the ht collection and determine if the instance is a child of the "x.y.z" Logger instance, and then update its parent node. Since each traversal will cause some performance problems, Log4J uses ProvisionNode to save all possible related child nodes in advance, and adds the ProvisionNode instance to the ht collection, so that you can find all relevant dependencies by finding the corresponding ProvisionNode instance. The child nodes are gone. For example, for the "xyzw" Logger instance, it will generate three ProvisionNode instances (of course, if the corresponding instance already exists, it will be added directly without creating it. In addition, if the corresponding node is already a Logger instance, then the parent of the "xyzw" Logger instance will be created. Directly point to it): ProvisionNode ("x"), ProvisionNode ("xy"), ProvisionNode ("xyz"), they all store the "xyzw" Logger instance as its child node.

 1 class ProvisionNode extends Vector {  2     ProvisionNode(Logger logger) {  3         super();  4         this.addElement(logger);  5     }  6 }  7 final private void updateParents(Logger cat) {  8     String name = cat.name;  9     int length = name.length(); 10     boolean parentFound = false; 11     // if name = "x.y.z.w", loop thourgh "x.y.z", "x.y" and "x" 12     for (int i = name.lastIndexOf('.', length - 1); i >= 0; i = name 13             .lastIndexOf('.', i - 1)) { 14         String substr = name.substring(0, i); 15         CategoryKey key = new CategoryKey(substr);  16         Object o = ht.get(key); 17         if (o == null) { 18             ProvisionNode pn = new ProvisionNode(cat); 19             ht.put(key, pn); 20         } else if (o instanceof Category) { 21             parentFound = true; 22             cat.parent = (Category) o; 23             break; // no need to update the ancestors of the closest 24                     // ancestor 25         } else if (o instanceof ProvisionNode) { 26             ((ProvisionNode) o).addElement(cat); 27         } else { 28             Exception e = new IllegalStateException( 29                     "unexpected object type " + o.getClass() + " in ht."); 30             e.printStackTrace(); 31         } 32     } 33     // If we could not find any existing parents, then link with root. 34     if (!parentFound) 35         cat.parent = root; 36 }

3. If you find a ProvisionNode instance, first create a new Logger instance using the factory, add the instance to the ht collection, and then update the parent field of all Loggers found in the ProvisionNode and the parent field of the newly created Logger. During the update process, you need to note that the Logger instance in ProvisionNode already points to the correct parent, so just update those parent attributes in the ProvisionNode that the Logger instance points to higher than the newly created Logger itself. For example, if you start to insert the "xyz" Logger instance and then insert the "xyzw" Logger instance, the ProvisionNode("x") thinks that the "xyz" Logger instance and the "xyzw" Logger instance are both its children, and then insert the "x" Logger. For example, you only need to update the parent node of the "xyz" Logger to the "x" Logger instance instead of updating the parent node of the "xyzw" Logger instance.

 1 final private void updateChildren(ProvisionNode pn, Logger logger) {  2     final int last = pn.size();  3     for (int i = 0; i < last; i++) {  4         Logger l = (Logger) pn.elementAt(i);  5         // Unless this child already points to a correct (lower) parent,  6         // make cat.parent point to l.parent and l.parent to cat.  7         if (!l.parent.name.startsWith(logger.name)) {  8             logger.parent = l.parent;  9             l.parent = logger; 10         } 11     } 12 }

综合, getLogger() method implementation code is as follows:

 1 public Logger getLogger(String name, LoggerFactory factory) {  2     CategoryKey key = new CategoryKey(name);  3     Logger logger;  4     synchronized (ht) {  5         Object o = ht.get(key);  6         if (o == null) {  7             logger = factory.makeNewLoggerInstance(name);  8             logger.setHierarchy(this);  9             ht.put(key, logger); 10             updateParents(logger); 11             return logger; 12         } else if (o instanceof Logger) { 13             return (Logger) o; 14         } else if (o instanceof ProvisionNode) { 15             logger = factory.makeNewLoggerInstance(name); 16             logger.setHierarchy(this); 17             ht.put(key, logger); 18             updateChildren((ProvisionNode) o, logger); 19             updateParents(logger); 20             return logger; 21         } else { 22             // It should be impossible to arrive here 23             return null; // but let's keep the compiler happy. 24         } 25     } 26 }

Other method implementation is relatively simple, for LoggerRepository, it can also register HierarchyEventListener listener like it, whenever you add to a Logger or Remove the Appender and the listener will fire.

 1 public interface HierarchyEventListener {  2     public void addAppenderEvent(Category cat, Appender appender);  3     public void removeAppenderEvent(Category cat, Appender appender);  4 }  5 private Vector listeners;  6 public void addHierarchyEventListener(HierarchyEventListener listener) {  7     if (listeners.contains(listener)) {  8         LogLog.warn("Ignoring attempt to add an existent listener.");  9     } else { 10         listeners.addElement(listener); 11     } 12 } 13 public void fireAddAppenderEvent(Category logger, Appender appender) { 14     if (listeners != null) { 15         int size = listeners.size(); 16         HierarchyEventListener listener; 17         for (int i = 0; i < size; i++) { 18             listener = (HierarchyEventListener) listeners.elementAt(i); 19             listener.addAppenderEvent(logger, appender); 20         } 21     } 22 } 23 void fireRemoveAppenderEvent(Category logger, Appender appender) { 24     if (listeners != null) { 25         int size = listeners.size(); 26         HierarchyEventListener listener; 27         for (int i = 0; i < size; i++) { 28             listener = (HierarchyEventListener) listeners.elementAt(i); 29             listener.removeAppenderEvent(logger, appender); 30         } 31     } 32 }

The threshold field is saved in Hierarchy, and the user can set the threshold. For the root instance, it is specified when Hierarchy is sufficient. The getCurrentLoggers() method takes all the Logger instances in the ht collection. The shutdown() method iterates through all the Logger instances and the root instance, calls all the Appender's close() methods attached to it, and removes all Appender instances from the Logger, and finally triggers the AppenderRemove event. The resetConfiguration() method initializes the root field, calls the shutdown() method to remove all Appenders in the Logger, initializes all Logger instances, and removes them from the LoggerRepository, clearing the data in the rendererMap and the throwableRender. The

RendererSupport interface

RendererSupport interface allows the user to set the corresponding ObjectRender instance for different classes, so that the toString() method can be called from the rendered class instead of the default.

1 public interface RendererSupport { 2 public RendererMap getRendererMap(); 3 public void setRenderer(Class renderedClass, ObjectRenderer renderer); 4 } 5 The Hierarchy class implements the RenderedSupprt interface, and its implementation is simple: 6 RendererMap rendererMap; 7 public Hierarchy(Logger root) { 8  9     rendererMap = new RendererMap(); 10      11 } 12 public void addRenderer(Class classToRender, ObjectRenderer or) { 13     rendererMap.put(classToRender, or); 14 } 15 public RendererMap getRendererMap() { 16     return rendererMap; 17 } 18 public void setRenderer(Class renderedClass, ObjectRenderer renderer) { 19     rendererMap.put(renderedClass, renderer); 20 }

In the RendererMap class implementation, it uses Hastable to save the rendered class instance and the corresponding ObjectRender instance. When looking for a class that has a registered render class, if it doesn't find it, you need to try its parent class. Whether the interface has the corresponding ObjectRender class registered, and if it is not found, it returns the default ObjectRender. The

 1 public ObjectRenderer get(Class clazz) {  2     ObjectRenderer r = null;  3     for (Class c = clazz; c != null; c = c.getSuperclass()) {  4         r = (ObjectRenderer) map.get(c);  5         if (r != null) {  6             return r;  7         }  8         r = searchInterfaces(c);  9         if (r != null) 10             return r; 11     } 12     return defaultRenderer; 13 } 14 ObjectRenderer searchInterfaces(Class c) { 15     ObjectRenderer r = (ObjectRenderer) map.get(c); 16     if (r != null) { 17         return r; 18     } else { 19         Class[] ia = c.getInterfaces(); 20         for (int i = 0; i < ia.length; i++) { 21             r = searchInterfaces(ia[i]); 22             if (r != null) 23                 return r; 24         } 25     } 26     return null; 27 } 28 public ObjectRenderer getDefaultRenderer() { 29     return defaultRenderer; 30 } 31 public void put(Class clazz, ObjectRenderer or) { 32     map.put(clazz, or); 33 }

ThrowableRendererSupport interface

ThrowableRendererSupport interface is used to support setting and getting the ThrowableRenderer so that the user can customize the rendering of the Throwable object. The

1 public interface ThrowableRendererSupport { 2     ThrowableRenderer getThrowableRenderer(); 3     void setThrowableRenderer(ThrowableRenderer renderer); 4 }

Hierarchy class implements this interface as an attribute, so each Hierarchy instance can only have one global ThrowableRenderer, rather than defining different renders for different classes like ObjectRender. At the time, this design was reasonable, because the main rendering of Throwable is the rendering of its stack. Others are not much different, and it is better to keep the same format for the stack rendering. The

 1 private ThrowableRenderer throwableRenderer = null;  2 public Hierarchy(Logger root) {  3       4     defaultFactory = new DefaultCategoryFactory();  5       6 }  7 public void setThrowableRenderer(final ThrowableRenderer renderer) {  8     throwableRenderer = renderer;  9 } 10 public ThrowableRenderer getThrowableRenderer() { 11     return throwableRenderer; 12 }

Configurator interface

Configurator interface is used to define the parsing of configuration files. All the information parsed in the configuration file in Log4J can be placed in the LoggerRepository, so the definition of the Configurator interface is very simple.

1 public interface Configurator { 2     public static final String INHERITED = "inherited"; 3     public static final String NULL = "null"; 4     void doConfigure(URL url, LoggerRepository repository); 5 }

Log4J supports two file configuration files: properties file and xml file. They correspond to the PropertyConfigurator class and the DOMConfigurator class. The

PropertyConfigurator class

PropertyConfigurator class parses the configuration information in the properties file. You can set log4j.debug to true to open the log information inside Log4J. In addition, the PropertyConfigurator also supports Linux-style variables, ie all ${variable } The variable of the form will be replaced by the corresponding attribute in the system or the attribute defined inside the configuration file (first find the attribute in the system, then look up the attribute defined inside the configuration file); but the PropertyConfigurator does not support some advanced functions in Log4J, such as Define ErrorHandler and define AsyncAppender and so on. The most important method in

Configurator is the doConfigure() method. In the PropertyConfigurator implementation, the URL corresponding to the configuration file is first read into the Properties object:

 1 public void doConfigure(java.net.URL configURL, LoggerRepository hierarchy) {  2     Properties props = new Properties();  3       4         uConn = configURL.openConnection();  5         uConn.setUseCaches(false);  6         istream = uConn.getInputStream();  7         props.load(istream);  8       9     doConfigure(props, hierarchy); 10 }

and then check if log4j.debug, log4j.reset is set. , log4j.threshold and other attributes, if there are, make the corresponding settings. Here, the property search and variable information replacement are implemented by the OptionConverter.findAndSubst() method.

 1 public void doConfigure(Properties properties, LoggerRepository hierarchy) {  2     repository = hierarchy;  3     String value = properties.getProperty(LogLog.DEBUG_KEY);  4     if (value == null) {  5         value = properties.getProperty("log4j.configDebug");  6         if (value != null)  7             LogLog.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");  8     }  9     if (value != null) { 10         LogLog.setInternalDebugging(OptionConverter.toBoolean(value, true)); 11     } 12     String reset = properties.getProperty(RESET_KEY); 13     if (reset != null && OptionConverter.toBoolean(reset, false)) { 14         hierarchy.resetConfiguration(); 15     } 16     String thresholdStr = OptionConverter.findAndSubst(THRESHOLD_PREFIX, 17             properties); 18     if (thresholdStr != null) { 19         hierarchy.setThreshold(OptionConverter.toLevel(thresholdStr, 20                 (Level) Level.ALL)); 21         LogLog.debug("Hierarchy threshold set to [" 22                 + hierarchy.getThreshold() + "]."); 23     } 24      25 }

Then parse the configuration information in three steps:

1. Parse the Root Logger configuration

first find the value of log4j.rootLogger, which is separated by a comma ',', where the first value is root Level information, followed by the Appender name to be added to root. For Level information, set it directly to root. For the Appender name, continue to parse.

 1 void parseCategory(Properties props, Logger logger, String optionKey,  2         String loggerName, String value) {  3     LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value  4             + "].");  5     StringTokenizer st = new StringTokenizer(value, ",");  6     if (!(value.startsWith(",") || value.equals(""))) {  7         if (!st.hasMoreTokens())  8             return;  9         String levelStr = st.nextToken(); 10         LogLog.debug("Level token is [" + levelStr + "]."); 11         if (INHERITED.equalsIgnoreCase(levelStr) 12                 || NULL.equalsIgnoreCase(levelStr)) { 13             if (loggerName.equals(INTERNAL_ROOT_NAME)) { 14                 LogLog.warn("The root logger cannot be set to null."); 15             } else { 16                 logger.setLevel(null); 17             } 18         } else { 19             logger.setLevel(OptionConverter.toLevel(levelStr, 20                     (Level) Level.DEBUG)); 21         } 22         LogLog.debug("Category " + loggerName + " set to " 23                 + logger.getLevel()); 24     } 25     logger.removeAllAppenders(); 26     Appender appender; 27     String appenderName; 28     while (st.hasMoreTokens()) { 29         appenderName = st.nextToken().trim(); 30         if (appenderName == null || appenderName.equals(",")) 31             continue; 32         LogLog.debug("Parsing appender named \"" + appenderName + "\"."); 33         appender = parseAppender(props, appenderName); 34         if (appender != null) { 35             logger.addAppender(appender); 36         } 37     } 38 }

The same Appender can be added to different Loggers, so the PropertyConfigurator caches the Appender. If the corresponding Appender class can be found from the cache, it will directly return the Appender found.

Then parse the following key name and attribute information of the corresponding class:

log4j.appender.appenderName=…

log4j.appender.appenderName.layout=…

log4j.appender.appenderName.errorhandler=…

log4j.appender.appenderName.filter.filterKey.name=…

 1 Appender parseAppender(Properties props, String appenderName) {  2     Appender appender = registryGet(appenderName);  3     if ((appender != null)) {  4         LogLog.debug("Appender \"" + appenderName  5                 + "\" was already parsed.");  6         return appender;  7     }  8     String prefix = APPENDER_PREFIX + appenderName;  9     String layoutPrefix = prefix + ".layout"; 10     appender = (Appender) OptionConverter.instantiateByKey(props, prefix, 11             org.apache.log4j.Appender.class, null); 12     if (appender == null) { 13         LogLog.error("Could not instantiate appender named \"" 14                 + appenderName + "\"."); 15         return null; 16     } 17     appender.setName(appenderName); 18     if (appender instanceof OptionHandler) { 19         if (appender.requiresLayout()) { 20             Layout layout = (Layout) OptionConverter.instantiateByKey( 21                     props, layoutPrefix, Layout.class, null); 22             if (layout != null) { 23                 appender.setLayout(layout); 24                 LogLog.debug("Parsing layout options for \"" + appenderName 25                         + "\"."); 26                 PropertySetter.setProperties(layout, props, layoutPrefix 27                         + "."); 28                 LogLog.debug("End of parsing for \"" + appenderName + "\"."); 29             } 30         } 31         final String errorHandlerPrefix = prefix + ".errorhandler"; 32         String errorHandlerClass = OptionConverter.findAndSubst( 33                 errorHandlerPrefix, props); 34         if (errorHandlerClass != null) { 35             ErrorHandler eh = (ErrorHandler) OptionConverter 36                     .instantiateByKey(props, errorHandlerPrefix, 37                             ErrorHandler.class, null); 38             if (eh != null) { 39                 appender.setErrorHandler(eh); 40                 LogLog.debug("Parsing errorhandler options for \"" 41                         + appenderName + "\"."); 42                 parseErrorHandler(eh, errorHandlerPrefix, props, repository); 43                 final Properties edited = new Properties(); 44                 final String[] keys = new String[] { 45                         errorHandlerPrefix + "." + ROOT_REF, 46                         errorHandlerPrefix + "." + LOGGER_REF, 47                         errorHandlerPrefix + "." + APPENDER_REF_TAG }; 48                 for (Iterator iter = props.entrySet().iterator(); iter 49                         .hasNext();) { 50                     Map.Entry entry = (Map.Entry) iter.next(); 51                     int i = 0; 52                     for (; i < keys.length; i++) { 53                         if (keys[i].equals(entry.getKey())) 54                             break; 55                     } 56                     if (i == keys.length) { 57                         edited.put(entry.getKey(), entry.getValue()); 58                     } 59                 } 60                 PropertySetter.setProperties(eh, edited, errorHandlerPrefix 61                         + "."); 62                 LogLog.debug("End of errorhandler parsing for \"" 63                         + appenderName + "\"."); 64             } 65         } 66         PropertySetter.setProperties(appender, props, prefix + "."); 67         LogLog.debug("Parsed \"" + appenderName + "\" options."); 68     } 69     parseAppenderFilters(props, appenderName, appender); 70     registryPut(appender); 71     return appender; 72 }

2. Parse the LoggerFactory configuration

to find the value of log4j.loggerFactory, save the created LoggerFactory instance, use log4j.loggerFactory.propName Set the properties of the LoggerFactory instance.

 1 protected void configureLoggerFactory(Properties props) {  2     String factoryClassName = OptionConverter.findAndSubst(  3             LOGGER_FACTORY_KEY, props);  4     if (factoryClassName != null) {  5         LogLog.debug("Setting category factory to [" + factoryClassName  6                 + "].");  7         loggerFactory = (LoggerFactory) OptionConverter  8                 .instantiateByClassName(factoryClassName,  9                         LoggerFactory.class, loggerFactory); 10         PropertySetter.setProperties(loggerFactory, props, FACTORY_PREFIX 11                 + "."); 12     } 13 }

3. Parse non-Root Logger and ObjectRender configuration

to parse log4j.logger., log4j.renderer., log4j.throwableRenderer.

In addition, the PropertyConfigurator also supports checking each time period through the PropertyWatchLog class. If the configuration file is found to be changed, the configuration information is automatically reloaded.

DOMConfigurator class

DOMConfigurator uses DOM to parse all the elements in the Log4J configuration file, and find the corresponding RootLoger, Logger, Appender, Layout and other modules according to the elements in the DOM. In addition, DOMConfigurator also supports checking the file for modification at regular intervals, and if so, reloading the newly modified configuration file. The implementation of DOMConfigurator here is similar to the implementation of PropertyConfigurator, and will not be described in detail.

LogManager class

The LogManager integrates the Configurator with the LoggerRepository, which finds the Log4J configuration file at initialization time and parses it into the LoggerRepository.

 1 static {  2     Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));  3     repositorySelector = new DefaultRepositorySelector(h);  4     String override = OptionConverter.getSystemProperty(  5             DEFAULT_INIT_OVERRIDE_KEY, null);  6     if (override == null || "false".equalsIgnoreCase(override)) {  7         String configurationOptionStr = OptionConverter.getSystemProperty(  8                 DEFAULT_CONFIGURATION_KEY, null);  9         String configuratorClassName = OptionConverter.getSystemProperty( 10                 CONFIGURATOR_CLASS_KEY, null); 11         URL url = null; 12         if (configurationOptionStr == null) { 13             url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE); 14             if (url == null) { 15                 url = Loader.getResource(DEFAULT_CONFIGURATION_FILE); 16             } 17         } else { 18             try { 19                 url = new URL(configurationOptionStr); 20             } catch (MalformedURLException ex) { 21                 url = Loader.getResource(configurationOptionStr); 22             } 23         } 24         if (url != null) { 25             LogLog.debug("Using URL [" + url 26                     + "] for automatic log4j configuration."); 27             try { 28                 OptionConverter.selectAndConfigure(url, 29                         configuratorClassName, 30                         LogManager.getLoggerRepository()); 31             } catch (NoClassDefFoundError e) { 32                 LogLog.warn("Error during default initialization", e); 33             } 34         } else { 35             LogLog.debug("Could not find resource: [" 36                     + configurationOptionStr + "]."); 37         } 38     } else { 39         LogLog.debug("Default initialization of overridden by " 40                 + DEFAULT_INIT_OVERRIDE_KEY + "property."); 41     } 42 }