一般不太可能在生产环境中使用调试工具。如果网站的流量很大,通常不可能把生成的所有日志消息都存储起来。下面将讨论与生产环境中调试代码有关的问题,并介绍可以采用哪些技术来帮助查找和修正生产环境中发现的问题。我们要讨论的生产环境中的第一个问题是预编译JSP。
调试生产系统的最好工具往往是要一个好的日志环境,以及代码生成的完备日志消息。前面讨论过几种日志解决方案,最灵活的方案是采用 Log4j结合日志标记库,这种方案在生产环境中也最有用。其他日志解决方案也很有用,不过更适用于需要快速实验的场合,比如刚开发完代码时。下面讨论时将假设使用 Log4j或者有类似功能的日志记录器。
在生产环境中部署JSP之前,往往会预编译各个JSP,以防止首次访问JSP时可能出现的长延迟。不过,预编译有很多种形式。有些Web服务器,如WebSphere,提供了在部署时对页面预编译的选项。如果使用参数?jsp_precompile=true来访问JSP,容器就不会执行这个JSP,而只是编译。servlet容器可能忽略这个请求而不编译页面,但是无法把请求传递给JSP来处理。
预编译页面还有一种方法,即使用Ant脚本和Jasper JSP编译器任务,将页面编译为servlet。然后servlet会编译并安装到web.xml文件,并像Web应用中其他servlet一样得到处理。如果使用了这个方法,必须记住在使用调试工具时,要调试所生成的servlet,而不要查看JSP文件本身。JSP预编译的更多细节见第11章。
日志消息通常是由编译得到的servlet生成的,而不是由JSP生成。使用日志标记库时,类别名应当设置为一个可识别的名字。下面的代码展示了如何使用EL表达式来得到作为类别名的完全限定类名:
<c:set var="logCat" value="${pageContext.page.class.name}" scope="page" />
<log:info category="${logCat}" message="informational log message" />
表达式 ${pageContext.page.class.name} 会唯一地标识日志中的各个JSP,这样就能在需要的情况下单独地配置各个JSP。如果JSP名有所变化,目录名会自动地调整。还有一种方法,可以使用请求的URL来得到类别名,不过这要求使用一个函数将/字符换成点号。
一般我们不太希望在生产环境中调试,不过往往需要有一个尽可能接近生产系统的实验环境。生产环境的目标是收集足够多的信息,并做出足够的观察,从而能够在调试或实验环境中让问题再生。不过,如果只有在访问系统的用户达到某个特定数目时才能发现问题,该怎么办呢?
使用系统的用户量一般称为负载(load)。表达这个值的方法有很多,不过通常表述为并发用户数。并发用户就是同时访问系统的用户。查找与负载相关的问题时,有一点很重要,就是要在调试系统中提供与实际生产系统同样的负载(如果需要,可以成比例缩放)。
调试系统不必与生产系统完全相同,但是必须考虑这二者在负载方面有什么差别。例如,如果生产系统由一个包括8台机器的集群组成,能处理6 000个并发用户,那么包括两台机器的调试系统就应该至少处理1 500个用户。
一般地,会使用负载生成器来模拟大量的用户。有关负载生成有一些免费的产品,另外也不乏相关的商业产品。比如Mercury Interactive 的Load Runner以及Jakarta JMeter和Apache Flood。
显然,如果系统达到我们所说的负载,日志文件会变得非常大。即使可以在系统上存储这些文件,要在日志文件的数百万行中找到需要的信息也很成问题。
可以使用很多技术来把感兴趣的某些日志消息独立出来。这些技术大多基于这样一种方法,即向每个日志项增加一些特定的信息。前面已经看到,可以向日志项增加时间戳信息。时间戳用于维护日志项的顺序,以便检查;还可以限制检查的日志项个数,即只检查某个事件发生前后的日志项。
下面来看另外一些常见的情况:
q 查找与特定应用相关的所有日志项。
q 查找与特定用户相关的所有日志项。
q 查找对应特定IP地址的所有日志项。
q 查找与特定会话相关的所有日志项。
根据我们对日志框架的了解,第一种情况看上去相当容易。对于构成应用的各个类,只需要启用这些类的日志记录就行了。但是如果某些类在多个应用中都会用到,会怎么样呢?没准有些应用并不希望这些类记录相应的消息。剩下的情况更显困难:如果一个类中的日志记录器甚至不知道自己是Web应用的一部分,它怎么会记录用户和会话信息呢?
这些问题的解决方案就是嵌套诊断上下文(Nested Diagnostic Context,或NDC)。NDC机制可以帮助我们识别来源不同的相互交织的日志消息。Log4j提供了一个名为NDC 的类,这个类支持增加和删除特定于线程的,可以作为日志记录的一部分记录下来的消息。对于Web应用,最好的办法是使用这个类在过滤器中增加NDC信息,然后使用同一个过滤器删除这些信息。下面是一个示例过滤器,它会向各个日志消息增加会话ID:
package com.wrox.book.chapt14;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.log4j.NDC;
public class NdcFilter implements Filter
{
private FilterConfig filterConfig = null;
public NdcFilter()
{
}
public void init(FilterConfig fc)
{
filterConfig = fc;
}
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
// adds the session ID to the NDC
HttpServletRequest hreq = null;
if( request instanceof HttpServletRequest )
{
hreq = (HttpServletRequest)request;
HttpSession session = hreq.getSession();
if( session != null )
NDC.push(session.getId());
else
NDC.push("No session present");
}
else
NDC.push( "Not an HttpServlet request!" );
chain.doFilter(request, response);
NDC.pop();
}
public void destroy()
{
}
}
其中:
if( request instanceof HttpServletRequest )
确保请求是一个HttpServletRequest,然后才会将其真正强制转换为HttpServlet-
Request。这里要一个HttpServletRequest是为了得到会话。我们希望总能得到一个会话ID,所以会话不存在时允许使用hreq.getSession() 创建一个会话。也可以要求会话不存在时hreq.getSession()不创建新会话,这就要将
HttpSession session = hreq.getSession();
替换为:
HttpSession session = hreq.getSession(false);
一旦有了会话,可以把会话ID压入NDC栈。这样在此线程执行期间调用的所有日志记录器都可以得到这个值了。
利用下面这行代码:
chain.doFilter(request, response);
允许过滤器链在需要时可以继续处理,最终使Web应用得到调用。servlet和JSP以及其他过滤器处理了请求和响应后,我们重新得到控制权,再利用下面这行代码将上下文从NDC栈中弹出:
NDC.pop();
需要在 web.xml 文件中配置过滤器,为此在<web-app>元素下面增加以下代码行:
<filter>
<filter-name>NdcFilter</filter-name>
<filter-class>com.wrox.book.chapt14.NdcFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>NdcFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
上述XML中的url-pattern 元素与这个Web应用中的所有URL匹配。如果只想应用于部分URL,可以相应地调整模式。
还需要对logj.properties 文件的配置做一些改变,以便看到NDC上下文。将本章前面提供的示例 log4j.properties 文件中的下一行:
log4j.appender.ConsoleOut.layout.ConversionPattern=%-5p: [%d] %c{1} - %m%n
替换为:
log4j.appender.ConsoleOut.layout.ConversionPattern=%-5p: [%d] %x %c{1} - %m%n
%x 表示会在每个日志行上打印当前NDC上下文。完成这个修改之后,记录消息的每个方法都会同时记录会话ID;尽管方法本身可能并不知道它在作为Web应用的一部分运行,而且对会话也一无所知。
希望你已经了解到如何利用这个特性来跟踪某个用户,或者跟踪其他特定于应用的信息。一旦这个信息记入日志,则能很容易地利用一个命令行工具将信息抽取出来,如grep。
前面的讨论基于一个假设:我们会捕获到所有日志项,然后在分析中对其过滤。还要指出,由于高负载环境中流量可能非常大,因此往往不可能收集到生成的所有日志项。对于这个问题,有几个可能的解决方案,不过最常用的就是使用日志过滤。我们已经看到,Log4j和SDK Logger都提供了一个功能来限制所生成日志消息的数目,即使用日志级别或优先级,同时用完全限制类名命名日志记录器。这两个框架还提供了一个过滤器功能以进一步限制所记录的日志消息。SDK Logger支持为日志记录器和日志处理器创建过滤器,Log4j支持为附加器建立过滤器。SDK没有提供任何过滤器。如果需要,必须自行提供。Log4j则提供了4个过滤器:
q DenyAll:阻塞所有日志消息。
q LevelMatch:只记录或阻塞指定级别的消息。
q LevelRange:记录或阻塞指定级别范围内的消息。
q StringMatch:根据消息文本中的匹配串来记录或阻塞消息。
Log4j过滤器扩展了抽象的org.apache.log4j.spi.Filter 类,而且必须实现一个decide()方法。







