ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Windows下Spring Boot后端一键bat部署脚本的设计与实践

Windows下Spring Boot后端一键bat部署脚本的设计与实践 赫兹威客HertzWiki这套前后端分离框架模板我自己维护了两年多。后端一直用bat脚本做部署不少同事第一次看到都觉得“太原始了”但真正经历过生产环境频繁发版的人应该懂在Windows服务器上一条bat脚本能稳定执行完打包、停服、启服、健康检查全流程比打开IDEA手点CtrlF5要可靠得多。今天就把这套部署脚本和背后的设计逻辑完整拆开从为什么选bat、脚本怎么写、遇到哪些坑一次讲透。1. 先理清楚这套框架模板后端部署到底要解决什么问题1.1 什么是赫兹威客框架模板赫兹威客HertzWiki是我一直在维护的内部快速开发框架模板定位和若依这类前后端分离项目类似把权限管理、菜单路由、代码生成、操作日志这些通用能力抽出来业务开发只需要在旁边写自己的模块。后端基于Spring Boot前端是Vue3数据库默认MySQL同时预留了Redis。这样一套东西很多团队拿过来直接做业务后台或企业管理系统。也因为它是“框架模板”项目交付时往往要部署到不同客户的服务器上其中不少客户内部就是Windows Server。所以部署这一环必须尽量简单、稳定、少依赖不能指望客户运维懂Linux、装Docker。用bat脚本做后端部署就成了最务实的选择。需要说明的是这里说的“后端部署”不是把War包丢进Tomcat那种传统方式而是Spring Boot打成Jar包配合外部配置文件在Windows下直接后台运行。整个过程包括Maven打包、杀掉旧进程、备份旧包、拷贝新包、启动服务、健康检查六个动作全部由一条脚本完成。1.2 为什么选bat脚本而不是Docker/Jenkins每次聊部署方案总有人问“现在都用Docker了怎么还写bat”其实这个选择是环境逼出来的。目标服务器是Windows Server 2016甚至2012上面没有Docker也不方便装WSL如果为了一套Jar包去搭Docker环境反而引入更多维护成本。Docker确实更适合大规模微服务但对于这种单后端、单机的框架模板项目一个bat脚本完全够用。Jenkins同样可以考虑但很多客户环境是内网不能访问外网也不给你装插件的权限。bat脚本是Windows原生支持的没有任何额外安装成本复制过去就能跑。而且脚本内容直白客户运维也能看懂出了问题不至于互相扯皮。选bat还有一层原因可维护性好。Spring Boot项目发版频繁时只要改动脚本里的jar包名、端口、目录这几个变量就好不用重新构建整个发布环境。再加上它能被Windows计划任务调用每天凌晨自动构建预览环境也特别方便。1.3 适用场景与运行环境说明这套脚本适合以下场景后端是Spring Boot可执行Jar包服务器是Windows 7及以上建议Windows Server 2016环境里有JDK 8或11构建机上有Maven。如果项目还没有编译过脚本里会选择跳过测试加快打包时间如果不想在服务器上跑Maven也可以只在开发机构建然后把Jar包丢到服务器上用后面的启动脚本直接起服务。我给的示例环境是系统Windows Server 2019JDK版本1.8.0_202Maven 3.6.3后端Spring Boot 2.7.x端口8080。实际项目里只要按自己环境改一下脚本顶部的参数就行。2. 部署方案设计与目录规范2.1 后端技术栈与项目结构部署脚本要稳定目录结构先得规划好。不要把Jar包、日志、配置全部扔在代码目录里否则升级时很容易把旧包和新包搞混。我的做法是分成两个大目录C:\hertz-wiki\ ├── backend\ 代码仓库目录git clone下来 │ ├── pom.xml │ └── src\ └── deploy\ 部署目录只放运行产物 ├── config\ │ └── application-prod.yml ├── logs\ ├── backup\ ├── deploy.bat ├── start.bat └── stop.bat代码目录backend负责Maven构建构建产物在target目录下。部署目录deploy才是服务真正运行的地方。新版本发布时从backend\target把新的Jar包复制到deploy同时把旧包移到backup做回滚。配置文件放在deploy\config不会被Maven clean覆盖。日志固定写到deploy\logs排错时只要看一个地方。2.2 一键部署的整体流程打包、停服、备份、替换、启动、检查这六个步骤是部署脚本的主线。无论项目是Spring Boot还是其它Java后端只要最终产物是Jar包这个流水线基本通用。打包Maven执行clean package -DskipTests保证每次从干净状态构建。停服通过netstat找到监听端口的PID再taskkill结束进程。备份把当前运行的Jar包挪到backup目录文件名带上时间戳。替换复制新包到deploy目录。启动用javaw.exe后台运行并指定外部配置文件和日志路径。检查循环请求健康检查接口确认服务活了再退出。这套流程最关键的细节是“先停旧再备份还是先备份再停旧”我推荐先停旧再备份。因为如果先备份停服失败后旧包还在运行接下来再复制新包就会覆盖正在使用的文件会造成文件占用导致复制失败。先停旧确保端口释放再动文件整个链路更安全。2.3 PID与端口管理的关键设计Windows下没有kill -9停服务最常用的组合是netstat -aon加taskkill /f /pid。但很多人在脚本里直接杀所有java.exe进程这很危险。一台服务器可能跑着多个Java应用还有Jenkins Agent之类全部杀掉会牵连无辜。更好的是按端口杀谁占用指定端口就杀谁精准又安全。还要注意netstat输出中PID列的位置可能随系统版本或系统语言变化。中文Windows和英文Windows输出标签不同但格式基本固定是用空格分隔的列。我在脚本里用tokens5取第五列在Windows Server 2019英文版实测没问题。如果你的系统PID列位置不一致可以先手动执行netstat -aon | findstr :8080看一眼输出再调整tokens。提示脚本里建议用管理员权限运行否则taskkill可能因为权限不足报“拒绝访问”。可以在bat文件上右键“以管理员身份运行”或者在计划任务里选择“使用最高权限运行”。3. 手把手编写后端bat部署脚本3.1 环境变量与公共参数脚本顶部一定要做参数集中化不要硬编码到处写路径。我这里定义了一套示例参数echo off setlocal enabledelayedexpansion chcp 65001 nul title HertzWiki Backend Deploy set APP_NAMEhertz-wiki-backend set JAR_NAMEhertz-wiki-backend.jar set BASE_DIRC:\hertz-wiki\backend set DEPLOY_DIRC:\hertz-wiki\deploy set LOG_DIR%DEPLOY_DIR%\logs set RUNNING_PORT8080 set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202 set MAVEN_HOMEC:\apache-maven-3.6.3 set PATH%JAVA_HOME%\bin;%MAVEN_HOME%\bin;%PATH% if not exist %LOG_DIR% mkdir %LOG_DIR%setlocal enabledelayedexpansion是为了在for循环内部使用变量时拿到实时值避免批处理变量延迟扩展的经典坑。chcp 65001是把控制台代码页切成UTF-8避免中文输出乱码。注意脚本文件本身要保存为UTF-8还是GBK这个问题我在后面的常见问题章节里专门讲。JAVA_HOME和MAVEN_HOME最好写全路径不要光靠PATH。因为某些客户机器上可能有多个JDKjava -version可能指向版本不对的JDK。通过在脚本内部把%JAVA_HOME%\bin放到PATH最前面能保证后续执行java和mvn使用的是你指定的版本。3.2 执行Maven打包并处理失败打包命令很简单但失败处理必须做。如果Maven构建失败还继续往下走旧服务已经被杀掉了新包又没造出来线上就空白了。所以我习惯在打包后立刻判断errorlevelcd /d %BASE_DIR% call mvn clean package -DskipTests -q if errorlevel 1 ( echo BUILD FAILED, please check code. exit /b 1 )注意这里用的是call mvn不能用直接写mvn。因为在bat脚本里执行另一个批处理或命令时如果不加call脚本会直接跳到那个命令去执行后边的代码不再继续。这一点很多新手踩过坑。加上call才能保证Maven执行完后回到当前脚本继续跑。-DskipTests会跳过测试用例执行但会编译测试代码。如果想连测试代码一起跳过可以用-Dmaven.test.skiptrue。日常发版我建议用-DskipTests就好至少能保证测试代码语法正确。如果是赶时间想快速出包也可以用-Dmaven.test.skiptrue。-q参数让Maven只输出错误日志避免终端里翻滚一屏幕构建信息。3.3 停止旧服务的正确姿势停旧服务是整个脚本里最容易出错的部分。我经过多次调整最终用的方式是先查端口再杀对应PIDfor /f tokens5 %%p in (netstat -aon ^| findstr :%RUNNING_PORT% ^| findstr LISTENING) do ( echo Port %RUNNING_PORT% is used by PID %%p, killing... taskkill /f /pid %%p nul 2nul )这里的for /f会遍历netstat -aon输出中同时包含:8080和LISTENING的行把第五列PID取出来赋给变量%%p然后执行taskkill。执行完杀掉进程后我建议再加一句timeout /t 3 /nobreak nul给端口释放和JVM退出留出时间。否则马上复制新包Windows偶尔会因为文件句柄还没释放而报错。如果端口没有进程监听for循环体不会执行脚本会继续向下走。这是没问题的。但需要注意如果服务是以Windows服务方式注册的比如用WinSW包装的那taskkill杀的不是持久化服务服务可能还会自动拉起。这种场景要改用net stop 服务名。我在常见问题里会再提。3.4 启动新服务的参数与后台运行启动Spring Boot Jar包建议用javaw.exe而不是java.exe。javaw不会弹出黑色控制台窗口适合后台服务。但要注意日志别弄丢所以启动参数里一定要指定日志文件。copy /y %BASE_DIR%\target\%JAR_NAME% %DEPLOY_DIR%\%JAR_NAME% nul cd /d %DEPLOY_DIR% start HertzWiki-Backend %JAVA_HOME%\bin\javaw.exe -Xms512m -Xmx1024m -jar %JAR_NAME% --spring.config.locationfile:%DEPLOY_DIR%\config\application-prod.yml --logging.file.path%LOG_DIR%这里--spring.config.location指到deploy\config下的外部配置文件而不是使用Jar包里的application.yml。这样改配置不用重新打包。--logging.file.path在Spring Boot 2.3以后建议改成--logging.file.name具体取决于你用的版本否则日志可能不出现在预期位置。-Xms512m -Xmx1024m是堆内存初始值和最大值。框架模板这种后台管理系统512M初始、1G最大足够。如果机器内存小可以改成-Xms256m -Xmx512m。调整后最好重启服务看有没有OutOfMemory不要盲目调高。start后面带的引号是窗口标题必须放在命令前面这是Windows start命令的固定语法。3.5 健康检查与自动拉起启动命令执行后脚本就退出了吗不能急着退出。生产环境里最常见的情况是Jar包启动过程中端口未监听如果部署脚本直接结束你以为服务已经起来了其实JVM可能刚启动就报错退出。所以我都会在启动后加一段健康检查echo Waiting for health check... for /l %%i in (1,1,20) do ( curl -s http://localhost:%RUNNING_PORT%/actuator/health nul 2nul if not errorlevel 1 ( echo Health check passed. exit /b 0 ) timeout /t 3 /nobreak nul ) echo Health check timeout, please check logs. exit /b 1这个循环最多等待60秒每3秒请求一次/actuator/health。只要curl命令成功返回不管内容是什么就认为HTTP服务已经可以响应了。如果你不想引入Spring Boot Actuator可以换成项目自己的登录页地址比如/login只要它返回200或302都算启动成功。注意Windows Server 2016自带的curl.exe有时是C:\Windows\System32\curl.exeWindows 10 1803以后才有。如果是老系统建议改用PowerShellpowershell -Command Invoke-WebRequest -Uri http://localhost:8080/actuator/health -UseBasicParsing | Out-Null。如果健康检查超时脚本返回错误码部署流程立即终止。这时候不要急着重复执行部署先去logs目录看启动日志定位是什么导致启动失败。后面常见问题里我会专门说排查方法。4. 完整部署实操从首次到日常升级4.1 首次部署的完整流程首次部署不像升级那样有旧包可杀所以建议分两步走先手工检查环境和配置文件再执行脚本。第一步检查JDK和Maven是否在PATH里。如果客户机器没装先装好并配置JAVA_HOME。第二步检查MySQL、Redis这类依赖服务是不是已经启动配置文件的数据库账号密码对不对。第三步把前端构建产物告诉运维或者自己放到Nginx目录这里不展开。第四步把deploy目录整体拷贝到服务器配置application-prod.yml。第五步右键管理员运行deploy.bat。首次运行即使脚本报了健康检查失败也不要慌去logs里看日志。最常见的是数据库连不上、端口被别的程序占了、Redis密码不对这三件事。把这三样确认好基本一次过。4.2 日常升级部署的正确节奏日常升级一定是增量更新不要每次全量复制代码。我先保证代码在git上打好tag然后在构建机上执行mvn clean package -DskipTests只把新的Jar包上传到服务器的backend目录最后在服务器上执行deploy.bat。脚本会替你做停服、备份、替换、启动、检查。因为备份是自动的所以不需要提前手动备份。但我强烈建议升级前记录当前版本号。记录版本号的好方法是在deploy目录放一个version.txt每次发布手动更新一行。如果想用脚本自动读取bat里可以用for /f指定行号来取内容例如读取第二行set VERSION_FILEC:\hertz-wiki\deploy\version.txt set VERSION for /f skip1 delims %%v in (type %VERSION_FILE%) do ( if not defined VERSION set VERSION%%v ) echo Current version is %VERSION%这个例子里的skip1会让for /f跳过第一行直接把第二行内容赋给变量。if not defined VERSION保证只取第一个匹配行避免循环体每次都覆盖。对于想要根据版本号决定是否回滚的团队这个技巧非常实用。如果项目用了Flyway或Liquibase这类数据库迁移工具还要注意升级时自动执行的数据库变更回滚时需要单独评估数据库结构是否已经变化。4.3 快速回滚回滚是部署流程里必须有的一环。因为脚本已经把旧包备份到backup目录回滚时只要写一个rollback.batecho off set DEPLOY_DIRC:\hertz-wiki\deploy set BACKUP_DIR%DEPLOY_DIR%\backup set JAR_NAMEhertz-wiki-backend.jar set LOG_DIR%DEPLOY_DIR%\logs set PORT8080 for /f tokens5 %%p in (netstat -aon ^| findstr :%PORT% ^| findstr LISTENING) do ( taskkill /f /pid %%p nul 2nul ) timeout /t 3 /nobreak nul for /f delims %%f in (dir /b /o-d %BACKUP_DIR%\%JAR_NAME%.* 2^nul) do ( set LATEST_BACKUP%%f goto :found ) :found if not defined LATEST_BACKUP ( echo No backup found. exit /b 1 ) copy /y %BACKUP_DIR%\%LATEST_BACKUP% %DEPLOY_DIR%\%JAR_NAME% nul cd /d %DEPLOY_DIR% start HertzWiki-Backend %JAVA_HOME%\bin\javaw.exe -Xms512m -Xmx1024m -jar %JAR_NAME% --spring.config.locationfile:%DEPLOY_DIR%\config\application-prod.yml --logging.file.path%LOG_DIR% echo Rollback to %LATEST_BACKUP% done.这段脚本会按dir /o-d按日期倒序找出最新的备份文件复制回deploy目录再启动。用到goto :found是为了跳出循环因为for循环里一旦找到第一个文件就可以停止遍历。实际使用中建议把这个脚本和deploy.bat放在同一目录下方便运维查找。需要提醒的是回滚只解决Jar包版本问题不解决数据库结构变化。如果新版本引入了数据库字段回滚到旧版本后代码可能因为字段不匹配而报错。所以我的经验是发布前评估数据库变更必要时先做一次数据库备份再执行部署回滚时如果数据库已经变过那就需要先手工恢复数据库备份再回滚Jar包。5. 常见问题与排查技巧实录5.1 端口被占用服务启动失败现象执行脚本后健康检查失败日志显示Web server failed to start. Port 8080 was already in use。原因端口被其它进程占用而脚本前面杀的是8080的PID但因为权限不足或进程是系统服务没杀成功。排查先在cmd执行netstat -ano | findstr :8080看LISTENING行的PID再用tasklist | findstr PID看是哪个进程。如果是其它业务程序就不要用部署脚本里的通用杀法要么改启动端口要么让运维协调停止那个程序。我在脚本里已经用了taskkill /f但如果你不是管理员运行杀进程会静默失败因为输出被重定向了。所以遇到端口占用第一件事不是改脚本而是确认是否以管理员身份运行。5.2 bat脚本中文乱码现象脚本执行时输出的中文全是一堆乱码甚至命令执行失败。原因bat脚本文件编码与控制台代码页不一致。如果脚本是GBK/ANSI编码而chcp 65001把控制台切换成UTF-8中文输出就会乱反之脚本是UTF-8编码而cmd默认代码页是GBK也会乱。解决办法有两个方向。一个方向是脚本不用中文全部英文输出这样什么代码页都不影响。另一个方向是统一编码脚本保存为ANSI/GBK同时去掉chcp 65001让cmd用默认GBK运行。如果你一定要用UTF-8就需要把脚本保存为UTF-8 with BOM并且保留chcp 65001否则批处理解析中文会有问题。我个人的建议部署脚本这种长期维护的工具echo文本尽量用英文路径和包名用英文避免不同客户服务器编码差异带来的麻烦。真正需要给客户看的中文提示可以放到注释里或者在部署完成后由一个单独的说明文件提供。5.3 脚本双击闪退现象双击bat文件窗口一闪就没了什么也看不到。原因脚本执行时遇到exit或异常退出而bat默认运行完就关闭窗口根本来不及看错误。解决在脚本里不要直接exit /b 1后不管可以在最后加pause让窗口停留等待按键。但部署脚本在计划任务里运行不能有交互pause会卡住任务。所以更推荐调试时用命令行窗口执行deploy.bat或者先用下面这种调试模式cmd /k deploy.bat这样窗口会保留能看到所有输出。等脚本稳定后再把pause去掉。另外如果双击后闪退且没有任何输出也可能是脚本编码问题导致第一行命令解析失败按5.2检查编码。5.4 配置文件被覆盖或丢失现象部署后服务配置变了数据库地址变成默认值或者配置目录下文件不存在。原因很多人直接把application.yml放在代码src/main/resources下然后Maven打包时会打入Jar包如果你的启动命令没指定外部configSpring Boot会优先使用Jar包内的配置外部配置文件形同虚设。更糟糕的是有人喜欢在代码目录里放config然后执行git clean -fdx配置文件直接被清掉。我的解决办法是配置文件一定放在deploy\config目录脚本启动参数里用--spring.config.location强制指定外部文件。这样代码目录里哪怕只有application.yml模板也不会影响生产配置。每次部署前我还建议检查一下外部配置文件的修改时间确认没有在部署过程中被意外覆盖。5.5 服务启动失败但日志无输出现象启动后健康检查失败logs目录下没日志文件。原因一种可能是日志路径参数没生效用了Spring Boot 2.3以后被废弃的--logging.file.path另一种可能是日志被Windows服务启动到别的用户目录权限不足导致写不了。处理方法是先看进程还在不在tasklist | findstr java。如果进程在但没日志多半是日志路径问题试着改成--logging.file.name%LOG_DIR%\hertz-wiki.log。如果进程没了说明启动异常把启动命令放到cmd里前台执行用java -jar而不是javaw这样错误会直接打印到控制台能看到具体报错。我在脚本里故意没用java而是用javaw是考虑到后台运行但排错时一定要反其道而行用前台java跑把真实的异常堆栈带出来。定位问题后再回到javaw后台运行。5.6 定时任务自动部署bat脚本的优势之一是可以被Windows计划任务调用。我想实现每天凌晨自动构建预览环境就建了一个计划任务schtasks /create /tn HertzWikiDeploy /tr C:\hertz-wiki\deploy\deploy.bat /sc daily /st 02:00 /ru SYSTEM /rl HIGHEST /f注意/ru SYSTEM会让计划任务以系统身份运行确保有管理员权限。但系统用户去访问网络共享路径或某些映射盘符时可能没有权限如果你脚本里依赖盘符请改成绝对路径或UNC路径。计划任务运行bat时不要在脚本里放pause否则任务永远结束不了。如果想实现“每次代码提交后自动部署”单靠bat做不到需要Git Webhook触发服务器上的脚本。但Windows上搞Webhook又绕不开认证和防火墙配置不如先把定时构建用好。定时构建非常适合做每日凌晨的自动联调环境手动发版仍由deploy.bat完成。5.7 读取配置文件或版本号指定行的技巧有些场景需要在bat里读取一个文本文件指定行的内容比如前面的version.txt、或者某个config.properties里第二行的配置项。直接暴力读取会拿到第一行或最后一行容易出错。这里分享一个通用模板读取指定文件第N行set FILEC:\hertz-wiki\deploy\version.txt set LINE2 set CONTENT set /a COUNT0 for /f delims %%a in (type %FILE%) do ( set /a COUNT1 if !COUNT! equ %LINE% ( set CONTENT%%a goto :done ) ) :done echo Line %LINE% content: %CONTENT%这段脚本的核心是set /a COUNT1逐行计数命中目标行后用goto :done跳出循环。由于启用了enabledelayedexpansion循环内变量必须用!COUNT!访问否则拿到的是初始值。这个技巧特别适合部署前校验版本号、环境标识等场景建议放到部署工具里备用。6. 我踩过的坑与后续扩展建议6.1 几个印象深刻的坑第一个坑是Maven打包时用了mvn而不是call mvn。第一次写部署脚本时执行到Maven打包后脚本直接结束后面停服、启动全部没走线上服务被停了却没起新的那次差点事故。从那以后我养成了习惯凡是bat里调用另一个bat都要加call。第二个坑是直接在服务器上用CTRLC强制关窗口后javaw进程还留在后台。后来通过端口查PID杀掉才解决。这也是我坚持在脚本里用端口定位进程而不是按名字杀的原因按名字误杀太多。第三个坑是备份文件名用时间戳时%time%里如果有空格比如上午10点前时间是9:30会导致文件名带空格。所以后来我使用%date:~0,4%%date:~5,2%%date:~8,2%只保留日期或者用time:~0,2%时先把空格替换掉。这个小细节可能让备份脚本在特定时间段失效。6.2 如何扩展成CI/CD虽然bat脚本本身已经解决了“在服务器上部署”这一步但如果你想更自动化可以把它和Git版本管理串起来。比如在构建机上写好一个build.bat先git pull再mvn package然后通过scp或robocopy把Jar包推到目标服务器最后远程调用服务器上的deploy.bat。这一步用PowerShell的Invoke-Command也能实现但客户内网环境可能有不小的坑。如果是公司内部开发环境建议把deploy.bat嵌入到GitLab Runner或Jenkins的Windows节点里。做法很简单构建节点上建一个Job执行deploy.bat把脚本当作发布动作的入口。服务器数量多了以后再用Ansible的win_command模块在Windows目标机上执行同一个脚本。再往后才是Docker、Kubernetes这些事。6.3 最终心得写这套脚本最大的收获不是脚本本身多高明而是让我重新理解了“部署”这件事它不只是把文件传上去、敲几个命令而是要把整个升级过程的可重复性和可回滚性都设计好。bat脚本虽然看起来土但它在特定的Windows环境下就是最不容易出错、最容易被运维接受的方式。如果你手里正好有这样一个框架模板后端要做部署不妨先把我这套脚本拿过去改一改。不要一上来就追求K8s、云原生先让你的服务能一键起停、能快速回滚再谈更高阶的自动化。踩过几次坑之后你会明白部署脚本里每一行命令背后都是真实的生产事故和补救经验。
返回列表