在 Windows 上使用 WinSW 将可执行程序包装为服务时,XML 配置中的环境变量引用是最容易出问题的环节之一。相当一部分 Java 开发者会直接写 %JAVA_HOME% 这样的值,但服务启动时却报找不到路径。这个现象背后不是 WinSW 不支持变量,而是服务运行账户与交互式用户的环境变量存在差异,同时 WinSW 对 XML 字段中变量的解析时机也有明确顺序。理解这两点,就能避免大部分配置错误。

一、WinSW 环境变量的解析机制与账户差异
WinSW 由服务控制管理器启动,它先读取 XML 配置文件,再根据配置创建子进程。在创建子进程之前,WinSW 会准备一份完整的环境变量块,这份环境块由两部分组成:一部分来自 Windows 服务控制管理器当前账户的环境变量,另一部分来自 XML 中 <env> 标签定义的值。对于 executable、arguments、workingdirectory、logpath 等字段,WinSW 会使用这份合并后的环境块来解析其中包含的百分号变量。
这意味着如果在 XML 字段中直接使用 %JAVA_HOME%,WinSW 会先去当前服务进程的环境变量里查找 JAVA_HOME。如果服务被注册为以 LocalSystem 账户运行,那么这个账户默认只能看到系统级环境变量,看不到当前登录用户的用户环境变量。不少开发者把 JAVA_HOME 设置为用户变量,而不是系统变量,结果服务启动时 %JAVA_HOME% 解析为空字符串,导致路径变成 \bin\java.exe,自然找不到文件。解决方法是把变量改为系统变量,或者在 WinSW 配置中通过 <env> 显式注入该变量。
另外,服务账户的环境变量在服务启动时是固定的,不会实时跟随控制面板中的修改。如果修改了系统环境变量,需要重启服务或重新启动系统后才能生效。对于需要长期稳定运行的服务,建议把关键路径直接写成绝对路径,或者在 XML 中静态声明,避免依赖外部环境变化。
二、在 executable、arguments、logpath 中正确引用变量
在 executable 字段中引用环境变量时,常见的写法是直接用百分号包裹变量名,并在后面接上子目录和可执行文件名。例如,假设 Java 安装在 C:\Program Files\Java\jdk-17,可以写成:
<executable>%JAVA_HOME%\bin\java.exe</executable>
这里要特别注意反斜杠不能省略,也不能替换为斜杠。Windows 服务启动子进程时使用 CreateProcess,如果路径中有斜杠,虽然多数情况下能工作,但在服务环境里可能因为路径解析规则不同而失败。保持完整的 C:\Program Files\Java\jdk-17\bin\java.exe 这种形式最稳妥。如果 JAVA_HOME 未被定义,这个字段会变成 \bin\java.exe,服务会记录错误 2 或 3,表示找不到文件。
在 arguments 字段中使用变量时,需要把整个参数字符串放在标签之间,而不是用属性。如果参数中包含空格,建议使用双引号包裹,这在 XML 中可以直接书写,不会影响解析。例如:
<arguments>-jar "%APP_HOME%\app.jar" --server.port=8080</arguments>
注意这里反斜杠同样原样保留。如果路径中包含 & 字符,必须转义为 &,否则 XML 解析会失败。百分号不参与 XML 转义,因此 %VAR% 可以安全使用。另外,在 arguments 中如果需要传入 < 或 > 符号,也要转义为 < 和 >,否则 XML 会认为标签未闭合。
对于 logpath 字段,WinSW 内置了一个 %BASE% 变量,它指向 XML 配置文件所在的目录。这个变量在服务运行期间始终有效,特别适合把日志输出到配置目录下。例如:
<logpath>%BASE%\logs</logpath>
如果你使用 %TEMP% 或 %USERPROFILE%,要注意 LocalSystem 账户下这些变量指向的位置与当前用户不同。%TEMP% 通常指向 C:\Windows\Temp,而 %USERPROFILE% 则可能是 C:\Windows\System32\config\systemprofile。如果你希望日志保存在自己的应用目录,建议使用 %BASE% 或绝对路径。
三、使用 env 标签显式定义服务专用变量
<env> 标签是 WinSW 提供的最直接的环境变量定义方式。它可以在 XML 中声明任意数量的变量,这些变量会被注入到子进程的环境块中,并且也能被前面提到的 executable、arguments 等字段引用。推荐把服务依赖的关键路径都通过 <env> 定义,而不是依赖外部系统变量,这样服务的可移植性更强。
下面是一个完整的 Java 服务配置示例,通过 <env> 定义 JAVA_HOME 和 APP_HOME,然后在执行文件和参数中引用:
<service> <id>myapp</id> <name>My Application</name> <description>My Java service</description> <env name="JAVA_HOME" value="C:\Program Files\Java\jdk-17"/> <env name="APP_HOME" value="C:\myapp"/> <executable>%JAVA_HOME%\bin\java.exe</executable> <arguments>-jar "%APP_HOME%\app.jar"</arguments> <logpath>%APP_HOME%\logs</logpath> </service>
注意这里 <env> 的 value 属性使用双引号包裹,里面的反斜杠保持原样。XML 属性值中的双引号不会影响外部标签,因为属性值由双引号界定,内部没有额外的双引号即可。如果路径中包含 & 字符,仍然要写 &。
多个 <env> 标签定义的变量不能互相引用。也就是说,你不能在 APP_HOME 的值中写 %JAVA_HOME%\app,因为 WinSW 在处理 <env> 列表时是按顺序逐一赋值的,但不会做二次展开。如果需要组合路径,请直接写成完整的绝对路径。
此外,<env> 定义的变量优先级高于系统已有的同名变量,但仅对子进程有效,不会修改 Windows 系统环境。如果有多个 <env> 同名,后出现的会覆盖先出现的,但最好避免重复定义。
四、常见错误与调试方法
环境变量引用失败最常见的一个表现是服务启动后立即停止,错误提示为“服务没有及时响应启动或控制请求”。这通常是因为 executable 路径解析失败。可以先打开 Windows 事件查看器,在“Windows 日志”下的“应用程序”中查看来源为 Service Control Manager 的记录,里面会给出具体的错误代码和描述。
另一个常见错误是在 arguments 中忘记加引号。如果路径中有空格,例如 C:\Program Files\myapp\app.jar,不加引号会导致命令行被拆成多个参数,程序接收到错误数据。务必用双引号把整个路径包起来。下面是对比:
<!-- 错误写法:路径含空格时未加引号 --> <arguments>-jar C:\Program Files\myapp\app.jar</arguments> <!-- 正确写法:用双引号包裹 --> <arguments>-jar "C:\Program Files\myapp\app.jar"</arguments>
注释中的双破折号在 XML 注释里是合法的,这里只是示意,实际配置中不要复制注释。注意反斜杠仍然保留。
如果想确认服务进程实际能读取到哪些环境变量,可以临时把服务的可执行文件改为 C:\Windows\System32\cmd.exe,然后把参数写成 /c set > C:\myapp\env_dump.txt。重新启动服务后,打开 C:\myapp\env_dump.txt 就能看到完整的变量列表。这个方法在排查 LocalSystem 账户下哪些变量缺失时非常有效。
<executable>C:\Windows\System32\cmd.exe</executable> <arguments>/c set > C:\myapp\env_dump.txt</arguments>
最后再强调一遍:凡是 Windows 路径,都要写成完整的反斜杠形式。无论是 C:\Windows\System32 还是 C:\Program Files\Java\jdk-17\bin,反斜杠一个都不能少,盘符后面必须紧跟反斜杠。把路径中的反斜杠改成斜杠或直接省略,都会导致服务无法正确定位文件。