在Web应用程序中使用Log4j

在Java EE web应用程序中使用Log4j或任何其他日志框架时,必须特别小心。

当容器关闭或web应用程序取消部署时,正确清理日志资源(关闭数据库连接、关闭文件等)是很重要的。由于web应用程序中的类加载器的特性,Log4j资源不能通过正常方式清理。

当web应用程序部署时,Log4j必须“启动”,当web应用程序取消部署时,Log4j必须“关闭”。如何工作取决于您的应用程序是Servlet 3.0或更新版本还是Servlet 2.5 web应用程序。

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

无论哪种情况,您都需要将log4j-web模块添加到您的部署中,详细信息请参见Maven、Ivy和Gradle Artifacts手册页面。

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

配置

Log4j允许使用log4jConfiguration上下文参数在web.xml中指定配置文件。

Log4j将通过以下方式搜索配置文件:

如果提供了一个位置,它将作为servlet上下文资源进行搜索。

例如,如果log4jConfiguration包含“logging.xml”,那么Log4j将在web应用程序的根目录中查找具有该名称的文件。

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

使用类路径和文件url的“正常”搜索顺序将用于定位配置文件。

Servlet 3.0和更新的Web应用程序

Servlet 3.0或更新的web应用程序是任何版本属性值为“3.0”或更高的 <web-app>

当然,应用程序还必须运行在兼容的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,相关的Filter和ServletContextListener类可以在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。属性并从jarsToSkip属性中删除“log4j*.jar”。如果其他容器跳过扫描Log4j JAR文件,您可能需要在它们上执行类似的操作。

长篇故事

Log4j 2 Web JAR文件是一个Web片段,配置为在应用程序中的任何其他Web片段之前排序。它包含一个容器自动发现和初始化的ServletContainerInitializer (Log4jServletContainerInitializer)。这会将Log4jServletContextListener和Log4jServletFilter添加到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启动代码)之前进行此初始化。

您可以使用log4jContextName、log4jConfiguration和/或isLog4jContextSelectorNamed上下文参数自定义侦听器和过滤器的行为。

在下面的上下文参数部分中了解更多信息。

除非使用isLog4jAutoInitializationDisabled禁用自动初始化,否则不能在部署描述符(web.xml)或Servlet 3.0或更新的应用程序中的其他初始化器或侦听器中手动配置Log4jServletContextListener或Log4jServletFilter。这样做将导致启动错误和未指定的错误行为。

Servlet 2.5 Web应用

Servlet 2.5 web应用程序是任何版本属性值为“2.5”的 <web-app>

version属性是唯一重要的东西;即使web应用程序运行在Servlet 3.0或更新的容器中,如果版本属性为“2.5”,它也是Servlet 2.5 web应用程序。

注意,Log4j 2不支持Servlet 2.4和更早的web应用程序。

如果您在Servlet 2.5 web应用程序中使用Log4j,或者如果您已经使用isLog4jAutoInitializationDisabled上下文参数禁用了自动初始化,则必须在部署描述符中或以编程方式配置Log4jServletContextListener和Log4jServletFilter。

过滤器应该匹配任何类型的所有请求。侦听器应该是应用程序中定义的第一个侦听器,过滤器应该是应用程序中定义和映射的第一个过滤器。

这很容易完成使用以下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

默认情况下,Log4j 2使用ServletContext的上下文名称作为LoggerContext名称,并使用标准模式来定位Log4j配置文件。

您可以使用三个上下文参数来控制此行为。

第一个是isLog4jContextSelectorNamed,指定是否应该使用JndiContextSelector来选择上下文。如果未指定isLog4jContextSelectorNamed或为非true,则假定它为false。

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

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

在指定这些上下文参数时,您必须在部署描述符(web.xml)中指定它们,即使在Servlet 3.0或never应用程序中也是如此。如果将它们添加到侦听器中的ServletContext中,Log4j将在上下文参数可用之前进行初始化,并且它们将不起作用。下面是这些上下文参数的一些示例用法。

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

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

Set the Configuration Path/File/URI to “/etc/myApp/myLogging.xml”

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

Use the 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开始,必须通过设置系统属性log4j22 . enablejndicontextselector =true来启用JNDI。

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

您可能希望在配置期间使用有关web应用程序的信息。例如,你可以将web应用程序的上下文路径嵌入到滚动文件追加器的名称中。有关更多信息,请参阅查找中的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。该问题已在Tomcat 7.0.43、Tomcat 8及更高版本中得到修复。在Tomcat 7 <7.0.43中,您需要更改catalina。属性并从jarsToSkip属性中删除“log4j.jar”。如果其他容器跳过扫描Log4j JAR文件,您可能需要在它们上执行类似的操作。

异步请求和线程

异步请求的处理很棘手,无论Servlet容器版本或配置如何,Log4j都无法自动处理所有请求。

当处理标准请求、转发、包含和错误资源时,Log4jServletFilter将LoggerContext绑定到处理请求的线程。请求处理完成后,过滤器从线程中解除LoggerContext的绑定。

类似地,当使用javax.servlet分派内部请求时。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 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上下文。

参考资料

https://logging.apache.org/log4j/2.x/manual/webapp.html