在 Web 应用程序中使用 Log4j 2

在 Java EE Web 应用程序中使用 Log4j 或任何其他日志记录框架时,必须格外小心。当容器关闭或取消部署 Web 应用程序时,正确清理日志记录资源(关闭数据库连接,关闭文件等)非常重要。由于 Web 应用程序中类加载器的性质,无法通过常规方式清除 Log4j 资源。在部署 Web 应用程序时必须“启动” Log4j,而在取消部署 Web 应用程序时必须“关闭”。其工作方式因您的应用程序是Servlet 3.0 或更高版本还是Servlet 2.5 Web 应用程序而异。

无论哪种情况,您都需要将 log4j-web 模块添加到您的部署中,如Maven,Ivy 和 Gradle 工件手册页中所述。

为避免出现问题,当包含 log4j-web jar 时,将自动禁用 Log4j 关闭钩子。

Configuration

Log4j 允许使用 log4jConfiguration 上下文参数在 web.xml 中指定配置文件。 Log4j 将通过以下方式搜索配置文件:

  • 如果提供了位置,则将其作为 servlet 上下文资源进行搜索。例如,如果 log4jConfiguration 包含“ logging.xml”,则 Log4j 将在 Web 应用程序的根目录中查找具有该名称的文件。

  • 如果未定义位置,则 Log4j 将在 WEB-INF 目录中搜索以“ log4j2”开头的文件。如果找到多个文件,并且存在以“ log4j2-name”开头的文件,其中 name 是 Web 应用程序的名称,则将使用该文件。否则,将使用第一个文件。

  • 使用 Classpath 和文件 URL 的“常规”搜索序列将用于查找配置文件。

Servlet 3.0 和更新的 Web 应用程序

Servlet 3.0 或更高版本的 Web 应用程序是任何\ ,其版本属性的值为“ 3.0”或更高。当然,该应用程序还必须在兼容的 Web 容器中运行。例如:Tomcat 7.0 和更高版本,GlassFish 3.0 和更高版本,JBoss 7.0 和更高版本,Oracle WebLogic 12c 和更高版本以及 IBM WebSphere 8.0 和更高版本。

短篇小说

Log4j 2 在 Servlet 3.0 和更新的 Web 应用程序中“可以正常工作”。它能够在应用程序部署时自动启动,并在应用程序取消部署时自动关闭。由于 Servlet 3.0 中添加了ServletContainerInitializer API,因此可以在 Web 应用程序启动时动态注册相关的 Filter 和 ServletContextListener 类。

重要提示! 出于性能原因,容器通常会忽略某些已知不包含 TLD 或 ServletContainerInitializer 的 JAR,并且不扫描它们以查找 Web 片段和初始化程序。重要的是,Tomcat 7 <7.0.43 会忽略所有名为 log4j * .jar 的 JAR 文件,这将阻止该功能的运行。这已在 Tomcat 7.0.43,Tomcat 8 和更高版本中修复。在 Tomcat 7 <7.0.43 中,您将需要更改 catalina.properties 并从 jarsToSkip 属性中删除“ log4j * .jar”。如果其他容器跳过对 Log4j JAR 文件的扫描,则可能需要对它们进行类似的操作。

长篇故事

Log4j 2 Web JAR 文件是一个 Web 片段,配置为在应用程序中的任何其他 Web 片段之前排序。它包含一个 ServletContainerInitializer(Log4jServletContainerInitializer),容器会自动发现并初始化它。这会将Log4jServletContextListenerLog4jServletFilter添加到 ServletContext。这些类可以正确地初始化和取消初始化 Log4j 配置。

对于某些用户,自动启动 Log4j 是有问题的或不希望的。您可以使用 isLog4jAutoInitializationDisabled 上下文参数轻松禁用此功能。只需将其添加到值为“ true”的部署 Descriptors 中即可禁用自动初始化。您必须在 web.xml 中定义上下文参数。如果以编程方式进行设置,则 Log4j 无法检测到该设置。

<context-param>
        <param-name>isLog4jAutoInitializationDisabled</param-name>
        <param-value>true</param-value>
    </context-param>

禁用自动初始化后,必须像Servlet 2.5 Web 应用程序一样初始化 Log4j。您必须以某种方式执行此初始化,然后才能执行任何其他应用程序代码(例如 Spring Framework 启动代码)。

您可以使用 log4jContextName,log4jConfiguration 和/或 isLog4jContextSelectorNamed 上下文参数来自定义侦听器和筛选器的行为。在下面的Context Parameters部分中了解有关此内容的更多信息。除非使用 isLog4jAutoInitializationDisabled 禁用自动初始化,否则,您不得在部署 Descriptors(web.xml)或 Servlet 3.0 或更高版本的应用程序中的另一个初始化程序或侦听器中手动配置 Log4jServletContextListener 或 Log4jServletFilter。这样做会导致启动错误和未指定的错误行为。

Servlet 2.5 Web 应用程序

Servlet 2.5 Web 应用程序是任何\ ,其版本属性的值为“ 2.5”。版本属性是唯一重要的事情;即使 Web 应用程序在 Servlet 3.0 或更高版本的容器中运行,如果 version 属性为“ 2.5”,它也是 Servlet 2.5 Web 应用程序。请注意,Log4j 2 不支持 Servlet 2.4 和较早的 Web 应用程序。

如果在 Servlet 2.5 Web 应用程序中使用 Log4j,或者使用 isLog4jAutoInitializationDisabled 上下文参数禁用了自动初始化,则必须在部署 Descriptors 中或以编程方式配置Log4jServletContextListenerLog4jServletFilter。过滤器应匹配任何类型的所有请求。侦听器应该是您的应用程序中定义的第一个侦听器,而过滤器应该是您的应用程序中定义和 Map 的第一个过滤器。使用以下 web.xml 代码可以轻松完成此操作:

<listener>
        <listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
    </listener>

    <filter>
        <filter-name>log4jServletFilter</filter-name>
        <filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>log4jServletFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>INCLUDE</dispatcher>
        <dispatcher>ERROR</dispatcher>
        <dispatcher>ASYNC</dispatcher><!-- Servlet 3.0 w/ disabled auto-initialization only; not supported in 2.5 -->
    </filter-mapping>

您可以使用 log4jContextName,log4jConfiguration 和/或 isLog4jContextSelectorNamed 上下文参数来自定义侦听器和筛选器的行为。在下面的Context Parameters部分中了解有关此内容的更多信息。

Context Parameters

默认情况下,Log4j 2 使用 ServletContext 的context name作为 LoggerContext 名称,并使用标准模式来查找 Log4j 配置文件。您可以使用三个上下文参数来控制此行为。第一个 isLog4jContextSelectorNamed 指定是否应使用JndiContextSelector选择上下文。如果未指定 isLog4jContextSelectorNamed 或为 true 以外的其他值,则假定为 false。

如果 isLog4jContextSelectorNamed 为 true,则必须在 web.xml 中指定 log4jContextName 或指定显示名称。否则,应用程序将无法启动并出现异常。在这种情况下,还应该指定 log4jConfiguration,并且必须是配置文件的有效 URI;但是,此参数不是必需的。

如果 isLog4jContextSelectorNamed 不为 true,则可以选择指定 log4jConfiguration,并且必须是有效的 URI 或配置文件的路径,或者以“ classpath:”开头以表示可以在 Classpath 上找到的配置文件。如果没有此参数,Log4j 将使用标准机制来查找配置文件。

指定这些上下文参数时,即使在 Servlet 3.0 或从不应用程序中,也必须在部署 Descriptors(web.xml)中指定它们。如果将它们添加到侦听器中的 ServletContext,则 Log4j 将在上下文参数可用之前初始化,并且它们将无效。以下是这些上下文参数的一些示例用法。

将日志记录上下文名称设置为“ myApplication”

<context-param>
        <param-name>log4jContextName</param-name>
        <param-value>myApplication</param-value>
    </context-param>

将配置路径/文件/ URI 设置为“ /etc/myApp/myLogging.xml”

<context-param>
        <param-name>log4jConfiguration</param-name>
        <param-value>file:///etc/myApp/myLogging.xml</param-value>
    </context-param>

使用 JndiContextSelector

<context-param>
        <param-name>isLog4jContextSelectorNamed</param-name>
        <param-value>true</param-value>
    </context-param>
    <context-param>
        <param-name>log4jContextName</param-name>
        <param-value>appWithJndiSelector</param-value>
    </context-param>
    <context-param>
        <param-name>log4jConfiguration</param-name>
        <param-value>file:///D:/conf/myLogging.xml</param-value>
    </context-param>

请注意,在这种情况下,还必须将“ Log4jContextSelector”系统属性设置为“ org.apache.logging.log4j.core.selector.JndiContextSelector”。

在配置过程中使用 Web 应用程序信息

您可能要在配置过程中使用有关 Web 应用程序的信息。例如,您可以将 Web 应用程序的上下文路径嵌入到 Rolling File Appender 的名称中。有关更多信息,请参见Lookups中的 WebLookup。

JavaServer 页面记录

您可以在 JSP 中使用 Log4j 2,就像在其他任何 Java 代码中一样。简单获取一个 Logger 并调用其方法来记录事件。但是,这要求您在 JSP 中使用 Java 代码,并且某些开发团队理所当然地不愿意这样做。如果您有不熟悉 Java 的专门用户界面开发团队,则甚至可能在 JSP 中禁用了 Java 代码。

因此,Log4j 2 提供了一个 JSP 标记库,使您无需使用任何 Java 代码即可记录事件。要了解有关使用此标签库的更多信息,请阅读 Log4j 标记库文档。

重要提示! 如上所述,容器通常会忽略某些已知不包含 TLD 的 JAR,也不会对它们进行 TLD 文件扫描。重要的是,Tomcat 7 <7.0.43 会忽略所有名为 log4j * .jar 的 JAR 文件,这会阻止自动发现 JSP 标记库。这不会影响 Tomcat 6.x,并且已在 Tomcat 7.0.43,Tomcat 8 和更高版本中修复。在 Tomcat 7 <7.0.43 中,您将需要更改 catalina.properties 并从 jarsToSkip 属性中删除“ log4j * .jar”。如果其他容器跳过对 Log4j JAR 文件的扫描,则可能需要对它们进行类似的操作。

异步请求和线程

异步请求的处理非常棘手,无论 Servlet 容器版本或配置如何,Log4j 都无法自动处理所有内容。处理标准请求,转发,包含和错误资源后,Log4jServletFilter 会将 LoggerContext 绑定到处理请求的线程。请求处理完成后,过滤器将 LoggerContext 与线程解除绑定。

类似地,当使用 javax.servlet.AsyncContext 调度内部请求时,Log4jServletFilter 还将 LoggerContext 绑定到处理请求的线程,并在请求处理完成时解除绑定。但是,这仅发生在通过 AsyncContext 分派的请求中。除了内部调度的请求之外,还有其他异步活动可以发生。

例如,在启动 AsyncContext 之后,您可以启动一个单独的线程来在后台处理请求,可能使用 ServletOutputStream 编写响应。筛选器无法拦截该线程的执行。筛选器也无法拦截非异步请求期间在后台启动的线程。无论您使用全新的线程还是从线程池中借用的线程都是如此。那么您可以为这些特殊线程做什么?

您可能不需要执行任何操作。如果您没有使用 isLog4jContextSelectorNamed 上下文参数,则无需将 LoggerContext 绑定到线程。 Log4j 可以安全地自行定位 LoggerContext。在这些情况下,过滤器仅在创建新的 Logger 时仅提供非常适度的性能提升。但是,如果确实将 isLog4jContextSelectorNamed 上下文参数指定为值“ true”,则需要将 LoggerContext 手动绑定到异步线程。否则,Log4j 将无法找到它。

值得庆幸的是,在这些特殊情况下,Log4j 提供了一种将 LoggerContext 绑定到异步线程的简单机制。最简单的方法是包装传递到 AsyncContext.start()方法的 Runnable 实例。

import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.web.WebLoggerContextUtils;

public class TestAsyncServlet extends HttpServlet {

    @Override
    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(WebLoggerContextUtils.wrapExecutionContext(this.getServletContext(), new Runnable() {
            @Override
            public void run() {
                final Logger logger = LogManager.getLogger(TestAsyncServlet.class);
                logger.info("Hello, servlet!");
            }
        }));
    }

    @Override
    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(new Runnable() {
            @Override
            public void run() {
                final Log4jWebSupport webSupport =
                    WebLoggerContextUtils.getWebLifeCycle(TestAsyncServlet.this.getServletContext());
                webSupport.setLoggerContext();
                // do stuff
                webSupport.clearLoggerContext();
            }
        });
    }
}

当使用 Java 1.8 和 lambda 函数时,这可能会更加方便,如下所示。

import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.web.WebLoggerContextUtils;

public class TestAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(WebLoggerContextUtils.wrapExecutionContext(this.getServletContext(), () -> {
            final Logger logger = LogManager.getLogger(TestAsyncServlet.class);
            logger.info("Hello, servlet!");
        }));
    }
}

或者,您可以从 ServletContext 属性获取Log4jWebLifeCycle实例,将其 setLoggerContext 方法作为异步线程中的第一行代码调用,并将其 clearLoggerContext 方法作为异步线程中的最后一行代码调用。以下代码演示了这一点。它使用容器线程池执行异步请求处理,将匿名的内部 Runnable 传递给 start 方法。

import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.web.Log4jWebLifeCycle;
import org.apache.logging.log4j.web.WebLoggerContextUtils;

public class TestAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         final AsyncContext asyncContext = req.startAsync();
        asyncContext.start(new Runnable() {
            @Override
            public void run() {
                final Log4jWebLifeCycle webLifeCycle =
                    WebLoggerContextUtils.getWebLifeCycle(TestAsyncServlet.this.getServletContext());
                webLifeCycle.setLoggerContext();
                try {
                    final Logger logger = LogManager.getLogger(TestAsyncServlet.class);
                    logger.info("Hello, servlet!");
                } finally {
                    webLifeCycle.clearLoggerContext();
                }
            }
        });
   }
}

请注意,一旦线程完成处理,就必须调用 clearLoggerContext。否则,将导致内存泄漏。如果使用线程池,它甚至可能破坏容器中其他 Web 应用程序的日志记录。因此,此处的示例显示了在 finally 块中清除上下文,该块将始终执行。

使用 Servlet Appender

Log4j 提供了一个 Servlet Appender,它使用 Servlet 上下文作为日志目标。例如:

<Configuration status="WARN" name="ServletTest">

    <Appenders>
        <Servlet name="Servlet">
            <PatternLayout pattern="%m%n%ex{none}"/>
        </Servlet>
    </Appenders>

    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Servlet"/>
        </Root>
    </Loggers>

</Configuration>

为了避免重复记录 servlet 上下文的异常,您必须在 PatternLayout 中使用%ex{none},如示例所示。消息文本中将忽略该异常,但会将其作为实际的 Throwable 对象传递给 Servlet 上下文。