首页 / 资讯中心 / 文章详情

JavaFX整合SSM架构解析:桌面客户端与Spring后端分离应用

JavaFX整合SSM架构解析:桌面客户端与Spring后端分离应用 ★ FEATURED ARTICLE
简介面向企业级桌面应用开发者的完整 JavaFX SSMSpringSpringMVCMybatisERP 项目源码包演示了如何用 JavaFX 构建大气美观的前端界面并整合 Spring 依赖注入、SpringMVC 请求调度与 Mybatis 持久层完成从用户交互到后台数据的完整闭环。包内共 2 个文件一个 7z 工程压缩包内含源码、Fxml 界面布局和 CSS 样式配置与一个 sanlu.sql 数据库脚本整体约 75.26MB导入数据库后即可搭建可运行环境体验前后端联调整体业务流程。项目覆盖 JavaFX 双向数据绑定、TableView 表格数据展示与编辑、表单组件与后端实体类映射等关键实现同时展示 ERP 场景下员工、订单等数据的管理方式非常适合正在学习 JavaFX 与 SSM 整合、或需要企业级管理系统参考项目的读者深度研读。目前已有 3415 人学习下载是理解桌面 GUI 与企业级后端协作的实用范例。1. javaFx 整合 SSM这套项目到底能打通什么我第一次打开这个 javaFx 项目时第一反应是“桌面客户端加 SSM 后端这不就是把网页后台搬到了桌面窗口里吗”。跑通之后才发现它的设计比想象中完整JavaFX 负责全部界面交互Spring SpringMVC MyBatis 负责业务和持久化两边通过 HTTP 接口通信登录、菜单、增删改查都是现成代码适合直接拿来当脚手架。对想同时学桌面客户端和服务端开发的 Java 新人或者需要快速交付一个内部管理工具的开发团队来说这套代码的价值在于“前后端分离但又不完全分离”——界面层和服务端各管一摊边界清楚改起来不打架。2. 架构先看清JavaFX 客户端与 SSM 服务端如何分工2.1 两层架构JavaFX 客户端不直连数据库这个项目用的是非常规整的两层架构JavaFX 客户端负责渲染界面和收集用户输入SSM 服务端负责处理业务逻辑和数据持久化。客户端层不直接操作数据库所有数据交互都通过 HTTP 接口完成这样设计的好处是数据库连接串、账号密码只出现在服务端配置里不会被拖到每台客户端机器上。前端工程结构大致长这样我把实际项目里的目录整理了一份javafx-client/ ├── src/main/java/com/demo/client/ │ ├── MainApp.java # JavaFX 启动入口 │ ├── controller/ # FXML 对应的控制器 │ │ ├── LoginController.java │ │ └── MainController.java │ ├── model/ # DTO和后端返回的 JSON 对应 │ │ ├── User.java │ │ └── ApiResponse.java │ ├── service/ # 远程服务调用层 │ │ └── RemoteService.java │ └── util/ # HTTP、JSON、会话工具 │ ├── HttpUtil.java │ ├── JsonUtil.java │ └── SessionManager.java ├── src/main/resources/ │ ├── fxml/ │ │ ├── LoginView.fxml │ │ └── MainView.fxml │ └── css/ │ └── style.css后端 SSM 服务端是标准的三层结构controller 层接收请求service 层写业务mapper 层做持久化ssm-server/src/main/java/com/demo/server/ ├── controller/ │ ├── LoginController.java │ └── UserController.java ├── service/ │ ├── UserService.java │ └── impl/UserServiceImpl.java ├── mapper/ │ └── UserMapper.java └── entity/ └── User.javaJavaFX 这边的 controller 和后端 controller 名字一样但职责完全不同前者处理按钮点击、文本框取值、界面跳转后者接收 HTTP 请求、调用 service、返回 JSON。初次看代码的人容易在这绕晕我的经验是“看到 FXML 注解就是界面层看到 RestController 或 Controller 就是服务端”。2.2 接口协议统一 JSON 返回结构定边界既然是两层架构客户端与服务端之间就得有一套稳定的接口协议。这个项目用的是 JSON 格式而且是统一包装的返回结构前后端所有接口共用同一个返回模型public class ApiResponse { private int code; // 0 成功非 0 失败 private String message; private Object data; public static ApiResponse success(Object data) { ApiResponse resp new ApiResponse(); resp.code 0; resp.message success; resp.data data; return resp; } public static ApiResponse error(String message) { ApiResponse resp new ApiResponse(); resp.code 500; resp.message message; return resp; } // getter / setter 省略 }代码逻辑很简单但它的价值在于定死了通信格式。客户端拿到返回结果后先判断 code 是否为 0再决定是读 data 还是弹错误提示不用每个接口单独处理异常结构。我在拆这个项目时还发现一个细节登录接口返回的 data 里除了用户信息还带了一个 token 字段客户端用 SessionManager 保存下来之后的每次请求都会在 HTTP Header 里带上它这就是简版的登录态控制。接口路径也做了统一约定服务端所有接口挂在/api前缀下客户端在配置里写死服务端地址。这种设计在后端接口数量增多时优势非常明显接口管理、权限拦截都有统一入口。2.3 技术选型与版本搭配技术栈选择是这个项目最值得说的一点。JavaFX 和 SSM 属于两个技术代差比较大的生态项目把它们拼在一起是出于实际场景考虑SSM 负责稳定地做数据管理和接口输出JavaFX 负责在 Windows 桌面环境提供界面不需要浏览器也不用考虑跨域问题。版本搭配上我拆的这个项目用的是 JDK 8 JavaFX 8 Spring 4.x SpringMVC MyBatis 3.x MySQL 5.7。这个组合比较稳网上资料也最多。如果你的环境是 JDK 11 及以上JavaFX 已经从 JDK 里拆出来了需要额外引入 OpenJFX 依赖我会在后面的常见问题部分展开说。MyBatis 版本 3.x 通用性很好3.5 之后的版本对 JDK 8 和 JDK 11 都能兼容。客户端 HTTP 通信用的是 OkHttpJSON 序列化用的 Jackson这两个都是社区使用率很高的库。OkHttp 3.x 在 Java 8 下运行良好回调简洁超时设置方便。Jackson 和 SSM 服务端天然匹配SpringMVC 的消息转换器默认就支持 Jackson所以客户端用同一个库解析 JSON 不会出现字段映射不一致的问题。3. 核心链路拆解登录请求从按钮点击到数据库返回3.1 入口MainApp 启动与 FXML 加载JavaFX 程序的入口是一个继承 Application 的类。拿这个项目的 MainApp 来说它做的事情只有三件加载 FXML 布局文件、构建 Scene、把主窗口显示出来。public class MainApp extends Application { Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader new FXMLLoader(); loader.setLocation(getClass().getResource(/fxml/LoginView.fxml)); Parent root loader.load(); Scene scene new Scene(root, 1000, 620); scene.getStylesheets().add( getClass().getResource(/css/style.css).toExternalForm()); primaryStage.setTitle(桌面登录); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }这里的关键是getResource(/fxml/LoginView.fxml)的路径斜杠开头代表从 classpath 根目录去找。resources 目录下的 fxml 文件会被编译进 classpath所以路径必须和文件实际所在目录一致。如果这个路径写错程序启动时会直接抛 Location is not set 异常这个坑很常见后面会在排查部分重点讲。Scene构造方法的宽度和高度是最初的窗口尺寸scene.getStylesheets().add(...)加载全局样式表。后续如果你要改窗口默认大小直接改这两个数字就行。3.2 登录界面FXML Controller 里的参数校验与远程调用FXML 是 JavaFX 的界面描述文件类似前端的 HTML对应的控制器就是 LoginController。FXML 里有个fx:controller属性指定控制器类控件通过fx:id和控制器里的FXML字段绑定事件通过onAction绑定到处理方法。这个登录控制器的核心逻辑就是点击登录按钮、取输入值、调远端接口、处理返回结果。public class LoginController { FXML private TextField usernameField; FXML private PasswordField passwordField; FXML private Label tipLabel; FXML private void handleLogin(ActionEvent event) { String username usernameField.getText().trim(); String password passwordField.getText().trim(); if (username.isEmpty() || password.isEmpty()) { tipLabel.setText(用户名和密码不能为空); return; } LoginRequest req new LoginRequest(username, password); ApiResponse resp HttpUtil.postJson( http://localhost:8080/ssm-server/api/login, req); if (resp.getCode() 0) { String token (String) resp.getData().get(token); SessionManager.setToken(token); navigateToMain(); } else { tipLabel.setText(resp.getMessage()); } } private void navigateToMain() { try { FXMLLoader loader new FXMLLoader(); loader.setLocation(getClass().getResource(/fxml/MainView.fxml)); Parent root loader.load(); Stage stage (Stage) usernameField.getScene().getWindow(); stage.setScene(new Scene(root, 1100, 700)); } catch (IOException e) { e.printStackTrace(); } } }HttpUtil.postJson是客户端封装好的 HTTP 请求工具完全屏蔽了 OkHttp 的调用细节控制器层面不需要关心网络请求怎么发、超时怎么处理。登录成功后的navigateToMain方法做了场景替换——把当前窗口的 Scene 换成主界面的 Scene这套做法比新建一个 Stage 更省资源也是 JavaFX 应用中常见的界面切换方式。3.3 HTTP 工具类OkHttp 封装与超时策略客户端的 HTTP 通信不能散落在各处业务代码里项目里统一封装了一个 HttpUtil 工具类。这个类在拆解时值得细看因为它决定了客户端所有接口请求的基础行为。public class HttpUtil { private static final OkHttpClient CLIENT new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); public static ApiResponse postJson(String url, Object data) { String json JsonUtil.toJson(data); RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), json); Request request new Request.Builder() .url(url) .post(body) .header(token, SessionManager.getToken()) .build(); try (Response response CLIENT.newCall(request).execute()) { String respBody response.body().string(); return JsonUtil.fromJson(respBody, ApiResponse.class); } catch (IOException e) { return ApiResponse.error(网络连接失败请检查服务端是否启动); } } }三个超时设置分别控制连接、读取、写入10 秒对大多数业务接口够用。在请求头里把 token 动态塞进去这是登录之后的身份凭证。如果服务端没启动或者网络断开IOException 会被捕获并转成统一的错误返回UI 层不用感知底层异常。这个工具类还可以扩展 get、put、delete 方法只需要按同样的套路加方法就行二次开发时不用动控制器代码。3.4 服务端SpringMVC 接收请求并返回统一结果请求到达服务端后SpringMVC 的中央调度器根据 URL 找到对应的 Controller。登录接口对应的是服务端的 LoginController注意它和客户端的 LoginController 同名但位置和职责完全不同。RestController RequestMapping(/api) public class LoginController { Autowired private UserService userService; PostMapping(/login) public ApiResponse login(RequestBody LoginRequest request) { User user userService.findByUsername(request.getUsername()); if (user null) { return ApiResponse.error(用户不存在); } String salt user.getSalt(); String md5 MD5Util.md5(request.getPassword() salt); if (!md5.equals(user.getPassword())) { return ApiResponse.error(密码错误); } String token UUID.randomUUID().toString().replace(-, ); SessionManager serverSession new SessionManager(); serverSession.add(token, user.getId()); return ApiResponse.success(serverSession.get(token)); } }RequestBody把客户端提交的 JSON 自动绑定到 LoginRequest 对象RestController表示返回值直接序列化为 JSON 写入响应体不需要再走视图解析器。这里密码不是明文比对而是把输入的密码加上用户盐值再做 MD5和数据库里存的值比对。token 用 UUID 生成随机字符串。项目里如果没引入 Redis用一个 ConcurrentHashMap 做内存级会话存储是完全够用的方案。用户不存在、密码错误、登录成功三种结果都通过 ApiResponse 统一返回客户端不需要特判某个异常类型只认 code 字段这就是上一章讲的统一协议在服务端的落地。3.5 持久层MyBatis 的 Mapper XML 与预编译防注入服务端再往下走就是数据层。UserMapper 是一个接口真正的 SQL 写在同名的 XML 文件里。mapper namespacecom.demo.server.mapper.UserMapper select idfindByUsername parameterTypestring resultTypecom.demo.server.entity.User SELECT id, username, password, salt, real_name, role_id FROM sys_user WHERE username #{username} /select /mappernamespace 必须和接口全限定名一致select 的 id 要对应接口方法名resultType指定返回实体类。这里最关键的是#{}用法它会被 MyBatis 解析成预编译参数占位符最终生成WHERE username ?这样的 PreparedStatement 语句能有效防止 SQL 注入。如果写成${username}就是字符串拼接那条虚线就是安全事故。resultType的属性映射依赖 MyBatis 的驼峰转换配置数据库字段 real_name 会自动映射到实体的 realName 属性。下面这段配置放在 mybatis-config.xml 里写不写这一步直接影响能不能查询到数据configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings /configurationJdbc 连接层由 Spring 的 SqlSessionFactory 管理mapper 接口通过扫描机制注册为 Spring Beanservice 层里直接Autowired注入 UserMapper 就能调用。到这里一条完整的链路就闭环了JavaFX 按钮点击 → HttpUtil 发 POST → SpringMVC Controller → UserService → UserMapper → MySQL → 逐层返回到界面。4. 环境搭建与启动从数据库初始化到双端联调4.1 环境清单与版本搭配建议这个项目的运行环境比较传统。JDK 8 自带 JavaFX是最省事的组合。后端需要 Tomcat 8 或 9MySQL 5.7 或 8.0 都可以。前端工程用 Maven 构建管理 OkHttp、Jackson、JavaFX 相关依赖后端工程也是 Maven依赖 Spring、SpringMVC、MyBatis、MySQL 驱动两边分开发布。如果本机只有 JDK 11 或更高版本JavaFX 需要单独加 OpenJFX 依赖以 Maven 为例dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version11.0.2/version /dependency对应的启动类也要改成用 Module 方式加载否则会报“JavaFX runtime components are missing”。最省心的做法还是直接装 JDK 8Network 上资料多出了问题好搜。4.2 数据库初始化建库、建表、造测试数据服务端代码里自带一份初始化 SQL这个项目里叫 init.sql建库建表一步到位。表结构不复杂核心就一张 sys_user 表CREATE DATABASE IF NOT EXISTS ssm_demo DEFAULT CHARSET utf8mb4; USE ssm_demo; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, salt VARCHAR(32) NOT NULL, real_name VARCHAR(32), role_id INT DEFAULT 2, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO sys_user (username, password, salt, real_name, role_id) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, abc123, 管理员, 1);这条插入语句的密码值是“123456”加上盐值“abc123”之后做的 MD5可以直接用在线 MD5 校验一下对得上号。正式使用时要把测试账号删掉改成自己生产环境的账号体系。4.3 修改服务端配置jdbc.properties 与 maven 打包数据库连接串集中在 jdbc.properties 里启动前必须改成自己机器的实际值jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ssm_demo?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456如果数据库是 MySQL 8这里的驱动类要换成com.mysql.cj.jdbc.DriverURL 里还得追加时区参数serverTimezoneAsia/Shanghai否则会报时区相关的 SQLException。后端通过 Maven 打成 WAR 包丢进 Tomcat 的 webapps 目录或者直接在 IDEA 里配置 Tomcat 运行。4.4 双端启动顺序与联调验证跑这个项目建议遵守固定顺序先启动 MySQL 并确认能连上再启动 Tomcat 加载后端最后运行 JavaFX 的 MainApp。后端启动成功的标志是 IDEA 控制台输出 Tomcat started on port 8080接口地址是/ssm-server/api/login。客户端里面写的 URL 是http://localhost:8080/ssm-server/api/login这种完整地址。如果客户端运行后发现登录按钮没反应第一步先打开浏览器手动访问一下这个地址能返回 JSON 就说明后端正常问题出在客户端配置。联调验证时我最常用的方式是让客户端登录一次然后查看服务端日志里有没有 MyBatis 的 SQL 输出。项目里配置了 MyBatis 日志SQL 执行情况一目了然 Preparing: SELECT id, username, password, salt, real_name, role_id FROM sys_user WHERE username ? Parameters: admin(String) Columns: id, username, password, salt, real_name, role_id Row: 1, admin, e10adc..., abc123, 管理员, 1从这条日志能直接定位问题在前端还是后端有 SQL 说明请求穿透到了数据层没有 SQL 说明请求在 Controller 或 service 层被拦住了。5. 常见问题排查五个拿来即用的踩坑记录5.1 启动报 JavaFX runtime components are missing现象运行 MainApp 时 JVM 直接抛出JavaFX runtime components are missing程序无法启动。原因JDK 11 及以上的发行版不再内置 JavaFX。项目代码编译用的是 JDK 11但运行环境没有把 OpenJFX 模块加到启动命令里JVM 找不到 JavaFX 相关类。解决最直接的做法是换成 JDK 8 运行整个工程如果必须用 JDK 11就引入 OpenJFX 依赖并在 VM options 里加--add-modules javafx.controls,javafx.fxml。我第二次接手这个项目时踩的就是这个坑换了 JDK 8 之后没改一行代码就启动了。5.2 FXML Location is not set 异常现象程序启动时控制台打印java.lang.IllegalStateException: Location is not set界面一片空白。原因getResource()没有在 classpath 下找到对应的 fxml 文件最常见冲突是 fxml 放在 src/main/java 目录下而不是 resources 目录下Maven 默认不会把 java 目录下的资源文件打包进 classpath。此外路径写错也会导致同样的异常比如少了开头的/。解决把 fxml 文件挪到 resources/fxml 目录路径写成/fxml/LoginView.fxml。IDEA 里编译后可以打开 target 目录确认 fxml 文件是否真的被拷贝进去了这是最直接的验证手段。5.3 登录接口 404现象客户端点登录按钮提示接口不存在浏览器直接访问http://localhost:8080/ssm-server/api/login返回 404。原因Tomcat 的 context-path 或 WAR 包名和客户端写死的 URL 不匹配。客户端写的是/ssm-server但 Tomcat 部署时实际上下文路径是别的名字。解决把 WAR 包改成和约定一致的名字或者在客户端里把 URL 中的/ssm-server换成实际路径。我一般会把服务端地址只保留 localhost:8080其余部分全部按实际部署时的 context-path 来配并写进配置文件而不是硬编码在 Java 类里。5.4 数据库连接不上报时区或驱动类错误现象Tomcat 启动时数据源初始化失败控制台提示Unknown initial character set index或Server returns invalid timezone。原因MySQL 8 移除了旧的驱动类同时新驱动对 timezone 参数要求更严格。项目里 jdbc.properties 写的是com.mysql.jdbc.Driver并且没有带serverTimezone这套配置在 MySQL 5.7 下能跑在 8.0 下就翻车。解决驱动的配置升级为com.mysql.cj.jdbc.DriverURL 追加?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。利用 MySQL 8 向下兼容的特性这种配置在 5.7 和 8.0 下都能正常工作。5.5 MyBatis 报 Invalid bound statement (not found)现象调用 userService.findByUsername 时报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): UserMapper.findByUsername。原因Mapper 接口编译进了 classpath但对应的 XML 文件没有被打包进去。Maven 默认只把 resources 目录下的内容纳入 classpath如果 XML 放在 src/main/java 下就会丢。另外 spring-mybatis.xml 里 mapper-locations 配置漏掉 xml 路径也会报这个错。解决在 pom.xml 里加资源配置或直接把 XML 放进 resources/mapper 目录并确认配置里的路径匹配property namemapperLocations valueclasspath:mapper/*.xml/每个原因都对应一条具体的配置修复动作。这类问题单看报错信息会觉得玄学实际上九十多都是资源和路径的问题。6. 二次开发思路把通用登录改造成自己的业务模块拿到这套脚手架后最先值得改的就是账号体系。sys_user 表目前只有最简单的用户字段实际业务通常需要增加角色、部门、状态。我的做法是新增一张 sys_role 表在 sys_user 里加 role_code 和 org_id 字段登录成功后的 data 里返回用户基本信息加权限列表客户端拿到权限列表动态渲染主界面的菜单项这样就把静态登录改造成了有权限控制的后台框架。第二个改造点是把客户端所有硬编码的服务端地址集中到一个配置类或者 properties 文件里。项目里HttpUtil.postJson(http://localhost:8080/ssm-server/api/login, req)这种写法在接口数量少时没问题等业务接口一多改一次服务端端口就要全文替换。我会定义 Config 类加上SERVER_BASE_URL常量所有请求都拼接这个常量和具体接口路径发布时只改一处。还有更进阶一点的把登录成功后的 Scene 切换改成动态加载。现在 MainView 是固定的 FXML你可以在中间加上一层菜单路由——根据用户角色决定加载哪个 Tab 页的 FXML比如普通用户只显示个人中心管理员额外显示数据管理。改造的核心是拆分 FXML 文件每个功能模块对应一套界面对应一个控制器切换时用 FXMLLoader 按需加载不会让主控制器膨胀成几百行的巨型类。从我接手过的几个基于这套项目改的桌面工具来看代码结构在初期定好了边界后面接需求会顺畅很多。后来我每次拿到新的 JavaFX 项目都会先花十分钟理清 FXML 路径、服务端地址、数据库连接串这三个黑匣子确认它们在配置文件里而不是散落在代码各处再开始动手改功能。这个习惯帮我避开了好几次改到一半才发现连不上库重新返工的情况。希望这篇拆解能帮你在跑通这个 javaFx 加 SSM 项目的路上少踩几个坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站