调试
这是 “使用 Android Studio 开发 Web 程序” 系列的第三篇文章,接续前一篇文章的内容,源代码照着规格打完了,第一件事当然就是先运行看看成果,如果有出现不符预期的结果,就要进到了调试的程序以便修正源代码。但如果负责的是 Server 端的元件,还要先准备好 Container 的环境、布署编译好的档案、调整设定档等等的程序。过往大部份人可能都会选择 Tomcat 做为 Container,而现在有一个轻量化的选择 Jetty。
Jetty 已在 Gradle 的支持范围内,透过 Gradle 的 Jetty Plugin 来运行 jettyRun Task 就可以直接启动 Jetty,并透过浏览器来检视程序运行后的网页结果,不需要再加外进行安装 Jetty 的动作。
使用 Jetty Plugin 的 build.gradle 档案内容,示范如下:
apply plugin: "war"
apply plugin: "jetty"
dependencies {
compile "javax.servlet:servlet-api:2.5"
}
httpPort = 8080
stopPort = 9090
stopKey = "stopKey"
以下是使用 Android Studio 的 Terminal 窗口下指令后显示的结果(运行前要先切换到项目所在的文件夹):
如果运行成功,就可以依照结果上出现的网址输入在浏览器里,浏览器会显示程序运行的结果。另外,由于 Android Studio 产生的 Gradle 项目预设会使用 Gradle Wrapper,Wrapper 会被产生在项目的相同文件夹下。运行时是使用名称为 gradlew 的 Wrapper,在 Windows 的平台是批文件、非 Windows 的平台则为 Shell Script,<u>Shell Script 要先设定运行的权限</u>。透过 Wrapper 会自动下载 Gradle 的程序后运行,所以不用先行安装 Gradle,但是如果对 Gradle 版本有特别要求的话,则要改为使用自行安装 Gradle 的方式。
只是要在 IDE 中设断点,并且在浏览器提出要求的过程中触发中断,还需要在命令列中增加以下的参数:
—Dorg.gradle.daemon=true -Dorg.gradle.jvmargs="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
第一个参数是要 Gradle 以 Daemon 模式运作,Daemon 模式下会重用 Process、减少 Gradle 程序启动的成本,可以加快每一次运行 Gradle 指令时的生成速度。当只有第二个参数被使用时,第一个参数是输出时会提示的选项,同时也是官网建议在进行开发、调试时使用的选项。如果是使用 Android Studio 来启动 Task 则不用特别设定,预设就是使用 Daemon 模式。
第二个参数是开启 JVM 调试功能,让 Debugger 可以透过 5005 Port
与 JVM 建立调试所需的通讯机制。address=5005
所指定的数字可以更改成任意没有被占用的 Port,但必须要与 Debugger 所设定的 Port 相同。
完成了准备的工作后,接下来就要让 Android Studio 的 Debugger 知道调试目标的资讯以便进行调试的工作。点选 Android Studio 的选单功能【Run -> Edit Configurations...】会出现 “Run/Debug Configurations” 窗口。
在 “Run/Debug Configurations” 窗口左上方点选【+】按钮,选择 “Remote” 类型,在下方清单的 Remote 分类就会多出一个项目,设定的画面如下方所示。
所输入的 “Name” 会被显示在 Toolbar 中【Run/Debug Configuration】的下拉菜单中。由于同时启动多个 Configuration 的运行个体会有 Port 被占用的问题,所以勾选了 “Single instance only” 选项。
在 Configuration 选项卡中,上半部是提示要设定 JVM 进行远端调试的参数范例,不同时期的 JDK 有不同的参数规格,每一个范例右方有复制按钮,可以直接复制到剪贴簿中,省去打字的麻烦。
“Settings” 群组内的选项,如果是在开发阶段于本机进行调试可以使用预设的内容。但如果是在系统测试或正式环境中调试,Host 值应该要依据实际的 Server 名称或 IP 位址填入。Port 则是依据 JVM 参数所下的 address 数值做调整。
完成以上的设定,就可以在 Terminal 窗口中运行之前示范并加上参数的指令,待等 Jetty 启动。回到源代码编辑窗口设定好正确的断点位置,接着在 Toolbar 中,选择【Run/Debug Configuration】的下拉清单里刚才设定的 Remote 类型的项目,按下右方 Debug 图标的按钮或 Shift+F9 的快速键。
如果一切顺利,会在 Android Studio 下方的 Console 窗口中看到以下讯息:
Connected to the target VM, address: 'localhost:5005', transport: 'socket'
代表 Debugger 已经成功的和 Jetty 的 JVM 连结上,可以开始进行调试的作业。此时就可以启动浏览器,输入网址,以便运行程序。当运行到所设定的断点就会和其他项目一样,IDE 就会停在断点对应的源代码上,并且可进行观察变量内容、单步执行等操作。
到这里虽然工作已经可以进行了,但是每一次运行调试都要先下一次指令太麻烦了,而且当源代码有修改,则 Jetty 要重启才会运行新的内容。然而先前所下的 gradlew 指令会 Block 住 Terminal,要先按下 Ctrl+C 之后才会关闭 Jetty,也才能再下一次启动 Jetty 的指令。
gradlew 指令会 Block 住的情况如果不消除,对以后自动化生成的作业也会造成问题、导致自动化无法进行。所幸 Jetty Plugin 也有提供 Daemon 参数,可以消除 jettyRun 运行后会 Block 住的情况。为了后续自动化生成也可以一体套用,所以在项目文件夹的 build.gradle 里加上以下的内容:
jettyRun.daemon = true
加完了之后,可以先在 Terminal 试看看,果然可以直接回到提示字元。但这时要记得再运行 jettyStop 的 Task,不然 Jetty 没有关闭,下次运行 jettyRun 时会出现 Port 被占用的错误讯息。
接下来为了确保 jettyRun 在运行前,Jetty 都有确实做关闭的程序,所以要在 build.gradle 里设定 jettyRun 运行之前要先运行 jettyStop。但目前示范使用的 Gradle 2.2.1 有一个 Bug,使用 Daemon 模式的 Jetty 无法被 jettyStop 正常地关闭,参考官方的解决方法后,必须要新增的内容如下:
import org.gradle.api.plugins.jetty.internal.Monitor
[jettyRun, jettyRunWar]*.doLast {
/**
* THIS IS A WORKAROUND! THE CURRENT VERSION OF THIS TASK DOESN'T START A WATCHER IN DAEMON MODE
*
* If starting the monitor fails, it may be because the jetty task was updated to fix this issue
* When that happens, we shouldn't need the custom task any more
*
* Copied From: AbstractJettyRunTask
*/
if (getStopPort() != null && getStopPort() > 0 && getStopKey() != null) {
Monitor monitor = new Monitor(getStopPort(), getStopKey(), server.getProxiedObject());
monitor.start();
}
}
[jettyRun, jettyRunWar].each { jetty ->
jetty.dependsOn jettyStop
}
剩下的就是让之前设定好的 Run/Debug Configuration 与 jettyRun 串连起来,先回到 “Run/Debug Configurations” 窗口,并新增一个 Gradle 类型的 Run/Debug Configuration。在 “Name” 的栏位里输入一个自己可以识别的名称,同时为了保险起见勾选了 “Single instance only” 选项,以避免 Debug Port 被不同 JVM Instance 占住了。
“Gradle project” 栏位直接由 Registered Gradle projects 里挑选对应的 Web 项目,“Tasks” 栏位输入 jettyRun
,“VM Option”输入 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=50005
,或是由 Remote 类型中复制后贴上。
这里补充说明一下,JVM 的参数也可以设定在 gradle.property 里,gradle.property 可放在以下几个位置:
- 要套用 JVM 参数的项目其 build.gradle 所在文件夹
- 登录使用者 Home 路径下的 .gradle 文件夹
但在命令列中指定,将会覆盖过 gradle.property 设定的内容,先前的 VM Option 则等同于在命令列中指定。选择在 Run/Debug Configuration 里设定是因为只有在 IDE 里才有 Debug 的需求,如果设在项目文件夹的 gradle.property 档案内,当档案被 Checkin 到版控系统,可能会被 CI 系统下载进而影响测试或自动化生成的结果。所以为了不影响测试或自动化生成,才会避免在 gradle.property 中进行相关的设定。
最后,点选回到最早建立的 Remote 类型项目,在设定画面的右下方空白清单中指定刚才新增 Gradle 类型项目为 Before execute。完成后,在【Run/Debug Configuration】的下拉清单指定 Remote 类型项目并点选 Toolbar 上的 Debug 图标,如果设定无误就可以看到过程中运行了 jettyStop 及 jettyRun,并把 Debugger 连结到指定的 Port 上一次完成。所以每次重启调试只要一个按键,再切换回浏览器重新载入页面即可。
如果不是要调试,只是要看运行的结果,可以将【Run/Debug Configuration】的下拉清单切换成 Gradle 类型的项目后,按下运行图标的按钮即可。
在 GitHub 上有人提供了一个可以使程序更简易的 Plugin 叫 Gretty,有兴趣可以自行研究看看。接下来会进入到系列文章的最后一篇:测试。
以下为完整的 build.gradle 的内容:
apply plugin: "war"
apply plugin: "jetty"
import org.gradle.api.plugins.jetty.internal.Monitor
[jettyRun, jettyRunWar]*.doLast {
/**
* THIS IS A WORKAROUND! THE CURRENT VERSION OF THIS TASK DOESN'T START A WATCHER IN DAEMON MODE
*
* If starting the monitor fails, it may be because the jetty task was updated to fix this issue
* When that happens, we shouldn't need the custom task any more
*
* Copied From: AbstractJettyRunTask
*/
if (getStopPort() != null && getStopPort() > 0 && getStopKey() != null) {
Monitor monitor = new Monitor(getStopPort(), getStopKey(), server.getProxiedObject());
monitor.start();
}
}
[jettyRun, jettyRunWar].each { jetty ->
jetty.dependsOn jettyStop
}
dependencies {
compile project(":utils")
compile "javax.servlet:servlet-api:2.5"
}
jettyRun.daemon = true
httpPort = 8080
stopPort = 9090
stopKey = "stopKey"