40、网页应用调试全攻略

网页应用调试全攻略

1. 调试网页应用的挑战

调试是软件开发中不可避免的环节,而网页应用调试更是困难重重。这些问题往往难以重现,通常在有大量用户访问应用时才会显现,也就是应用投入生产之后。调试网页应用之所以困难,主要有以下五个原因:
- 分布式特性 :网页应用会在多台机器上运行,即使不在多台物理机器上调试,也需要处理应用与运行环境(如浏览器)之间的逻辑边界。
- 多线程处理 :Java 中的 Web API 在开发时会隐藏 Servlet 引擎的多线程特性,但调试时仍需处理线程问题。因为 Java 中的局部变量与所属线程相关,查看局部变量值时无法避开线程。不过,一些 IDE 中的调试器可简化这一任务。
- 执行上下文不受控 :网页应用在 Servlet 引擎的上下文中运行,大部分方法调用由 Servlet 引擎完成。习惯完全控制代码执行的开发者可能会因此感到沮丧。
- 源代码访问受限 :如果使用 JSP,无法直接访问运行的源代码。JSP 会被编译成 Servlet 由 Servlet 引擎执行,虽然可以让 Servlet 引擎保存源代码,但需要将生成的源代码映射回代码执行的页面。
- 调试器影响代码行为 :调试器可能会改变代码的行为,导致调试时问题消失,而在调试器外运行时问题又重现。虽然这种情况在 Java 中比其他语言少见,但仍有可能发生。

以下是一个简单的 JSP 示例及其生成的 Servlet 源代码:

<html>
<head>
<title>
Hello
</title>
</head>
<body bgcolor="#ffffff">
<h3>Hello!</h3>
<p>
<%  for (int i = 1; i < 6; i++) { %>
        Hello for the <%=i%><%= i == 1 ? "st" : i == 2 ? "nd" :
            i == 3 ? "rd" : "th" %>  time<br>
<%  } %>
</P>
</body>
</html>

生成的 Servlet 源代码部分如下:

// HTML // begin [file="/Hello.jsp";from=(0,0);to=(9,0)]
    out.write("<html>\r\n<head>\r\n<title>\r\nHello\r\n</title>"+
    "\r\n</head>\r\n<body bgcolor=\"#ffffff\">\r\n<h3>Hello!"+
    "</h3>\r\n<p>\r\n");
// end
// begin [file="/Hello.jsp";from=(9,2);to=(9,34)]
      for (int i = 1; i < 6; i++) { 
// end
// HTML // begin [file="/Hello.jsp";from=(9,36);to=(10,22)]
    out.write("\r\n        Hello for the ");
// end
// begin [file="/Hello.jsp";from=(10,25);to=(10,26)]
    out.print(i);
// end
// begin [file="/Hello.jsp";from=(10,31);to=(11,33)]
    out.print( i == 1 ? "st" : i == 2 ? "nd" :
i == 3 ? "rd" : "th" );
// end
// HTML // begin [file="/Hello.jsp";from=(11,35);to=(12,0)]
    out.write("  time<br>\r\n");
// end
// begin [file="/Hello.jsp";from=(12,2);to=(12,6)]
      } 
// end
// HTML // begin [file="/Hello.jsp";from=(12,8);to=(16,0)]
    out.write("\r\n</P>\r\n</body>\r\n</html>\r\n");
// end

可以看到,生成的代码非常复杂。使用自定义标签库(如 JSP 标准标签库 JSTL)会使情况更糟。以下是使用 JSTL 简化代码的示例:

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<html>
<head>
<title>
Hello
</title>
</head>
<body bgcolor="#ffffff">
<h3>Hello!</h3>
<p>
<c:forEach var="i" begin="1" end="5">
Hello for the <c:out value="${i}" />
  <c:choose>
    <c:when test="${i == 1}">
      <c:out value="st" />
    </c:when>
    <c:when test="${i == 2}">
      <c:out value="nd" />
    </c:when>
    <c:when test="${i == 3}">
      <c:out value="rd" />
    </c:when>
    <c:otherwise>
      <c:out value="th" />
    </c:otherwise>
  </c:choose>
<br>
</c:forEach>
</P>
</body>
</html>

其生成的 Servlet 源代码开头部分如下:

// begin [file="/HelloSTL.jsp";from=(10,0);to=(10,37)]
/* ----  c:forEach ---- */
org.apache.taglibs.standard.tag.el.core.
  ForEachTag _jspx_th_c_forEach_0 = new org.apache.taglibs.standard.
  tag.el.core.ForEachTag();
_jspx_th_c_forEach_0.setPageContext(pageContext);
_jspx_th_c_forEach_0.setParent(null);
_jspx_th_c_forEach_0.setVar("i");
_jspx_th_c_forEach_0.setBegin("1");
_jspx_th_c_forEach_0.setEnd("5");
try {
    int _jspx_eval_c_forEach_0 = _jspx_th_c_forEach_0.doStartTag();
    if (_jspx_eval_c_forEach_0 == 
            javax.servlet.jsp.tagext.BodyTag.EVAL_BODY_BUFFERED)
        throw new JspTagException(
        "Since tag handler class org.apache.taglibs.standard.tag."+
        "el.core.ForEachTag does not implement BodyTag, it can't "+
        "return BodyTag.EVAL_BODY_TAG");
    if (_jspx_eval_c_forEach_0 != 
            javax.servlet.jsp.tagext.Tag.SKIP_BODY) {
        do {
        // end
        // HTML // begin [file="/HelloSTL.jsp";
        // from=(10,37);to=(11,14)]
            out.write("\r\nHello for the ");
        // end
        // begin [file="/HelloSTL.jsp";from=(11,14);to=(11,36)]
            /* ----  c:out ---- */
            org.apache.taglibs.standard.tag.el.core.
                OutTag _jspx_th_c_out_0 = new org.apache.taglibs.
                standard.tag.el.core.OutTag();
            _jspx_th_c_out_0.setPageContext(pageContext);
            _jspx_th_c_out_0.setParent(_jspx_th_c_forEach_0);
            _jspx_th_c_out_0.setValue("${i}");
            try {
              int _jspx_eval_c_out_0 =_jspx_th_c_out_0.doStartTag();
              if (_jspx_eval_c_out_0 != 
                    javax.servlet.jsp.tagext.Tag.SKIP_BODY) {
              try {
              if (_jspx_eval_c_out_0 != 
                  javax.servlet.jsp.tagext.Tag.EVAL_BODY_INCLUDE) {
              out = pageContext.pushBody();
              _jspx_th_c_out_0.setBodyContent(
                    (javax.servlet.jsp.tagext.BodyContent) out);
              _jspx_th_c_out_0.doInitBody();
          }
          do {
          // end
          // begin [file="/HelloSTL.jsp";from=(11,14);to=(11,36)]
          } while (_jspx_th_c_out_0.doAfterBody() == 
                javax.servlet.jsp.tagext.BodyTag.EVAL_BODY_AGAIN);
      } finally {
          if (_jspx_eval_c_out_0 != 
                javax.servlet.jsp.tagext.Tag.EVAL_BODY_INCLUDE)
              out = pageContext.popBody();
      }
  }
  if (_jspx_th_c_out_0.doEndTag() == 
        javax.servlet.jsp.tagext.Tag.SKIP_PAGE)
      return;
} finally {
  _jspx_th_c_out_0.release();
}
// end

使用 JSTL 虽然使代码更易读,非 Java 程序员也更容易理解,但调试却成了噩梦。原本的 JSP 生成的 Servlet 代码有 91 行,而使用 JSTL 后生成的 Servlet 代码多达 419 行。

2. 使用 SDK 进行调试

SDK 提供了一个基于命令行的调试器 jdb。虽然使用命令行调试器调试分布式网页应用看似不是最佳选择,但掌握它可以获取重要信息。在某些情况下,可能只能使用 SDK 调试器,例如生产应用服务器上可能没有安装 IDE。而且,作为顾问工作时,通常无法选择可用的工具。由于 jdb 是 SDK 的一部分,它随时可用。

2.1 启动调试器

jdb 调试器的文档在 JavaDocs 中,其中包含启动调试器的基本信息和可用的命令行选项。启动 jdb 调试器有两种方式:
- 替代 java 命令启动应用 :jdb 命令可替代 java 命令启动应用,调试器会启动一个调试虚拟机(VM)。
- 连接到已运行的 VM :可以连接到本地或远程机器上已运行的 VM。如果连接到远程服务器上运行的 VM,该 VM 必须使用特定的命令行选项启动,以确保调试的安全性。

以下是启用调试所需的命令行选项:
| 选项 | 用途 |
| ---- | ---- |
| -Xdebug | 启用 VM 中的调试支持 |
| -Xrunjdwp:transport=dt_shmem,server=y,suspend=n | 加载进程内调试库并选择通信的传输机制 |

第二个选项指定了加载库的默认值,它使用共享内存模型交换信息,仅在 Windows 上可用,因为它使用 Windows 共享内存原语在调试器应用程序和目标 VM 之间通信。要以调试模式启动 VM,可以使用以下命令:

java -Xdebug -Xrunjdwp:transport=dt_shmem,address=jdbcon,server=y,suspend=n MyClass

应用运行后,可以使用以下命令启动调试器并连接到它:

jdb -attach jdbcon 

其中,jdbcon 是启动 VM 时指定的地址。

当 VM 在远程机器上运行时,无法使用共享内存。另一种传输机制 dt_socket 适用于所有平台和跨机器通信,它允许调试器和 VM 通过套接字通信。使用套接字传输启动调试会话的命令与上述类似,只需将传输参数改为 dt_socket。传递给 VM 的第二个参数有许多调优选项,如下表所示:
| 名称 | 是否必需 | 默认值 | 描述 |
| ---- | ---- | ---- | ---- |
| help | 否 | none | 打印简要帮助信息并退出 VM |
| transport | 是 | none | 包含连接调试器时要使用的传输名称 |
| server | 否 | n | 如果为 y,则监听调试器应用程序的连接;否则,连接到指定地址的调试器应用程序。如果为 y 且未指定地址,则选择一个传输地址监听调试器应用程序,并将地址打印到标准输出流 |
| address | 如果 server=n 则是,否则否 | 无 | 包含连接的传输地址。如果 server=n,则尝试连接到该地址的调试器应用程序;如果 server=y,则监听该地址的连接 |
| launch | 否 | none | 在 Java 调试有线协议(JDWP)初始化完成后,启动此字符串中指定的进程。此选项与 onthrow 和/或 onuncaught 结合使用,以提供即时调试,即在 VM 中发生特定事件时启动调试器进程 |
| onthrow | 否 | none | 延迟 JDWP 库的初始化,直到此 VM 中抛出指定类的异常。异常类名必须包含包名。连接建立包含在 JDWP 初始化中,因此在异常抛出之前不会开始 |
| onuncaught | 否 | n | 如果为 y,则延迟 JDWP 库的初始化,直到此 VM 中抛出未捕获的异常。连接建立包含在 JDWP 初始化中,因此在异常抛出之前不会开始 |
| stdalloc | 否 | n | 默认情况下,JDWP 参考实现使用替代分配器进行内存分配。如果为 y,则使用标准 C 运行时库分配器。此选项主要用于测试,使用时需谨慎。禁用替代分配器可能会导致 VM 中出现死锁 |
| strict | 否 | n | 如果为 y,则假设严格符合 Java 虚拟机调试接口(JVMDI)。这将禁用所有已知 JVMDI 实现中错误的解决方法。此选项主要用于测试,使用时需谨慎 |
| suspend | 否 | y | 如果为 y,VMStartEvent 的 suspendPolicy 为 SUSPEND_ALL;如果为 n,VMStartEvent 的 suspendPolicy 为 SUSPEND_NONE |

以下是使用套接字传输在服务器上启用调试的示例命令:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000 MyClass

这些选项适用于启动托管应用的 VM。在网页应用中,Servlet 引擎管理该 VM。不同的 Servlet 引擎有不同的启动语义,但大多数允许修改启动参数(通常是整个命令行)。此时,可以替换表中的命令选项,或将 Servlet 引擎的调用从 java 改为 jdb。例如,本书示例中使用的 Tomcat 4 版本,其启动批处理文件(Catalina.bat)包含调试命令行选项。使用此选项启动 Tomcat 会打印配置信息并在调试器命令提示符处暂停。

2.2 运行调试器

启动调试器后,它会暂停等待下一个命令,此时尚未实际启动 Servlet 引擎。在应用启动前,可能需要研究一些选项(如类路径)或设置断点。在提示符处输入问号可以查看所有可用的调试器命令。

第一个命令是 run,用于启动应用。执行 run 命令会启动 Servlet 引擎,此时会看到 Servlet 引擎的正常启动日志信息。之后,可以通过浏览器调用其中一个应用。以下是一些重要的调试器选项:
| 命令 | 描述 |
| ---- | ---- |
| run [class [args]] | 启动应用主类的执行 |
| threads [threadgroup] | 列出应用中所有运行和挂起的线程 |
| thread | 设置默认线程 |
| suspend | 挂起线程(默认挂起所有线程) |
| resume [thread id(s)] | 恢复线程(默认恢复所有线程) |
| where [thread id] | all | 转储线程的堆栈 |
| up [n frames] | 向上移动线程的堆栈 |
| down [n frames] | 向下移动线程的堆栈 |
| kill | 使用给定的异常对象终止线程 |
| print | 打印表达式的值 |
| dump | 打印整个对象的信息 |
| set = | 为字段/变量/数组元素分配新值 |
| locals | 打印所有局部变量信息 |
| fields | 列出类的字段 |
| stop in . [(argument type, …)] | 在方法中设置断点(如果方法重载,必须提供参数签名) |
| stop at : | 在特定源代码行设置断点 |
| clear . [(argument type, …)] | 清除方法断点 |
| clear : | 清除行号断点 |
| catch [uncaught|caught|all] | 发生异常时中断 |
| watch [access|all] . | 设置对字段访问/修改的监视 |
| unwatch [access|all] . | 取消之前设置的监视 |
| trace methods [thread] | 跟踪方法的进入和退出 |
| step | 执行当前行 |
| step up | 执行到当前方法返回 |
| next | 执行一行(跳过方法调用) |
| cont | 从断点继续执行 |
| list [line number|method] | 打印源代码 |
| use (or sourcepath) [source file path] | 使源代码可用于列表显示 |
| classpath | 打印当前类路径信息 |
| pop | 弹出堆栈,包括当前帧 |
| redefine | 重新定义类的代码 |
|!! | 重复上一个命令 |
| | 重复命令 n 次 |
| help (or?) | 列出所有命令 |
| exit (or quit) | 退出调试器 |

由于 Servlet 引擎是多线程的,需要找出应用使用的线程 ID。使用 threads 命令可以查看 Servlet 引擎中所有当前运行的线程。输出左侧的十六进制数字标识线程,通过右侧的名称找到应用对应的线程 ID。如果条目较多,精确匹配行可能会很困难。在 Windows 中,可以启用标记功能来方便匹配。

综上所述,调试网页应用虽然困难,但通过掌握 SDK 调试器的使用,可以在一定程度上解决调试问题。后续还可以了解 IDE 中的调试器,进一步提高调试效率。

3. 使用集成开发环境(IDE)调试

除了使用 SDK 中的 jdb 调试器,还可以利用集成开发环境(IDE)进行调试。IDE 提供了图形化界面,操作更加直观便捷,能大大提高调试效率。这里介绍两款常见的 IDE:免费开源的 NetBeans 和商业的 JBuilder。

3.1 NetBeans 调试

NetBeans 是一款功能强大的开源 IDE,对 Java 开发提供了很好的支持,其调试功能也十分出色。以下是在 NetBeans 中进行调试的一般步骤:
1. 打开项目 :在 NetBeans 中打开要调试的网页应用项目。
2. 设置断点 :在代码编辑器中,点击行号旁边的空白处,设置断点。当程序执行到断点处时会暂停。
3. 启动调试 :选择“运行”菜单中的“调试项目”选项,或者使用快捷键启动调试。NetBeans 会启动应用并在调试模式下运行。
4. 调试操作
- 单步执行 :使用“单步进入”“单步跳过”“单步跳出”等按钮,逐行执行代码,观察程序的执行流程和变量的值。
- 查看变量 :在调试窗口中,可以查看当前作用域内的变量值,方便分析程序状态。
- 检查调用栈 :查看调用栈信息,了解方法的调用顺序和层次。
5. 结束调试 :调试完成后,点击“停止调试”按钮结束调试会话。

3.2 JBuilder 调试

JBuilder 是一款商业的 Java IDE,具有丰富的调试功能。以下是在 JBuilder 中进行调试的基本步骤:
1. 打开项目 :在 JBuilder 中打开要调试的项目。
2. 设置断点 :在代码编辑器中设置断点,方法与 NetBeans 类似。
3. 启动调试 :选择“运行”菜单中的“调试”选项,JBuilder 会启动应用并进入调试模式。
4. 调试操作
- 控制执行 :使用调试工具栏上的按钮,如“继续”“暂停”“单步执行”等,控制程序的执行。
- 查看变量和表达式 :在调试窗口中查看变量的值,还可以输入表达式进行计算。
- 调试线程 :由于网页应用通常是多线程的,JBuilder 提供了线程调试功能,可以查看和控制各个线程的执行。
5. 结束调试 :调试结束后,点击“停止调试”按钮退出调试模式。

3.3 IDE 调试的优势

与使用 SDK 调试器相比,IDE 调试具有以下优势:
- 图形化界面 :IDE 提供直观的图形化界面,操作更加方便,无需记忆大量的命令行选项。
- 可视化调试 :可以直观地查看变量的值、调用栈信息等,便于分析程序的执行状态。
- 集成开发环境 :IDE 集成了代码编辑、编译、调试等功能,开发和调试过程更加流畅。

4. 应用日志调试

创建应用日志是一种简单有效的调试方法,被称为“穷人的调试器”。虽然使用日志作为追踪 bug 的低技术解决方案可能有一些负面含义,但它对于分布式应用(如网页应用)非常有效。

4.1 日志调试的优点

  • 部署后调试 :在应用部署到服务器后,很难直接在服务器上调试应用。日志可以记录应用运行时的信息,无论应用在哪里运行,都可以通过查看日志来进行调试。
  • 持续监控 :日志代码可以在应用部署后继续保留,持续记录应用中出现的问题,为后续的分析和优化提供依据。

4.2 SDK 内置日志支持

从 Java SDK 1.4 版本开始,内置了日志支持。以下是一个简单的使用 SDK 内置日志的示例:

import java.util.logging.Level;
import java.util.logging.Logger;

public class LogExample {
    private static final Logger LOGGER = Logger.getLogger(LogExample.class.getName());

    public static void main(String[] args) {
        LOGGER.log(Level.INFO, "This is an info message");
        LOGGER.log(Level.WARNING, "This is a warning message");
        LOGGER.log(Level.SEVERE, "This is a severe message");
    }
}

在上述示例中,使用 java.util.logging 包中的 Logger 类记录日志。可以设置不同的日志级别,如 INFO WARNING SEVERE 等,根据需要记录不同重要程度的信息。

4.3 开源日志框架 log4j

log4j 是一个流行的开源日志框架,功能强大且灵活。以下是使用 log4j 的基本步骤:
1. 添加依赖 :在项目中添加 log4j 的依赖。如果使用 Maven 项目,可以在 pom.xml 中添加以下依赖:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
  1. 配置 log4j :创建 log4j.properties log4j.xml 文件,配置日志输出的格式、级别和目标。以下是一个简单的 log4j.properties 示例:
log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
  1. 使用 log4j 记录日志 :在代码中使用 Logger 类记录日志。
import org.apache.log4j.Logger;

public class Log4jExample {
    private static final Logger LOGGER = Logger.getLogger(Log4jExample.class);

    public static void main(String[] args) {
        LOGGER.debug("This is a debug message");
        LOGGER.info("This is an info message");
        LOGGER.warn("This is a warning message");
        LOGGER.error("This is an error message");
    }
}

4.4 日志调试流程

使用日志进行调试的流程如下:

graph LR
    A[确定调试目标] --> B[添加日志记录代码]
    B --> C[部署应用并运行]
    C --> D[收集日志信息]
    D --> E[分析日志]
    E --> F{是否找到问题}
    F -- 是 --> G[修复问题]
    F -- 否 --> B[添加更多日志记录代码]
    G --> H[验证修复结果]

5. 总结

调试网页应用是一项具有挑战性但又必不可少的工作。本文介绍了多种调试方法,包括使用 SDK 调试器、集成开发环境(IDE)调试和应用日志调试。
- SDK 调试器 :如 jdb,虽然操作相对复杂,但在某些情况下(如生产环境)是必不可少的调试工具。
- IDE 调试 :NetBeans 和 JBuilder 等 IDE 提供了图形化界面和丰富的调试功能,能提高调试效率。
- 应用日志调试 :通过记录应用运行时的信息,为调试提供了有力的支持,尤其适用于分布式应用。

在实际调试过程中,可以根据具体情况选择合适的调试方法,或者结合多种方法进行调试,以快速定位和解决问题。同时,不断积累调试经验,提高调试技能,才能更好地应对各种复杂的调试场景。希望这些调试方法能帮助开发者更高效地开发和维护网页应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值