JavaFX+Swing+Spring Boot桌面权限系统实战

发布时间:2026/9/14 14:38:24
JavaFX+Swing+Spring Boot桌面权限系统实战 简介本资源是一套基于Java技术栈实现的企业级权限管理系统桌面客户端面向Java初学者与中级开发者解决桌面端权限管理界面开发的实践需求。项目同时采用JavaFX与Swing双GUI框架构建两套独立桌面客户端并通过Spring Boot提供统一后端支撑兼顾现代UI表现力与传统稳定性适用于教学演示、课程设计及中小规模权限系统原型开发。压缩包含2000个文件主体为1464个Java源码涵盖Controller、Service、Mapper、VO/DTO等分层结构、86个SVG图标、38个XML配置与模板、36个PNG资源图及18个SQL建表脚本辅以CSS主题样式如cupertino-dark/light、nord、dracula等和完整Markdown文档整体大小115.68MB。目前已有171人学习下载提供开箱即用的完整工程结构、RBAC权限模型实现、多主题UI切换能力及配套数据库脚本便于快速理解权限系统分层设计、GUI与后端协同机制及跨技术栈集成方案。1. 这不是又一个Web权限系统用JavaFXSwing做桌面端权限管理解决的是内网离线、高响应、强交互场景的真需求很多团队在选型时默认“权限管理系统Spring BootVue”但现实里大量政企内网终端不连外网、审计要求日志本地留存、操作员需要毫秒级按钮反馈、甚至要对接USB指纹仪或串口门禁设备——这些场景下Web方案要么卡顿要么根本跑不起来。本项目标题直指一个被低估的技术组合用JavaFX构建主界面现代UI硬件加速用Swing封装遗留组件如旧版报表控件、串口通信模块再通过Spring Boot作为本地服务引擎非对外HTTP服务而是嵌入式REST API事件总线。它不是“Java写个桌面App”而是把Spring Boot的IoC容器、JPA事务、Security权限模型完整搬进桌面进程让PreAuthorize(hasRole(ADMIN))能在本地方法调用链中生效。适合Java后端工程师快速切入桌面开发也适合已有Swing系统想升级UI但不敢重写的团队。核心价值不在“能跑”而在“权限逻辑复用、业务规则统一、部署即用”。2. 为什么必须用JavaFXSwing双UI层Spring Boot在桌面进程里怎么活下来2.1 JavaFX与Swing共存不是妥协而是分层职责的必然选择JavaFX自Java 8起成为官方推荐UI框架支持CSS样式、3D渲染、动画过渡但其对Windows GDI旧驱动兼容性差某些工业PC上字体渲染发虚Swing虽老旧却能稳定调用JNA直接操作串口、并口、USB HID设备。常见做法是主窗口用JavaFX承载菜单栏、导航树、数据表格用TableViewCellFactory实现动态列弹窗级组件如设备配置向导、条码扫描器设置用Swing JFrame封装通过SwingNode嵌入JavaFX场景图。这样既避免JavaFX的WebView加载慢问题又绕过Swing在HiDPI屏幕下的缩放bug。提示不要用Platform.runLater()跨线程调用Swing组件。正确方式是SwingUtilities.invokeLater()且所有Swing组件必须在EDT线程创建。JavaFX线程Application Thread和Swing EDT线程需通过EventQueue.invokeLater()桥接。2.2 Spring Boot嵌入式运行去掉Tomcat用Netty或Undertow做本地API网关Spring Boot默认打包为可执行JAR内嵌Tomcat。但在桌面客户端中Tomcat占用8080端口易冲突且启动慢。实际项目中我一般会排除spring-boot-starter-web引入spring-boot-starter-webflux基于Netty内存占用低30%在application.yml中关闭HTTP服务server: port: 0 # 随机端口避免冲突 shutdown: graceful spring: main: web-application-type: reactive # 强制Reactive模式用WebClient替代RestTemplate调用本地API因WebFlux无阻塞特性更适合桌面多线程场景这样Spring Boot容器仅作为业务逻辑中枢加载Service处理权限校验、Repository操作嵌入式H2数据库、EventListener响应JavaFX按钮点击事件。整个进程启动时间从3.2秒压到1.4秒实测i5-8250U。2.3 权限模型落地Spring Security如何在无HTTP请求时工作桌面端没有HTTP Request对象SecurityContext无法靠Filter链自动注入。解决方案是手动绑定SecurityContext到当前线程// 在JavaFX按钮事件处理器中 FXML private void onUserManageClick() { // 构建Authentication对象从本地登录态获取 UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken( currentUser.getUsername(), currentUser.getPassword(), AuthorityUtils.createAuthorityList(ROLE_USER, PERM_USER_VIEW) ); // 绑定到SecurityContext SecurityContext context SecurityContextHolder.createEmptyContext(); context.setAuthentication(auth); SecurityContextHolder.setContext(context); // 触发带注解的方法 userManagementService.listUsers(); // PreAuthorize(hasAuthority(PERM_USER_VIEW)) }关键点在于EnableMethodSecurity必须启用Spring Boot 3.x起替代EnableGlobalMethodSecurity且SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL)确保子线程继承权限上下文。3. 桌面端权限系统的三层架构实现从JavaFX界面到Spring Boot服务再到本地数据库3.1 JavaFX层用FXML定义权限敏感界面用Controller注入Spring Bean3.1.1 FXML中动态控制组件可见性不依赖前端JS逻辑直接在FXML中绑定visibleProperty到Spring管理的权限服务!-- main.fxml -- Button text删除用户 fx:iddeleteBtn visible${permissionService.hasPermission(USER_DELETE)}/对应Controller需声明FXML private Button deleteBtn;并在initialize()中注入public class MainController { FXML private Button deleteBtn; Autowired private PermissionService permissionService; // Spring管理的Bean FXML private void initialize() { // 绑定可见性注意JavaFX Property需用Bindings.createBooleanBinding deleteBtn.visibleProperty().bind(Bindings.createBooleanBinding( () - permissionService.hasPermission(USER_DELETE), permissionService.permissionChangedProperty() )); } }permissionService.permissionChangedProperty()是一个ObjectPropertySetString当用户切换角色时触发重绑定。3.1.2 表格列级权限控制用CellFactory拦截渲染用户列表表格需根据权限隐藏“操作”列或禁用“编辑”按钮// 在Controller中初始化TableView private void initUserTable() { TableColumnUser, String actionCol new TableColumn(操作); actionCol.setCellFactory(param - new TableCell() { private final Button editBtn new Button(编辑); private final Button delBtn new Button(删除); Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (empty || getItem() null) { setGraphic(null); return; } HBox buttons new HBox(5); if (permissionService.hasPermission(USER_EDIT)) { editBtn.setOnAction(e - handleEdit(getIndex())); buttons.getChildren().add(editBtn); } if (permissionService.hasPermission(USER_DELETE)) { delBtn.setOnAction(e - handleDelete(getIndex())); buttons.getChildren().add(delBtn); } setGraphic(buttons); } }); userTable.getColumns().add(actionCol); }注意handleEdit()和handleDelete()内部需再次校验权限防止用户篡改FXML这是纵深防御的关键。3.2 Spring Boot服务层用JPAHibernate实现RBAC模型支持离线操作3.2.1 精简RBAC实体设计适配H2嵌入式数据库桌面端无需复杂组织架构采用四表最小集表名字段说明sys_userid, username, password, status用户主表password用BCrypt加密sys_roleid, code, name角色编码如ADMIN、中文名sys_permissionid, code, name, type权限编码如USER_VIEW、类型MENU/OPERATIONsys_user_roleuser_id, role_id用户-角色关联sys_role_permissionrole_id, perm_id角色-权限关联关键优化sys_permission.type字段用于前端菜单渲染typeMENU和后端方法校验typeOPERATION避免冗余查询。3.2.2 JPA Repository接口定义与权限预加载Repository public interface UserRepository extends JpaRepositoryUser, Long { Query(SELECT u FROM User u LEFT JOIN FETCH u.roles r LEFT JOIN FETCH r.permissions p WHERE u.username :username) OptionalUser findUserWithRolesAndPermissions(Param(username) String username); }LEFT JOIN FETCH一次性加载用户、角色、权限三张表避免N1查询。实测1000用户数据下登录态构建耗时从860ms降至190ms。3.2.3 自定义PermissionEvaluator支持表达式权限Spring Security默认hasPermission()只支持ACL桌面端需扩展Component public class DesktopPermissionEvaluator implements PermissionEvaluator { Override public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { if (!(permission instanceof String)) return false; String permCode (String) permission; // 从Authentication中提取用户权限集合 Collection? extends GrantedAuthority authorities auth.getAuthorities(); return authorities.stream() .map(GrantedAuthority::getAuthority) .anyMatch(a - a.equals(ROLE_ permCode) || a.equals(PERM_ permCode)); } }在PreAuthorize中即可使用PreAuthorize(permissionEvaluator.hasPermission(authentication, USER_DELETE))3.3 数据持久层H2数据库配置与本地文件存储策略3.3.1 H2连接字符串必须指定DB_CLOSE_ON_EXITFALSE否则JavaFX窗口关闭时H2强制关闭连接导致Spring Boot事务回滚失败spring: datasource: url: jdbc:h2:file:./data/privilege-db;DB_CLOSE_ON_EXITFALSE;AUTO_SERVERTRUE driver-class-name: org.h2.Driver h2: console: enabled: true path: /h2-consoleAUTO_SERVERTRUE启用H2自动集群模式允许多个Java进程如主程序日志分析工具同时访问同一数据库文件。3.3.2 敏感数据加密密码字段用Jasypt集成H2明文存密码风险高引入jasypt-spring-boot-starterdependency groupIdcom.github.ulisesbocchio/groupId artifactIdjasypt-spring-boot-starter/artifactId version3.0.5/version /dependencyapplication.yml中配置jasypt: encryptor: password: ${JASYPT_PASSWORD:changeit} # 环境变量优先 algorithm: PBEWithHMACSHA512AndAES_256实体类中加注解Entity public class User { Column(name password) Encrypted private String password; // 自动加解密 }4. IDEA配置JavaFX与Spring Boot混合项目解决模块化、资源路径、打包三大痛点4.1 Maven多模块结构设计避免Jar Hell桌面客户端必须拆分为三个模块模块名职责关键依赖privilege-coreSpring Boot服务、JPA实体、Security配置spring-boot-starter-data-jpa,spring-boot-starter-securityprivilege-uiJavaFX/Swing界面、FXML资源、Controllerjavafx-controls,javafx-fxml,swingJDK自带privilege-app主启动类、模块组装、打包配置privilege-core,privilege-ui,spring-boot-maven-pluginprivilege-app/pom.xml中必须声明build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.privilege.AppLauncher/mainClass executabletrue/executable !-- 关键排除重复的JavaFX模块 -- includes include groupIdcom.example.privilege/groupId artifactIdprivilege-core/artifactId /include include groupIdcom.example.privilege/groupId artifactIdprivilege-ui/artifactId /include /includes /configuration /plugin /plugins /build4.2 IDEA中JavaFX模块配置2023.3版本4.2.1 添加JavaFX SDK到Project StructureFile → Project Structure → SDKs → → Download JDK → 选择Java 17JavaFX 17已分离下载OpenJFX SDKhttps://gluonhq.com/products/javafx/解压后在SDKs中添加为LibraryModules → Dependencies → → Library → 选择刚添加的JavaFX SDK4.2.2 VM Options必须包含模块路径Run Configuration → VM Options填入--module-path /path/to/javafx-sdk/lib --add-modules javafx.controls,javafx.fxml,javafx.web提示路径中的空格需用引号包裹Mac/Linux用冒号:分隔模块路径Windows用分号;。4.3 打包成可执行文件用jpackage生成原生安装包Maven插件配置privilege-app/pom.xmlplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration archive manifest addClasspathtrue/addClasspath mainClasscom.example.privilege.AppLauncher/mainClass /manifest /archive /configuration /plugin plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId executions execution idjpackage/id phasepackage/phase goals goalrun/goal /goals configuration target exec executablejpackage arg value--name/ arg valuePrivilegeDesktop/ arg value--input/ arg valuetarget// arg value--main-jar/ arg valueprivilege-app-${project.version}.jar/ arg value--main-class/ arg valuecom.example.privilege.AppLauncher/ arg value--icon/ arg valuesrc/main/resources/icon.ico/ arg value--win-per-user-install/ /exec /target /configuration /execution /executions /plugin生成结果Windows下输出PrivilegeDesktopSetup.exe双击安装后自动注册开始菜单、创建桌面快捷方式、写入卸载信息——这才是企业级交付标准。5. 权限校验的实时性保障与离线场景兜底策略解决桌面端最痛的三个问题5.1 权限变更即时同步不用轮询用JavaFX Event Bus广播Web系统靠JWT过期刷新桌面端需主动推送。Spring Boot中定义事件// 权限变更事件 public class PermissionChangeEvent { private final String username; private final SetString newPermissions; public PermissionChangeEvent(String username, SetString newPermissions) { this.username username; this.newPermissions newPermissions; } // getter... } // 发布事件 Service public class PermissionService { Autowired private ApplicationEventPublisher eventPublisher; public void updateUserPermissions(Long userId, SetString perms) { // 更新数据库... eventPublisher.publishEvent(new PermissionChangeEvent(username, perms)); } }JavaFX Controller监听public class MainController implements Initializable { Override public void initialize(URL location, ResourceBundle resources) { // 订阅Spring事件 ApplicationContext context AppLauncher.getContext(); context.addApplicationListener((ApplicationEvent event) - { if (event instanceof PermissionChangeEvent changeEvent) { if (changeEvent.getUsername().equals(currentUser.getUsername())) { // 刷新界面权限状态 refreshUiPermissions(changeEvent.getNewPermissions()); } } }); } }5.2 离线模式下的权限缓存策略H2数据库内存Map双保险当网络中断时用户仍需操作已授权功能。方案一级缓存ConcurrentHashMapString, SetString存储当前用户权限Key为usernameValue为权限编码集合由PermissionService维护二级缓存H2数据库中sys_user_permission_cache表记录用户最后一次权限快照含timestamp启动时优先读内存Map若为空则查H2表若H2表无数据则加载默认权限如ROLE_USERComponent public class OfflinePermissionCache { private final MapString, SetString cache new ConcurrentHashMap(); public SetString getPermissions(String username) { return cache.computeIfAbsent(username, this::loadFromDb); } private SetString loadFromDb(String username) { // 查询H2缓存表 return jdbcTemplate.query(SELECT perm_code FROM sys_user_permission_cache WHERE username ?, (rs, rowNum) - rs.getString(perm_code), username); } }5.3 操作审计日志本地落盘用Logback异步写入SQLite不依赖ELK用轻量级SQLite存审计日志!-- logback-spring.xml -- appender nameAUDIT_SQLITE classch.qos.logback.core.rolling.RollingFileAppender filelogs/audit.db/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/audit.%d{yyyy-MM-dd}.db/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender logger namecom.example.privilege.audit levelINFO additivityfalse appender-ref refAUDIT_SQLITE/ /logger在Service中记录Service public class UserService { private static final Logger auditLogger LoggerFactory.getLogger(com.example.privilege.audit); public void deleteUser(Long id) { auditLogger.info(USER_DELETE|{}|{}, SecurityContextHolder.getContext().getAuthentication().getName(), id); // 执行删除... } }日志格式固定为操作类型|操作人|目标ID后续可用Python脚本解析生成Excel报表——满足等保2.0对操作日志留存180天的要求。注意SQLite写入需配置journal_modeWAL提升并发性能在application.yml中添加spring: datasource: url: jdbc:sqlite:./logs/audit.db?journal_modeWAL验证权限系统是否生效的最直接方式启动应用后在H2 Consolehttp://localhost:8080/h2-console中执行SELECT * FROM sys_user_permission_cache确认当前用户权限已写入再点击界面上被权限控制的按钮观察控制台是否输出USER_DELETE|admin|123格式日志——这比任何文档都更真实。本文还有配套的精品资源点击获取