如何借助Apache Commons Daemon实现NanoHTTPD Windows服务跨进程启停?
解决NanoHTTPD服务通过procrun管理及外部进程停止的问题
看起来你需要搞定两个核心问题:一是让你的NanoHTTPD服务能被Apache Commons Daemon的procrun作为Windows服务管理,二是从独立Java进程触发服务停止。下面我一步步给你拆解解决方案:
1. 修改PrintServer类,集成Daemon接口并添加停止逻辑
首先,你的PrintServer需要实现org.apache.commons.daemon.Daemon接口——这是procrun管理服务生命周期的标准入口。同时我们要加线程安全的停止机制,以及一个用于外部触发停止的HTTP接口(用HTTP方式最贴合你的服务场景,当然你也可以选监听特定端口、文件标记等方式)。
修改后的完整代码如下:
import org.apache.commons.daemon.Daemon; import org.apache.commons.daemon.DaemonContext; import org.apache.commons.daemon.DaemonInitException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import fi.iki.elonen.NanoHTTPD; import fi.iki.elonen.NanoHTTPD.IHTTPSession; import fi.iki.elonen.NanoHTTPD.Response; import fi.iki.elonen.NanoHTTPD.Response.Status; public class PrintServer extends NanoHTTPD implements Daemon { private static final Logger log = LoggerFactory.getLogger(PrintServer.class); private static final int SERVER_PORT = 11100; private static final String MIMEJSON = "application/json"; // 自定义停止请求的验证密钥,防止非法调用 private static final String SHUTDOWN_KEY = "your_secure_random_key_here"; // 线程安全的停止标记,避免重复触发停止 private volatile boolean isShuttingDown = false; public PrintServer() throws IOException { super(SERVER_PORT); } @Override public void init(DaemonContext context) throws DaemonInitException { // 这里可以加初始化逻辑,比如加载配置文件,暂时留空也没问题 log.info("PrintServer daemon initialized"); } @Override public void start() throws Exception { start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); log.info("Cinebot PrintServer avviato e in ascolto su porta {}", SERVER_PORT); } @Override public void stop() throws Exception { shutdownServer(); log.info("PrintServer stopped via daemon command"); } @Override public void destroy() { // 清理资源,比如关闭数据库连接、释放文件句柄等 log.info("PrintServer daemon destroyed"); } private void shutdownServer() { if (!isShuttingDown) { isShuttingDown = true; super.stop(); } } @Override public Response serve(IHTTPSession session) { // 处理停止请求 if ("/shutdown".equals(session.getUri())) { String providedKey = session.getParms().get("key"); if (SHUTDOWN_KEY.equals(providedKey)) { // 异步执行停止逻辑,避免阻塞HTTP响应 new Thread(this::shutdownServer).start(); return newFixedLengthResponse(Status.OK, MIMEJSON, "{\"status\":\"shutdown initiated\"}"); } else { return newFixedLengthResponse(Status.UNAUTHORIZED, MIMEJSON, "{\"error\":\"invalid shutdown key\"}"); } } // 原有业务逻辑 return newFixedLengthResponse(Status.OK, MIMEJSON, null); } public static void main(String[] args) { try { PrintServer server = new PrintServer(); server.start(); } catch (IOException ioe) { log.error("Non posso avviare PrintServer", ioe); } } }
关键改动说明:
- 实现
Daemon接口:procrun会自动调用init()、start()、stop()、destroy()来管理服务的启停和销毁,替代了你原来在构造方法里直接启动的逻辑。 - 线程安全的停止标记:用
volatile修饰isShuttingDown,确保多线程环境下停止操作的正确性,避免重复停止。 - 安全的停止接口:新增
/shutdown接口,通过密钥验证后才触发停止,防止恶意访问;停止逻辑放在异步线程里,避免阻塞HTTP请求的响应。
2. 配置procrun安装Windows服务
procrun需要明确指定Daemon实现类、类路径、JVM参数等。假设你的项目打包成print-server.jar,所有依赖(commons-daemon.jar、nanohttpd.jar、slf4j相关包)都放在lib目录下,可以用以下命令安装服务:
# 安装名为PrintServer的Windows服务 prunsrv.exe //IS//PrintServer ^ --DisplayName="Cinebot Print Server" ^ --Description="HTTP Server for Cinebot Print Service" ^ --Classpath=print-server.jar;lib\* ^ --StartMode=jvm ^ --StartClass=com.yourpackage.PrintServer ^ --StartMethod=start ^ --StopMode=jvm ^ --StopClass=com.yourpackage.PrintServer ^ --StopMethod=stop ^ --JvmOptions="-Dfile.encoding=UTF-8" ^ --LogPath=.\logs ^ --StdOutput=auto ^ --StdError=auto
命令要点:
//IS//PrintServer:指定要安装的服务名称为PrintServer。--StartClass/--StopClass:填写你的PrintServer类的全限定名(比如com.yourpackage.PrintServer)。--StartMethod=start/--StopMethod=stop:对应Daemon接口的方法,procrun会自动调用这些方法完成启停。--Classpath:要包含你的项目jar和所有依赖jar,用分号分隔。
卸载服务的命令:
prunsrv.exe //DS//PrintServer
3. 从独立Java进程停止服务
你可以写一个简单的Java程序,通过发送HTTP请求到/shutdown接口来触发停止:
import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class StopPrintServer { public static void main(String[] args) { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:11100/shutdown?key=your_secure_random_key_here")) .GET() .build(); try { HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("Shutdown response: " + response.body()); if (response.statusCode() == 200) { System.out.println("PrintServer shutdown initiated successfully"); } else { System.err.println("Failed to shutdown PrintServer: " + response.body()); } } catch (IOException | InterruptedException e) { System.err.println("Error sending shutdown request: " + e.getMessage()); } } }
运行这个程序,只要密钥正确,就能触发PrintServer的停止逻辑,同时procrun会感知到服务停止,完成完整的服务停止流程。
注意事项
- 务必把
SHUTDOWN_KEY设置为安全的随机字符串,避免被恶意调用。 - 如果你的服务需要等待正在处理的请求完成后再停止,可以在
shutdownServer()方法里添加等待逻辑,比如遍历并等待所有工作线程结束。 - 确保
prunsrv.exe的位数(32/64位)和你的Java版本匹配,否则可能出现服务启动失败的问题。
内容的提问来源于stack exchange,提问作者Tobia
相关产品推荐
相关产品推荐

