1. 程式人生 > >java傳統web專案新增maven管理jar包,log4j無法正常輸出日誌

java傳統web專案新增maven管理jar包,log4j無法正常輸出日誌

背景

  筆者最近在給公司一個老的web專案改造升級,專案使用springmvc+mybatis,由於專案比較久遠,沒有使用maven管理jar版本,有可能是當時開發任務比較緊迫,不同的同事在不同的時期放入了jar版本各不相同,

看到那麼多混亂的jar,真是操心。筆者曾花了大概半個下午的時間,把jar版本整理好,編入pom.xml中,從那個時候,筆者本地專案的jar版本算是交給maven託管了。頓時間心裡舒暢了一會兒。心裡也計劃著和專案組大

家一起統一使用maven管控jar版本混亂的問題。可是事實有時候並不會常常如人所有。就在部署web專案的時候,問題來了。控制檯那麼多可愛的日誌怎麼變少了呢。

原來正常的時候,日誌輸出大概如下:

而當筆者部署web專案到servlet容器(tomcat)中,問題來了,日誌少的捉襟見肘,怎麼也難應付平時的專案開發啊,開發測試過程中,怎麼能少的了日誌。

 

如此一來,這個maven上的有點讓人措手不及,這不有點像邯鄲學步,顧著把專案列入現代化管理方式,卻失去了初衷:日誌功能淡化,開發除錯難度增加。

 

排錯過程

本地日誌輸入程式碼如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xxx.system.auth.User;
/*** * 許可權校驗類 */ public class AuthManager { private static Logger logger = LoggerFactory.getLogger(AuthManager.class); public static void check(HttpSession session) { User user = (User)session.getAttribute("userData"); if (user == null) { if(logger.isDebugEnabled()) logger.debug(
"當前使用者session無效!"); return false; } } }

筆者按: 程式碼片段中使用了slf4j框架,slf4j是一個優秀的slf4j框架,他使用了介面卡設計模式,由他來遮蔽不同日誌框架的差異性,實現與不同日誌框架的協作。

 

  以上一段簡單的程式碼,在專案中不同地方高頻次的呼叫,卻一直不見控制檯有日誌輸出,筆者起初以為是ide在作祟,各種更改ide配置也無效,

把編譯好的專案單獨放到servlet容器tomcat中,仍不湊效,即便捶胸頓足也無計可施,以上說明不是ide問題,也不是servlet容器的問題,這可憋壞了筆者。

解決方法

 log4j一般性的用法如下:

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

public class LogTest {
    static Logger logger = Logger.getLogger(LogTest.class.getName());

    public static void main(String[] args) {
        PropertyConfigurator.configure("src/log4j.properties");
        logger.debug("Debug ...");
        logger.info("Info ...");
        logger.warn("Warn ...");
        logger.error("Error ...");
    }

}

我們通過對Logger.getLogger檢視原始碼,在我們面前呈現出一個類LogManager

static
  public
  Logger getLogger(String name) {
    return LogManager.getLogger(name);
  }

 

Log4jManager程式碼如下:

package org.apache.log4j;

import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.LoggerFactory;
import org.apache.log4j.spi.RepositorySelector;
import org.apache.log4j.spi.DefaultRepositorySelector;
import org.apache.log4j.spi.RootLogger;
import org.apache.log4j.spi.NOPLoggerRepository;
import org.apache.log4j.helpers.Loader;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.helpers.LogLog;

import java.net.URL;
import java.net.MalformedURLException;


import java.util.Enumeration;
import java.io.StringWriter;
import java.io.PrintWriter;

/**
 * Use the <code>LogManager</code> class to retreive {@link Logger}
 * instances or to operate on the current {@link
 * LoggerRepository}. When the <code>LogManager</code> class is loaded
 * into memory the default initalzation procedure is inititated. The
 * default intialization procedure</a> is described in the <a
 * href="../../../../manual.html#defaultInit">short log4j manual</a>.
 *
 * @author Ceki G&uuml;lc&uuml;
 */
public class 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";


    static private Object guard = null;
    static private RepositorySelector repositorySelector;

    static {
        // By default we use a DefaultRepositorySelector which always returns 'h'.
        Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
        repositorySelector = new DefaultRepositorySelector(h);

        /** Search for the properties file log4j.properties in the CLASSPATH.  */
        String override = OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY,
                null);

        // if there is no default init override, then get the resource
        // specified by the user or the default config file.
        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;

            // if the user has not specified the log4j.configuration
            // property, we search first for the file "log4j.xml" and then
            // "log4j.properties"
            if (configurationOptionStr == null) {
                url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
                if (url == null) {
                    url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
                }
            } else {
                try {
                    url = new URL(configurationOptionStr);
                } catch (MalformedURLException ex) {
                    // so, resource is not a URL:
                    // attempt to get the resource from the class path
                    url = Loader.getResource(configurationOptionStr);
                }
            }

            // If we have a non-null url, then delegate the rest of the
            // configuration to the OptionConverter.selectAndConfigure
            // method.
            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.");
        }
    }

    /**
     * Sets <code>LoggerFactory</code> but only if the correct
     * <em>guard</em> is passed as parameter.
     *
     * <p>Initally the guard is null.  If the guard is
     * <code>null</code>, then invoking this method sets the logger
     * factory and the guard. Following invocations will throw a {@link
     * IllegalArgumentException}, unless the previously set
     * <code>guard</code> is passed as the second parameter.
     *
     * <p>This allows a high-level component to set the {@link
     * RepositorySelector} used by the <code>LogManager</code>.
     *
     * <p>For example, when tomcat starts it will be able to install its
     * own repository selector. However, if and when Tomcat is embedded
     * within JBoss, then JBoss will install its own repository selector
     * and Tomcat will use the repository selector set by its container,
     * JBoss.
     */
    static
    public void setRepositorySelector(RepositorySelector selector, Object guard)
            throws IllegalArgumentException {
        if ((LogManager.guard != null) && (LogManager.guard != guard)) {
            throw new IllegalArgumentException(
                    "Attempted to reset the LoggerFactory without possessing the guard.");
        }

        if (selector == null) {
            throw new IllegalArgumentException("RepositorySelector must be non-null.");
        }

        LogManager.guard = guard;
        LogManager.repositorySelector = selector;
    }


    /**
     * This method tests if called from a method that
     * is known to result in class members being abnormally
     * set to null but is assumed to be harmless since the
     * all classes are in the process of being unloaded.
     *
     * @param ex exception used to determine calling stack.
     * @return true if calling stack is recognized as likely safe.
     */
    private static boolean isLikelySafeScenario(final Exception ex) {
        StringWriter stringWriter = new StringWriter();
        ex.printStackTrace(new PrintWriter(stringWriter));
        String msg = stringWriter.toString();
        return msg.indexOf("org.apache.catalina.loader.WebappClassLoader.stop") != -1;
    }

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

    /**
     * Retrieve the appropriate root logger.
     */
    public
    static Logger getRootLogger() {
        // Delegate the actual manufacturing of the logger to the logger repository.
        return getLoggerRepository().getRootLogger();
    }

    /**
     * Retrieve the appropriate {@link Logger} instance.
     */
    public
    static Logger getLogger(final String name) {
        // Delegate the actual manufacturing of the logger to the logger repository.
        return getLoggerRepository().getLogger(name);
    }

    /**
     * Retrieve the appropriate {@link Logger} instance.
     */
    public
    static Logger getLogger(final Class clazz) {
        // Delegate the actual manufacturing of the logger to the logger repository.
        return getLoggerRepository().getLogger(clazz.getName());
    }


    /**
     * Retrieve the appropriate {@link Logger} instance.
     */
    public
    static Logger getLogger(final String name, final LoggerFactory factory) {
        // Delegate the actual manufacturing of the logger to the logger repository.
        return getLoggerRepository().getLogger(name, factory);
    }

    public
    static Logger exists(final String name) {
        return getLoggerRepository().exists(name);
    }

    public
    static Enumeration getCurrentLoggers() {
        return getLoggerRepository().getCurrentLoggers();
    }

    public
    static void shutdown() {
        getLoggerRepository().shutdown();
    }

    public
    static void resetConfiguration() {
        getLoggerRepository().resetConfiguration();
    }
}

 

  筆者本地使用log4j-1.2.16,通過閱讀靜態域程式碼瞭解到,log4j尋找配置檔案順序如下

      1、檢測當前JVM是否配置log4j.configuration屬性,如果有,載入對應的配置檔案,也就是說,你可以在JVM啟動時載入引數

        

-Dlog4j.configuration=d:\log4j.properties

或者在程式裡註冊系統屬性

System.setProperty("log4j.configuration","d:\\log4j.properties"); 

  2、當前jvm環境中log4j.configuration屬性查詢不到,再從當前類載入路徑依次查詢log4j.xml、log4j.properties。顯然,是要從jar包載入了。這不是我們希望的結果,

而我們需要的從當前web應用類路徑載入,問題的癥結也便在此。

公司專案是web專案,顯然更適合在Servlet上下文監聽器(ServletContextListener)註冊這個屬性,如果專案比較緊急,到這一步已經基本算是臨時性解決問題了,其他的留作後期程式碼重構再解決。

 

根本性解決方法

話到此處,有一些潔癖的程式設計師心裡嘀咕著:這一點也不優雅,一點都不適合我的作風,這樣不行,肯定有更好的辦法。好吧,筆者也是這麼認為(竊笑中.gif),能有別人封裝好的成熟程式碼最好不過,

不要重複造輪子;當然寫博文這個時間算是充足,考慮到專案用了spring,以往經常用spring來託管log4j。spring針對log4j 1.x有一個Log4jConfigListener

package org.springframework.web.util;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * Bootstrap listener for custom log4j initialization in a web environment.
 * Delegates to {@link Log4jWebConfigurer} (see its javadoc for configuration details).
 *
 * <b>WARNING: Assumes an expanded WAR file</b>, both for loading the configuration
 * file and for writing the log files. If you want to keep your WAR unexpanded or
 * don't need application-specific log files within the WAR directory, don't use
 * log4j setup within the application (thus, don't use Log4jConfigListener or
 * Log4jConfigServlet). Instead, use a global, VM-wide log4j setup (for example,
 * in JBoss) or JDK 1.4's {@code java.util.logging} (which is global too).
 *
 * <p>This listener should be registered before ContextLoaderListener in {@code web.xml}
 * when using custom log4j initialization.
 *
 * @author Juergen Hoeller
 * @since 13.03.2003
 * @see Log4jWebConfigurer
 * @see org.springframework.web.context.ContextLoaderListener
 * @see WebAppRootListener
 * @deprecated as of Spring 4.2.1, in favor of Apache Log4j 2
 * (following Apache's EOL declaration for log4j 1.x)
 */
@Deprecated
public class Log4jConfigListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent event) {
        Log4jWebConfigurer.initLogging(event.getServletContext());
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        Log4jWebConfigurer.shutdownLogging(event.getServletContext());
    }

}

通過跟蹤程式碼,我們發現spring採用迂迴的方式,通過我們剛剛提及的類Log4jManager,初始化了日誌功能。

當然,在spring4.2.1以後,已經過期,各位酌情使用,時間太晚,筆者沒有再繼續追下去。

我們只需要在web.xml中配置一個監聽器和上下文引數,即可解決問題

  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:log4j.properties</param-value>
  </context-param>

 <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>

 當然,通過檢視Log4jWebConfigurer的文件註釋,我們也可以指定日誌的存放路,放到當前應用的上下文路徑中。

log4j.appender.myfile.File=${webapp.root}/WEB-INF/demo.log}

 

至此,可愛的log4j日誌又在控制檯活蹦亂跳了。