ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SpringBoot打包部署全攻略:jar包在cmd窗口的启动与排错指南

SpringBoot打包部署全攻略:jar包在cmd窗口的启动与排错指南

本地开发的时候,SpringBoot项目点一下IDE里的绿色箭头就跑起来了,这谁都会。但真正到了部署这一步,很多人上来就被卡住:目标机器上没有开发工具,甚至没有图形界面,就是一个Windows cmd窗口。我印象很深的一次,是在给某团队迁移一个内部报表服务的时候,那台机器上装的东西少得可怜,唯一的执行方式就是把项目打成jar包,然后cmd里一行命令拉起服务。那一套流程走通之后我才意识到,SpringBoot项目打包后在cmd运行,看起来基础,实际上就是大多数中小项目最通用的部署形态。

这篇文章就把这条链路完整拆开:打包前要准备什么,Maven怎么打出能跑的jar,cmd里的启动命令怎么写,启动之后怎么判断它真的起来了,以及最常见的那几个翻车点——端口、乱码、多环境配置——该怎么处理。不管是第一次接触部署的应届生,还是平时只跟IDEA打交道的业务开发,照着做基本都能落地。

1. 为什么还要用cmd跑SpringBoot:打包部署的真实场景

1.1 IDE里跑得好好的,为什么非要打成jar再cmd运行

很多人在IDE里点启动已经习惯了,觉得部署也跟在IDE里跑没什么区别。其实差别非常大。IDEA帮你干了大量隐藏工作:classpath怎么组织、资源目录在哪、运行参数怎么传、日志输出到哪里,这些都是开发工具替你准备好的。到了生产环境,或者到了客户那边的服务器上,根本不会有这些开发工具,可能只有一个操作系统和直接执行的命令行环境。

SpringBoot项目打包后,本质上变成了一个自带运行环境的东西。它内嵌了Tomcat,依赖的第三方库全部打进了fat jar里面,运行时只需要一个JDK环境,一个java -jar命令就能把整个Web服务拉起来。这种形态不依赖IDE,不依赖额外的中间件安装,甚至不需要往系统里注册什么服务,拷过去就能跑,删掉就没了,非常干净。

用cmd运行还有一层实际好处:操作透明。服务起没起来、日志报了什么错、JVM参数是多少,全在窗口里一目了然。相比之下,在IDE里跑一次、然后拍个脑袋说“应该没问题”,这种交付方式在运维侧是很不受欢迎的。把jar包和运行命令作为交付物,对方随便一个会看命令行的人都能接手。

1.2 哪些场景、哪些人最需要这个技能

我总结下来,下面几类场景碰到cmd运行方式的概率最高:

  • 给外部团队或客户交付系统:对方机器上往往没有开发环境,你也不可能要求客户去装一个IDEA,最简单的方式就是把jar包和启动脚本一起给对方。
  • Windows服务器上的部署:有些公司内部服务器就是Windows Server,没有图形界面的情况下,cmd或PowerShell是唯一入口。
  • 临时演示环境、测试环境搭建:快速起一个服务给别人看效果或跑验证,不想在环境准备上花时间。
  • 版本回滚:生产环境出了问题要快速恢复,换成旧版jar包重启一下,比改代码重新打包高效得多。
  • 新人或非全职运维的人接手服务:一条java -jar命令,教学成本极低,不像复杂中间件的管理那么吓人。

所以别觉得cmd跑jar是“土办法”,它反而是一种高兼容性的交付标准。尤其是中小团队里没有专职运维的时候,这套方式能省掉大量环境问题的扯皮。

2. 打包前的准备:JDK版本、Maven配置与打包类型选择

2.1 先弄清楚JDK版本对应关系,不然打包出来跑不了

这一步看着基础,实际上是打包部署的第一大坑。很多人本地开发用JDK 17写着挺顺手,打包出来丢到一台只装了JDK 8的服务器上,一执行直接报:

Exception in thread "main" java.lang.UnsupportedClassVersionError: org/springframework/boot/loader/Launcher has been compiled by a more recent version of the Java Runtime (class file version 61.0)

这个报错的意思就是:字节码版本比运行环境JDK高,跑不动了。所以在打包之前,先明确两件事:项目需要哪个JDK版本,目标运行机器上装的是哪个JDK版本。一般遵守一个原则:打包环境的JDK版本不高于运行环境的JDK版本,至少两者一致。

Spring Boot版本和JDK版本有明确的对应关系,给大家一个参照:

Spring Boot 版本最低 JDK 版本常见使用场景
2.7.xJava 8老项目、客户环境比较老旧
3.0.x / 3.1.x / 3.2.xJava 17新项目的主流选择
3.3.x及以上Java 17保持更新的常用版本

检查环境最直接的两个命令:

java -version mvn -version

java -version看的是JRE版本,mvn -version里会输出Maven使用的JDK版本。如果Maven跑在一个JDK 8上,而pom.xml里声明的Spring Boot是3.x,编译阶段就会直接失败。这时候要么升级Maven所用的JDK,要么调低Spring Boot版本。

另外还有一个容易被忽略的点:pom.xml里的<java.version>标签。如果这里写的是17,哪怕你命令行里的JDK是8,编译也会报错。这个标签的作用是规定源码编译的字节码级别,它跟Maven运行时的JDK必须兼容。

2.2 jar包还是war包:90%的情况下你只需要jar包

早期做Java Web开发的时候,大家习惯把项目打成war包,丢进Tomcat的webapps目录,靠外置容器来运行。Spring Boot出现后,整个逻辑变了:项目自带内嵌Tomcat,打jar包反而更顺路。

区别可以看得很直白:

  • jar包:Spring Boot项目默认打包方式,内嵌Tomcat,java -jar直接运行,部署简单。
  • war包:需要外置Tomcat容器,先把war放进去再启动容器,多了一层管理和环境依赖。

除非有硬性要求,比如公司运维强制要求统一放到某个外置Tomcat里做集中管理,否则我建议一律用jar包。选jar包还能省掉外置容器和项目之间的版本兼容问题,Tomcat版本冲突这种经典头疼事也基本不会碰到。

2.3 Maven的安装与镜像配置:打包命令能不能跑通就看这里

打包依赖Maven,先把Maven本身准备好。下载对应版本的zip包,解压到某个目录,然后配置两个环境变量:

  • MAVEN_HOME:指向Maven解压目录,比如D:\apache-maven-3.9.6
  • PATH:追加%MAVEN_HOME%\bin

配置完之后,新开一个cmd窗口,执行mvn -v能出来版本信息就说明环境通了。

还有一个东西直接影响打包速度和质量:Maven仓库的下载源。默认中央仓库在国内访问速度很慢,依赖稍微多一点,第一次打包能卡到让人怀疑人生。建议在settings.xml的<mirrors>节点里加一个国内镜像。

<mirrors> <mirror> <id>aliyun-public</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>

如果你所在公司有内网私服,优先用内网私服地址,那是最快的。改完镜像后,把本地仓库里已经下载失败的内容清理掉,再重新打包,基本就顺畅了。

3. Maven打包的完整链路:从clean package到target目录

3.1 pom.xml里必须有的插件配置:没有它打出来的jar跑不了

Spring Boot项目跟普通Java项目最大的区别是:它要打出一个“可执行jar”,把所有依赖jar都塞进BOOT-INF/lib目录,还要生成一个特殊的启动器。这个工作Maven默认的打包插件干不了,必须靠spring-boot-maven-plugin。

很多项目打包后运行报错:

no main manifest attribute, in device-manage-0.0.1-SNAPSHOT.jar

就是因为pom.xml里没配这个插件,或配置不完整。一个标准的配置长这样:

<build> <finalName>device-manage</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.devicemanage.DeviceManageApplication</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

finalName的作用是自定义jar包名,不配置的话默认是项目名-版本号.jar,像device-manage-0.0.1-SNAPSHOT.jar,又长又容易打错。建议每个人都改成简短的名字,打包产物直接叫device-manage.jar,后面命令行敲起来省事不少。

spring-boot-maven-plugin的repackagegoal会在Maven的package阶段之后执行,把原本的普通jar重新处理成可执行fat jar。不理解不必纠结,只需要知道:这个插件就是决定jar能不能用java -jar直接跑的关键。

3.2 打包命令执行过程与生成物说明

Maven打包的最常用命令就是:

mvn clean package

实际执行中,它内部做了一系列事情:clean删掉旧的target目录,compile编译Java源码,test跑单元测试,package把编译产物打成jar包,然后repackage改造成可执行jar。平时开发如果不需要跑测试,加一个参数跳过测试阶段,速度会快很多:

mvn clean package -DskipTests

注意区分:-DskipTests是“跳过测试执行但会编译测试代码”,-Dmaven.test.skip=true是“测试代码都不编译”。CI上为了速度一般用后者,本地打包想保留测试类就用前者。从这俩参数的选择就能看出Maven的粒度控制做得挺细。

打完包去target目录下看,会有两个关键的jar:

  • device-manage.jar:经过repackage后的可执行jar,部署用这个。
  • device-manage.jar.original:repackage之前的原始jar,只有项目自身代码和资源,一般不部署。

device-manage.jar比device-manage.jar.original大得多,因为前者把成百上千个依赖jar全部合并进去了。有时候大家看到jar有几十甚至上百MB就慌,其实这是正常的,fat jar就长这样。

如果你是Maven多模块工程,打包命令必须在最外层的聚合pom目录下执行,不然只打当前子模块,很可能因依赖关系报错找不到其他模块。经验是:先mvn clean install把公共模块装进本地仓库,再业务模块package。

3.3 多环境配置怎么打进包:Profile的正确用法

一个项目通常有开发环境、测试环境、生产环境,数据库地址、缓存地址都不同。不少项目会在application.yml旁边放一堆application-dev.yml、application-prod.yml,然后在打包命令里指定用哪个环境:

mvn clean package -DskipTests -Pprod

这样做可以把对应环境的配置打进jar包。但实际部署中,我更推荐“配置不进jar包,而是运行时指定”。原因很简单:生产环境改一次数据库地址,就得重新打包一次,效率太低了。正确做法是让jar里只保留默认配置,运行的时候通过命令行参数或者外部配置文件覆盖。这个后面第5节会详细说。

4. 把jar包在cmd里跑起来:完整启动过程与验证方法

4.1 基本启动命令:一行java -jar解决

把打包好的device-manage.jar复制到目标机器的某个目录,比如D:\apps\device-manage,在cmd里切到该目录:

cd /d D:\apps\device-manage java -jar device-manage.jar

注意这里有个细节:为什么建议先cd到jar所在目录再执行。因为在Spring Boot里,默认的工作目录会影响相对路径的读取,比如日志文件路径、外部配置文件路径。如果你在别的目录直接java -jar D:\apps\device-manage\device-manage.jar,应用的相对路径会基于当前目录而不是jar所在目录,配置里的相对路径就容易找不到文件。先切目录再启动,是最省心的一种姿势。

如果要在启动的同时覆盖某个配置项,可以直接在java -jar后面加参数,比如:

java -jar device-manage.jar --server.port=8081

Spring Boot会把命令行参数作为最高优先级的配置来源。这个特性在生产上实用性极高:同一套jar包,想让它在不同端口起来,完全不需要改配置文件。

4.2 如何确认应用真的启动成功

cmd窗口里刷出一堆日志,很多人不知道看到什么才算成功。真正的启动完成标志是这一行:

Tomcat started on port(s): 8080 (http) with context path '' Started DeviceManageApplication in 12.35 seconds (process running for 12.63)

看到Started这个单词,说明Spring容器初始化完毕,Tomcat也监听了指定端口,服务已经对外了。别只看日志前半部分没有报错就觉得没问题,Spring Boot启动过程中,Spring容器初始化失败的场景很多,日志前面一切正常,最后一步给你个APPLICATION FAILED TO START也不是没有见过。

日志层面确认后,再验证网络层面。在另一个cmd窗口里执行:

curl http://localhost:8080/你的接口路径

能返回正常响应就说明HTTP服务是真的通了。验证进程本身可以用:

jps -l

输出里能看Java进程完整路径和jar包名。jps是JDK自带的工具,不需要额外安装。

4.3 别只验证启动成功,接口和静态资源要一起测

很多项目不止有后端接口,还有内嵌的页面、静态资源。启动成功只是一半,建议把项目涉及的入口都过一遍:

  • 后端接口:调用几个核心的GET/POST接口,确认返回正常。
  • 静态页面:如果项目里包含了前端页面,浏览器访问对应地址,确认页面能打开。
  • 第三方依赖:如果项目依赖数据库、Redis等外部服务,确认应用启动后能正常连接,且数据读写正常。

有一次我部署一个管理系统,启动日志完全正常,但一访问首页就报500,查半天发现是前端静态资源的路径在打包后多了一层目录。这种事在IDE里跑从来不暴露,因为IDE的资源路径处理跟fat jar里BOOT-INF/classes的路径结构不一样。所以部署完一定得做一轮真实访问验证,不能只盯着启动日志看。

5. 高频翻车现场:端口冲突、中文乱码与配置分离

5.1 端口被占用:从报错到定位再到处理的完整链路

端口占用是cmd启动里最常见的提示之一。启动时如果日志末尾出现类似:

Web server failed to start. Port 8080 was already in use.

说明8080端口被别的进程占了。这时候不要慌,按下面的链路排查:

  1. 先查谁占用了端口:
netstat -ano | findstr 8080

输出最后一列就是占用该端口的进程PID。

  1. 再查PID对应的进程是什么:
tasklist | findstr PID

看到是无关紧要的残留进程,再决定是否处理。

  1. 确认无误后结束进程:
taskkill /F /PID PID

这里必须强调一个心态上的问题:能改端口尽量改端口,不要一上来就kill。8080端口很常用,有可能这个端口是别的业务服务正在使用的,你随手kill掉,别人一脸懵。更合理的操作是确认了占用的确实是无效或废弃进程再动手;如果这台机器端口紧张,直接换个端口启动更省事:

java -jar device-manage.jar --server.port=8081

5.2 中文乱码:控制台乱码和返回数据乱码是两码事

cmd里启动SpringBoot项目,中文乱码是我见过第二高频的问题。但乱码要分情况讨论:

现象原因处理方式
控制台里日志和错误信息中文全是乱码cmd默认代码页是936(GBK),而应用输出UTF-8先执行chcp 65001切到UTF-8代码页,再启动
Java进程内输出的日志乱码JVM默认文件编码跟代码文件编码不一致启动参数加-Dfile.encoding=UTF-8
接口返回给前端的中文乱码应用层编码没有统一检查Spring Boot的Spring MVC编码配置
数据库读写的中文乱码JDBC连接字符集不对检查数据库连接串是否带useUnicode=true&characterEncoding=utf8

先看第一个情况,启动前在cmd窗口先敲一下:

chcp 65001

这个命令把当前控制台的代码页切换成UTF-8,之后输出中文不会再变成乱码。

再补一个JVM层面的保障,完整启动命令:

java -Dfile.encoding=UTF-8 -jar device-manage.jar

-Dfile.encoding告诉JVM读取和输出文件时用UTF-8编码,能覆盖一部分因为系统默认编码不同导致的乱码。

至于接口本身返回中文乱码,一般是Spring Boot配置里的编码字符集没统一。比较靠谱的做法是在application.yml里显式声明:

server: servlet: encoding: charset: UTF-8 force: true

数据库连接串里也要注意:

jdbc:mysql://localhost:3306/devicemanage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

一个乱码问题,从控制台、JVM、应用层、数据库四个层面全查一遍,基本都能定位到具体是哪个环节没统一。

5.3 环境差异导致的配置问题:把配置从jar包里面解放出来

打了同一个jar包,在开发环境的配置跟生产环境肯定不一样。如果配置全写死在jar包里的application.yml里,每次环境切换都要重新打包。更合理的方法是把配置放到jar包外部,运行的时候单独指定。

实际操作我建议这样做:在jar所在目录下建一个config目录,里面放一个运行环境专用的配置文件,比如application-prod.yml,启动时通过两个参数配合使用:

java -jar device-manage.jar --spring.profiles.active=prod --spring.config.additional-location=file:./config/

--spring.profiles.active=prod告诉应用使用prod这个profile,--spring.config.additional-location=file:./config/告诉应用从当前目录的config子目录额外加载配置文件。外部配置的优先级高于jar包内部的配置文件,所以只要config目录里有对应的配置,就以后者为准。

这样带来的直接好处是:生产环境的数据库密码、第三方密钥这类内容,可以不进入代码仓库,也不必打进jar包。配置形成外部化,运维同事改配置只需编辑文本文件,不需要碰代码。

6. 让cmd运行更顺手:JVM参数、后台运行与批处理脚本

6.1 先学会加JVM参数,再谈稳定运行

直接java -jar虽然能跑,但默认的JVM内存参数并不一定适合生产环境。JVM默认初始堆内存通常很小,流量稍微上来一点,频繁触发GC,接口响应就变慢。启动时显式指定堆内存空间是基本操作:

java -Xms256m -Xmx1024m -jar device-manage.jar
  • -Xms256m:初始堆内存256MB,启动时直接分配。
  • -Xmx1024m:最大堆内存1GB,垃圾回收最多允许堆扩展到1GB。

经验值上,如果服务器内存是4GB,一个中等规模的SpringBoot服务,-Xmx可以给到1GB到2GB,留出余量给系统和其他进程。别贪心直接给到接近机器总内存,OutOfMemory反而可能先从操作系统层面来。

还有一个参数建议加上:

java -XX:+HeapDumpOnOutOfMemoryError -jar device-manage.jar

效果是内存溢出时自动生成heap dump文件,后面排查内存泄漏问题有第一手材料,不然OOM之后进程直接没了,啥都查不到。

6.2 写一个start.bat:把启动过程固化下来

命令行一次一次敲没问题,但如果部署的人不是你也懂命令行,或者你自己隔几个月再维护,难免忘参数。我习惯把启动写成批处理脚本,放到jar包同目录下。

start.bat内容如下:

@echo off title device-manage-service cd /d %~dp0 echo [INFO] Starting device-manage service... java -Xms256m -Xmx1024m -Dfile.encoding=UTF-8 -jar device-manage.jar --spring.profiles.active=prod pause

几个值得解释的细节:

  • cd /d %~dp0:%~dp0是批处理文件所在目录,配合/d参数切换过去。这样无论你是双击bat还是从别的目录调用,工作目录都固定下来,相对路径不会错。
  • title:把cmd窗口标题改成服务名,机器上同时跑多个服务的时候一眼就能分辨窗口。
  • pause:如果程序崩溃退出,窗口不会瞬间消失,能看到最后的错误日志,方便排查。

6.3 停止服务的两种方式:优雅关闭和强制关闭

停止SpringBoot服务,最不推荐的方式是直接关掉cmd窗口,这样等于强杀进程,可能导致数据没落盘或者连接没释放。

比较推荐的做法是:先在cmd窗口按Ctrl+C,Spring Boot会收到关闭信号,执行优雅关闭流程。如果你的项目里用了@PreDestroy之类的回调,这一步能正常执行。

如果服务是后台运行、或者Ctrl+C没反应,需要强制关闭。先通过jps -l找出Java进程的PID:

jps -l

输出类似:

12345 device-manage.jar

然后强制结束进程:

taskkill /F /PID 12345

需要提醒的是,taskkill /F属于强制杀进程,能不用就不用,常规操作先给优雅关闭机会。

6.4 日志不要只输出到控制台,让日志文件滚动起来

cmd窗口里日志会一直滚动,窗口关闭日志就没了。生产环境一定要让日志同时写到文件里。最简单的办法是启动命令里重定向:

java -jar device-manage.jar > app.log 2>&1

2>&1是把错误输出也一起写进app.log。这个方法零成本,但app.log会无限增长,用得久了一个文件几个GB都很正常。

更规范一点的做法是在项目里配logback,按天滚动、保留最近7天日志。如果你接手的老项目没配过,先从重定向开始,至少日志有落到磁盘。额外说一句,落盘的日志文件编码也要注意,最好在logback配置里显式指定UTF-8,不然在Windows上打开日志文件看到中文乱码,排查问题的时候又得先解决日志可读性,纯浪费时间。

这些年搬过很多次服务,我自己的固定动作是把每次上线的jar按日期归档,脚本里只用相对路径,出问题切旧版本就是替换文件的事。这套“打包成jar、cmd启动、脚本固化”的链路虽然看着不炫,但足够稳。把这条路练熟了,以后不管上什么环境,你心里都有底。

返回列表