
以前我也是java -jar app.jar一把梭窗口开着就万事大吉。直到有一次我在 Windows Server 上部署订单服务远程桌面关掉后睡了一觉第二天业务方告诉我服务挂了日志里什么都没留下进程也找不到了。从那天起我就开始研究怎么把 jar 包真正托管给 Windows最终固定在生产环境里用 WinSW-x64 来做这件事。它做的事情一句话就能概括把 Spring Boot或其他 Java 应用的 JVM 进程包装成一个标准 Windows 服务注册进 services.msc开机自启、崩溃重启、日志落盘全程不需要挂着黑色命令行窗口。这篇文章就把我这几年的使用经验、配置细节和踩过的坑一次说清楚。1. 为什么要把 Jar 包做成 Windows 服务四个绕不开的真实场景1.1 关掉终端进程就没了这是所有裸奔 jar 包最痛的问题。在 Windows 上通过命令行执行java -jar app.jar创建出的进程基本绑死在启动它的控制台会话里。你可以做个测试开一个 CMD 窗口跑 jar然后直接点右上角关闭JVM 进程多半会跟着一起消失。因为控制台关闭事件会把子进程一并终止这是 Windows 的默认行为。偶尔你运气好进程没立刻退出但你会发现它变成了一个孤儿状态不可控、日志中断、没人能准确告诉你它是否还正常。服务模式下完全没有这个问题服务进程挂靠在服务控制管理器SCM下运行在独立的 Session 0和用户是否登录、是否开着远程桌面、是否误关窗口完全无关。只要能开机它就能活着。1.2 服务器重启后应用能不能自己回来Windows Server 偶尔会重启Windows Update 导致的自动更新、机房断电后的来电恢复你没法保证机器一直是开机状态。如果没有把应用注册成服务每次重启后你都要人肉远程桌面、打开目录、重新敲一遍java -jar。就算你聪明一点把启动命令塞到启动文件夹或者计划任务里系统的开机时机也会带来新问题应用在网络栈、数据库、注册中心就绪之前就启动可能启动失败或者疯狂重试。WinSW 可以把启动模式设为 Automatic甚至用depend声明要等哪些服务先起来这样重启之后应用能自己回到可用状态。1.3 统一管理services.msc 是你的总控台把 jar 注册成服务后在services.msc里就能看到它的运行状态、启动类型、登录身份。你可以右键启动/停止/重启也可以在恢复选项卡里配置失败后自动重启。运维交接的时候新接手的同事打开服务管理器就能看到所有线上应用而不是靠看某个 README 或者跑一遍启动脚本来猜。服务状态还能和事件查看器联动服务的启动、停止、崩溃都有系统日志记录。这对排查问题太重要了——尤其是那种半夜挂掉你不在场的情况系统事件能帮你定位崩溃发生的时间点和触发动作。1.4 权限边界和安全审计命令行启动应用默认使用你当前登录账户的权限如果运维图省事一直拿 Administrator 跑安全审计那边基本过不去。WinSW 允许为服务指定一个专用账户比如.\svc_java只给它必要的权限。服务还能以 LocalSystem、NetworkService 或指定域账户运行满足不同的资源共享场景。这一点是被很多人忽略的隐藏价值。2. 方案选型对比WinSW、NSSM 与 sc 命令的取舍2.1 先别急着用 sc create有人觉得 Windows 自带服务管理直接用sc create注册不就行了我试过最典型的写法是sc create MyApp binPath cmd /c java -jar D:\apps\app.jar start auto这个服务能建出来但很容易出现启动即失败、状态显示异常、无法正确停止之类的问题。原因在于sc create期望注册的目标程序是一个真正的服务程序——它必须能和 SCM 握手按协议上报启动进度和状态。而cmd只是一个命令行解释器它启动java后自己是等还是退出、怎么跟 SCM 通信统统不管。于是 SCM 认为服务启动超时直接把进程干掉。另外sc create语法本身也坑binPath后面必须有一个空格再跟值写错就报参数不正确。所以sc适合管理 Windows 自带的原生 exe 服务不适合包装你的 Java 应用。2.2 NSSM好用但不够工程化NSSMNon-Sucking Service Manager是我以前常推荐的工具它的优点是带图形界面右键就能把任意程序注册成服务还自带日志轮转、崩溃重启、App 环境变量配置。如果你只在个人服务器上搞一个服务NSSM 真的很香开箱即用。但我在团队协作中遇到几个实际麻烦NSSM 的配置主要写在注册表里不是纯文本。你要审计这台机器上服务是怎么配的得去注册表翻要把配置带到另一台机器也没有一个标准的配置文件可以直接带上。公司安全部门对第三方二进制有审批流程NSSM 这种相对小众的工具过审要看运气。32 位和 64 位版本要对应否则在 64 位系统上用 32 位 NSSM 管 64 位程序会撞上注册表重定向这种隐蔽问题。2.3 WinSW配置即文本适合长期维护WinSW 是 GitHub 上一个开源项目和 NSSM 的思路不太一样它的核心是一个可执行文件 一个同名 XML 配置文件。WinSW-x64.exe就是一个封装器启动后读取同目录下同名的.xml文件然后创建子进程也就是java.exe再把子进程的标准输出/错误输出接到日志文件同时替子进程跟 SCM 做服务握手。这带来一个和 NSSM 完全不同的协作优势配置文件可以进 Git 仓库每次改了什么有 diff、有评审、有回滚。新机器部署只需要拷贝目录、执行一句 install就能克隆出一个一模一样的服务。为什么我要用 WinSW一句话它把服务配置从机器上的一份状态变成了仓库里的一段文本这对多机部署和团队协作来说太重要了。3. WinSW-x64 实操全流程从下载到服务自启3.1 下载与文件摆放一个服务一个目录去 WinSW 项目的 GitHub Releases 页面下载WinSW-x64.exe。注意如果你是 32 位系统需要WinSW-x86.exe新版 WinSW 一般要求 Windows 7 以上现代 Windows Server 基本都能直接跑。下载后不要图省事把 exe 扔到下载目录再全局 PATH 调用。建议一个服务一个文件夹目录结构这样放D:\apps\order-service\ ├── order-service.exe # 由 WinSW-x64.exe 改名而来 ├── order-service.xml # 与 exe 同名的配置文件 ├── app.jar └── logs\为什么强调改名因为 WinSW 的约定是exe 会找和自己同名的.xml文件。你把WinSW-x64.exe改成order-service.exe它就会自动读取order-service.xml。这样你一眼就能看出这个服务是谁也避免多个服务共用同一个默认文件名时配置互相冲突。3.2 XML 配置逐项拆解下面给一份我实际用过的配置模板先别急着抄后面我会逐条讲为什么这么写service idorder-service/id nameOrder Service/name description订单中心 Spring Boot 服务/description env nameJAVA_HOME valueC:\Program Files\Java\jdk-17 / executableC:\Program Files\Java\jdk-17\bin\java.exe/executable arguments-Xms256m -Xmx1024m -Dfile.encodingUTF-8 -jar D:\apps\order-service\app.jar --server.port8080/arguments workingdirectoryD:\apps\order-service/workingdirectory logpathD:\apps\order-service\logs/logpath log moderoll-by-size sizeThreshold10240 keepFiles8 / onfailure actionrestart delay10 sec / startmodeAutomatic/startmode /service几个关键点id是服务在 SCM 里的唯一标识安装后可以在注册表里看到这个名字改 id 相当于换一个服务建议一开始就定好不要随手下。executable我建议用 java.exe 的绝对路径而不是写java。因为服务启动时运行环境不是你交互登录时的环境PATH 里有没有 java 完全取决于系统级环境变量很容易出现你手动敲命令能启动服务一启动就说找不到 java的情况。用绝对路径省掉这一层依赖。env是可选的用来给服务进程设置环境变量。如果你有多台机器 JDK 路径不同把这个 value 变成脚本渲染出来的变量维护体验会好很多。arguments是 JVM 和应用的参数。路径里出现空格时一定要加双引号否则会被拆成多个参数。这个错误占据我处理 WinSW 问题的一半以上。log指定日志轮转模式。roll-by-size表示按大小滚动sizeThreshold单位是 KBkeepFiles是保留历史文件数。无限追加的append模式千万别在生产用日志能把磁盘撑爆。3.3 安装、启动与验证一切就绪后用管理员身份打开 CMD切到目录cd /d D:\apps\order-service order-service.exe install order-service.exe start order-service.exe status新版 WinSW 还提供了test命令可以在安装前先验证配置里有没有明显错误order-service.exe test看到Running之后打开services.msc应该能看到 Order Service 这一条。此时我强烈建议先别激动去翻开logs\order-service.out.log找到 Spring Boot 的Started Application字样再确认成功。因为服务模式下没有黑窗口日志文件就是你唯一能确认它真的健康的依据。如果你在命令行窗口里按 CtrlC 或者直接关闭窗口服务进程依然在后台运行——这正是支持后台运行的含义进程的生命周期已经和你这个终端解耦了。4. 别小看这几个参数决定服务稳定性的细节4.1 workingdirectory看似无关出事致命服务启动时的当前工作目录默认不一定是 exe 所在目录。SCM 启动进程时如果没有指定工作目录经常是C:\Windows\System32。如果应用的配置用相对路径读取比如spring.config.additional-location./conf/就会找不到文件服务看着是 Running业务功能却全都不正常。我接手过一个老项目服务状态一直是正在运行但所有接口都报配置缺失排查到最后发现就是工作目录不对。解决办法就是在 XML 里显式声明workingdirectoryD:\apps\order-service/workingdirectory4.2 服务账号与作为服务登录权限默认情况下WinSW 服务以 LocalSystem 身份运行。这个账号对本机权限极高内部临时服务凑合用可以但生产环境最好按最小权限原则来。如果你在 XML 里指定了服务账号serviceaccount domainYOURDOMAIN/domain usersvc_order/user passwordxxxxx/password /serviceaccount那你还必须做一步到本地安全策略或组策略里给这个账号授予作为服务登录的权限。漏掉这一步服务启动时大概率报错误 1069服务登录失败。另外如果服务要访问网络共享目录或者别的机器资源账号的网络权限也要同步配好。4.3 depend让依赖服务先起来如果应用启动时立刻要连数据库、Redis、Nacos、消息队列而 Windows 开机阶段这些服务还没起来应用就会疯狂重试日志里刷满连接拒绝。与其在代码里做重试不如让服务管理器帮你编排启动顺序dependMySQL80,RabbitMQ/dependdepend里的名称不是显示名而是服务管理器里服务名称列的值。直接去services.msc里右键服务看属性就能找到。这种显式依赖比应用内重试要靠谱得多也让系统层面的启动顺序一目了然。4.4 服务 id 的生命周期教训服务一旦安装SCM 就以id为准来跟踪它。你可以在安装后跑到服务属性里改显示名称但别随手改 XML 里的 id——因为注册表里已经有一份旧记录强行 install 同名或改 id 都可能造成混乱。想改 id正确做法是uninstall之后修改 XML 再重新install。上线前把 id 定好和把数据库表结构定好是一个道理后面改起来都疼。5. 服务能装但跑不稳几类典型故障的完整排查链路5.1 服务显示 Running业务却请求失败这是最迷惑人的情况。服务状态是运行中但你请求 80 端口没反应。原因很简单WinSW 把进程存活映射为服务 Running它不知道你的 Spring Boot 是刚启动、还在初始化还是已经处于半死不活的热加载状态。排查链路分三步走看应用日志确认有没有Started或者没报错。用netstat -ano | findstr :8080看端口是否真的在监听。如果有健康检查接口比如/actuator/health直接 curl 一下。不要只盯着服务状态要相信业务侧探测结果。生产环境尤其应该让负载均衡定期探测健康接口而不是依赖服务管理器的显示。5.2 启动后几秒变成 Stopped日志里却没报错这类问题的头号元凶是 WinSW 的 PID 残留文件。WinSW 在 exe 同目录下会生成一个.db文件比如order-service.exe.db用来记录运行中的进程 ID。如果上次服务是异常终止的、或者你直接杀了进程旧 PID 信息还留在里面下次启动时 WinSW 会认为已经有一个实例在跑于是服务很快就退出。处理办法先把服务停掉到目录下删掉所有*.db文件再重新启动。如果问题消失说明是残留 PID 造成的。另一个很常见的启动即退出原因是 JVM 参数错误日志里往往能看到Error: Could not find or load main class看到这个十有八九是arguments里的双引号被拆了或者路径写错。把 jar 路径重新用引号包起来八成能好。5.3 中文日志乱码Windows 系统默认编码和 Java 应用默认编码经常不一致。Java 输出到 stdout 的日志在重定向到文件时如果 JVM 没有明确指定编码中文字符就可能变成??或乱码。我的处理方案是三层一起设在arguments里加-Dfile.encodingUTF-8在 XML 里设置环境变量env nameJAVA_TOOL_OPTIONS value-Dfile.encodingUTF-8/让日志框架Logback/Log4j2的输出 charset 显式设为 UTF-8另外注意XML 文件本身如果包含中文注释或说明保存时用 UTF-8 编码避免配置解析出现意外。5.4 安装/卸载失败的谜之场景安装时报服务已存在或者指定的服务已标记为删除常见于你之前装过同名服务或者服务管理器窗口还开着。推荐的处理链路是确认当前是否在管理员命令行下执行。服务还在运行就先stop。关闭已打开的services.msc窗口等几秒。执行uninstall然后重新install。如果uninstall一直报错可以暂时用sc delete 服务id兜底但不要养成混用习惯WinSW 管理的服务日常用它的 install/uninstall 是最干净的。另外服务名一旦装有冲突要先删旧的再装新的否则怎么 install 都会失败。5.5 开机不自启startmode 只是第一步检查 XML 里startmode是不是Automatic这是最基础的。但如果你没配置depend应用可能在网络和数据库没就绪时就启动导致启动失败然后 SCM 可能按失败计数逻辑放弃继续拉起。还有一个隐蔽点WinSW 服务启动时如果依赖了环境变量比如JAVA_HOME而你没有在 XML 里通过env设置开机阶段的服务进程是拿不到你登录用户的环境变量的。所以配置里把所有关键路径都写成绝对路径或显式 env才能消除这类环境差异。6. 日常升级与多实例部署把运维流程规范化6.1 替换 Jar 包的正确姿势我见过很多人直接在服务运行的时候拷贝新的 jar 包进去然后发现拷贝失败——因为 Windows 对正在运行的进程持有文件锁jar 文件被 JVM 占着复制动作不是被拒绝就是写入不完整。正确流程是执行order-service.exe stop。执行order-service.exe status确认服务确实已经是 Stopped。备份旧 jar比如加上当天日期后缀。拷贝新 jar 覆盖。执行order-service.exe start。查看日志确认成功启动后再让流量进来。这套流程里最忌我以为停了——服务停掉后JVM 退出需要一点时间最好看到 status 输出 Stopped 再操作。每次升级保持同一套流程出问题你能很快定位是代码问题还是部署问题。6.2 一台机器跑多个实例在 Windows 服务器上做灰度、多租户或者端口隔离时同一套应用可能要跑多个实例。这时候最简单可靠的做法是准备多个目录每份都放自己的 exe xml jarexe 命名、服务 id、日志路径全部独立D:\apps\order-service-8080\ D:\apps\order-service-8081\两个服务分别 install 后在 services.msc 里就是两个独立条目。注意规划好每个实例的内存参数-Xmx加起来不要超过物理机内存否则多个 JVM 互相挤兑会出现莫名其妙的 Full GC 性能问题。6.3 用 deploy.bat 固化部署动作我习惯于在每个服务目录下放一个deploy.bat把升级动作固化下来避免每次靠人肉记忆、击错命令。脚本大概长这样echo off set SERVICE_NAMEorder-service cd /d %~dp0 %SERVICE_NAME%.exe stop timeout /t 2 /nobreak nul if exist app.jar copy /Y app.jar app.jar.bak copy /Y app.new.jar app.jar %SERVICE_NAME%.exe start %SERVICE_NAME%.exe status注意脚本里用%~dp0取脚本自身所在目录。因为双击运行脚本和从命令行手敲时当前工作目录可能不一样如果不切目录后面的相对路径全都会错。这个小细节救过我多次。我在实际维护中使用 WinSW-x64 三四年了最大的体会是它不是一个炫技工具而是把Java 进程在 Windows 上怎么活着这件事变得可预期、可管理、可交接。它当然不会帮你解决业务代码里的 Bug但它的确能消除一大类进程莫名其妙没了、没人知道怎么办的运维焦虑。如果你手头还有几台 Windows 服务器上挂着没人管的 Java 进程建议花半天时间把它们全部做成服务之后你会觉得整个世界都安静了。