本地开发的时候SpringBoot项目点一下IDE里的绿色箭头就跑起来了这谁都会。但真正到了部署这一步很多人上来就被卡住目标机器上没有开发工具甚至没有图形界面就是一个Windows cmd窗口。我印象很深的一次是在给某团队迁移一个内部报表服务的时候那台机器上装的东西少得可怜唯一的执行方式就是把项目打成jar包然后cmd里一行命令拉起服务。那一套流程走通之后我才意识到SpringBoot项目打包后在cmd运行看起来基础实际上就是大多数中小项目最通用的部署形态。这篇文章就把这条链路完整拆开打包前要准备什么Maven怎么打出能跑的jarcmd里的启动命令怎么写启动之后怎么判断它真的起来了以及最常见的那几个翻车点——端口、乱码、多环境配置——该怎么处理。不管是第一次接触部署的应届生还是平时只跟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 -versionjava -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项目默认打包方式内嵌Tomcatjava -jar直接运行部署简单。war包需要外置Tomcat容器先把war放进去再启动容器多了一层管理和环境依赖。除非有硬性要求比如公司运维强制要求统一放到某个外置Tomcat里做集中管理否则我建议一律用jar包。选jar包还能省掉外置容器和项目之间的版本兼容问题Tomcat版本冲突这种经典头疼事也基本不会碰到。2.3 Maven的安装与镜像配置打包命令能不能跑通就看这里打包依赖Maven先把Maven本身准备好。下载对应版本的zip包解压到某个目录然后配置两个环境变量MAVEN_HOME指向Maven解压目录比如D:\apache-maven-3.9.6PATH追加%MAVEN_HOME%\bin配置完之后新开一个cmd窗口执行mvn -v能出来版本信息就说明环境通了。还有一个东西直接影响打包速度和质量Maven仓库的下载源。默认中央仓库在国内访问速度很慢依赖稍微多一点第一次打包能卡到让人怀疑人生。建议在settings.xml的mirrors节点里加一个国内镜像。mirrors mirror idaliyun-public/id mirrorOfcentral/mirrorOf urlhttps://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 finalNamedevice-manage/finalName plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.devicemanage.DeviceManageApplication/mainClass /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin /plugins /buildfinalName的作用是自定义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.skiptrue是“测试代码都不编译”。CI上为了速度一般用后者本地打包想保留测试类就用前者。从这俩参数的选择就能看出Maven的粒度控制做得挺细。打完包去target目录下看会有两个关键的jardevice-manage.jar经过repackage后的可执行jar部署用这个。device-manage.jar.originalrepackage之前的原始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.port8081Spring 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端口被别的进程占了。这时候不要慌按下面的链路排查先查谁占用了端口netstat -ano | findstr 8080输出最后一列就是占用该端口的进程PID。再查PID对应的进程是什么tasklist | findstr PID看到是无关紧要的残留进程再决定是否处理。确认无误后结束进程taskkill /F /PID PID这里必须强调一个心态上的问题能改端口尽量改端口不要一上来就kill。8080端口很常用有可能这个端口是别的业务服务正在使用的你随手kill掉别人一脸懵。更合理的操作是确认了占用的确实是无效或废弃进程再动手如果这台机器端口紧张直接换个端口启动更省事java -jar device-manage.jar --server.port80815.2 中文乱码控制台乱码和返回数据乱码是两码事cmd里启动SpringBoot项目中文乱码是我见过第二高频的问题。但乱码要分情况讨论现象原因处理方式控制台里日志和错误信息中文全是乱码cmd默认代码页是936GBK而应用输出UTF-8先执行chcp 65001切到UTF-8代码页再启动Java进程内输出的日志乱码JVM默认文件编码跟代码文件编码不一致启动参数加-Dfile.encodingUTF-8接口返回给前端的中文乱码应用层编码没有统一检查Spring Boot的Spring MVC编码配置数据库读写的中文乱码JDBC连接字符集不对检查数据库连接串是否带useUnicodetruecharacterEncodingutf8先看第一个情况启动前在cmd窗口先敲一下chcp 65001这个命令把当前控制台的代码页切换成UTF-8之后输出中文不会再变成乱码。再补一个JVM层面的保障完整启动命令java -Dfile.encodingUTF-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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai一个乱码问题从控制台、JVM、应用层、数据库四个层面全查一遍基本都能定位到具体是哪个环节没统一。5.3 环境差异导致的配置问题把配置从jar包里面解放出来打了同一个jar包在开发环境的配置跟生产环境肯定不一样。如果配置全写死在jar包里的application.yml里每次环境切换都要重新打包。更合理的方法是把配置放到jar包外部运行的时候单独指定。实际操作我建议这样做在jar所在目录下建一个config目录里面放一个运行环境专用的配置文件比如application-prod.yml启动时通过两个参数配合使用java -jar device-manage.jar --spring.profiles.activeprod --spring.config.additional-locationfile:./config/--spring.profiles.activeprod告诉应用使用prod这个profile--spring.config.additional-locationfile:./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.encodingUTF-8 -jar device-manage.jar --spring.profiles.activeprod pause几个值得解释的细节cd /d %~dp0%~dp0是批处理文件所在目录配合/d参数切换过去。这样无论你是双击bat还是从别的目录调用工作目录都固定下来相对路径不会错。title把cmd窗口标题改成服务名机器上同时跑多个服务的时候一眼就能分辨窗口。pause如果程序崩溃退出窗口不会瞬间消失能看到最后的错误日志方便排查。6.3 停止服务的两种方式优雅关闭和强制关闭停止SpringBoot服务最不推荐的方式是直接关掉cmd窗口这样等于强杀进程可能导致数据没落盘或者连接没释放。比较推荐的做法是先在cmd窗口按CtrlCSpring Boot会收到关闭信号执行优雅关闭流程。如果你的项目里用了PreDestroy之类的回调这一步能正常执行。如果服务是后台运行、或者CtrlC没反应需要强制关闭。先通过jps -l找出Java进程的PIDjps -l输出类似12345 device-manage.jar然后强制结束进程taskkill /F /PID 12345需要提醒的是taskkill /F属于强制杀进程能不用就不用常规操作先给优雅关闭机会。6.4 日志不要只输出到控制台让日志文件滚动起来cmd窗口里日志会一直滚动窗口关闭日志就没了。生产环境一定要让日志同时写到文件里。最简单的办法是启动命令里重定向java -jar device-manage.jar app.log 2121是把错误输出也一起写进app.log。这个方法零成本但app.log会无限增长用得久了一个文件几个GB都很正常。更规范一点的做法是在项目里配logback按天滚动、保留最近7天日志。如果你接手的老项目没配过先从重定向开始至少日志有落到磁盘。额外说一句落盘的日志文件编码也要注意最好在logback配置里显式指定UTF-8不然在Windows上打开日志文件看到中文乱码排查问题的时候又得先解决日志可读性纯浪费时间。这些年搬过很多次服务我自己的固定动作是把每次上线的jar按日期归档脚本里只用相对路径出问题切旧版本就是替换文件的事。这套“打包成jar、cmd启动、脚本固化”的链路虽然看着不炫但足够稳。把这条路练熟了以后不管上什么环境你心里都有底。