之前在手机上写 Java 时,总是绕不开“代码写好了,但环境跑不起来”的尴尬。手机上不是不能装编译器,而是很多人不知道该怎么把 JDK、源码目录、编译命令、运行参数串成一整套可复用的流程。网上资料大多只讲“装个 Termux、装个 JDK、敲 javac 编译”,一旦遇到多文件项目、第三方 jar 包、重复编译清理,就又回到手动敲命令的老路。这篇文章就把“手机构建 Java 项目脚本”这件事完整拆开,从环境搭建到脚本设计,再到打包、备份、常见报错排查,给出一套能直接照做的方案。不管是 Java 入门练手,还是临时写个工具类,都能在这套流程里快速跑通。
1. 背景与核心概念
1.1 手机为什么能构建 Java 项目
一提到 Java 项目构建,本能反应是“用 IDEA 或 Eclipse”。但 Java 的核心工具链其实非常轻量:只要有javac(Java 编译器)和java(Java 运行时),就能完成编译和运行。手机端的 Android 系统虽然不能直接运行桌面版 JDK,但我们可以通过 Termux 这类终端模拟器,在手机上创建一个 Linux 用户态环境,再安装 OpenJDK。这样手机就具备了和电脑相似的 Java 开发能力。
所谓“手机构建 Java 项目脚本”,就是在 Termux 环境中编写一个 shell 脚本,把javac、java、jar、rm、find等命令封装成统一的入口。程序员只需要执行./build.sh build、./build.sh run,就能完成编译和运行,不需要记住一串文件名和参数。
这里要先区分一个概念:手机上运行 Java 程序,不等于开发 Android 应用。Android 应用需要 Android SDK、Gradle、AAPT 等一系列工具链,而且在手机上做完整 APK 构建非常吃力。本文讨论的是普通 Java 项目,比如算法练习、命令行工具、小型后端原型,这类项目在手机上完全可以跑得很顺畅。
1.2 适合与不适合的场景
先说说适合的场景。
第一个是 Java 语法学习和算法练习。手机上写个Main.java,里面实现冒泡排序、链表反转等经典题目,编译运行非常方便。第二个是写小型命令行工具。比如做一个批量文件重命名工具、日志分析脚本,用 Java 写完后直接编译执行。第三个是临时验证代码思路。比如调试某个正则表达式、测试某个 jar 包的 API 行为,不需要桌面 IDE 的重型工程。
不适合的场景也很明显。大型多模块 Maven/Gradle 项目,依赖解析和增量构建在手机上非常缓慢,而且磁盘和内存都受限。完整 Android 应用构建则依赖大量 SDK 组件,Termux 中配置难度极高,不建议尝试。需要重度调优和断点调试的场景,手机端的命令行调试工具明显不如桌面 IDE 顺滑。
1.3 一条完整的手机构建链路
要在手机上实现“编写 Java 代码 → 脚本构建 → 运行结果”,需要三层结构:
第一层是终端环境。Termux 提供了类 Linux 的包管理能力,通过pkg命令安装软件。第二层是 Java 工具链。OpenJDK 包含javac、java、jar、javadoc等命令,是构建过程的核心。第三层是自动化脚本。用 bash 编写命令组合,处理目录、classpath、编译参数、运行参数。
下面开始从环境准备讲起,一步步把所有环节打通。
2. 环境准备与版本说明
2.1 在 Termux 中安装 JDK
Termux 的安装方式很简单:在酷安、GitHub Releases 或 F-Droid 中搜索 Termux,安装后打开。初次启动会执行基础文件初始化,然后就可以使用 apt/pkg 包管理器了。
这里强调一点:Termux 的软件源版本会随时间变化,不同时期的 OpenJDK 包名可能不同。最稳妥的方式是先更新源,再搜索可用的 JDK 包。
pkg update && pkg upgrade pkg search openjdk执行pkg search openjdk后,会看到类似openjdk-17的包名。以常见的 openjdk-17 为例,安装命令如下:
pkg install openjdk-17如果你的网络环境较慢,可以考虑使用国内镜像源,这一步在 Termux 的官方文档中有详细说明。安装完成后,验证 Java 环境:
java -version javac -version预期输出类似:
openjdk version "17.0.x" 2024-xx-xx LTS OpenJDK Runtime Environment (build 17.0.x+xx) OpenJDK 64-Bit Server VM (build 17.0.x+xx, mixed mode, sharing)2.2 需要提前准备的配置项
Termux 默认只允许访问应用内部目录/data/data/com.termux/files/home。如果需要读写手机的公共目录,比如/sdcard/Download,需要先执行:
termux-setup-storage执行后手机会弹窗请求存储权限,选择允许即可。之后 Termux 会在用户目录下生成storage/快捷目录,可以通过~/storage/downloads访问下载目录。如果访问外置 SD 卡遇到权限问题,说明没有授权或手机文件系统策略较严,这时建议把项目放在 Termux 内部目录中,避免干扰。
为了后续脚本稳定运行,还需要确认 bash 环境正常。Termux 默认自带 bash,但有些精简包可能没有安装。如果不确定,可以执行:
pkg install bash版本说明:本文示例面向通用环境,JDK 版本以 17 为例,如果你安装的是 21,命令也是通用的。关键是java和javac能正常打印版本号。
2.3 规划项目目录结构
一个规范的 Java 项目,无论放在手机还是服务器上,目录结构都建议保持一致。推荐在手机主目录下创建如下结构:
~/java-demo/ ├── src/ # Java 源码目录 │ ├── Main.java │ └── com/ │ └── demo/ │ └── Greet.java ├── lib/ # 第三方 jar 包目录 ├── out/ # 编译输出目录 ├── logs/ # 运行日志目录 └── build.sh # 构建脚本src放源码,lib放外部依赖,out由脚本自动生成,logs记录运行输出,build.sh是统一入口。这种结构符合大多数 Java 项目和 CI 工具的习惯,以后把项目转移到电脑或服务器,也能无缝衔接。
创建目录的命令:
mkdir -p ~/java-demo/src ~/java-demo/lib ~/java-demo/logs接下来进入编码环节,先写几个示例源码文件。
3. 从手动编译到脚本构建的进化
3.1 单文件编译运行
先看最简单的单文件 Java 程序。在src目录下创建Main.java:
// 文件路径:~/java-demo/src/Main.java public class Main { public static void main(String[] args) { System.out.println("Hello from Phone Java!"); for (String arg : args) { System.out.println("arg: " + arg); } } }手动编译运行需要两步:
cd ~/java-demo javac -encoding UTF-8 -d out src/Main.java java -cp out Main 你好 世界预期输出:
Hello from Phone Java! arg: 你好 arg: 世界这个流程虽然能跑,但每次都需要手动输入javac和java命令。如果文件多了,或者引用了外部 jar 包,命令会越来越长,这时候脚本的作用就体现出来了。
3.2 多文件与包结构
Java 项目通常会有包结构。比如我们再创建一个Greet类,放在com.demo包下。
// 文件路径:~/java-demo/src/com/demo/Greet.java package com.demo; public class Greet { public static void say(String name) { System.out.println("Hello, " + name + "!"); } }修改Main.java,让主类使用Greet:
// 文件路径:~/java-demo/src/Main.java import com.demo.Greet; public class Main { public static void main(String[] args) { System.out.println("Hello from Phone Java!"); Greet.say("CSDN"); for (String arg : args) { System.out.println("arg: " + arg); } } }手动编译时,需要把两个 java 文件都传给javac:
javac -encoding UTF-8 -d out src/Main.java src/com/demo/Greet.java如果源码文件有几十个,手动列出文件就不现实了。更通用的做法是用find命令收集所有 java 文件:
find src -name "*.java" -type f | sort在脚本中,我们会把这条命令的结果作为javac的输入,这样无论项目有多少源码文件,都能一次编译。
3.3 引入第三方依赖库
实际项目很少零依赖。假设我们的 Java 项目需要使用某个第三方库,比如 JSON 处理库、Apache Commons 工具包等。第一步是下载 jar 包到lib目录,然后编译时通过-cp指定依赖路径。
手动命令会变成这样:
javac -encoding UTF-8 -d out -cp "lib/commons-lang3.jar" src/Main.java src/com/demo/Greet.java运行时的 classpath 也要同时包含out和lib下的 jar:
java -cp "out:lib/commons-lang3.jar" Main依赖增多后,classpath 会变得很长。bash 脚本里可以用循环拼接所有 jar 包路径,也可以直接使用通配符。有一点需要注意:java -cp支持目录后直接跟*表示加载该目录下所有 jar,但javac和java对*的处理存在差异,为了跨环境稳定,建议在脚本中用循环拼出完整 classpath。
下面进入重头戏,编写完整的构建脚本。
4. 完整构建脚本实战
4.1 脚本功能设计
构建脚本要解决的核心问题是:无论源码结构多复杂,使用者都只需要记住极少数命令。我设计的功能如下:
init:初始化项目目录结构。build:编译所有源码到out目录。run:编译后运行主类,支持传入参数。clean:清理编译输出目录。pack:将编译结果打成可执行 Jar 包。
脚本要求:
- 出错立即退出,避免“编译失败还继续运行”的情况。
- 自动收集源码文件,不手动维护文件列表。
- 自动拼接 classpath,加载
lib目录下所有 jar。 - 输出友好提示,方便在手机上直接查看构建状态。
4.2 完整 build.sh 源码
#!/data/data/com.termux/files/usr/bin/bash # 手机构建 Java 项目脚本 # 用法:./build.sh [init|build|run|clean|pack] set -e PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" SRC_DIR="$PROJECT_DIR/src" LIB_DIR="$PROJECT_DIR/lib" OUT_DIR="$PROJECT_DIR/out" LOG_DIR="$PROJECT_DIR/logs" MAIN_CLASS="Main" # 收集所有 Java 源码文件 collect_sources() { find "$SRC_DIR" -name "*.java" -type f | sort } # 拼接 classpath:out 目录 + lib 目录下所有 jar make_classpath() { LOCAL_CP="$OUT_DIR" if [ -d "$LIB_DIR" ]; then for jar in "$LIB_DIR"/*.jar; do if [ -e "$jar" ]; then LOCAL_CP="$LOCAL_CP:$jar" fi done fi echo "$LOCAL_CP" } # 初始化目录结构 init() { mkdir -p "$SRC_DIR" "$LIB_DIR" "$LOG_DIR" echo "[INFO] 目录结构已就绪:src / lib / logs" } # 编译项目 build() { echo "[INFO] 开始编译项目..." mkdir -p "$OUT_DIR" SOURCES="$(collect_sources)" if [ -z "$SOURCES" ]; then echo "[ERROR] $SRC_DIR 目录下没有找到 .java 文件" exit 1 fi javac -encoding UTF-8 -d "$OUT_DIR" -cp "$(make_classpath)" $SOURCES echo "[INFO] 编译完成,class 文件输出到 $OUT_DIR" } # 运行主类 run() { if [ ! -d "$OUT_DIR" ]; then echo "[ERROR] 尚未编译,请先执行 ./build.sh build" exit 1 fi java -cp "$(make_classpath)" "$MAIN_CLASS" "$@" } # 清理编译输出 clean() { if [ -d "$OUT_DIR" ]; then rm -rf "$OUT_DIR" echo "[INFO] 已清理 $OUT_DIR" else echo "[INFO] 没有需要清理的目录" fi } # 打包 Jar pack() { if [ ! -d "$OUT_DIR" ]; then build fi MANIFEST_FILE="$OUT_DIR/MANIFEST.MF" printf 'Main-Class: %s\n' "$MAIN_CLASS" > "$MANIFEST_FILE" jar cfm "$PROJECT_DIR/app.jar" "$MANIFEST_FILE" -C "$OUT_DIR" . echo "[INFO] 已生成 $PROJECT_DIR/app.jar" } case "$1" in init) init ;; build) build ;; run) shift run "$@" ;; clean) clean ;; pack) pack ;; *) echo "用法: $0 {init|build|run|clean|pack}" exit 1 ;; esac脚本说明:
set -e:任何一条命令返回非零状态,脚本立即退出,避免后续步骤在错误状态下继续执行。collect_sources:使用find收集所有.java文件,并通过管道传给javac,解决了多文件手动列举的问题。make_classpath:把out目录和lib下每个 jar 都拼到 classpath 中,防止频繁修改依赖路径。case子命令:让脚本具备“类工具”体验,后续加新功能不需要改调用方。
4.3 使用方式与预期输出
首先给脚本添加执行权限:
chmod +x ~/java-demo/build.sh初始化目录结构,然后执行构建:
cd ~/java-demo ./build.sh init ./build.sh build预期输出:
[INFO] 目录结构已就绪:src / lib / logs [INFO] 开始编译项目... [INFO] 编译完成,class 文件输出到 /data/data/com.termux/files/home/java-demo/out运行程序并传入参数:
./build.sh run 手机构建 Java预期输出:
Hello from Phone Java! Hello, CSDN! arg: 手机构建 arg: Java再验证打包功能:
./build.sh pack java -jar app.jar预期输出:
[INFO] 已生成 /data/data/com.termux/files/home/java-demo/app.jar Hello from Phone Java! Hello, CSDN!到这里,一个完整的“手机构建 Java 项目脚本”流程已经跑通。接下来在此基础上扩展一些工程化能力。
5. 进阶能力扩展
5.1 一键打包 Jar 的细节
pack函数中使用了jar cfm命令,这里解释几个容易踩坑的点。
jar命令的参数含义:c表示创建新归档,f表示输出到文件,m表示使用指定的 manifest 文件。写Main-Class时,类名不能带.class后缀,而且必须和包名完全匹配。如果主类在com.demo.Main,就写Main-Class: com.demo.Main。
manifest 文件末尾需要换行,否则jar命令可能报错或无法识别。我们在脚本中用printf输出并在结尾加\n,就是为了避免这个问题。
打包目录时,用-C "$OUT_DIR" .表示进入out目录后把所有内容打进 jar。如果当前目录已经有MANIFEST.MF文件,也不需要额外处理,因为它会被一起打入 jar 包内。为了整洁,也可以在打包后删除 manifest 文件,这里保持现状不影响运行。
5.2 自动备份与日志记录
手机上开发项目,最怕误删源码。在脚本中增加一个backup功能,用当前日期命名备份包:
backup() { BACKUP_DIR="$PROJECT_DIR/backups" mkdir -p "$BACKUP_DIR" STAMP="$(date +%Y%m%d_%H%M%S)" tar czf "$BACKUP_DIR/java-demo-$STAMP.tar.gz" \ -C "$PROJECT_DIR" src lib build.sh echo "[INFO] 已备份到 $BACKUP_DIR/java-demo-$STAMP.tar.gz" }再把这段逻辑加到case分支中:
backup) backup ;;同时在run中把运行输出写入日志文件,方便回看:
run() { if [ ! -d "$OUT_DIR" ]; then echo "[ERROR] 尚未编译,请先执行 ./build.sh build" exit 1 fi mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/run_$(date +%Y%m%d_%H%M%S).log" java -cp "$(make_classpath)" "$MAIN_CLASS" "$@" | tee "$LOG_FILE" }tee命令会把标准输出同时打印到终端和日志文件。这样跑完程序后,既能看到结果,也保留了完整记录。手机内存有限,建议定期清理logs目录中的旧文件。
5.3 结合 Git 进行版本管理
Termux 中安装 Git 很简单:
pkg install git项目初始化后,创建.gitignore文件,忽略编译输出、日志和备份文件:
# 文件路径:~/java-demo/.gitignore out/ logs/ backups/ *.jar *.class然后执行:
cd ~/java-demo git init git add . git commit -m "初始化手机Java项目"以后每次代码修改,执行git commit即可形成版本记录。手机端直接使用 Git 命令行操作,和电脑上的流程一致。如果需要远程备份,可以关联 GitHub、Gitee 等仓库,但注意不要在脚本中写死账号密码,推荐使用 SSH Key 或凭据管理器,避免泄露。
这里强调安全边界:涉及远程仓库操作时,不要使用简单的用户名密码认证,建议使用 SSH 公钥认证,并且不要把私钥文件提交到代码仓库。如果你只是本地学习,Git 本地版本管理已经完全够用。
6. 常见问题与排查思路
手机端构建 Java 项目,最常遇到的问题集中在环境、权限、编码、内存几个维度。下面整理一张排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
java: command not found | JDK 未安装或 PATH 异常 | 执行pkg install openjdk-17,再执行which java确认路径 |
javac: command not found | 只安装了 JRE 没有安装 JDK | 错误安装 openjdk 包,确认包名是否包含完整 JDK |
| 编译报乱码或非法字符 | 源码使用中文但没有指定 UTF-8 | 编译命令加上-encoding UTF-8,Termux 默认 locale 可能不是 UTF-8 |
Permission denied | 脚本没有执行权限 | 执行chmod +x build.sh |
bad interpreter: /bin/bash^M | 脚本在 Windows 下编辑过,存在 CRLF 换行符 | 执行sed -i 's/\r$//' build.sh转换换行符 |
| Android 文件读写失败 | Termux 未获得存储权限 | 执行termux-setup-storage,在系统弹窗中授权 |
| 手机息屏后构建中断 | Termux 进程被系统省电策略回收 | 执行termux-wake-lock保持前台运行,或在系统设置中允许 Termux 后台运行 |
OutOfMemoryError: insufficient memory | 手机物理内存不足或未限制堆内存 | 运行命令增加-Xmx256m或-Xmx512m参数 |
| 打包 jar 后运行找不到主类 | manifest 的 Main-Class 配置错误 | 检查类名是否包含完整包名,且不包含.class后缀 |
下面重点展开三个高频问题的排查流程。
第一个是“编译命令找不到”。如果java -version正常但javac -version报错,很可能是安装的包只包含运行时,不包含编译工具。这时需要重新安装完整 JDK 包。Termux 的openjdk-17包一般同时包含java和javac,确认安装成功后重启终端再试。
第二个是“中文乱码”。Termux 编译时如果源码包含中文注释或中文字符串,而终端 locale 不是 UTF-8,就可能出现“编码 GBK 的不可映射字符”之类的报错。解决方式是在脚本编译参数中加入-encoding UTF-8,同时保证 Java 源码文件本身是 UTF-8 编码保存。
第三个是“内存不足”。手机内存和电脑差距很大,JVM 默认堆大小可能不适合手机环境。可以在run()函数的java命令中加上堆内存控制:
java -Xmx256m -cp "$(make_classpath)" "$MAIN_CLASS" "$@"如果程序对内存不敏感,设置 256MB 到 512MB 即可。实际项目中,更推荐通过脚本参数动态控制,比如增加环境变量JAVA_OPTS。
7. 最佳实践与工程建议
7.1 保持项目的小而美
手机端的核心优势是轻量和随时可用,不适合把大型工程塞进手机。建议每个手机上的 Java 项目保持单模块、单入口、源码不超过几十个文件。如果项目规模变大,优先考虑迁移到电脑或服务器,用 CI 工具完成构建。手机上的脚本适合做“快速原型验证”,不适合做“全量生产构建”。
7.2 脚本命名与参数设计
build.sh的子命令设计建议参考常见 CI 工具的习惯。比如:
build:编译test:运行测试run:运行主类clean:清理产物pack:打包
这样的命令语义清晰,开发者不需要读脚本就能上手。脚本内部使用函数拆分功能,每个函数只负责一个动作,方便后续维护和扩展。
7.3 重视编译参数与编码规范
所有源码统一使用 UTF-8 编码,编译命令固定加-encoding UTF-8。这是避免中文乱码最直接的手段。javac建议额外加上-Xlint:all参数,让编译器输出更多告警,及早发现潜在问题。例如:
javac -encoding UTF-8 -Xlint:all -d "$OUT_DIR" -cp "$(make_classpath)" $SOURCES在命令行工具项目中,良好的告警信息能帮助你在运行前发现空指针、资源未关闭等问题。
7.4 安全与权限控制
脚本中使用rm -rf时要格外小心,尤其是clean和backup涉及删除或覆盖操作。建议在clean中只删除明确的out目录,不要对项目根目录做递归删除。rm -rf的路径变量必须用双引号包裹,避免路径中存在空格导致误删。
远程仓库相关操作建议使用 SSH 密钥认证,不要把明文密码写在脚本中。对于任何涉及敏感信息的配置,都应该使用环境变量或单独配置文件,并加入.gitignore。
7.5 充分利用手机特性
手机构建有一个独特优势:可以随时随地在碎片时间里写一段代码并立刻验证。建议把常用项目放在 Termux 主目录下,利用termux-wake-lock避免息屏中断长时间任务。也可以配合 GalaxyTermux:Tasker这类工具做自动化调用,但对大多数开发者来说,一个build.sh已经足够。
还有一个实用建议:脚本中加入info子命令,打印当前 Java 版本、项目路径、源码文件数量等环境信息,这样在换手机或重新配置环境后,可以快速确认环境是否正常:
info() { echo "Java 版本:" java -version 2>&1 echo "源码数量: $(collect_sources | wc -l)" echo "项目目录: $PROJECT_DIR" }把info加入case分支后,执行./build.sh info即可查看。这种“自检”思路在移动端场景下非常实用,因为手机环境不像电脑那么稳定,随时可能被系统策略影响。
7.6 下一步可以怎么扩展
本文的脚本是一个最小可用的闭环方案。在此基础上,你可以继续扩展以下能力:
- 增加
test子命令,运行 JUnit 测试类。 - 引入 Maven 或 Gradle Wrapper,在手机上构建小型标准工程。
- 增加资源文件管理,把配置文件从
src分离到resources目录。 - 结合定时任务脚本,实现手机端的定时 Java 程序执行。
如果后面有机会,我会继续写一篇“手机端运行 Spring Boot 小型接口服务”的实践笔记,把java -jar和内嵌 Tomcat 在 Termux 中的配置细节梳理一遍。当前这篇文章的内容,已经足够你在手机上跑通一套完整的 Java 项目构建流程了。