1. 先从为什么说起:neoj-community 为什么要做成 Windows 服务
1.1 直接跑命令行的痛,老运维都懂
先说结论:任何需要长期在后台跑的程序,都不应该裸跑在控制台窗口里。neoj-community 这种社区版服务,本地开发测试还好说,一旦你想让它 7x24 小时稳定在线,比如搭个团队内部的代码评审环境、跑一个自动任务节点,就会撞上一连串问题。
我最早用 neoj 的时候也没想太多,直接在命令行里java -jar neoj-community.jar一把梭。结果是什么?第一个坑就是关窗口即断服务。Windows 默认情况下,控制台窗口一关,子进程直接跟着退。哪怕你当时用的是最小化窗口,哪天手一抖点错关闭按钮,服务就没了。第二个坑是开机自启。为了让这个 jar 在电脑重启后自动爬起来,我试过把快捷方式塞进启动文件夹,也试过在计划任务里配开机触发,但效果都不太稳定,尤其是计划任务的触发器偶尔会被 Windows 更新重置。第三个坑更实际——你没一个统一的视角去管它。进程死了你不知道,日志散落在控制台里,重启还要手动找命令。
做成 Windows 服务,本质上就是把 neoj-community 的进程交给 Windows 的服务控制管理器(SCM)去托管。SCM 是 Windows 从 NT 时代就有的核心组件,它负责服务的启动、停止、重启、崩溃恢复这些脏活累活。你唯一要做的就是把 neoj-community 适配成 SCM 认识的形态。
1.2 为什么不是双击 bat,也不是注册任务计划
有人会问,写个 .bat 扔开机启动里不行吗?还真不行。不是不能跑,而是管理粒度太粗糙。
- 开机启动文件夹里的 bat,只是“启动了”而已,没有任何状态监控。进程崩了你没法自动拉起。
- 任务计划程序(Task Scheduler)可以配“启动时触发”,但它的强项是定时任务,对于“常驻服务 + 崩溃自动恢复 + 启动依赖”这类场景,配置起来非常别扭。而且任务计划跑交互式程序偶尔会出现奇怪的权限问题。
- 系统服务的好处非常实在:开机自启、崩溃自动重启、可以被
net start/sc统一管理、可以在服务列表里看状态、出了问题重启起来有迹可循。
尤其你是把 neoj-community 部署在 Windows Server 上的,服务模式基本是唯一正经解法。运维同学登录服务器,第一眼就是看服务列表,没有服务条目反而会被看成“野路子部署”。
提示:别把 neoj-community 直接放 C 盘系统目录下跑。Windows 对 Program Files、System32 这些目录有额外的权限保护和虚拟化重定向,容易引发奇怪的文件访问异常。规矩点,单独建个目录放程序,这是最省心的做法。
2. 思路先理顺:把一个 Java 进程变成 Windows 服务,到底有几种做法
2.1 底层 API 派:用 sc.exe 或者直接调 SCM
最正统的 Windows 服务,是这种:程序本身实现了服务主函数(ServiceMain),能跟 SCM 的协议对话。Java 进程天然不是这种形态,它是普通的一个用户态进程。所以理论上你不能拿sc create直接注册这个 jar。
sc create虽然能把任意 exe 注册成服务,但那是“伪服务”。你注册一个普通 exe 进去,启动时它确实能跑,但 SCM 和进程之间没有服务控制协议通信,结果就是:启动之后 SCM 以为服务一直处于“启动中”(START_PENDING),停服务的时候也杀不掉进程,状态永远不一致。这条路看起来简单,实际上是坑王之路。
2.2 服务包装器派:winsw、NSSM 这类“服务壳子”是正解
这里就要请出今天的主角——服务包装器(Service Wrapper)。原理其实很简单:用一个小型的、用 C/C++ 或 .NET 写的原生程序,它本身是一个合法的 Windows 服务,能跟 SCM 完整对话;同时它负责拉起你的 Java 进程,并监控这个子进程。你把 neoj-community.jar 变成这个包装器的“子任务”,系统服务见到了 neoj-community,SCM 见到的是包装器进程。
市面上主流的工具就两个:WinSW 和 NSSM。我做这类部署首选 WinSW,原因如下:
- WinSW 是 XML 配置驱动,配置全部文本化,方便写进 Git 做版本管理,部署时复制粘贴就是一次启用。
- 它和 Java 生态配合得很好,可以显式配置环境变量、工作目录、启动参数。
- 项目一直在更新,对 Windows 新版系统(Win10/11/Server 2019/2022)兼容性不错。
- 安装和卸载都很简单:一个 install 命令注册服务,一个 uninstall 命令移除。
NSSM 也不错,它是纯 GUI 操作,适合临时调试、手动点点点。但脚本化、批量部署能力弱一些。既然是写博客分享,我按 WinSW 的方式完整走一遍。
2.3 其他方案:Spring Boot 自带插件、Java Service Wrapper
如果你的 neoj-community 是基于 Spring Boot 的,它自带的 spring-boot-maven-plugin 有一个windows-service功能(用 WinSW 实现),能在 Maven 构建时直接生成服务安装文件。不过这个做法需要你的项目源码和构建环境,假如你手里只是发布好的 jar 包,用通用 WinSW 更灵活。
Java Service Wrapper 也很经典,在很多商业软件里用得多。纯 Java 场景我倒不太推荐,配置比 WinSW 复杂,而且社区版功能被砍过,遇到问题排查也不够透明。
3. 实操准备:环境、目录和文件清单
3.1 确认环境基线
先把底子摸清楚:
- 系统版本:Windows 10 及以上,或者 Windows Server 2016 以上,都行。WinSW 比较新版本对老系统也能跑,但没必要自我设限。
- Java 环境:确认
java -version能正常输出。neoj-community 社区版一般要求在 JDK 17 或 21 左右,具体看项目 release note。建议用 JDK,别用 JRE,因为有些基于 Java 的社区服务运行时可能需要额外的模块,JDK 开箱即用更省心。 - 管理权限:后面安装服务、改服务配置都需要管理员权限。普通用户权限注册服务,大概率会碰壁。
注意:如果系统里装了多个 Java 版本,最好在服务配置里显式绑定 JAVA_HOME 或者指定
java.exe的绝对路径,否则服务由系统账户启动时,读到的 PATH 环境变量可能和你手动开命令行时不一样,经常出现“命令行能跑、服务起不来”的诡异现象。
3.2 目录规划
这里我给一个通用目录规划,你可以按实际习惯调整,但核心思路是“程序、日志、配置分离”。
- 程序目录:
D:\apps\neoj-community\ - 日志目录:
D:\apps\neoj-community\logs\ - 数据目录:看项目需求,比较常见的社区版项目会把数据存在程序目录下的 data 子目录,或者是独立的数据库实例目录。
先把 jar 包放进去。假设你拿到的发布包名称是neoj-community-xxx.jar,文件名里带了版本号。我建议做一件事:复制一份不带版本号的文件名。原因是版本升级时,服务配置里的 jar 名不用变,只要新 jar 覆盖上去,重启服务就是新版本。省得每次改动 XML 里的文件名。
3.3 下载 WinSW 并准备 XML 配置模板
WinSW 的发布包在 GitHub 的winsw/winsw仓库里,下载的时候注意架构:x64 选WinSW-x64.exe,x86 选WinWS-x86.exe。下载下来后,把winsw.exe重命名为跟你的服务名一致,这是 WinSW 的一个约定:neoj-community.exe,然后放在和 jar 同一个目录下。
为什么要这样命名?因为 WinSW 在寻找配置文件时会优先找“自己名字同名的 .xml 文件”。如果你把 exe 改名成neoj-community.exe,配置文件就是neoj-community.xml。好处是:服务名、可执行文件名、配置文件名三者对得上,管理起来一眼就能找对,尤其目录里可能同时跑多个服务实例时,这种命名习惯能救命。
提示:下载后右键 exe 文件,在属性里设置“解除锁定”。从互联网下载的文件默认带 Zone.Identifier 数据流,有时候会在运行时导致奇怪的安全拦截。不解除锁定,后面安装服务的时候可能直接报“未知发布者”或者被安全软件拦掉。
4. 核心实操:用 WinSW 把 neoj-community 注册为 Windows 服务
4.1 编写 neoj-community.xml
这一步是整个操作的核心。我直接把一份可用的配置贴在下面,然后逐行解释关键参数:
<service> <id>neoj-community</id> <name>NeoJ Community Service</name> <description>NeoJ Community Edition Background Service</description> <executable>java</executable> <arguments>-Xms512m -Xmx1024m -jar "%BASE%\neoj-community.jar"</arguments> <workingdirectory>%BASE%</workingdirectory> <env name="JAVA_HOME" value="C:\Program Files\Java\jdk-17" /> <log mode="roll-by-size"> <sizeThreshold>10240</sizeThreshold> <keepFiles>8</keepFiles> </log> <onfailure action="restart" delay="10 sec" /> <onfailure action="restart" delay="20 sec" /> <onfailure action="restart" delay="30 sec" /> <onfailure action="none" /> <priority>Normal</priority> <stoptimeout>30 sec</stoptimeout> <stopexecutable>taskkill</stopexecutable> <stoparguments>/PID %pid% /T /F</stoparguments> </service>几个关键点逐一说明:
id:服务标识,这个要唯一。命令行里sc query会用到它。取个短一点的、无空格的名字,比如neoj-community,后面用起来方便。executable:指向 java 可执行文件。我这里写的是java,依赖系统 PATH。其实更稳妥的做法是写完整路径,比如C:\Program Files\Java\jdk-17\bin\java.exe。原因前面提过——系统环境变量和用户环境变量在不同启动场景下可能不一样。arguments:JVM 参数和 jar 启动参数。-Xms512m -Xmx1024m是初始堆和最大堆,根据你机器的内存来调。neoj-community 社区版我一般建议至少 512M 起步,如果数据量大、并发高就往 2G 以上调。"%BASE%\neoj-community.jar"这里注意,%BASE%是 WinSW 的内置变量,代表 exe 所在目录。这样就避免了在 XML 里写死 D 盘路径,整个目录挪位置也不用改配置。workingdirectory:工作目录。Java 程序很多会依赖当前工作目录读写相对路径文件。必须设成%BASE%,否则服务由系统账户启动时,默认工作目录是C:\Windows\System32,然后你会在那个目录里莫名其妙发现一堆日志文件。env:显式指定 JAVA_HOME 环境变量。有些 Java 程序启动时不但用 PATH,还会读 JAVA_HOME,比如依赖某些 JNI 库的场景。这里先把它写死,防患于未然。log:WinSW 的自带日志重定向功能。它会把 Java 进程的 stdout/stderr 重定向到日志文件,按 size 滚动。这里配置成每个文件 10MB,保留 8 份,避免日志无限膨胀把磁盘挤爆。onfailure:崩溃恢复策略。我配了三次重启,间隔分别是 10 秒、20 秒、30 秒。如果连续失败三次,就不再自动重启。防止服务启动就崩时进入无限重启死循环。stopexecutable/stoparguments:默认情况下 WinSW 停服务是向 Java 进程发 Ctrl+C 或者调用jvmkill,但有时候不够干净。用taskkill强制终止更符合 Windows 生态的习惯。/PID %pid%中的%pid%是 WinSW 提供的变量,代表被管理进程的 PID;/T表示连子进程一起杀,/F是强制终止。
4.2 安装服务的完整命令序列
配置写好了,接下来以管理员身份打开 PowerShell 或 CMD,切到程序目录执行:
cd D:\apps\neoj-community .\neoj-community.exe install按我之前的命名约定,neoj-community.exe就是下载来的 WinSW 重命名后的文件。install 命令干的事就是向 SCM 注册服务,服务名取的是 XML 里的<id>。
这时看一下输出,如果一切正常,会提示安装成功。然后再启动:
net start neoj-community服务应该会进入 RUNNING 状态。你可以用下面的命令查看状态:
sc query neoj-community如果输出里STATE那一行是RUNNING,说明服务已经在跑了。可以用浏览器访问 neoj-community 的默认端口,确认业务正常。
注意:不要把
install和start混为一谈。install只是注册服务,相当于买了一辆车上了牌照,还没有真正发动引擎。很多新手在这里容易踩坑,以为 install 之后服务就自动跑起来了,结果却没启动。另外,安装服务之后,默认的启动类型是Automatic(自动),也就是说以后系统开机时会自动拉起这个服务,不需要你手动再net start。
4.3 常用管理命令速查
服务装好之后,日常管理就是几行命令的事:
- 打开服务管理器:
services.msc,在里面找到 neoj-community,右键可以查看状态、停止、重启。这是最直观的方式。 - 命令行停服务:
net stop neoj-community - 命令行重启:
net stop neoj-community && net start neoj-community - 改配置后刷新服务:WinSW 的配置是启动时读取的,改了 XML 之后,需要重启服务才生效。不需要重新 install。
其实还有一招:sc stop neoj-community和sc start neoj-community,跟net stop/start效果类似。net命令提示信息更友好,sc命令在批处理脚本里更容易判断返回码,按自己习惯来。
5. 日志、环境变量和自启策略:服务化之后还要管好这几件事
5.1 日志切分和保留策略
服务跑起来的最大麻烦就是日志文件无限增长。尤其是 Java 进程,输出堆栈、GC日志、业务日志,一累积就是几个 G,把 C 盘挤爆是常有的事,一下子把整个机器搞挂,损失不可估量。
我在前面 XML 里已经配置了 WinSW 的重定向日志,按大小滚动。这种方式比单纯让 jar 往控制台输出然后重定向到单个文件强得多。要注意的是:这种日志滚动只在“你让 WinSW 捕获 stdout/stderr”时才有效。如果你的 neoj-community 项目本身配置了 logback 或 log4j 独立写文件,那 WinSW 的日志滚动只能管住没被框架接住的杂散输出。处理方式见下面的表格:
| 日志类型 | 处理方式 | 说明 |
|---|---|---|
| Java 里 logback/log4j 写出的业务日志 | 项目自己的配置管 | 改 logback.xml 设置滚动策略 |
| 启动时的系统输出、未捕获的 stdout 异常 | WinSW<log>管 | 配置 roll-by-size,保留若干份 |
| Windows 事件查看器里的服务状态日志 | 系统自动管 | 用于排查“服务为什么没起来” |
经验之谈:把 neoj-community 的日志目录单独建到一个磁盘空间充裕的位置,比如D:\neoj-logs\,然后在项目配置里改输出路径。相比之下,别把日志放在系统盘和程序目录,不然程序目录会被日志撑爆,连排查的空间都没有。
5.2 环境变量传递的本质
WinSW 在启动子进程时,默认会把系统环境变量和当前用户环境变量继承给 Java 进程,但这个行为跟启动账户有关。如果你在 XML 里没有特别指定用户,服务默认以LocalSystem账户运行,而它读取的环境变量和普通管理员用户用 PowerShell 看到的可能不同。
很多部署失败的问题,表面上是“Java 找不到类”、“数据库连接失败”,查到底其实是环境变量不对。所以我建议在 XML 里显式声明依赖的变量。除了前面写的 JAVA_HOME,如果 neoj-community 需要数据库连接字符串、Redis 地址、License 文件路径等,都可以用:
<env name="NEOJ_DB_URL" value="jdbc:mysql://127.0.0.1:3306/neoj" /> <env name="NEOJ_DATA_DIR" value="D:\neoj-data" />这样服务启动时不管你登录账户是什么,关键配置始终指向同一组值,可复现性极强。这也方便版本管理和多机部署——只要 XML 一致,服务行为就一致。
5.3 服务崩溃自动恢复和依赖关系
Windows 服务管理器在服务崩溃时不会主动重启服务,除非你在服务的“恢复”选项卡里配置。WinSW 的<onfailure>参数会在服务崩溃时触发重启操作。这一点尤其重要:直接跑命令行程序,进程崩了没有人知道;配置了 onfailure 之后,SCM 会在 10 秒内把 neoj-community 重新拉起来,实现无人值守。
如果你的 neoj-community 依赖数据库、Redis 这类外部服务,建议在 XML 里配置依赖关系:
<depend>MySQL</depend> <depend>Redis</depend>这样 Windows 在开机启动时会按照依赖顺序拉起服务,避免 neoj-community 先于数据库启动而导致启动失败。
注意:这里的服务依赖名对应服务管理器里显示的服务名称,不一定是 exe 文件名。比如 MySQL 的服务名可能是
MySQL84或者MYSQL,要先sc query看清楚。依赖配置不对,服务管理器会报“服务名无效”。
6. 踩过的坑:常见问题排查与实录
6.1 服务启动失败,但手动执行 java -jar 没问题
这个是最常见的现象。原因我在前面都埋了伏笔:工作目录不对、环境变量缺失、Java 版本不对。
排查思路从三条线来:
- 查看 eventvwr 里的 Windows 日志(应用程序日志),WinSW 会写详细的错误信息到日志里。
- 检查 WinSW 生成的状态文件(
.status结尾)和日志文件,日志往往直接告诉你启动命令是什么、返回码是什么。 - 对比手动运行环境和服务运行环境的差异:手动运行时的 PATH 和系统账户 PATH 是否一致、当前目录是否一致。
实操技巧:先把 XML 里的<arguments>简化到最简,比如-jar "%BASE%\neoj-community.jar",去掉 JVM 调优参数,排除参数写错导致 JVM 启动失败的可能。然后逐步加回参数,缩小问题范围。
6.2 服务启动后立刻退出,SCM 报 1053 错误
ERROR 1053: The service did not respond to the start or control request in a timely fashion.这个错误在 WinSW 部署场景里几乎都指向同一个问题:要么是 winSW 版本和 Windows 不兼容,要么是杀毒软件把 Java 子进程秒杀了,要么是 jar 包启动过程漫长的超过了 SCM 的等待时间。
应对措施:
- 确认使用的是 WinSW 最新版,避免用几年前的远古版本。
- 把服务账户临时换成管理员账户测试,看是否是权限拦截。
- 适当调大 XML 里的
stoptimeout和 SCM 里的“启动服务超时”时间。Windows 默认服务启动超时是 30 秒,如果你的 jar 启动特别慢,可以在注册表HKLM\SYSTEM\CurrentControlSet\Control下加一个ServicesPipeTimeout的 DWORD 值,设为 60000(毫秒),然后重启系统才会生效。
6.3 端口被占用导致服务反复重启
neoj-community 默认端口需要查阅项目文档,比较常见的这类服务是 8080 或 9000 系列。如果端口被其他进程占用了,Java 进程启动时会抛BindException: Address already in use,然后退出。WinSW 会根据 onfailure 策略不断拉起它,然后它又崩,看起来就是“服务一直处在启动/停止循环”。
处理办法:
netstat -ano | findstr :8080 tasklist | findstr 12345找到占用端口的进程,看是不是自己以前手动启动的 jar,如果是就直接停掉;如果是无关紧要的程序,就换个端口给 neoj-community,或者结束占用进程。注意结束进程之前确认它确实不用,免得误杀。
6.4 改了配置重启服务之后,服务没按新配置生效
这个坑我踩过不少次。WinSW 读取 XML 配置的时机是“服务启动时”。你改了 XML,但服务并没有真正重启——很多新手用services.msc右键“重新启动”其实也没问题,但如果遇到sc stop后立刻sc start,Windows 偶尔会认为 stop 还没完全结束,返回一个“服务尚未停止”的错误,导致新配置没生效。
最稳妥的做法:
net stop neoj-community tasklist | findstr /i neoj-community net start neoj-community确认中间产物进程完全消失再启动。甚至极端情况先sc query neoj-community查看STATE已经变成STOPPED,再执行net start。
6.5 常见报错与排查速查表
整理一份我在多次部署中反复遇到的典型问题,按出现频率排个序:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| 服务安装时报“拒绝访问” | 当前 Shell 不是管理员 | 用管理员身份重新打开 CMD/PowerShell |
| 服务启动后访问不到端口 | 防火墙未放行 | netsh advfirewall firewall add rule name="neoj-community" dir=in action=allow protocol=TCP localport=端口 |
日志里报Could not find or load main class | classpath 错误 / jar 路径包含空格未加引号 | 确认 arguments 里 jar 路径加引号,使用%BASE%引用 |
日志里报OutOfMemoryError | 堆内存参数太小 | 调大-Xmx,比如 2g 或 4g |
| 服务重启后数据丢失 | 数据目录配置为相对路径,工作目录不对 | 数据目录写绝对路径,比如D:\neoj-data |
| jar 更新后服务无变化 | 服务没真正重启 | 确认旧进程已退出,再重新启动服务 |
| Windows 自动重启后服务一直不亮 | 依赖服务没起来 | 配置<depend>,或者把服务启动类型设为自动(延迟) |
6.6 平滑升级 neoj-community 的经验
最后补充一个升级过程的小技巧。社区版项目更新很快,新版本往往修复安全漏洞和 bug。按照下面的顺序升级,基本不会出问题:
- 先备份原有 jar 和 data 目录。
- 停止服务
net stop neoj-community。 - 确认进程结束:
tasklist | findstr neoj-community无输出。 - 备份旧版本配置和日志(日志不用全备,建议留最近一两份就够了)。
- 替换新 jar。
- 启动服务
net start neoj-community。 - 打开页面或调用健康检查接口,确认服务正常。
升级时改配置文件的情况很少,但一旦 jar 主版本变动(比如从 1.x 升到 2.x),需要仔细阅读官方升级文档,看有没有配置项被废弃或数据库结构变更。过程中如果服务起不来,第一件事是把日志翻出来看,不要盲目重启——因为重启只会让 onfailure 策略一直把坏进程拉起来,反而让日志被刷掉。
我在实际操作中最大的体会是:只要把 XML 里的关键路径、环境变量、日志策略都配置得清清楚楚,服务化之后几乎一劳永逸,再也不用有事没事去看控制台窗口。最后再分享一个很多人会忽略的小技巧:把 neoj-community.xml 连同部署步骤写进项目仓库里的 deploy 目录,用 Git 管理起来,下次在新机器上部署只需要复制这几个文件、执行 install 命令,十分钟内搞定。