在手机上学 Java、写 Java,最尴尬的不是语法,而是每次想跑一个小程序都得打开电脑、启动 IDE、等编译环境加载完。如果你手边只有一台手机或平板,又想保持练习节奏,那本文要聊的这套“手机构建 Java 项目脚本”就非常实用:它把编译、运行、清理、批量构建这些固定操作全部封装成 shell/bat 脚本,你在手机终端里敲一条命令就能完成一次完整构建,整个流程不依赖重型 IDE,也不需要图形界面。
这套方案的核心价值不是替代电脑,而是把 Java 项目的“构建过程”从 IDE 按钮变成了一组可复制、可修改、可自动执行的脚本。对于 Java 基础学习、命令行工具开发、算法题验证、临时写个小服务这类场景,它完全够用。如果你还想顺便练一练 shell 脚本、理解 javac 和 java 命令的参数、搞明白 CLASSPATH 和目录结构,这套脚本就是最好的练习载体。本文会详细拆解脚本的设计思路、手机端环境准备、编译运行测试、批量任务扩展、接口化思路和常见问题排查,你照着操作即可在 Android 手机 Termux、UserLAnd 或任意 Linux 终端里跑通一个 Java 项目构建流程。
1. 核心能力速览
先给结论:这套“手机构建 Java 项目脚本”不是一个完整的框架,也不涉及 Gradle/Maven 那套重量级工具链,它的定位是轻量级构建脚本集合。下面用一张表格快速看清它能做什么、需要什么。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 本地 Java 项目构建脚本集合,面向基础 Java 开发与命令行工具场景 |
| 核心功能 | 环境自检、编译全部源文件、运行指定入口类、清理输出目录、批量编译多个子项目、参数传递、日志记录 |
| 运行平台 | Android Termux、UserLAnd、AidLux、任意 Linux 终端、Windows 自带终端/CMD |
| 依赖要求 | JDK 17 或其它可用的 JDK 版本,终端环境,bash或cmd支持 |
| 启动方式 | 终端命令执行:bash menu.sh或./menu.sh,Windows 下执行build.bat |
| 是否支持 API | 脚本本身不提供 REST API,但可通过扩展 Java HTTP Server 或配合后台任务实现基础接口服务 |
| 是否支持批量任务 | 支持。可对多个子项目目录执行顺序构建,也支持在单个项目中批量编译多个.java文件 |
| 硬件要求 | 手机端建议 4GB 以上内存,小项目 2GB 内存可运行,大型项目更稳妥的方案仍是电脑或云服务器 |
| 存储占用 | 主要占用来自 JDK 和项目文件,JDK 安装包通常在 200MB 到 400MB 之间,安装后占用约 300MB 到 600MB |
| 更适合谁 | Java 初学者、命令行工具开发者、临时需要手机写代码验证思路的程序员 |
脚本本身不追求替代 IDE 的代码补全和调试能力,它的重点是把“构建”变成一个稳定、可重复、可在手机低性能环境下运行的动作。你不用每次手敲javac -d out -encoding UTF-8 src/com/xxx/xxx.java,只要在菜单里选一个数字,或者直接执行bash build.sh,剩下的交给脚本来处理。
2. 适用场景与使用边界
这套方案最典型的适用场景是手机端 Java 基础练习和轻量级项目构建。例如你在通勤路上想验证一个排序算法、想写一个命令行文本处理工具、想练习多文件项目下的类调用关系,手机 Termux 里安装 JDK 后配合脚本,就能完成从“写代码”到“看到运行结果”的完整闭环。它还可以帮助你把构建习惯自动化:每次只改代码,不用关心编译输出目录、源文件列表和 classpath 拼写,脚本统一处理。
另外,它对学习 shell 脚本本身也很有价值。你会发现,javac、java、find、command -v、if [ $? -ne 0 ]这些命令组合在一起,能形成一个完整的工程化流程。很多 Java 开发者对 IDE 按钮背后的编译过程没有感知,而脚本恰好补上了这个盲区。遇到OutOfMemoryError、source/target 版本不匹配、找不到主类这些问题时,你也能从脚本日志里更快定位。
不过这套方案有明确的使用边界。它不适合用来构建大型 Android 项目,因为 Android 工程依赖 Gradle 和大量 SDK 组件,手机内存和存储很容易成为瓶颈;也不适合构建需要复杂依赖管理的 Spring Boot 微服务项目,因为 Maven/Gradle 在手机端运行效率不高,依赖下载和容器化部署在手机上不是合理的工程选择。它更适合的是“单机运行的 Java 程序”和“纯 JDK 自带 API 的项目”。
使用边界还包括合规和安全。脚本只应该用来构建你自己编写、或者已获得授权的代码,不要用它批量循环请求外部服务、尝试绕过任何访问控制、抓取未授权数据。手机端保存敏感代码或密钥时要注意隐私,不要将带密钥的脚本提交到公开仓库,也不要在共享终端上直接展示config.properties内容。如果后续把脚本服务化,对外提供接口,必须先限制访问范围并进行身份校验,避免未授权调用。
3. 手机端 Java 环境准备
想要在手机上跑通“手机构建 Java 项目脚本”,第一步是在手机终端里安装一个可用的 JDK。这里以 Android 手机最常用的 Termux 为例说明通用流程,实际命令需要按你的 Termux 版本和包管理策略调整。
Termux 安装 JDK 主要有两条路径。一条是使用包管理器安装发行版自带的 OpenJDK,例如在较新的 Termux 中执行:
pkg update pkg install openjdk-17如果你的 Termux 包源中不叫openjdk-17,可以搜索一下可用的 JDK 包名:
pkg search openjdk另一条路径是使用sdkman这类版本管理工具安装自定义 JDK。sdkman在手机终端上也能运行,缺点是下载体积较大,优点是不同项目可以切换 JDK 版本,适合需要测试多个 Java 版本的场景。通用安装流程如下:
curl -s "https://get.sdkman.io" | bash source ~/.bashrc sdk install java 17.0.9-tem安装完成后,先验证基础环境是否可用:
java -version javac -version echo $JAVA_HOME如果java命令能输出版本信息,javac也能正常输出版本,说明 JDK 已可用。JAVA_HOME在 Termux 中不一定是必需的,因为终端会从 PATH 中找到java和javac;但如果之后要跑 Maven/Gradle 或任何需要显式定位 JDK 的工具,就需要把JAVA_HOME写进~/.bashrc。
接下来准备编码环境。手机端可以选择直接用nano或vim编辑源文件:
pkg install nano vim nano src/com/example/Main.java如果你更习惯图形界面,可以在手机上安装 code-server,通过浏览器访问 VS Code Web 版,再用终端执行脚本。这种情况下,脚本本身不改变,只是编辑体验更接近电脑。
环境准备阶段还需要注意目录规划和文件权限。建议在 Termux 的~/projects/下创建 Java 项目,例如:
mkdir -p ~/projects/demo/src/com/example cd ~/projects/demo后续所有脚本都放在项目根目录下,让脚本的SRC_DIR指向src,OUT_DIR指向out,这样目录结构清晰,也便于备份。
4. 手机构建 Java 项目脚本核心设计
脚本设计的核心思路是:用变量统一管理目录路径,用函数和分支组织构建动作,用退出码标记失败,用日志记录每个步骤。这里给出一个完整可运行的脚本集合,你将其复制到项目根目录后,修改主类名和目录结构即可使用。
4.1 项目目录结构
推荐的结构如下:
demo/ ├── build.sh # 编译脚本 ├── run.sh # 运行脚本 ├── menu.sh # 菜单脚本 ├── check_env.sh # 环境检查脚本 ├── build.bat # Windows 下的编译脚本 ├── batch_build.sh # 批量编译脚本 ├── src/ │ └── com/example/ │ └── Main.java ├── lib/ # 存放第三方 jar,可选 └── out/ # 编译输出目录,由脚本自动创建这个结构把“源文件目录”和“输出目录”分开,避免编译产物污染源码目录。lib目录用来放外部 jar,如果项目只用 JDK 自带 API,lib可以完全省略。
4.2 编译脚本 build.sh
#!/usr/bin/env bash # build.sh - 编译 src 目录下所有 Java 源文件 SRC_DIR="src" OUT_DIR="out" if ! command -v javac >/dev/null 2>&1; then echo "[ERROR] 未找到 javac,请先安装 JDK 并确认 PATH 配置" exit 1 fi mkdir -p "$OUT_DIR" find "$SRC_DIR" -name "*.java" > sources.txt if [ ! -s sources.txt ]; then echo "[ERROR] $SRC_DIR 目录下没有找到任何 Java 文件" rm -f sources.txt exit 1 fi echo "[INFO] 开始编译,共 $(wc -l < sources.txt) 个源文件" javac -encoding UTF-8 -d "$OUT_DIR" @sources.txt if [ $? -ne 0 ]; then echo "[ERROR] 编译失败,请检查上面的错误信息" rm -f sources.txt exit 1 fi rm -f sources.txt echo "[OK] 编译完成,class 文件输出到 $OUT_DIR 目录"这个脚本有几个关键点值得注意。find "$SRC_DIR" -name "*.java" > sources.txt会把所有源码文件名写入sources.txt,然后javac ... @sources.txt表示从文件读取源文件列表,这样即使源码分布在多个子包中也能一次编译完成。-encoding UTF-8是为了避免中文注释和字符串乱码。做完编译后用$?检查javac的退出码,如果非 0,说明编译过程有错误,脚本立即退出并返回失败状态。
如果你在电脑上还装了多个 JDK 版本,想在编译时显式指定,可以把javac换成全路径,或者在脚本开头设置:
JAVAC=javac不推荐在脚本里写死/usr/lib/jvm/java-17-openjdk-amd64/bin/javac这类绝对路径,因为手机 Termux 和电脑 JDK 安装位置不一样,写死路径会降低脚本的可移植性。更好的做法是确保javac在 PATH 中,然后直接调用javac。
4.3 运行脚本 run.sh
#!/usr/bin/env bash # run.sh - 运行编译后的主类,支持传入命令行参数 OUT_DIR="out" MAIN_CLASS="${1:-com.example.Main}" shift 2>/dev/null || true if [ ! -d "$OUT_DIR" ]; then echo "[ERROR] 未找到 $OUT_DIR 目录,请先执行 build.sh 编译" exit 1 fi if [ ! -f "$OUT_DIR/${MAIN_CLASS//.//}.class" ]; then echo "[WARN] 未找到主类 $MAIN_CLASS,请确认类名是否正确" echo "[INFO] 可先执行 find $OUT_DIR -name '*.class' 查看已编译的类" fi java -cp "$OUT_DIR" "$MAIN_CLASS" "$@"运行脚本接收两个部分:第一个参数是主类全限定名,如果没传,默认使用com.example.Main;第一个参数之后的参数都会传给 Java 程序。这里的${MAIN_CLASS//.//}是 bash 的变量替换语法,把com.example.Main转换成com/example/Main.class,用来做文件存在性检查,方便提前发现类名写错的问题。
4.4 一键构建菜单 menu.sh
对于手机端使用,记忆命令本身就是成本。菜单脚本把常用操作集中在一个命令里,适合每天高频使用。
#!/usr/bin/env bash # menu.sh - Java 项目构建菜单 while true; do echo "======================================" echo " Java 项目构建菜单" echo "1. 检查 Java 环境" echo "2. 编译项目" echo "3. 运行项目" echo "4. 清理编译输出" echo "0. 退出" echo "======================================" read -rp "请输入选项编号: " choice case "$choice" in 1) bash check_env.sh ;; 2) bash build.sh ;; 3) bash run.sh com.example.Main ;; 4) rm -rf out && echo "[OK] 已清理 out 目录" ;; 0) echo "退出菜单" break ;; *) echo "[WARN] 无效选项,请重新输入" ;; esac done使用时只需要在项目根目录执行:
bash menu.sh脚本会循环显示菜单,直到你输入 0 退出。手机端不需要记忆复杂的命令,这是“手机构建 Java 项目脚本”对新手最友好的一个点。
4.5 Windows 环境下的 build.bat
如果项目需要在电脑端验证,但暂时不想打开 IDE,也可以在电脑上复制一个同样逻辑的 bat 脚本。手机端生成的源码直接放进 Windows 项目目录即可复用。
@echo off chcp 65001 >nul set SRC_DIR=src set OUT_DIR=out if not exist "%SRC_DIR%" ( echo [ERROR] src 目录不存在 exit /b 1 ) if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" dir /s /b "%SRC_DIR%\*.java" > sources.txt javac -encoding UTF-8 -d "%OUT_DIR%" @sources.txt if errorlevel 1 ( echo [ERROR] 编译失败 del sources.txt 2>nul exit /b 1 ) del sources.txt 2>nul echo [OK] 编译完成这里chcp 65001是为了让控制台支持 UTF-8 编码,避免中文输出乱码。dir /s /b "%SRC_DIR%\*.java" > sources.txt的作用等同于 bash 里的find,递归查找所有 Java 文件。这样同一个项目在手机 Termux 和 Windows CMD 下都能编译,脚本逻辑保持相似,只是语法不同。
4.6 批量编译脚本 batch_build.sh
如果你的日常练习会拆分成多个独立小项目,例如config、core、service、app,可以在项目根目录放一个批量编译脚本,一次跑完所有子项目。
#!/usr/bin/env bash # batch_build.sh - 批量编译多个子项目 SUB_PROJECTS=("config" "core" "service" "app") ROOT_DIR=$(pwd) for project in "${SUB_PROJECTS[@]}"; do if [ -d "$ROOT_DIR/$project/src" ]; then echo "" echo "========== 编译 $project ==========" (cd "$ROOT_DIR/$project" && bash build.sh) if [ $? -ne 0 ]; then echo "[ERROR] $project 编译失败,批量任务终止" exit 1 fi else echo "[WARN] 跳过 $project,未找到 $ROOT_DIR/$project/src 目录" fi done echo "" echo "[OK] 所有子项目编译完成"批量脚本的关键点是(cd "$ROOT_DIR/$project" && bash build.sh),这里用子 shell 切换目录,不会污染当前 shell 的工作目录。任何一个子项目编译失败,脚本立刻终止并返回非 0 退出码,避免后续步骤在没有上一个模块产物的情况下继续执行。
如果项目数量不是固定的,也可以改为自动扫描当前目录下所有包含src的子目录:
for project in */; do project="${project%/}" if [ -d "$project/src" ]; then (cd "$project" && bash build.sh) fi done5. 脚本功能测试与效果验证
脚本写完之后,需要逐项验证功能是否正常。下面给出一套可复现的测试流程,建议从最小项目开始,确认每个环节的预期输出,再逐步扩展到多文件、多子项目和批处理场景。
5.1 环境检查测试
创建一个验证脚本check_env.sh:
#!/usr/bin/env bash # check_env.sh - 检查 Java 环境 echo "========== Java 环境检查 ==========" if command -v java >/dev/null 2>&1; then echo "[OK] java 已安装" java -version 2>&1 else echo "[ERROR] 未安装 java" fi if command -v javac >/dev/null 2>&1; then echo "[OK] javac 已安装" javac -version else echo "[ERROR] 未安装 javac" fi if [ -n "$JAVA_HOME" ]; then echo "[INFO] JAVA_HOME=$JAVA_HOME" else echo "[WARN] JAVA_HOME 未设置,如果 javac 命令可用则暂不影响构建" fi执行:
bash check_env.sh预期能看到java 17.x、javac 17.x类似的版本信息。如果提示找不到javac,优先检查是否安装了 JVM 而不是完整 JDK。有些包管理器里java和javac是分开安装的,需要把 JDK 完整安装后两个命令才会同时出现。
5.2 单文件项目编译运行测试
创建最小项目:
// src/com/example/Main.java package com.example; 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); } } }依次执行:
bash build.sh bash run.sh com.example.Main第一次编译时,脚本会找到 1 个源文件,执行javac,成功后在out/com/example/下生成Main.class。运行脚本会在 classpath 上添加out目录,然后加载com.example.Main类。预期输出:
Hello from phone Java!如果run.sh提示找不到主类,用find out -name "*.class"检查 class 文件路径,确认包名和类名是否匹配。
5.3 多文件项目编译测试
在src/com/example/下再创建一个辅助类:
// src/com/example/Utils.java package com.example; public class Utils { public static String hello(String name) { return "Hello, " + name + "!"; } }修改Main.java:
package com.example; public class Main { public static void main(String[] args) { System.out.println(Utils.hello("phone")); } }重新执行:
bash build.sh bash run.sh com.example.Mainfind "$SRC_DIR" -name "*.java" > sources.txt会一次收集到两个源文件,javac @sources.txt自动处理相互依赖关系。这里验证的是“多文件项目能否一次构建成功”,如果脚本中遗漏某个文件,编译时会直接报错,这是脚本设计里最关键的自检机制。
5.4 命令行参数传递测试
执行带参数的运行命令:
bash run.sh com.example.Main hello java phone预期java进程收到三个参数并逐个打印。这个测试验证run.sh中"$@"的传递逻辑,确保你在脚本菜单里输入参数或从命令后面拼参数时能原样传到main方法。
5.5 批量任务测试
批量脚本更适合有多个独立小项目的场景。假设项目根目录如下:
batch-demo/ ├── core/ │ └── src/com/example/core/Core.java ├── service/ │ └── src/com/example/service/Service.java └── batch_build.sh在根目录创建batch_build.sh后,手动指定待编译项目:
SUB_PROJECTS=("core" "service")执行:
bash batch_build.sh预期输出依次显示core、service两个项目的编译过程。故意在service源码中制造一个语法错误,再次执行时脚本应该在service阶段退出并返回非 0 状态码,这验证了“失败即终止”的行为是否符合你的预期。如果你希望某个项目失败后继续编译其它项目,可以把脚本改为出现失败时只打印警告而不退出:
if [ $? -ne 0 ]; then echo "[WARN] $project 编译失败,继续执行下一个项目" fi这个选择取决于场景:单体项目依赖链较强时建议失败即终止,独立子项目集合建议跳过失败继续执行,这样日志里能暴露所有问题。
6. 接口化与批量任务的扩展思路
脚本本身没有/api/build这样的 REST 接口,但如果你需要把“手机构建 Java 项目”变成可被外部工具调用的服务,可以考虑两条扩展路径。
第一条路径是写一个极简的本地 HTTP Server,由它接收构建请求并执行build.sh。Java 自带com.sun.net.httpserver.HttpServer,JDK 8 之后可用,不需要额外引依赖。下面是一个演示性质的代码,重点不是生产环境高并发,而是展示“脚本如何被接口触发”。
// src/com/example/BuildServer.java package com.example; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; public class BuildServer { public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/health", new HealthHandler()); server.createContext("/build", new BuildHandler()); server.setExecutor(null); server.start(); System.out.println("Build server started at http://localhost:8080"); } static class HealthHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { String response = "OK"; exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8"); exchange.sendResponseHeaders(200, response.getBytes(StandardCharsets.UTF_8).length); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes(StandardCharsets.UTF_8)); } } } static class BuildHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { String response; int status = 200; if ("POST".equals(exchange.getRequestMethod())) { try { Process process = new ProcessBuilder("bash", "build.sh") .directory(new java.io.File(".")) .redirectErrorStream(true) .start(); String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); int exitCode = process.waitFor(); if (exitCode == 0) { response = "BUILD_OK\n" + output; } else { response = "BUILD_FAIL\n" + output; status = 500; } } catch (Exception e) { response = "BUILD_ERROR: " + e.getMessage(); status = 500; } } else { response = "Method Not Allowed"; status = 405; } exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8"); exchange.sendResponseHeaders(status, response.getBytes(StandardCharsets.UTF_8).length); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes(StandardCharsets.UTF_8)); } } } }这个示例演示的是脚本服务化的一种写法,如果真正对外提供接口,必须增加认证和权限控制,并且只允许授权用户触达构建命令。启动服务后可以在手机浏览器或同一个局域网内访问http://localhost:8080/health验证是否在线。
第二条路径是不写 HTTP 服务,直接利用 Linux 的cron或 Termux 的termux-job-scheduler定时执行批量脚本。例如每天定时跑一次bash build.sh,然后把编译输出写入日志文件:
* 9 * * * cd ~/projects/demo && bash build.sh >> logs/build.log 2>&1这里日志文件需要目录存在,提前在项目中创建logs目录即可。定时任务适合做“自动构建检查”,比如每天早上自动编译整个项目,如果编译失败就把错误写入build.log,你一打开手机就能看到失败原因。
接口化和定时任务都是围绕同一批脚本做外层封装。建议先把脚本本身跑稳,再考虑加接口和定时任务,否则排查问题时会同时面对脚本逻辑、端口占用、进程管理等多个变量,难度会变大。
7. 资源占用与性能观察
手机端跑 Java 编译,性能和电脑没法比,但通过合理设置可以控制资源占用和编译耗时。先看几个观察维度。
第一个维度是内存。手机终端的 Termux 进程不是普通 App,编译大项目时 JVM 会申请数百 MB 内存。用free -h可以在编译期间观察内存剩余量。如果编译到一半出现:
java.lang.OutOfMemoryError: insufficient memory说明 javac 内部的 JVM 堆内存设置太大或者系统剩余内存严重不足。可以在执行javac时给它限制堆内存:
javac -J-Xmx256m -encoding UTF-8 -d "$OUT_DIR" @sources.txt-J-Xmx256m会把 javac 进程的最大堆限制在 256MB,适合小项目和手机端。如果项目稍大,可以调成512m。这个值需要按实际项目测试,不要认为越大越快,手机总内存有限,给编译进程分配过多内存反而会挤占系统内存,导致 Termux 被系统后台杀掉。
第二个维度是编译耗时。小项目通常几秒到十几秒完成,依赖环境、文件数量和机器性能。批量编译多个子项目时,每增加一个子项目都会增加一次进程启动和编译开销。观察耗时可以用:
time bash build.shtime命令会输出实际耗时,方便对比不同参数下的性能。对于手机端,编译优化选项建议保持默认,不要给 javac 加-g之外的额外参数,也不要尝试启用高优化的 JIT 参数,那对编译阶段没有明显收益。
第三个维度是磁盘和进程。编译产生的out目录会随时间积累,如果每天反复测试,旧 class 文件不会自动消失。菜单里已经提供了“清理编译输出”选项,也可以手动执行:
rm -rf out另外,如果前面运行了BuildServer,记得用pkill -f BuildServer或jobs -l查看后台进程,避免 8080 端口一直被占用。端口被占用时,后续再次启动服务会报Address already in use,排查办法是:
lsof -i :8080如果lsof不可用,Termux 可以用:
netstat -tulpn | grep 8080性能优化的核心结论是:手机适合跑小项目、单模块、少量文件的编译;不要试图拿手机编译一个几千个文件的完整 Spring Boot 工程,更合理的做法是在手机编辑源码,推送到 Git 仓库后在电脑或 CI 服务上构建。脚本的价值是让你“随时能构建和验证小改动”,而不是取代电脑的构建能力。
8. 常见问题与排查方法
脚本用起来一定会遇到问题,下面按故障现象、可能原因、排查方式和解决方案整理成一张排查表,你在手机端或电脑端遇到类似问题可以直接对照处理。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
bash提示脚本没有执行权限 | 脚本文件缺少执行权限 | 执行ls -l build.sh查看权限位 | 执行chmod +x build.sh,之后可直接./build.sh运行 |
提示javac: command not found | JDK 没有安装完成,或 PATH 未包含 javac 所在目录 | 执行pkg list-installed | grep -i jdk和which javac | 安装完整 JDK;安装后重新加载 shell:source ~/.bashrc |
| 编译时中文乱码 | 源文件编码和 javac 默认编码不一致 | 检查文件编码,通常是 UTF-8 还是 GBK 的差异 | 统一使用 UTF-8 编码保存源文件,编译命令加-encoding UTF-8 |
javac: 源发行版 17 需要目标发行版 17 | 当前 javac 版本和项目编译的 source/target 不匹配 | 执行javac -version,并查看项目是否有--release参数 | 指定版本编译:javac --release 17 -encoding UTF-8 -d out @sources.txt,或安装对应版本 JDK |
OutOfMemoryError: insufficient memory | javac 进程堆内存不足或手机剩余内存不足 | 编译时用free -h查看内存 | 给 javac 加-J-Xmx256m限制堆;关闭 Termux 后台无用进程后重试 |
| 运行脚本提示找不到主类 | 类名或包名写错,或out目录中没有对应的 class 文件 | 执行find out -name "*.class"查看实际 class 路径 | 检查MAIN_CLASS是否与package声明一致,例如com.example.Main对应out/com/example/Main.class |
| 菜单脚本输入选项后无响应 | read读取异常或脚本被 CRLF 换行符污染 | 执行file menu.sh,若输出包含CRLF说明换行符有问题 | 安装dos2unix后执行dos2unix menu.sh,或者把脚本从 Windows 重新保存为 LF 换行 |
执行bash build.sh后sources.txt为空 | find命令和src目录路径不匹配 | 执行ls src查看目录是否存在,执行find src -name "*.java"手动测试 | 确认当前工作在项目根目录,SRC_DIR变量设置正确,例如src或src/main/java |
| 批量脚本在某个子项目失败后继续或停止 | 脚本里的退出策略不符合预期 | 查看batch_build.sh中的exit 1位置 | 如果希望失败后终止就保留exit 1,如果希望继续编译其余项目则改为只打印[WARN] |
| 手机端编译非常慢 | 手机 CPU 性能弱、项目文件多或内存紧张 | 执行time bash build.sh查看耗时 | 减小项目范围、清理out目录、减少同时打开的后台 App、关闭不必要的动画或后台同步 |
BuildServer启动后一直卡住或无法访问 | 端口被占用、服务尚未编译成功、防火墙限制 | 执行lsof -i :8080或netstat -tulpn | grep 8080 | 更换端口,例如改为new InetSocketAddress(8081, 0);先编译成功再运行 |
这套排查表覆盖了脚本最常见的问题。大部分情况下先看脚本日志的第一行[ERROR]或[WARN],问题会自动定位。不要一出问题就怀疑脚本写得不对,先确认环境变量、目录路径、换行符、主类名这四个高频变量,能避免一半以上的坑。
9. 最佳实践与使用建议
把“手机构建 Java 项目脚本”真正用熟,建议遵循下面几条工程化习惯。
第一,第一次先小参数测试。不要一上来就编译一个十几层的包结构,先写一个单文件Main.java跑通build.sh和run.sh,再逐步增加多文件、参数传递、批量编译。这样可以随时判断哪一层出错,脚本里面也可以先保留完整的echo输出,等稳定后再删掉调试信息。
第二,保留一套最小可运行配置。把check_env.sh、build.sh、run.sh放到一个template目录,以后每次新建项目,直接复制这个模板,改主类名即可。模板里的SRC_DIR、OUT_DIR、MAIN_CLASS全部用变量,新项目只需要修改变量值,不需要改逻辑。
第三,模型文件、输入素材、输出结果分目录管理。对 Java 项目来说,就是src只放源码,out只放编译产物,lib放外部依赖,logs放脚本日志。不要在src里放生成文本、临时文件或二进制资源,否则find src -name "*.java"不会出错,但后续打包或迁移时会产生混乱。
第四,批量任务要加日志和失败重试。batch_build.sh执行时如果看不到完整日志,失败后只能从头跑一遍。建议在脚本中添加日志输出到文件:
LOG_DIR="logs" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/build_$(date +%Y%m%d_%H%M%S).log"然后所有echo输出同时写到屏幕和日志:
echo "[INFO] 开始编译" | tee -a "$LOG_FILE"tee -a可以兼顾屏幕输出和文件记录,失败时直接看最后一段日志即可。
第五,接口服务要限制访问范围。如果按第 6 节启动了BuildServer,不要让它监听0.0.0.0并暴露在公网。手机端通常只需要127.0.0.1本地访问,或者和电脑处于同一局域网时用192.168.x.x访问。任何外部请求都可能触发构建命令,不加上认证结果会很危险。更稳妥的方案是使用内网穿透类工具之前,先把服务绑定到 loopback 地址,并只允许本机或受信任设备调用。
第六,涉及人脸、声音、版权素材时必须确认授权。这条对 Java 项目脚本同样适用:如果脚本要处理图片、音频、视频等素材,必须确认这些素材的版权和授权范围,不要在脚本里做批量下载、批量转换、批量去水印等可能侵权的操作。手机本地处理的素材如果是自己创建或已获授权的文件,没有问题;如果是抓取第三方数据、调用第三方接口批量处理,必须先确认服务条款和合规边界。
第七,发布或商用前要做效果复核。不要相信脚本第一次跑成功就万事大吉。不同手机型号、不同 JDK 版本、不同locale下,脚本的编码和路径可能表现不一致。发布前要在干净环境重新安装 JDK、拷贝源码、执行完整构建流程,确认脚本不依赖某个终端里残留的变量和路径。
10. 总结与下一步
这套“手机构建 Java 项目脚本”最值得尝试的点,是它把 Java 项目的构建过程从 IDE 的图形按钮变成了一组可读、可改、可复用的 shell/bat 脚本。你在手机上不仅能继续练 Java,还能顺手掌握javac、java、find、command -v、$?、exit code这些构建基础,这在以后用 Maven、Gradle、Jenkins 时会成为底层能力。
建议拿到脚本后,最先验证的不是批量编译,也不是 HTTP 服务,而是最简单的单文件编译运行。把环境检查脚本跑一遍,确认<your-project>/src/com/example/Main.java能编译、能运行、能传参数,这五个字是整套流程的地基。最容易踩的坑有三个:javac没安装完整导致找不到命令、MAIN_CLASS和实际包名不一致导致找不到主类、Windows 编辑器保存出的 CRLF 换行符导致 bash 脚本执行报错。记住三个排查方向,脚本在使用中会顺畅很多。
后续可以继续扩展的方向也很明确:把脚本接入 Git,在提交代码后自动执行编译检查;把BuildServer加上简单的 Token 认证,做成局域网内可调用的构建接口;基于batch_build.sh增加多模块按需编译的配置,让手机端先写单元测试再跑构建脚本,最后把构建产物推到远端服务器。每一步都不需要重型 IDE 参与,一台手机加上这套脚本,足够支撑一段扎实的 Java 基础学习周期。建议收藏备用,下次想在没电脑的环境里写 Java 时,直接按本文搭一套,实测会比想象中靠谱。