如何通过Java/JavaFX实现Windows关机时阻止关机的提示功能
纯Java标准API无法实现系统级的关机阻止效果,网上流传的addShutdownHook方案只能在关机流程触发后拿到几秒钟的清理时间,根本拦不住关机流程,也弹不出你说的「Application x prevents shutdown」系统提示。要实现这个效果必须对接Windows原生的关机拦截机制,目前最通用的方案是用JNA直接调用Windows User32.dll的官方API,不需要写C++原生代码,打出来的jar或者exe都能正常生效。
核心实现逻辑
Windows Vista及之后版本(包括Win10/Win11)的关机拦截规则很明确:
- 程序必须有一个可见的顶层窗口(JavaFX正常显示的Stage完全满足这个要求)
- 调用
ShutdownBlockReasonCreate接口向系统注册阻止关机的原因文本 - 响应系统发送的
WM_QUERYENDSESSION窗口消息,需要拦截时返回拒绝响应,系统就会自动弹出包含你注册的原因文本的阻止关机提示页,用户可以选择强制关机或者取消关机返回系统 - 不需要拦截的时候调用
ShutdownBlockReasonDestroy接口解除注册,系统关机就不会再被你的程序阻断
具体编码步骤
1. 引入JNA依赖
用Maven/Gradle引入JNA和JNA平台库,官方已经封装好了所有Windows API的绑定,不需要自己写原生声明:
<!-- Maven依赖示例 --> <dependencies> <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.13.0</version> </dependency> <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna-platform</artifactId> <version>5.13.0</version> </dependency> </dependencies>
如果是Java 9+模块化项目,记得在module-info.java中开放JNA需要的反射权限,打包时加对应JVM参数即可。
2. 获取JavaFX窗口的原生句柄
所有Windows API都需要拿到窗口的HWND句柄才能操作,必须在stage.show()执行之后再获取,否则句柄为空会导致调用失败:
private static HWND getStageHwnd(Stage stage) { // Java 8 可直接用impl_getPeer获取 long peerHandle = ((com.sun.javafx.tk.TKStage) stage.impl_getPeer()).getRawHandle(); // Java 11+高版本如果impl_getPeer不可用,替换为下面的反射获取逻辑 // try { // Method getPeerMethod = Window.class.getDeclaredMethod("getPeer"); // getPeerMethod.setAccessible(true); // Object tkStage = getPeerMethod.invoke(stage); // Method getHandleMethod = tkStage.getClass().getDeclaredMethod("getRawHandle"); // getHandleMethod.setAccessible(true); // long peerHandle = (long) getHandleMethod.invoke(tkStage); // } catch (Exception e) { // throw new RuntimeException("获取窗口句柄失败", e); // } return new HWND(new Pointer(peerHandle)); }
高版本JavaFX打包如果报模块访问错误,给JVM加启动参数--add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED即可。
3. 注册关机拦截回调
在你需要阻止关机的场景(比如用户有未保存的文档、正在进行不能中断的文件导出等),先注册阻止原因,再挂载窗口消息处理回调:
// 拿到主窗口句柄 HWND mainHwnd = getStageHwnd(primaryStage); // 注册阻止原因,这个文本会显示在系统的阻止关机提示页 String blockReason = "当前有未保存的编辑内容,关机将导致数据丢失"; if (!User32.INSTANCE.ShutdownBlockReasonCreate(mainHwnd, blockReason.toCharArray())) { // 注册失败一般是句柄无效、窗口被最小化到托盘无可见界面导致 System.err.println("注册关机阻止失败,错误码:" + Native.getLastError()); } // 挂载窗口消息回调,响应系统的关机询问 User32.INSTANCE.SetWindowLongPtr(mainHwnd, User32.GWL_WNDPROC, new WindowProc() { @Override public LRESULT callback(HWND hWnd, int msg, WPARAM wParam, LPARAM lParam) { // 收到系统关机询问消息 if (msg == WinUser.WM_QUERYENDSESSION) { // 自定义判断逻辑:有未保存内容就拦截,否则放行 if (checkHasUnsavedWork()) { // 返回0表示拒绝关机,触发系统阻止提示 return new LRESULT(0); } // 放行关机,提前销毁阻止标记 User32.INSTANCE.ShutdownBlockReasonDestroy(mainHwnd); return new LRESULT(1); } // 其他消息交给系统默认处理 return User32.INSTANCE.DefWindowProc(hWnd, msg, wParam, lParam); } });
当用户完成保存、中断的任务执行结束后,主动调用User32.INSTANCE.ShutdownBlockReasonDestroy(mainHwnd)解除拦截即可,避免无意义阻断用户关机。
避坑提示
- 不要无理由全程拦截关机,按照微软的设计规范,只有存在用户未保存的状态数据时才允许调用该接口,否则会被系统安全组件判定为恶意程序。
- 如果程序最小化到系统托盘,要保证有一个隐藏的顶层窗口存在,否则API调用会失效。
- 不要把程序注册为Windows服务,服务运行在会话0隔离层,没有交互桌面窗口,无法弹出用户可见的阻止提示。
- 用jpackage、exe4j等工具打exe包时不需要额外配置,只要JNA的依赖正常打包进去就能生效。
内容的提问来源于stack exchange,提问作者yck

