Linux下部署Java JAR包:从零基础到开机自启
发布时间:2026/10/7 12:22:53 锦皓数字建站

我记得第一次在公司服务器上部署Java项目的时候下午三点开始折腾到晚上九点还没跑起来。不是JAR包有问题也不是服务器有问题而是我压根不知道部署Java项目到底需要准备什么、按什么顺序做。后来踩过的坑多了才慢慢理清楚Linux下部署一个带JAR包的Java项目本质上就三件事——准备好JAR包、装对Java环境、用一种能长期稳定运行的方式启动它。这篇文章就是把这些事拆开揉碎零基础也能照着做完从上传JAR包到配置开机自启一条龙讲清楚。1. 部署前的第一件事先搞清楚JAR包是怎么来的别拿到一个“打不开”的包先说个扎心的事实很多人第一次部署失败根本不是Linux的问题而是他手里的JAR包压根就不是“能直接运行的包”。Java项目打包有很多种方式最常见的是Spring Boot的Fat JAR可执行JAR这种包内部自带了Tomcat等Web服务器和所有依赖用java -jar就能直接启动。但如果你用的是一般Maven打成普通JAR或者甚至是从网上随手下的某个依赖包那不管Linux环境配得多好都跑不起来。1.1 Fat JAR、普通JAR和依赖JAR三种包别搞混我把JAR包分三类零基础直接按这个对照判断JAR包类型特征能否直接java -jar运行Spring Boot可执行JARFat JAR包很大可能几十MB甚至上百MB内含BOOT-INF目录可以普通应用JAR指定Main-Class包较小MANIFEST里声明了Main-Class可以但依赖需另外解决依赖库JAR比如mysql-connector.jar、fastjson.jar不行它不是应用是给别人用的判断方法也简单拿到JAR包后执行jar tf 包名.jar看看里面有没有BOOT-INF或者META-INF/MANIFEST.MF文件。再用unzip -p 包名.jar META-INF/MANIFEST.MF看一下这个包是否声明了Main-Class。如果没有Main-Class又没有内置依赖那就是个“半成品”包你把它传到服务器上怎么折腾都没用。1.2 从IDEA和Maven里把JAR包正确导出来如果你的Java项目还在本地开发环境那打包这一步就得在本地完成。我见过有人直接从IDEA的out目录把class文件拷到服务器上那不是JAR包那叫自找麻烦。正确做法有两种。IDEA方式右侧Maven面板找到项目根节点下的Lifecycle双击package然后在target目录下就能拿到打好的JAR包。注意IDEA里双击出来的可能是普通JAR如果是Spring Boot项目要在pom.xml里确认引入了spring-boot-maven-plugin这个插件会把项目重新打成一个可执行的Fat JAR。Maven命令行方式在项目根目录执行mvn clean package -DskipTests跳过测试能省很多时间。命令行方式的好处是干净不会受IDEA插件状态影响。打包完之后先别急着上传在本地终端执行一下java -jar target/xxx.jar确认要么启动成功要么至少能看到Spring Boot的启动日志开始打印。这一步能过滤掉90%的“包本身有问题”的情况。1.3 上传JAR包的四条路径新手建议用最简单的那种把JAR包传上Linux服务器方案有scp、rz/sz、XFTP、以及Git仓库配合服务器拉取。对于一个零基础的人来说我建议用rz命令因为不需要记任何IP和路径参数。在服务器上先安装yum install -y lrzsz # CentOS系列 apt install -y lrzsz # Debian/Ubuntu系列然后建一个专门的部署目录比如/opt/myapp进入目录执行rz会弹出一个文件选择窗口选中本地JAR包上传即可。上传完执行ls -lh看文件大小是否和本地一致这一眼能省下后面无数排查时间——我遇到过一次JAR包传了一半断了文件大小明显偏小结果启动时报“Invalid or corrupt jarfile”排查了半天才想起去比对大小。2. JDK安装与环境变量零基础最容易翻车的环节JAR包就位之后接下来是Java环境。这一节我尽量把为什么这么做讲透因为很多人就是在这里把JAVA_HOME、PATH、CLASSPATH三个变量搞混导致java命令能用了但老项目就是启动报错。2.1 先确认系统里有没有自带Java有的话是哪个版本登录服务器后先执行which java java -version如果没有任何输出说明环境干净直接往下走。如果有输出注意看是OpenJDK还是Oracle JDK以及版本号。Java 8和Java 11、17的启动行为有一些差异比如某些老项目用到jaxb库在Java 11以后就需要额外引入依赖。先搞清楚现状再决定是复用还是重装。2.2 手动解压安装JDK路径可控版本可控有些系统的包管理器自带的JDK版本很老或者你根本拿不到root权限那最稳的方式就是把JDK解压到自己目录下。比如你拿到了一个jdk-8u202-linux-x64.tar.gz这样操作mkdir -p /opt/java tar -zxvf jdk-8u202-linux-x64.tar.gz -C /opt/java mv /opt/java/jdk1.8.0_202 /opt/java/jdk8解压完直接改环境变量将下面内容写入/etc/profile文件末尾或者用户级的~/.bashrcexport JAVA_HOME/opt/java/jdk8 export PATH$JAVA_HOME/bin:$PATH export CLASSPATH.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar然后执行source /etc/profile使其立刻生效再执行java -version验证。这里要解释几个关键点JAVA_HOME是很多框架Tomcat、Hadoop、Gradle查找Java的入口不配置的话有些软件启动时根本找不到Java环境报“Cannot find Java”之类错误。热搜词里提到的“hadoop已编译jar包 配置hadoop_home环境变量”就是同一个道理——Hadoop自己有一套环境变量逻辑它不会自动去/usr/bin/java找而是读JAVA_HOME。PATH配置$JAVA_HOME/bin是为了让你在任意目录敲java、javac都能找到命令。CLASSPATH在Java 8时代比较重要Java 9以后模块化之后很多场景不需要你手动设置但配置了也不算错兼容老项目的习惯。2.3 两个最常见的环境变量坑坑一改了/etc/profile但没重新登录只在当前shell里source了。这会导致当前会话能用java但用systemd启动的服务读不到环境变量。因为systemd启动的进程环境跟你的登录shell环境不是一回事它只读自己unit文件里配的环境变量。后面讲到systemd我会专门提醒这点。坑二环境变量里只有一个export JAVA_HOME但PATH里没加$JAVA_HOME/bin。我见过有人配完JAVA_HOME后执行java -version还是报“command not found”因为JAVA_HOME本身不参与命令查找PATH才是决定java命令去哪找的变量。验证环境的完整三板斧echo $JAVA_HOME which java java -version三条输出都正常环境基本就是稳的。3. 把JAR包真正“跑起来”三种启动方式从试运行到生产级环境好了JAR包也传上去了现在进入最核心的阶段——启动。我按从简单到复杂的顺序讲三种方式你在实际部署中可以根据场景选。3.1 前台运行只适合第一次“试跑”进入JAR包目录cd /opt/myapp java -jar demo.jar这种方式的优点是日志直接刷在终端上启动过程有没有报错一目了然非常适合第一次验证项目能不能跑。但缺点也明显窗口一关进程就没了SSH断开进程也没了所以只能在确认项目能启动时用。试跑时重点看两类日志一是Spring Boot的Started Application in X seconds看到这个基本成功二是Tomcat started on port(s): 8080这个确认端口起来了。看到这两行说明项目本身没问题可以放心用下面两种方式正式部署。3.2 用nohup放到后台快速但欠管理cd /opt/myapp nohup java -jar -Xms256m -Xmx512m demo.jar app.log 21 拆开解释一下nohup让进程忽略挂断信号把进程放到后台 app.log 21把标准输出和错误输出都重定向到日志文件。执行完后立刻用echo $!可以看到刚启动的进程PID。这种方式的优点是简单适合临时演示或快速重启。缺点是进程如果崩溃不会自动拉起全靠人工盯而且日志文件如果不管会一直涨直到磁盘满。3.3 systemd管理生产环境推荐崩溃自动重启、开机自启这是我最推荐的方式尤其适合服务器重启后还要让服务自己跑起来的场景。在/etc/systemd/system/下新建一个myapp.service文件[Unit] DescriptionMy Java App Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/myapp EnvironmentJAVA_HOME/opt/java/jdk8 EnvironmentSPRING_PROFILES_ACTIVEprod ExecStart/opt/java/jdk8/bin/java -jar -Xms512m -Xmx1024m /opt/myapp/demo.jar Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后执行systemctl daemon-reload systemctl start myapp systemctl enable myapp这里面的细节值得逐条说Environment专门解决我在2.3里提到的“systemd读不到登录shell环境变量”的问题。有些Spring Boot项目需要读取环境变量里的数据库密码就在这里配比写死在配置里安全。Restarton-failure进程以非零状态退出时自动拉起10秒后重试。应用OOM或者异常崩溃了不需要人盯。ExecStart我直接用绝对路径/opt/java/jdk8/bin/java而不是写java原因还是环境变量隔离——systemd的PATH默认不包含/opt/java/jdk8/bin写java很可能报“Executable not found”。WantedBymulti-user.target一个“开机自启”的开关配合systemctl enable生效。如果要临时不重启但先启动就执行systemctl start要让服务下次开机自动起就执行systemctl enable两个是不同的指令新手经常忘记enable。启动后检查状态systemctl status myapp会显示Active: active (running)按q退出查看界面。想看日志就用journalctl -u myapp -f实时滚动输出。3.4 三种启动方式对比维度前台运行nohup后台systemd适合场景首次试跑临时运行生产长期运行崩溃自动重启否否是开机自启否否是日志管理终端重定向文件journald集成配置难度零低中等我个人建议第一次用前台第二次用systemd跳过nohup也行。因为有systemd之后nohup的“快速”优势其实没有多少反而不如systemd的restart省心。4. 端口、内存和日志上线之后你一定会遇到的三个麻烦项目能启动只算了第一步。接下来要处理的是“上线之后一定会遇到”的三个通用问题每一个单拎出来都够新手喝一壶。4.1 端口被占用了怎么找到“凶手”进程Spring Boot默认端口是8080但如果服务器上已经有一个Tomcat占着8080你会看到Web server failed to start. Port 8080 was already in use.排查步骤ss -lntp | grep 8080这条命令会输出类似LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:((java,pid1234,fd12))拿到PID之后你可以选择kill -9 1234或者杀掉之前的服务进程或者改自己项目的端口。如果不想改代码可以在启动时指定java -jar demo.jar --server.port8081这里我想提醒一句先搞清楚占用者是谁再决定杀不杀。曾经有个同事在客户服务器上看到8080被占想都没想直接kill结果那是客户的另一个生产系统喜提一次“重大事故演练”。4.2 JVM内存参数别再“裸奔”启动生产Java应用很多人启动时就是java -jar demo.jar什么都不配。这在开发环境没问题但生产环境我强烈建议至少加上堆内存控制java -jar -Xms512m -Xmx1024m demo.jar其中-Xms是JVM启动时分配的初始堆内存-Xmx是最大堆内存。这两个值为什么不配会产生大问题因为JVM默认的堆内存是物理内存的1/4比如一台8G内存的服务器JVM理论上最多能占2G。如果你的项目实际只需要512M这2G的“上限”也没事但如果同一台服务器上还跑着数据库、Nginx几个服务各占1/4内存就爆了触发系统OOM Killer到时候说不清是哪个进程“甩锅”。合理的做法是估算项目峰值内存比如你线上并发1000压测观察到JVM堆用量稳定在600M左右那-Xms512m -Xmx1024m是比较合适的配置区间。再配合-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt/myapp/logs/一旦OOM还能留下堆快照供后续排查。4.3 日志不落盘启动三小时后你会后悔我见过一些“跑起来就行”的项目没有重定向日志或者重定向到了nohup.out然后磁盘满了才发现。正确的做法是让日志落到一个专门目录并配合Linux自带的logrotate做按天滚动。/etc/logrotate.d/myapp文件示例/opt/myapp/logs/*.log { daily rotate 7 copytruncate compress missingok }含义是每天滚动一次保留7天压缩旧日志。如果遇到应用自身又写了日志文件copytruncate能先复制后清空避免杀掉需要持续写文件的Java进程。这一步不配好轻则日志难查重则磁盘写满是上线之后最先碰到的“基础设施欠账”。5. 经典启动报错与完整排查链路用一次真实事故讲透思路这一节不只是罗列报错而是用一个真实场景把“排查思路”带出来。你以后遇到任何启动问题都可以按这个链条走看进程状态 → 看日志关键行 → 确认环境变量 → 确认端口和依赖。5.1 现场还原一个Spring Boot应用启动不了假设你现在执行了systemctl start myapp然后systemctl status myapp显示Active: failed (Result: exit-code) Process: 12345 ExecStart/opt/java/jdk8/bin/java -jar /opt/myapp/demo.jar (codeexited, status1/FAILURE)很多人到这里就慌了。别慌按顺序排查。第一步看日志。systemd管的服务日志会进journald执行journalctl -u myapp -n 50 --no-pager如果日志里出现Error: Could not find or load main class com.example.demo.DemoApplication这就是我在第一部分说的“不是环境问题是JAR包问题”。可能原因把依赖库JAR当应用包了或者普通JAR没打进去Main-Class。处理方式回到本地检查pom.xml的spring-boot-maven-plugin配置用mvn clean package重新打包。**第二步确认环境变量。“Could not reserve enough space for 2097152KB object heap”**这句报错代表JVM申请不到足够堆内存。大部分情况下是启动参数里的-Xmx配得太大超过了机器实际可用内存。执行free -h看可用内存把-Xmx调小。同样还有“执行java命令时报Cannot find Java”的情况检查systemd unit里有没有配EnvironmentJAVA_HOME以及ExecStart是不是写成了裸的java。第三步确认端口冲突。日志里看到“Port 8080 already in use”就按4.1节的方法查杀进程或者换端口。这里我特别强调一个细节如果你用ss -lntp查不到PID比如权限不够只显示users:((“java”,pid?,fd?))先加sudo再看。曾经有新手因为权限不够查不到PID就直接断定“没进程占用”结果折腾半天。5.2 排查链路总结整理成一张你可以贴在工位上的表现象首要查看项常见根因处理手段status1/FAILUREjournalctl -u 服务名 -n 50JAR包损坏/缺少Main-Class重新打包上传比对文件大小Cannot find Javaunit文件ExecStart和Environmentsystemd没有继承shell环境写绝对路径和EnvironmentJAVA_HOMEPort already in usess -lntp | grep 端口端口被别的进程占用kill旧进程或换端口OutOfMemoryErrorfree -h、JVM参数-Xmx超出物理内存调小-Xms/-Xmx启动成功但访问不了curl localhost:端口防火墙/安全组/SELinux逐层排查上述三项这套排查链路我在生产上验证过无数次。记住不要跳过日志直接猜日志里往往写了原因不要只看最终错误往上翻10行可能真相在那里。6. 从手工到脚本化写一个能让你“一键发布”的部署脚本当你部署过两三次之后会意识到手动操作太容易出错——可能忘了先备份、可能忘了重启、可能忘了切换环境变量。所以我强烈建议把发布过程收敛成一个脚本也顺手解决“重启应用”和“回滚”两个高频动作。6.1 一个可直接抄作业的deploy.sh在/opt/myapp/下放一个deploy.sh#!/bin/bash # 应用名和目录按需修改 APP_NAMEdemo APP_DIR/opt/myapp BACKUP_DIR/opt/myapp/backup JAR_FILEdemo.jar NEW_JARdemo.jar.new # 上传的新版本 # 1. 备份当前版本如果存在 if [ -f $APP_DIR/$JAR_FILE ]; then echo 备份当前版本到 $BACKUP_DIR/${JAR_FILE}.$(date %Y%m%d%H%M%S) mv $APP_DIR/$JAR_FILE $BACKUP_DIR/${JAR_FILE}.$(date %Y%m%d%H%M%S) fi # 2. 把新版本放到正式位置 mv $APP_DIR/$NEW_JAR $APP_DIR/$JAR_FILE # 3. 通过systemd重启 echo 重启服务 systemctl stop $APP_NAME systemctl start $APP_NAME # 4. 轮询等待服务起来最多等60秒 for i in $(seq 1 12); do if systemctl is-active --quiet $APP_NAME; then echo 服务启动成功 exit 0 fi sleep 5 done echo 服务启动超时请查看 journalctl -u $APP_NAME exit 1使用步骤本地打好新JAR包后上传到服务器的/opt/myapp/demo.jar.new然后执行bash deploy.sh。脚本会自动备份旧版本、替换新文件、重启并做健康检查。6.2 为什么把健康检查放在脚本里很多部署事故不是包有问题而是发布顺序错了——比如新包启动失败旧包已经被删了回不去了。这个脚本的核心逻辑就是先备份再换包最后做健康检查。Systemd本身有Restarton-failure但那只解决进程崩溃不解决“进程正常但服务其实不可用”的假启动。实际操作中我还会在健康检查里加上一个HTTP探测比如curl -sf http://127.0.0.1:8080/actuator/health /dev/null || exit 1如果你用的是Spring Boot Actuator这一步能让部署脚本从“进程检查”升级到“业务检查”只有健康检查通过才算发布成功。6.3 回滚出问题时的“后悔药”回滚的思路是保留最近几份备份发布失败时把上一个备份拷贝回来再重启。简单版ls -t /opt/myapp/backup/ | head -3选中最新的那份cp /opt/myapp/backup/demo.jar.20250121120000 /opt/myapp/demo.jar systemctl restart myapp如果你的团队规范一点还可以把备份策略收敛成“保留最近5份”在deploy.sh里加一行ls -t $BACKUP_DIR/*.jar* | tail -n 6 | xargs rm -f不过新手阶段先跑通最重要。7. 常见的“我也说不上为什么但就这样解决了”的问题清单这一节是我私藏的一些高频小问题特性和排查方式不像上面那样成体系但遇到时能帮你快十分钟。7.1 中文乱码启动日志或者项目输出的中文全部变成???大概率是Linux系统默认的UTF-8 locale没配好。在启动命令前加上JAVA_TOOL_OPTIONS-Dfile.encodingUTF-8或者在systemd unit的[Service]段加EnvironmentLANGzh_CN.UTF-8 EnvironmentLC_ALLzh_CN.UTF-87.2 时区不对日志时间比本地慢8小时Java应用记录日志默认取服务器系统时区。如果服务器是UTC而你在北京时间看日志总感觉“日志时间穿越”。解决timedatectl set-timezone Asia/Shanghai或者在启动参数加EnvironmentTZAsia/Shanghai7.3 JAR包上传之后执行权限没了有些同学上传完JAR包执行java -jar demo.jar报权限错误。其实java -jar并不要求JAR包有x权限r权限就够了。如果你习惯性chmod x demo.jar也没毛病但如果发现“Permission denied”先别急着改权限先看是不是把JAR包放到了/root以外的无权限目录或者是不是用了非root用户执行。7.4 修改了JAR包里的配置但不生效Spring Boot的配置优先级是命令行参数 java:comp/env application-{profile}.yml application.yml。如果你在JAR包内的application.yml改了一个端口但启动时末尾却带着--server.port8081那生效的是命令行参数不是包内配置。我之前就在这里绕晕过改了包里的端口结果启动还是旧端口怎么都想不通。8. 部署完成后的检查清单以及我这两年反复用的一招项目最终部署完走一遍这个清单能让你少被半夜电话吵醒[ ]systemctl status 服务名为active (running)[ ]curl -I http://127.0.0.1:端口返回200/302等正常HTTP状态码[ ]ss -lntp确认端口监听正常[ ] 日志目录按天滚动正常没有写满磁盘的风险[ ] 开机自动启动已配置systemctl is-enabled 服务名显示enabled[ ] 内存配置-Xms/-Xmx符合机器实际规格最后再分享一个小技巧我后来一直这么用部署完不要马上关终端先做一次“断电演练”式的验证即重启一遍服务器或者至少执行systemctl restart确认服务能自己拉起来。很多配置在热启动时看不出来问题一冷启动就露馅——比如忘记enable或者WorkingDirectory写了个不存在的路径重启过后服务直接failed。这个验证顶多花五分钟但能在真出问题时救你一次。把这一步当成习惯Linux下部署Java项目这件事就真的从“能跑”进化成“跑得稳”了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。