简介:面向企业级桌面应用开发者的完整 JavaFX + SSM(Spring+SpringMVC+Mybatis)ERP 项目源码包,演示了如何用 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 通信用的是 OkHttp,JSON 序列化用的 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; charset=utf-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 namespace="com.demo.server.mapper.UserMapper"> <select id="findByUsername" parameterType="string" resultType="com.demo.server.entity.User"> SELECT id, username, password, salt, real_name, role_id FROM sys_user WHERE username = #{username} </select> </mapper>namespace 必须和接口全限定名一致,select 的 id 要对应接口方法名,resultType指定返回实体类。这里最关键的是#{}用法:它会被 MyBatis 解析成预编译参数占位符,最终生成WHERE username = ?这样的 PreparedStatement 语句,能有效防止 SQL 注入。如果写成${username}就是字符串拼接,那条虚线就是安全事故。
resultType的属性映射依赖 MyBatis 的驼峰转换配置,数据库字段 real_name 会自动映射到实体的 realName 属性。下面这段配置放在 mybatis-config.xml 里,写不写这一步直接影响能不能查询到数据:
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>Jdbc 连接层由 Spring 的 SqlSessionFactory 管理,mapper 接口通过扫描机制注册为 Spring Bean,service 层里直接@Autowired注入 UserMapper 就能调用。到这里,一条完整的链路就闭环了:JavaFX 按钮点击 → HttpUtil 发 POST → SpringMVC Controller → UserService → UserMapper → MySQL → 逐层返回到界面。
4. 环境搭建与启动:从数据库初始化到双端联调
4.1 环境清单与版本搭配建议
这个项目的运行环境比较传统。JDK 8 自带 JavaFX,是最省事的组合。后端需要 Tomcat 8 或 9,MySQL 5.7 或 8.0 都可以。前端工程用 Maven 构建,管理 OkHttp、Jackson、JavaFX 相关依赖;后端工程也是 Maven,依赖 Spring、SpringMVC、MyBatis、MySQL 驱动,两边分开发布。
如果本机只有 JDK 11 或更高版本,JavaFX 需要单独加 OpenJFX 依赖,以 Maven 为例:
<dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>11.0.2</version> </dependency>对应的启动类也要改成用 Module 方式加载,否则会报“JavaFX runtime components are missing”。最省心的做法还是直接装 JDK 8,Network 上资料多,出了问题好搜。
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.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm_demo?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456如果数据库是 MySQL 8,这里的驱动类要换成com.mysql.cj.jdbc.Driver,URL 里还得追加时区参数serverTimezone=Asia/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.Driver,URL 追加?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。利用 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 name="mapperLocations" value="classpath: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 项目的路上少踩几个坑。
本文还有配套的精品资源,点击获取