log4j2工作原理源碼剖析


前言

本文建立在log4j-core 2.12.1版本為基礎,在此基礎上進行的源碼剖析
Log4j2的配置可以通過以下四種方式之一完成

  1. 通過以XML、JSON、YAML或屬性格式編寫的配置文件。
  2. 通過創建ConfigurationFactory和Configuration以編程方式實現
  3. 通過調用配置界面中公開的API,以編程方式將組建添加到默認配置
  4. 通過調用內部Logger類上的方法以編程實現。

本文的執行過程建立在XML配置的基礎上,部分代碼實現類用會直接使用XML的實現類。
由於本人技術尚淺,故有錯誤的地方,希望各位不吝賜教。

1.基礎名詞解釋

Named Hierarchy

在Log4j1.x中,通過Logger之間的關系維護Logger層次結構。在Log4j2中,此關系不再存在。而是在LoggerConfig對象之間的關系中維護層次結構。
Logger和LoggerConfigs是命名實體。Logger名稱區分大小寫,並且遵循分層命名規則 ->Named Hierarchy

如果LoggerConfig的名稱后跟一個點,則該LoggerConfig被稱為另一個 LoggerConfig 的祖先。
如果LoggerConfig與子LoggerConfig之間沒有祖先,則稱該LoggerConfig為子LoggerConfig的父級。

例如,名為“ com.foo”的 LoggerConfig 是名為“ com.foo.Bar”的 LoggerConfig 的父級。
同樣,“ java”是“ java.util”的父代,也是“ java.util.Vector”的祖先。

LoggerContextFactory

LoggerContextFactory將Log4jAPI綁定到其實現。

LoggerContext

LoggerContext充當Logging系統的定位點,一個應用程序中可能有多個活動LoggerContext.

Configuration

每個LoggerContext都有一個活動的Configuration。由Configuration管理配置文件結構轉化的Java對象。

Logger

通過調用LogManager.getLogger來創建。Logger本身不執行任何直接操作。Logger本身不執行任何直接操作。Logger本身不執行任何直接操作。
它僅具有一個名稱,並與LoggerConfig關聯。它擴展了AbstractLogger並實現了所需的方法。

Appender

可以理解為log4j2輸出目標,決定日志的輸出方式。包含日志的輸出格式、輸出路徑等一系列配置信息。

2.Log4j2的初始化過程(LogManager的啟動過程)

LogManager類中存在以下代碼塊,作為LogManager的啟動入口:

	static {
        PropertiesUtil managerProps = PropertiesUtil.getProperties();
        String factoryClassName = managerProps.getStringProperty("log4j2.loggerContextFactory");
        if (factoryClassName != null) {
            try {
                factory = (LoggerContextFactory)LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
            } catch (ClassNotFoundException var8) {
                LOGGER.error("Unable to locate configured LoggerContextFactory {}", factoryClassName);
            } catch (Exception var9) {
                LOGGER.error("Unable to create configured LoggerContextFactory {}", factoryClassName, var9);
            }
        }

        if (factory == null) {
            SortedMap<Integer, LoggerContextFactory> factories = new TreeMap();
            if (ProviderUtil.hasProviders()) {
                Iterator i$ = ProviderUtil.getProviders().iterator();

                while(i$.hasNext()) {
                    Provider provider = (Provider)i$.next();
                    Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
                    if (factoryClass != null) {
                        try {
                            factories.put(provider.getPriority(), factoryClass.newInstance());
                        } catch (Exception var7) {
                            LOGGER.error("Unable to create class {} specified in provider URL {}", factoryClass.getName(), provider.getUrl(), var7);
                        }
                    }
                }

                if (factories.isEmpty()) {
                    LOGGER.error("Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                    factory = new SimpleLoggerContextFactory();
                } else if (factories.size() == 1) {
                    factory = (LoggerContextFactory)factories.get(factories.lastKey());
                } else {
                    StringBuilder sb = new StringBuilder("Multiple logging implementations found: \n");
                    Iterator i$ = factories.entrySet().iterator();

                    while(i$.hasNext()) {
                        Entry<Integer, LoggerContextFactory> entry = (Entry)i$.next();
                        sb.append("Factory: ").append(((LoggerContextFactory)entry.getValue()).getClass().getName());
                        sb.append(", Weighting: ").append(entry.getKey()).append('\n');
                    }

                    factory = (LoggerContextFactory)factories.get(factories.lastKey());
                    sb.append("Using factory: ").append(factory.getClass().getName());
                    LOGGER.warn(sb.toString());
                }
            } else {
                LOGGER.error("Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                factory = new SimpleLoggerContextFactory();
            }
        }

該靜態代碼塊主要有以下幾個步驟:

  1. 根據配置文件的配置信息獲取LoggerContextFactory。
    在這段邏輯中,LogManager優先通過配置文件”log4j2.component.properties”通過配置項”log4j2.loggerContextFactory”來獲取LoggerContextFactory,
    如果用戶做了對應的配置,通過newCheckedInstanceOf方法實例化LoggerContextFactory的對象。

    默認情況下,不存在初始的默認配置文件log4j2.component.properties。

  2. 若LoggerContextFactory獲取失敗則通過ProviderUtil中的getProviders()方法載入providers,隨后通過provider的loadLoggerContextFactory方法載入LoggerContextFactory的實現類。
    代碼如下:

            final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();
            // note that the following initial call to ProviderUtil may block until a Provider has been installed when
            // running in an OSGi environment
            if (ProviderUtil.hasProviders()) {
                for (final Provider provider : ProviderUtil.getProviders()) {
                    final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
                    if (factoryClass != null) {
                        try {
                            factories.put(provider.getPriority(), factoryClass.newInstance());
                        } catch (final Exception e) {
                            LOGGER.error("Unable to create class {} specified in provider URL {}", factoryClass.getName(), provider
                                    .getUrl(), e);
                        }
                    }
                }

其中有兩個方法的設計比較有新意分別為ProviderUtil.hasProviders()以及ProviderUtil.getProviders()這兩個方法首先都會調用lazyInit(),這個方法使用了線程安全的機制懶加載ProviderUtil類對象的實例。

	protected static void lazyInit() {
        // noinspection DoubleCheckedLocking
        if (instance == null) {
            try {
                STARTUP_LOCK.lockInterruptibly();
                try {
                    if (instance == null) {
                        instance = new ProviderUtil();
                    }
                } finally {
                    STARTUP_LOCK.unlock();
                }
            } catch (final InterruptedException e) {
                LOGGER.fatal("Interrupted before Log4j Providers could be loaded.", e);
                Thread.currentThread().interrupt();
            }
        }
    }

其中對於可重入鎖的lockInterruptibly和lock的區別在於:

	lock 與 lockInterruptibly比較區別在於:
	lock 優先考慮獲取鎖,待獲取鎖成功后,才響應中斷。
	lockInterruptibly 優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取。

	ReentrantLock.lockInterruptibly允許在等待時由其它線程調用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回,這時不用獲取鎖,而會拋出一個InterruptedException。 
	ReentrantLock.lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。
	只是在最后獲取鎖成功后再把當前線程置為interrupted狀態,然后再中斷線程。

在創建新的providerUtil實例的過程中就會直接實例化provider對象,其過程是先通過getClassLoaders方法獲取provider的類加載器,然后通過loadProviders(classLoader)加載類。
在providerUtil實例化的最后,會統一查找”META-INF/log4j-provider.properties”文件中對應的provider的url,會考慮從遠程加載provider。

	private ProviderUtil() {
        for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
            try {
                loadProviders(classLoader);
            } catch (final Throwable ex) {
                LOGGER.debug("Unable to retrieve provider from ClassLoader {}", classLoader, ex);
            }
        }
        for (final LoaderUtil.UrlResource resource : LoaderUtil.findUrlResources(PROVIDER_RESOURCE)) {
            loadProvider(resource.getUrl(), resource.getClassLoader());
        }
    }

在2.12.1版本中,不再通過log4j-provider.properties配置文件獲取具體的LoggerContextFactory(從jar包中找不到log4j-provider.properties配置),而是通過實例化Log4jProvider類添加Provider,
再通過provider.loadLoggerContextFactory()獲取對應的LoggerContextFactory->Log4jContextFactory。

  1. 如果provider中沒有獲取到LoggerContextFactory的實現類或provider為空,則使用SimpleLoggerContextFactory作為LoggerContextFactory。
    factory = new SimpleLoggerContextFactory();

3.LogManager.getLogger()方法的執行過程。

靜態方法LogManager.getLogger()用於檢索logger。
該方法鏈式調用LoggerContextFactory.getContext返回一個啟動的LoggerContext實例。默認情況下,LoggerContextFactory的子類是log4jContextFactory。
log4jLoggerFactory.getContext方法如下:

@Override
    public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
                                    final boolean currentContext) {
        final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext);
        if (externalContext != null && ctx.getExternalContext() == null) {
            ctx.setExternalContext(externalContext);
        }
        if (ctx.getState() == LifeCycle.State.INITIALIZED) {
            ctx.start();
        }
        return ctx;
    }

該方法的主要邏輯是從ContextSelector中獲取一個LoggerContext並啟動。
ContextSelector:
由Log4jLoggerContext工廠調用。他們執行查找或創建LoggerContext的實際工作,這是Logger及其配置的基礎。
ContextSelector可以自由地實現他們希望ManagementLoggerContext的任何機制。
默認的Log4jContextFactory檢查是否存在名為“Log4jContextSelector”的系統屬性。如果找到,則該屬性應包含實現要使用的ContextSelector的Class的名稱。
默認使用ClassLoaderContextSelector,該ContextSelector將LoggerContexts與創建getLogger調用的調用程序的ClassLoader關聯。

LoggerContext.start用於啟動LoggerContext,方法如下

public void start() {
        LOGGER.debug("Starting LoggerContext[name={}, {}]...", getName(), this);
        if (PropertiesUtil.getProperties().getBooleanProperty("log4j.LoggerContext.stacktrace.on.start", false)) {
            LOGGER.debug("Stack trace to locate invoker",
                    new Exception("Not a real error, showing stack trace to locate invoker"));
        }
        if (configLock.tryLock()) {
            try {
                if (this.isInitialized() || this.isStopped()) {
                    this.setStarting();
                    reconfigure();
                    if (this.configuration.isShutdownHookEnabled()) {
                        setUpShutdownHook();
                    }
                    this.setStarted();
                }
            } finally {
                configLock.unlock();
            }
        }
        LOGGER.debug("LoggerContext[name={}, {}] started OK.", getName(), this);
    }

該方法主要有以下幾個步驟:

  1. 使用ReentrantLock加鎖
  2. this.setStarting->將LoggetContext的狀態設置為正在啟動
  3. reconfigure(核心)根據配置文件的位置,讀取相應的log4j2配置文件,解析配置文件,最終解析為各種Appender以及Logger的Java對象並啟動。
  4. this.setStareded->將LoggetContext的狀態設置為已啟動
  5. ReentrantLock解鎖

LoggerContext.reconfigure方法如下

	/**
     * Reconfigures the context.
     */
    private void reconfigure(final URI configURI) {
        final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;
        LOGGER.debug("Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
                contextName, configURI, this, cl);
        final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
        if (instance == null) {
            LOGGER.error("Reconfiguration failed: No configuration found for '{}' at '{}' in '{}'", contextName, configURI, cl);
        } else {
            setConfiguration(instance);
            /*
             * instance.start(); Configuration old = setConfiguration(instance); updateLoggers(); if (old != null) {
             * old.stop(); }
             */
            final String location = configuration == null ? "?" : String.valueOf(configuration.getConfigurationSource());
            LOGGER.debug("Reconfiguration complete for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
                    contextName, location, this, cl);
        }
    }

該方法主要有以下幾個步驟:

  1. 雙重檢查鎖定獲取ConfigurationFactory
    雙重檢查鎖定:首先測試鎖定標准而不實際獲取鎖定來減少獲取鎖定的開銷。僅當鎖定標准檢查指示需要鎖定時,實際鎖定邏輯才會繼續。
  2. 獲取Configuration,根據各個ConfigurationFactory中的后綴匹配對應的文件,返回Configuration實例。
  3. 若獲取Configuration實例成功,則調用setConfiguration方法

setConfiguration方法的核心是調用config.start方法啟動Configuration,該方法首先會調用AbstractConfiguraion的start方法將配置文件中所有元素轉化為對應的logger或Appender對象並啟動。
AbstractConfiguration.start方法如下:

	@Override
    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);
    }

該方法主要有以下幾個步驟:

  1. 初始化,主要執行兩個操作,在XML配置的條件下,在setup方法中將配置文件中所有的元素解析成node對象,在doConfiguration方法中將解析出來的node對象轉化為對應的Logger、Appender、Properties等一系列對象。
  2. 設置Configuration為正在啟動狀態
  3. asyncLoggerConfigDisruptor(Disruptor會在另外一文中詳細描述,先挖坑)
  4. 啟動所有的logger
  5. 啟動所有的appender
  6. 設置Configuration為已啟動狀態

XML配置的條件下,AbstractConfiguration的實現類為XMLConfiguration,XMLConfiguration的setup方法主要通過constructHierarchy方法進行XML文件解析。
XMLConfiguration.constructHierarchy方法如下:

	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);
        }
    }

該方法是標准的樹的深度優先遍歷,通過遍歷XML構建Node樹。

參考資料

https://bryantchang.github.io/categories/Log4j/
log4j2中文文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM