第 11 章 Webx日志系统的配置

11.1. 名词解释
11.1.1. 日志系统(Logging System)
11.1.2. 日志框架(Logging Framework)
11.2. 在Maven中组装日志系统
11.2.1. 在Maven中配置logback作为日志系统
11.2.2. 在Maven中配置log4j作为日志系统
11.3. 在WEB应用中配置日志系统
11.3.1. 设置WEB应用
11.3.2. 定制/WEB-INF/logback.xml(或/WEB-INF/log4j.xml
11.3.3. 同时初始化多个日志系统
11.4. 常见错误及解决
11.4.1. 查错技巧
11.4.2. 异常信息:No log system exists
11.4.3. 异常信息:NoSuchMethodError: org.slf4j.MDC.getCopyOfContextMap()
11.4.4. STDERR输出:Class path contains multiple SLF4J bindings
11.4.5. 看不到日志输出
11.5. 本章总结

日志系统是一个应用中必备的部分,提供了查看错误信息、了解系统状态的最直接手段。

本章介绍了基于Webx框架的应用如何配置、使用日志系统的方法。

11.1. 名词解释

11.1.1. 日志系统(Logging System)

表 11.1. 日志系统

名称说明
Log4j

http://logging.apache.org/log4j/

较早出现的比较成功的日志系统是Log4j。

Log4j开创的日志系统模型(Logger/Appender/Level)行之有效,并一直延用至今。

JUL(java.util.logging.*

http://download.oracle.com/javase/6/docs/technotes/guides/logging/overview.html

JDK1.4是第一个自带日志系统的JDK,简称(JUL)。

JUL并没有明显的优势来战胜Log4j,反而造成了标准的混乱 —— 采用不同日志系统的应用程序无法和谐共存。

Logback

http://logback.qos.ch/

是较新的日志系统。

它是Log4j的作者吸取多年的经验教训以后重新做出的一套系统。它的使用更方便,功能更强,而且性能也更高。

Logback不能单独使用,必须配合日志框架SLF4J来使用。

11.1.2. 日志框架(Logging Framework)

JUL诞生以后,为了克服多种日志系统并存所带来的混乱,就出现了“日志框架”。日志框架本身不提供记录日志的功能,它只提供了日志调用的接口。日志框架依赖于实际的日志系统如Log4j或JUL来产生真实的日志。

使用日志框架的好处是:应用的部署者可以决定使用哪一种日志系统(Log4j还是JUL),或者在多种日志系统之间切换,而不需要更改应用的代码。

表 11.2. 日志框架

名称说明
JCL(Jakarta Commons Logging)

http://commons.apache.org/logging/

这是目前最流行的一个日志框架,由Apache Jakarta社区提供。

Spring框架、许多老应用都依赖于JCL。

SLF4J

http://www.slf4j.org/

这是一个最新的日志框架,由Log4j的作者推出。

SLF4J提供了新的API,特别用来配合Logback的新功能。但SLF4J同样兼容Log4j。

由于Log4j原作者的感召力,SLF4J和Logback很快就流行起来。Webx的新版本也决定使用SLF4J作为其日志框架;并推荐Logback作为日志系统,但同时支持Log4J。

11.2. 在Maven中组装日志系统

要在应用中使用日志系统,必须把正确的jar包组装起来。本章假设你的应用是用maven构建的。

日志系统的组成

图 11.1. 日志系统的组成

如图所示,

  • 由于JCL-over-SLF4J和原来的JCL具有完全相同的API,因此两者是不能共存的。

  • Logback和slf4j-log4j12也不能并存,否则SLF4J会迷惑并产生不确定的结果。

组装完整的日志系统将涉及如下部件:

表 11.3. 日志系统的组成

类别组件名称说明
日志框架SLF4J

Webx框架以及所有新应用,直接依赖于SLF4J。

JCL

Spring框架、许多以前的老应用,都使用JCL来输出日志。

好在SLF4J提供了一个“桥接”包:JCL-over-SLF4J,它重写了JCL的API,并将所有日志输出转向SLF4J。这样就避免了两套日志框架并存的问题。

日志系统Logback

Webx推荐使用logback来取代log4j。

Logback可直接被SLF4J识别并使用。

Log4j

由于客观原因,有些系统暂时不能升级到Logback。

好在SLF4J仍然支持Log4j。Log4j需要一个适配器slf4j-log4j12才能被SLF4J识别并使用。

11.2.1. 在Maven中配置logback作为日志系统

以logback作为日志系统

图 11.2. 以logback作为日志系统

例 11.1. 配置pom.xml以使用logback

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
</dependencies>

<dependencyManagement> 
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
            <scope>runtime</scope> 
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
            <scope>provided</scope> 
        </dependency>
    </dependencies>
</dependencyManagement>

把所依赖jar包的版本定义在<dependencyManagement>中,而不是<dependencies>中。因为前者可影响间接依赖,后者只能影响直接依赖。

如果你的项目指定了parent pom,那么建议把<dependencyManagement>放在parent pom中,以便多个子项目共享配置。

将logback日志系统的依赖设定为<scope>runtime</scope>,因为应用程序永远不需要直接调用日志系统,而是通过SLF4J或JCL这样的日志框架来调用它们。

由于和jcl-over-slf4j存在冲突,因此JCL(commons-logging)是必须被排除的。由于maven目前缺少这样一个功能:它不能全局地排除一个jar包依赖,所以建议将commons-logging设置成<scope>provided</scope>,这样在最终的依赖关系中,将不会包含commons-logging包。

将commons-logging设置成<scope>provided</scope>可以用来排除commons-logging,然而这样做有一个缺点 —— 你无法从单元测试中将commons-logging排除。假如这个影响了你的单元测试的话,请使用另一种方案:

例 11.2. 另一种排除commons-logging的方法

        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>99.0-does-not-exist</version> 
        </dependency>

<version>99.0-does-not-exist</version>”是一个特殊的版本,这个版本的jar包里空无一物。这样就可以“欺骗”maven使用这个空的jar包来取代commons-logging,达到排除它的目的。

最后,你需要在项目文件夹下面,执行一下命令:“mvn dependency:tree”,确保没有jar包直接或间接依赖了slf4j-log4j12。如果有的话,你可以用下面的配置来排除掉:

例 11.3. 排除间接依赖的slf4j-log4j12

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>yourGroupId</groupId>
            <artifactId>yourArtifactId</artifactId>
            <version>yourVersion</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</dependencyManagement>

事实上,如果有其它的jar包依赖slf4j-log4j12,这本身就是有错误的。因为应用不应该直接依赖于这些包中的API —— 它们只应该依赖于日志框架API。任何应用都应该把下列和日志系统相关的依赖(如:slf4j-log4j12、logback-classic)设置成<scope>runtime</scope>的。

11.2.2. 在Maven中配置log4j作为日志系统

以log4j作为日志系统

图 11.3. 以log4j作为日志系统

例 11.4. 配置pom.xml以使用log4j

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

配置log4j的方法和关注要点和logback相似,请参见例 11.1 “配置pom.xml以使用logback”。除此以外,你需要在项目文件夹下面,执行一下命令:“mvn dependency:tree”,确保没有jar包间接依赖了logback-classic。如果有的话,你可以用下面的配置来排除掉:

例 11.5. 排除间接依赖的logback-classic

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>yourGroupId</groupId>
            <artifactId>yourArtifactId</artifactId>
            <version>yourVersion</version>
            <exclusions>
                <exclusion>
                    <groupId>ch.qos.logback</groupId>
                    <artifactId>logback-classic</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</dependencyManagement>

事实上,如果有其它的jar包依赖logback-classic,这本身就是有错误的。因为应用不应该直接依赖于这些包中的API —— 它们只应该依赖于日志框架API。任何应用都应该把下列和日志系统相关的依赖(如:slf4j-log4j12、logback-classic)设置成<scope>runtime</scope>的。

11.3. 在WEB应用中配置日志系统

11.3.1. 设置WEB应用

例 11.6. 设置/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/j2ee  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd
    ">

    <context-param>
        <param-name>loggingRoot</param-name> 
        <param-value>/tmp/logs</param-value>
    </context-param>
    <context-param>
        <param-name>loggingLevel</param-name> 
        <param-value>INFO</param-value>
    </context-param>
    <context-param>
        <param-name>loggingCharset</param-name> 
        <param-value>UTF-8</param-value>
    </context-param>
    <context-param>
        <param-name>log...</param-name> 
        <param-value>...</param-value>
    </context-param>

    <listener>
        <listener-class>com.alibaba.citrus.logconfig.LogConfiguratorListener</listener-class> 
    </listener>

    <filter>
        <filter-name>mdc</filter-name>
        <filter-class>com.alibaba.citrus.webx.servlet.SetLoggingContextFilter</filter-class> 
    </filter>

    <filter-mapping>
        <filter-name>mdc</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

指定日志系统的参数。

在WEB应用启动的时候,这个listener会被激活,并初始化日志系统。

将当前请求的信息放到日志系统的MDC中(Mapped Diagnostic Context)。

11.3.1.1. 日志系统的参数

可以在/WEB-INF/web.xml配置文件中集中定义日志系统的参数。

表 11.4. 可配置的日志参数

参数名称说明
loggingRoot指定保存日志文件的根目录。如不指定,默认为:“${user.home}/logs”。
loggingLevel指定日志级别,低于指定级别的日志将不被输出。如不指定,默认为“INFO”。
loggingCharset指定用来生成日志文件的字符集编码。如不指定,默认为当前操作系统的默认字符集编码。
log*名称以“log”开头的任意<context-param>参数,都将被用作日志系统的参数。

日志系统的参数可被替换到log4j或logback的配置中去,例如,在logback的配置文件中,你可以指定${loggingRoot}来取得存放日志文件的根目录。

将日志参数配置在/WEB-INF/web.xml中,有如下优点:

  • 使一套配置参数可同时应用于任意日志系统,包括logback和log4j。

  • 便于管理。通常,我们可以利用maven的filtering机制,或者autoconfig插件来生成/WEB-INF/web.xml文件,以便定制上述参数。

11.3.1.2. 自动识别并初始化日志系统

LogConfiguratorListener负责在系统启动的时候初始化日志系统。LogConfiguratorListener会根据下面所列的条件,来自动识别出当前的日志系统,并正确地配置它:

  • 假如你在maven的pom.xml中指定log4j为日志系统,那么该listener就会试图用/WEB-INF/log4j.xml来初始化日志系统。

  • 假如你在maven的pom.xml中指定logback为日志系统,那么该listener就会试图用/WEB-INF/logback.xml来初始化日志系统。

  • 假如你在maven的pom.xml中未指定任何日志系统(不存在logback-classic或slf4j-log4j12),那么listener会报错并失败,整个WEB应用会退出,服务器报告应用启动失败。

  • 假如/WEB-INF/logback.xml(或/WEB-INF/log4j.xml)不存在,那么listener会用默认的配置文件来初始化日志。默认的配置会:

    • WARN级别以上的日志打印在STDERR中,

    • WARN级别以下的日志打印在STDOUT中。

11.3.1.3. 初始化MDC

SetLoggingContextFilter将当前请求的信息放到日志系统的MDC中(Mapped Diagnostic Context)。这样,日志系统就可以打印出诸如下面所示的日志信息:

例 11.7. 利用MDC输出的日志

30377 [2010-06-02 15:24:29] - GET /wrongpage.htm [ip=127.0.0.1, ref=http://localhost:8081/index, ua=Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; zh-CN; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3, sid=scnd32ei11a7] ……

这段日志信息中包含了如下信息:

用户请求的类型:GET

请求的URI:/wrongpage.htm

用户IP:127.0.0.1

上一个页面的链接referrer:http://localhost:8081/index

用户的浏览器:Mac版的mozilla浏览器。

Session ID:scnd32ei11a7

用户客户端的详细信息,对于发现和追踪错误非常有帮助。

SetLoggingContextFilter是一个可选的filter —— 即使没有它,Webx的<setLoggingContext /> valve也会做同样的事情。但是把这些信息放在filter中,有利于及早记录用户的信息。

11.3.2. 定制/WEB-INF/logback.xml(或/WEB-INF/log4j.xml

11.3.2.1. 可用的参数

在日志配置文件中,你可以使用以下参数:

表 11.5. 日志配置文件中可用的参数

/WEB-INF/web.xml中定义的所有日志参数
${loggingRoot}代表保存日志文件的根目录。
${loggingCharset}代表用来生成日志文件的字符集编码。
${loggingLevel}代表日志级别,低于指定级别的日志将不被输出。
${log*}自定义参数,其中“*”代表任意名称。
由系统自动取得的参数
${loggingHost}代表当前的服务器名称
${loggingAddress}代表当前的服务器IP地址

11.3.2.2. 可用的MDC参数

在appender pattern中,你可以使用以下MDC参数:

表 11.6. 日志配置文件中可用的MDC参数

参数名说明
%X{productionMode}系统运行的模式。如果系统以开发模式运行,将会显示Development Mode;否则将会显示Production Mode。在生产环境中启动开发模式会引起严重的性能和安全问题。将系统运行的模式打印在日志中,可以作为一种提醒。
%X{method}请求类型,如:GETPOST
%X{requestURL}完整的URL,如:http://localhost/test
%X{requestURLWithQueryString}完整的URL,以及query string,如:http://localhost/test?id=1
%X{requestURI}不包括host信息的URI,如:/test
%X{requestURIWithQueryString}不包括host信息的URI,以及query string,如:/test?id=1
%X{queryString}URL参数,如:id=1
%X{remoteAddr}客户端地址
%X{remoteHost}客户端域名
%X{userAgent}客户端浏览器信息
%X{referrer}上一个页面的URL
%X{cookies}所有cookies的名称,如:[cookie1, cookie2]
%X{cookie.*}特定cookie的值,如:%X{cookie.JSESSIONID},将显示当前session的ID
%X{*}其它由应用程序或框架置入MDC的参数

11.3.2.3. Logback配置示例

例 11.8. Logback配置示例(/WEB-INF/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <encoding>${loggingCharset}</encoding>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern><![CDATA[
%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n
            ]]></pattern>
        </layout>
        <filter class="com.alibaba.citrus.logconfig.logback.LevelRangeFilter">
            <levelMax>INFO</levelMax>
        </filter>
    </appender>

    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <encoding>${loggingCharset}</encoding>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern><![CDATA[
%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n
            ]]></pattern>
        </layout>
        <filter class="com.alibaba.citrus.logconfig.logback.LevelRangeFilter">
            <levelMin>WARN</levelMin>
        </filter>
    </appender>

    <appender name="PROJECT" class="ch.qos.logback.core.FileAppender">
        <file>${loggingRoot}/${localHost}/petstore.log</file>
        <encoding>${loggingCharset}</encoding>
        <append>false</append>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern><![CDATA[
%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n
            ]]></pattern>
        </layout>
    </appender>

    <root>
        <level value="${loggingLevel}" />
        <appender-ref ref="STDERR" />
        <appender-ref ref="STDOUT" />
        <appender-ref ref="PROJECT" />
    </root>
</configuration>

更详细配置方法请参考logback官方文档:http://logback.qos.ch/manual/configuration.html

请特别留意示例中参数的写法,如“${loggingRoot}”;以及appender pattern中MDC参数的的写法,如:“%X{method}”、“%X{requestURIWithQueryString}”等。

11.3.2.4. Log4j配置示例

例 11.9. Log4j配置示例(/WEB-INF/log4j.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
        <param name="target" value="System.out" />
        <param name="encoding" value="${loggingCharset}" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                value="%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n"
             />
        </layout>
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="levelMax" value="INFO" />
        </filter>
    </appender>

    <appender name="STDERR" class="org.apache.log4j.ConsoleAppender">
        <param name="target" value="System.err" />
        <param name="encoding" value="${loggingCharset}" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                value="%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n"
             />
        </layout>
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="levelMin" value="WARN" />
        </filter>
    </appender>

    <appender name="PROJECT" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRoot}/${localHost}/myapp.log" />
        <param name="encoding" value="${loggingCharset}" />
        <param name="append" value="true" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                value="%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n"
             />
        </layout>
    </appender>

    <root>
        <level value="${loggingLevel}" />
        <appender-ref ref="STDOUT" />
        <appender-ref ref="STDERR" />
        <appender-ref ref="PROJECT" />
    </root>
</log4j:configuration>

更详细配置方法请参考log4j官方文档:http://logging.apache.org/log4j/1.2/manual.html

请特别留意示例中参数的写法,如“${loggingRoot}”;以及appender pattern中MDC参数的的写法,如:“%X{method}”、“%X{requestURIWithQueryString}”等。

11.3.3. 同时初始化多个日志系统

在某些遗留系统中,有些代码直接用到了Log4j API(例如Log4j Appender)。假如,我们仍然希望SLF4J以logback作为日志系统,但是保持这些老代码继续不变地使用log4j来记录日志。这样我们就需要同时初始化logback和log4j。

例 11.10. 同时初始化Logback和Log4j

首先,你需要确保在pom.xml中,同时包含log4j和logback-classic这两个依赖,但是请一定不要包含slf4j-log4j12这个包,因为它会和logback-classic起冲突。

下面的配置在例 11.1 “配置pom.xml以使用logback”基础上,添加了log4j的依赖:

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

然后,你需要在/WEB-INF/web.xml中增加logSystem参数。

下面的配置在例 11.6 “设置/WEB-INF/web.xml基础上,添加了所需的参数:

<web-app>

    <context-param>
        <param-name>logSystem</param-name>
        <param-value>log4j, logback</param-value>
    </context-param>
    ...

</web-app>

以上这段/WEB-INF/web.xml的配置,告诉LogConfiguratorListener同时初始化两个日志系统:log4j和logback。它们的配置文件分别是:/WEB-INF/log4j.xml/WEB-INF/logback.xml。假如文件不存在也没关系,LogConfiguratorListener会用系统默认的配置文件来初始化它们。

11.4. 常见错误及解决

11.4.1. 查错技巧

11.4.1.1. 检查提示信息

分析错误前,先检查一下日志系统输出的提示信息,往往可以节省很多时间。当LogConfiguratorListener启动时,将会在STDERR中打印信息,像下面这个样子:

例 11.11. 日志初始化时的提示信息(STDERR

2010-06-02 16:57:28.021:INFO:/:Initializing log4j system 
INFO: configuring "log4j" using file:/Users/…/WEB-INF/log4j.xml 
 - with property localAddress = 10.16.58.5 
 - with property localHost = baobao-macbook-pro.local 
 - with property loggingCharset = UTF-8 
 - with property loggingLevel = warn 
 - with property loggingRoot = /tmp/logs 

通过这些信息,你可以检查如下内容:

是否选择了正确的日志系统,如:log4j或logback,抑或两样都有。

是否选择了正确的日志配置文件,如:/WEB-INF/log4j.xml

日志文件的参数,如根目录、字符集编码、日志级别等信息。

11.4.1.2. 查看class真实归属的jar包位置

有时,因为各种原因导致应用找到了错误的jar包,从而产生神秘的错误。例如,你以为你使用了SLF4J的最新版,然而在服务器上存在一个SLF4J的老版本,并且其class loader优先级比新版本更高。在这种情况下,应用会引用高优先级class loader中的老版本的class。这可能导致错误。

发现这类错误的有效的方法,是在应用程序的任意点设置断点(利用eclipse远程调试功能),当系统停留在断点处时,执行如下的java代码,查看其值:

例 11.12. 查看class真实归属的jar包位置

getClass().getClassLoader().getResource(getClass().getName().replace('.', '/') + ".class")

另外,Webx开发模式所提供的详细出错页面中,也会列出stacktrace中每一个class的真实jar包位置。

11.4.2. 异常信息:No log system exists

报这个错的原因可能是:

  • 不存在slf4j-log4j12、logback-classic等任何一个日志系统的实现。

  • Slf4j的版本和日志系统的版本不匹配,例如,slf4j为1.4.3版,而slf4j-log4j12为1.7.5版。

解决方法:

11.4.3. 异常信息:NoSuchMethodError: org.slf4j.MDC.getCopyOfContextMap()

报这个错的原因是:

  • SLF4J的版本过老。MDC.getCopyOfContextMap()方法是从SLF4J 1.5.1时加入的,假如你的SLF4J是之前的版本,就会报错。

解决方法:

  • mvn dependency:tree查看所有的依赖包,排除以上错误。

  • 查看服务器环境(如jboss),查看是不是存在不正确版本的jar包,被优先于应用jar包而加载了。

11.4.4. STDERR输出:Class path contains multiple SLF4J bindings

SLF4J在STDERR报如下错误:

例 11.13. Class path contains multiple SLF4J bindings

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/…/WEB-INF/lib/logback-classic-0.9.18.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/…/WEB-INF/lib/slf4j-log4j12-1.5.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.

报这个错的原因是:

  • classpath中存在多个日志系统,使SLF4J无所适从。

解决方法:

  • SLF4J已经列出了classpath中所有的日志系统的位置。根据这些信息,你可以调整应用的依赖,或者整理服务器的环境,使之只剩下一个日志系统。

11.4.5. 看不到日志输出

原因可能是日志的配置文件可能有错。

解决方法:

  • 首先,查看LogConfiguratorListener输出到STDERR中的信息(参见第 11.4.1.1 节 “检查提示信息”),确定系统:

    • 选择了正确的日志系统;

    • 选择了正确的配置文件;

    • 设置了正确的参数(loggingRootloggingLevel等)。

    [注意]注意

    在JBOSS环境中,STDOUTSTDERR会被重定向到Log4j中,然后被输出到一个文件中,通常是log/server.log。你必须从这个日志文件中查看LogConfiguratorListener的输出。

  • 假如以上信息均正确,查看日志配置文件/WEB-INF/log4j.xml/WEB-INF/logback.xml,是否引用了正确的参数,例如:${loggingRoot}${loggingLevel}等。

  • 检查文件系统权限,确保应用有权限创建和修改日志文件。

  • 假设你使用log4j作为日志系统,以jboss作为应用服务器。在JBOSS环境中,当log4j被初始化后,STDOUTSTDERR可能会被重新配置到不同的appender中。原先用来记录STDOUT和STDERR的日志文件log/server.log将不会再被使用。建议你设置/WEB-INF/log4j.xml,增加如下内容:

    例 11.14. 在log4j中配置jboss服务器日志

    <appender name="JBOSS_APPENDER" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRootJboss}/server.log" />
        <param name="encoding" value="${loggingCharset}" />
        <param name="append" value="true" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                value="%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer}, ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n"
             />
        </layout>
    </appender>
    
    <logger name="STDOUT">
        <appender-ref ref="JBOSS_APPENDER" />
    </logger>
    <logger name="STDERR">
        <appender-ref ref="JBOSS_APPENDER" />
    </logger>

    这里用到了一个新的变量:${loggingRootJboss},你需要把它定义在/WEB-INF/web.xml中。

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app>
        ...
        <context-param>
            <param-name>loggingRootJboss</param-name>
            <param-value>${jboss}/log</param-value>
        </context-param>
        ...
    </web-app>

    如果你使用logback作为日志系统,则不需要作如上配置。

11.5. 本章总结

LogConfiguratorListener目前只提供了logback和log4j的支持,尽管支持一种新的日志系统是非常容易的,但现在看来,这两种日志系统已经足够我们使用了。

LogConfiguratorListener以SLF4J为基础。SLF4J还提供了更多的功能:

  • 除了log4j和logback以外,SLF4J还支持几种其它的日志系统;

  • 除了jcl-over-slf4j以外,SLF4J还提供了几种对其它legacy日志系统的桥接功能。

详情请见SLF4J的文档:http://www.slf4j.org/docs.html