log4j2 source code analysis -- Configuration

Keywords: Programming xml Attribute Java log4j

Acquisition and initialization of Configuration

LoggerContext is introduced in the previous article. One of the main functions of this class is to convert the structure of the configuration file into Java objects, such as Appender and Logger, according to the configuration file. Configuration is mainly managed by the configuration class. This is a base class with several important subclasses. Each subclass corresponds to a configuration file. They are:

JSONConfiguration
PropertiesConfiguration
YAMLConfiguration
XMLConfiguration

Each Configuration is managed by its corresponding ConfigurationFactory, which is responsible for managing the file name suffix contained in each type of Configuration configuration and generating corresponding Configuration instances. Therefore, the following call is used in the reconfigure method of LoggerContext to get the Configuration.

ConfigurationFactory.getInstance().getConfiguration()

These initial ConfigurationFactory class information exists in META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat file. In the getInstance method, it is initialized by the collectPlugins method of PluginManager, and then it is consolidated, sorted and put into the factories list of the base class ConfigurationFactory.

The getInstance method has been introduced in the previous article, which will not be covered here. This article focuses on the getconfiguration method.

public Configuration getConfiguration(final String name, final URI configLocation, final ClassLoader loader) {
        if (!isActive()) {
            return null;
        }
        if (loader == null) {
            return getConfiguration(name, configLocation);
        }
        if (isClassLoaderUri(configLocation)) {
            final String path = extractClassLoaderUriPath(configLocation);
            final ConfigurationSource source = getInputFromResource(path, loader);
            if (source != null) {
                final Configuration configuration = getConfiguration(source);
                if (configuration != null) {
                    return configuration;
                }
            }
        }
        return getConfiguration(name, configLocation);
    }

Since there is no classLoader now, this method is skipped. This method is the static internal class Factory class of ConfigurationFactory

public Configuration getConfiguration(final String name, final URI configLocation) {

            if (configLocation == null) {
                final String configLocationStr = this.substitutor.replace(PropertiesUtil.getProperties()
                        .getStringProperty(CONFIGURATION_FILE_PROPERTY));
                if (configLocationStr != null) {
                    final String[] sources = configLocationStr.split(",");
                    if (sources.length > 1) {
                        final List<AbstractConfiguration> configs = new ArrayList<>();
                        for (final String sourceLocation : sources) {
                            final Configuration config = getConfiguration(sourceLocation.trim());
                            if (config != null && config instanceof AbstractConfiguration) {
                                configs.add((AbstractConfiguration) config);
                            } else {
                                LOGGER.error("Failed to created configuration at {}", sourceLocation);
                                return null;
                            }
                        }
                        return new CompositeConfiguration(configs);
                    }
                    return getConfiguration(configLocationStr);
                }
                for (final ConfigurationFactory factory : getFactories()) {
                    final String[] types = factory.getSupportedTypes();
                    if (types != null) {
                        for (final String type : types) {
                            if (type.equals("*")) {
                                final Configuration config = factory.getConfiguration(name, configLocation);
                                if (config != null) {
                                    return config;
                                }
                            }
                        }
                    }
                }
            } else {
                // configLocation != null
                final String configLocationStr = configLocation.toString();
                for (final ConfigurationFactory factory : getFactories()) {
                    final String[] types = factory.getSupportedTypes();
                    if (types != null) {
                        for (final String type : types) {
                            if (type.equals("*") || configLocationStr.endsWith(type)) {
                                final Configuration config = factory.getConfiguration(name, configLocation);
                                if (config != null) {
                                    return config;
                                }
                            }
                        }
                    }
                }
            }

            Configuration config = getConfiguration(true, name);
            if (config == null) {
                config = getConfiguration(true, null);
                if (config == null) {
                    config = getConfiguration(false, name);
                    if (config == null) {
                        config = getConfiguration(false, null);
                    }
                }
            }
            if (config != null) {
                return config;
            }
            LOGGER.error("No log4j2 configuration file found. Using default configuration: logging only errors to the console.");
            return new DefaultConfiguration();
        }

The code is very long, and the actual function is quite clear, that is to match the corresponding files according to the order of file suffixes in each ConfigurationFactory, and then return the corresponding Configuration instance. The order of matching is as follows:

1,log4j.configurationFile---ConfigurationFactory 
2,log4j2-test.properties---PropertiesConfigurationFactory
3,log4j2-test.yaml/log4j2-test.yml---YAMLConfigurationFactory
4,log4j2-test.json/log4j2-test.jsn---JsonConfigurationFactory
5,log4j2-test.xml---XMLConfigurationFactory
6,log4j2.properties---PropertiesConfigurationFactory
7,log4j2.yaml/log4j2.yml---YAMLConfigurationFactory
8,log4j2.json/log4j2.jsn---JSONConfigurationFactory
9,log4j2.xml---XMLConfigurationFactory
10,console Console---DefaultConfiguration

In this paper, take the XML file as an example. We put the configuration file in the corresponding log4j2.xml. The content of the configuration file is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="info">
    <appenders>
        <Console name="Console" target="SYSTEM_OUT" >
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
        </Console>
        <File name="File"  fileName="logs/test.log">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
        </File>
    </appenders>

    <loggers>
        <!--Define a name"test"Of Appender,Category is Console-->
        <logger name="test" additivity="false">
            <appender-ref ref="Console"/>
        </logger>
        <root level="info">
            <appender-ref ref="File"/>
        </root>
    </loggers>
</configuration>

Briefly analyze the structure. There is a root configuration, which starts with the configuration tab. There is a status attribute of info, and then two Appenders are defined. The two Appenders are Console and File. In the Console appender, there is a target that is system ﹐ out, and a PatternLayout that defines the format of the log. The FileAppender has a fileName and a P atternLayout. Finally, two loggers are defined, one is rootLogger, and a reference to an appender is defined. The reference is FileAppender, and another is named test. The additivity property is false. Then an appender is referenced, which is the ConsoleAppender defined earlier. According to the content of this configuration File, configuration will automatically resolve. Let's continue to look at the code.

In the above configuration matching, we will get an XMLConfigurationFactory, which contains the file name, file content, etc. then we will call the start method of config in setConfiguration. For this method, we will first call the start method of AbstractConfiguration, and the code is as follows:

 public void start() {
        // Preserve the prior behavior of initializing during start if not initialized.
        if (getState().equals(State.INITIALIZING)) {
            initialize();
        }
        LOGGER.debug("Starting configuration {}", this);
        this.setStarting();
        if (watchManager.getIntervalSeconds() > 0) {
            watchManager.start();
        }
        if (hasAsyncLoggers()) {
            asyncLoggerConfigDisruptor.start();
        }
        final Set<LoggerConfig> alreadyStarted = new HashSet<>();
        for (final LoggerConfig logger : loggerConfigs.values()) {
            logger.start();
            alreadyStarted.add(logger);
        }
        for (final Appender appender : appenders.values()) {
            appender.start();
        }
        if (!alreadyStarted.contains(root)) { // LOG4J2-392
            root.start(); // LOG4J2-336
        }
        super.start();
        LOGGER.debug("Started configuration {} OK.", this);
    }

This includes the following key steps:

1. Initialization
2. Set lifecycle state
3. Start all logger s
4. Start all Appenders

During the initialization process, all elements in the configuration file will be converted into corresponding Logger or Appender objects, and then they will be started. First, we enter the initialization method

public void initialize() {
        LOGGER.debug("Initializing configuration {}", this);
        subst.setConfiguration(this);
        scriptManager = new ScriptManager(watchManager);
        pluginManager.collectPlugins(pluginPackages);
        final PluginManager levelPlugins = new PluginManager(Level.CATEGORY);
        levelPlugins.collectPlugins(pluginPackages);
        final Map<String, PluginType<?>> plugins = levelPlugins.getPlugins();
        if (plugins != null) {
            for (final PluginType<?> type : plugins.values()) {
                try {
                    // Cause the class to be initialized if it isn't already.
                    Loader.initializeClass(type.getPluginClass().getName(), type.getPluginClass().getClassLoader());
                } catch (final Exception e) {
                    LOGGER.error("Unable to initialize {} due to {}", type.getPluginClass().getName(), e.getClass()
                            .getSimpleName(), e);
                }
            }
        }
        setup();
        setupAdvertisement();
        doConfigure();
        setState(State.INITIALIZED);
        LOGGER.debug("Configuration {} initialized", this);
    }

This method first implements the search for plug-ins with category level. There is no plug-in by default, so it enters the setup method. The core of the setup is to parse the configuration file according to the incoming xml dom tree. The parsed object is Node object, and the parsed method is construct hierarchy (RootNode, rootelement). The specific method is as follows:

private void constructHierarchy(final Node node, final Element element) {
        processAttributes(node, element);
        final StringBuilder buffer = new StringBuilder();
        final NodeList list = element.getChildNodes();
        final List<Node> children = node.getChildren();
        for (int i = 0; i < list.getLength(); i++) {
            final org.w3c.dom.Node w3cNode = list.item(i);
            if (w3cNode instanceof Element) {
                final Element child = (Element) w3cNode;
                final String name = getType(child);
                final PluginType<?> type = pluginManager.getPluginType(name);
                final Node childNode = new Node(node, name, type);
                constructHierarchy(childNode, child);
                if (type == null) {
                    final String value = childNode.getValue();
                    if (!childNode.hasChildren() && value != null) {
                        node.getAttributes().put(name, value);
                    } else {
                        status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
                    }
                } else {
                    children.add(childNode);
                }
            } else if (w3cNode instanceof Text) {
                final Text data = (Text) w3cNode;
                buffer.append(data.getData());
            }
        }

        final String text = buffer.toString().trim();
        if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
            node.setValue(text);
        }
    }

Before introducing this parsing method, let's first look at the definition of Node class

public class Node {
    public static final String CATEGORY = "Core";
    private final Node parent;
    private final String name;
    private String value;
    private final PluginType<?> type;
    private final Map<String, String> attributes = new HashMap<>();
    private final List<Node> children = new ArrayList<>();
    private Object object;
}

From the definition of Node, it can be seen that this is a typical attribute structure. Each Node has a parent Node and a list of children's nodes. Through getParent and getChildren, the entire Node tree can be operated conveniently

Let's talk about this method again. It can be seen clearly that this is a deep priority process to traverse the entire xml dom and build a Node tree, mainly to set a series of information such as attribute, type, children, parent, etc., in which type is the full qualifier of the java object to be loaded later. After this method, the configuration file above is parsed into a Node tree as shown below.

After the Node tree has been constructed, the corresponding Appender and Logger need to be constructed according to the tree. This method is completed by the doConfigure method. The specific definition is as follows:

 protected void doConfigure() {
        preConfigure(rootNode);
        configurationScheduler.start();
        if (rootNode.hasChildren() && rootNode.getChildren().get(0).getName().equalsIgnoreCase("Properties")) {
            final Node first = rootNode.getChildren().get(0);
            createConfiguration(first, null);
            if (first.getObject() != null) {
                subst.setVariableResolver((StrLookup) first.getObject());
            }
        } else {
            final Map<String, String> map = this.getComponent(CONTEXT_PROPERTIES);
            final StrLookup lookup = map == null ? null : new MapLookup(map);
            subst.setVariableResolver(new Interpolator(lookup, pluginPackages));
        }

        boolean setLoggers = false;
        boolean setRoot = false;
        for (final Node child : rootNode.getChildren()) {
            if (child.getName().equalsIgnoreCase("Properties")) {
                if (tempLookup == subst.getVariableResolver()) {
                    LOGGER.error("Properties declaration must be the first element in the configuration");
                }
                continue;
            }
            createConfiguration(child, null);
            if (child.getObject() == null) {
                continue;
            }
            if (child.getName().equalsIgnoreCase("Scripts")) {
                for (final AbstractScript script : child.getObject(AbstractScript[].class)) {
                    if (script instanceof ScriptRef) {
                        LOGGER.error("Script reference to {} not added. Scripts definition cannot contain script references",
                                script.getName());
                    } else {
                        scriptManager.addScript(script);
                    }
                }
            } else if (child.getName().equalsIgnoreCase("Appenders")) {
                appenders = child.getObject();
            } else if (child.isInstanceOf(Filter.class)) {
                addFilter(child.getObject(Filter.class));
            } else if (child.getName().equalsIgnoreCase("Loggers")) {
                final Loggers l = child.getObject();
                loggerConfigs = l.getMap();
                setLoggers = true;
                if (l.getRoot() != null) {
                    root = l.getRoot();
                    setRoot = true;
                }
            } else if (child.getName().equalsIgnoreCase("CustomLevels")) {
                customLevels = child.getObject(CustomLevels.class).getCustomLevels();
            } else if (child.isInstanceOf(CustomLevelConfig.class)) {
                final List<CustomLevelConfig> copy = new ArrayList<>(customLevels);
                copy.add(child.getObject(CustomLevelConfig.class));
                customLevels = copy;
            } else {
                final List<String> expected = Arrays.asList("\"Appenders\"", "\"Loggers\"", "\"Properties\"",
                        "\"Scripts\"", "\"CustomLevels\"");
                LOGGER.error("Unknown object \"{}\" of type {} is ignored: try nesting it inside one of: {}.",
                        child.getName(), child.getObject().getClass().getName(), expected);
            }
        }

        if (!setLoggers) {
            LOGGER.warn("No Loggers were configured, using default. Is the Loggers element missing?");
            setToDefault();
            return;
        } else if (!setRoot) {
            LOGGER.warn("No Root logger was configured, creating default ERROR-level Root logger with Console appender");
            setToDefault();
            // return; // LOG4J2-219: creating default root=ok, but don't exclude configured Loggers
        }

        for (final Map.Entry<String, LoggerConfig> entry : loggerConfigs.entrySet()) {
            final LoggerConfig loggerConfig = entry.getValue();
            for (final AppenderRef ref : loggerConfig.getAppenderRefs()) {
                final Appender app = appenders.get(ref.getRef());
                if (app != null) {
                    loggerConfig.addAppender(app, ref.getLevel(), ref.getFilter());
                } else {
                    LOGGER.error("Unable to locate appender \"{}\" for logger config \"{}\"", ref.getRef(),
                            loggerConfig);
                }
            }

        }

        setParents();
    }

The main work of this method is to automatically assign values to the object field of Node according to the Node dependency tree above. At the same time, all logconfigs are built into a list, and corresponding logconfigs can be obtained by name. In LogConfig, the corresponding relationships of the appender, layout and other instances corresponding to each logger are maintained, and all loggers and Appenders are started at the same time. For each logger, a LogConfig of its own will be maintained, but the class will be encapsulated to an instance of the PrivateConfig class. In the next article, I will show you how to create a corresponding logger based on name, that is, the general process of getLogger method.

Posted by borabora12 on Wed, 08 Jan 2020 02:00:15 -0800