在 Web 应用程序中使用 Log4j 2

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

由于命名空间从 javax 更改为 jakarta,因此对于 Servlet 5.0 或更高版本,您需要使用 log4j-jakarta-web 而不是 log4j-web

在任何情况下,您都需要将 log4j-web 模块添加到您的部署中,如 Maven、Ivy 和 Gradle 工件 手册页面中所述。

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

配置

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

  1. 如果提供了位置,则将作为 servlet 上下文资源进行搜索。例如,如果 log4jConfiguration 包含“logging.xml”,则 Log4j 将在 Web 应用程序的根目录中查找具有该名称的文件。
  2. 如果未定义位置,Log4j 将搜索 WEB-INF 目录中以“log4j2”开头的文件。如果找到多个文件,并且存在以“log4j2-name”开头的文件,其中 name 是 Web 应用程序的名称,则将使用该文件。否则,将使用第一个文件。
  3. 将使用使用类路径和文件 URL 的“正常”搜索序列来定位配置文件。

Servlet 3.0 及更高版本 Web 应用程序

Servlet 3.0 或更高版本 Web 应用程序是任何 <web-app>,其 version 属性的值为“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 应用程序中“正常工作”。它能够在应用程序部署时自动启动,并在应用程序取消部署时关闭。由于 ServletContainerInitializer API 添加到 Servlet 3.0,因此相关的 FilterServletContextListener 类可以在 Web 应用程序启动时动态注册。

重要提示! 出于性能原因,容器通常会忽略某些已知不包含 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”即可禁用自动初始化。您必须web.xml 中定义上下文参数。如果您以编程方式设置,则 Log4j 将无法及时检测到该设置。

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

禁用自动初始化后,您必须像初始化 Servlet 2.5 Web 应用程序 一样初始化 Log4j。您必须以在任何其他应用程序代码(例如 Spring Framework 启动代码)执行之前发生的方式执行此操作。

您可以使用 log4jContextNamelog4jConfiguration 和/或 isLog4jContextSelectorNamed 上下文参数自定义侦听器和过滤器的行为。在下面的 上下文参数 部分中阅读更多相关信息。您不得在您的部署描述符 (web.xml) 中或在 Servlet 3.0 或更高版本应用程序中的另一个初始化程序或侦听器中手动配置 Log4jServletContextListenerLog4jServletFilter除非您使用 isLog4jAutoInitializationDisabled 禁用自动初始化。这样做会导致启动错误和未指定的错误行为。

Servlet 2.5 Web 应用程序

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

如果您在 Servlet 2.5 Web 应用程序中使用 Log4j,或者如果您使用 isLog4jAutoInitializationDisabled 上下文参数禁用了自动初始化,则您必须在部署描述符中或以编程方式配置 Log4jServletContextListenerLog4jServletFilter。过滤器应匹配任何类型的请求。侦听器应该是应用程序中定义的第一个侦听器,过滤器应该是应用程序中定义和映射的第一个过滤器。这可以通过以下 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>

您可以使用 log4jContextNamelog4jConfiguration 和/或 isLog4jContextSelectorNamed 上下文参数自定义侦听器和过滤器的行为。在下面的 上下文参数 部分中阅读更多相关信息。

上下文参数

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

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

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

在指定这些上下文参数时,您必须在部署描述符 (web.xml) 中指定它们,即使在 Servlet 3.0 或永不应用程序中也是如此。如果您将它们添加到侦听器内的 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"。

出于安全原因,从 Log4j 2.17.0 开始,必须通过设置系统属性 log4j2.enableJndiContextSelector=true 来启用 JNDI。

在配置期间使用 Web 应用程序信息

您可能希望在配置期间使用有关 Web 应用程序的信息。例如,您可以将 Web 应用程序的上下文路径嵌入到滚动文件追加器的名称中。有关更多信息,请参阅 查找 中的 WebLookup。

JavaServer Pages 日志记录

您可以在 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 时才会提供。但是,如果您确实使用值为 "true" 的 isLog4jContextSelectorNamed 上下文参数,则需要手动将 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 追加器

Log4j 提供了一个 Servlet 追加器,它使用 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 上下文。