
之前在手机上写 Java 时总是绕不开“代码写好了但环境跑不起来”的尴尬。手机上不是不能装编译器而是很多人不知道该怎么把 JDK、源码目录、编译命令、运行参数串成一整套可复用的流程。网上资料大多只讲“装个 Termux、装个 JDK、敲 javac 编译”一旦遇到多文件项目、第三方 jar 包、重复编译清理就又回到手动敲命令的老路。这篇文章就把“手机构建 Java 项目脚本”这件事完整拆开从环境搭建到脚本设计再到打包、备份、常见报错排查给出一套能直接照做的方案。不管是 Java 入门练手还是临时写个工具类都能在这套流程里快速跑通。1. 背景与核心概念1.1 手机为什么能构建 Java 项目一提到 Java 项目构建本能反应是“用 IDEA 或 Eclipse”。但 Java 的核心工具链其实非常轻量只要有javacJava 编译器和javaJava 运行时就能完成编译和运行。手机端的 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 中安装 JDKTermux 的安装方式很简单在酷安、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.xxx) OpenJDK 64-Bit Server VM (build 17.0.xxx, 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 文件都传给javacjavac -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下的 jarjava -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_CLASSMain # 收集所有 Java 源码文件 collect_sources() { find $SRC_DIR -name *.java -type f | sort } # 拼接 classpathout 目录 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 foundJDK 未安装或 PATH 异常执行pkg install openjdk-17再执行which java确认路径javac: command not found只安装了 JRE 没有安装 JDK错误安装 openjdk 包确认包名是否包含完整 JDK编译报乱码或非法字符源码使用中文但没有指定 UTF-8编译命令加上-encoding UTF-8Termux 默认 locale 可能不是 UTF-8Permission denied脚本没有执行权限执行chmod x build.shbad 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 21 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 项目构建流程了。