简介:本资源是一个基于JavaFX与SSM(Spring+SpringMVC+MyBatis)框架融合开发的企业级ERP系统完整项目,面向Java桌面应用开发者、企业级Web后端学习者及希望打通GUI前端与服务层协同开发的进阶学习者。项目实现了从JavaFX高交互UI界面(含TableView表格展示、表单数据绑定、CSS样式定制)到Spring容器管理、SpringMVC请求调度、MyBatis数据库操作的全链路整合,适用于课程设计、毕业项目或轻量级内部管理系统原型开发。压缩包为7z格式,共2个文件:1个主项目压缩包(含FXML界面、Java源码、配置文件等完整工程结构)和1个SQL脚本文件(用于初始化ERP核心业务数据),整体大小75.26MB,结构精炼、开箱即用。目前已有3415人学习下载,读者可直接导入IDE运行,快速掌握JavaFX与SSM协同开发模式、双向数据绑定实践、FXML+Controller分离设计及ERP典型模块(如员工/订单管理)的端到端实现逻辑。
1. 为什么用 JavaFX 做前端,却坚持用 SSM 做后端?——一份能过答辩、能跑通、能改出新功能的完整项目方案
这不是一个“JavaFX + SSM 能不能搭”的理论探讨,而是一份我带过的三个课程设计、两个毕设项目里反复验证过的落地组合:JavaFX 做桌面端界面(响应快、本地能力足、学生易上手),Spring+SpringMVC+MyBatis 做后端服务(解耦清晰、事务可控、数据库操作不玄学)。很多同学一上来就纠结“JavaFX 不是被 Swing 取代了吗?”“SSM 过时了是不是该上 Spring Boot?”——但现实是:课程设计要求你两周内交出可运行的增删改查+登录+报表导出;毕设答辩要现场演示数据从界面上录入、经后端校验、落库、再查出来渲染成表格——这时候,稳定压倒一切,结构比炫技重要。本方案就是为这类真实交付场景准备的:它不追求最新技术栈,但保证每个模块有明确职责、每处接口有可测契约、每次部署不靠玄学重启。适合正在赶课设 deadline 的大三同学,也适合需要快速搭建管理类桌面工具的初级工程师。
2. 总体架构设计:四层分离与五个核心模块的边界定义
本方案采用经典四层架构(表现层 → 控制层 → 服务层 → 持久层),但针对 JavaFX 桌面应用做了关键适配:去掉 HTTP 中间层,用本地 RPC 风格调用替代 REST 请求。这不是倒退,而是规避网络抖动、跨域、CORS、JSON 序列化失败等在桌面环境毫无意义的翻车点。后端仍按标准 SSM 结构组织,但 Controller 层不返回 ModelAndView 或 ResponseEntity,而是统一返回 POJO(如Result<T>),由 JavaFX 端直接反序列化消费。这种设计让调试像写单机程序一样直观——断点打在 Service 层,数据流一眼可见。
2.1 模块划分与职责清单
| 模块名 | 所属层级 | 核心职责 | 关键交付物 |
|---|---|---|---|
fx-client | 表现层 | JavaFX 主窗体、TabPane 页面管理、事件绑定、表单校验(非空/格式)、异步任务封装(Task/VoidTask) | MainApp.java、LoginController.java、UserListViewController.java |
web-api | 控制层 | 接收 JavaFX 端传入的 Map<String, Object> 参数(模拟轻量 JSON),调用 Service,包装Result.success(data)或Result.fail("msg")返回 | UserController.java、Result.java(含泛型 T)、统一异常处理器GlobalExceptionHandler |
service-impl | 服务层 | 业务逻辑编排、事务控制(@Transactional)、跨表操作、权限校验(简单角色码比对) | UserServiceImpl.java(含 saveWithRole()、listByDept())、TransactionConfig.java |
dao-mapper | 持久层 | MyBatis Mapper 接口 + XML 实现、动态 SQL( )、分页插件 PageHelper 集成 | UserMapper.java、UserMapper.xml(含<select id="listPage" ...>)、PageHelperConfig.java |
common-utils | 工具层 | 公共实体类(User/Role)、常量类(StatusEnum)、加密工具(BCryptPasswordEncoder 封装)、日期格式化工具 | User.java(Lombok @Data)、EncryptUtil.java(含 salt 生成逻辑)、DateUtils.java |
提示:所有模块均以 Maven 多模块方式组织,
pom.xml中fx-client模块依赖web-api,web-api依赖service-impl,依此类推。禁止循环依赖——这是后期加功能时最常踩的坑。
2.2 数据流向图:从点击按钮到数据库写入的完整链路
用户在 JavaFX 界面点击【保存用户】按钮后:
UserEditViewController.java中saveBtn.setOnAction(e -> { ... })触发;- 收集
TextField和ComboBox值,组装为Map<String, Object>(key 为字段名,value 为字符串或数字); - 调用
ApiService.saveUser(Map)—— 此处是本地方法调用,非 HTTP; UserController.saveUser()接收 Map,转换为User对象(手动 set 或用 BeanUtils.copyProperties);UserServiceImpl.saveUser()开启事务,校验手机号唯一性(查库)、密码加密(EncryptUtil.encode(pwd))、保存主表 + 关联角色表;- 成功则返回
Result.success(user.getId()),失败则抛出自定义异常BusinessException("手机号已存在"),由全局异常处理器捕获并转为Result.fail(...); ApiService解析 Result,若 success 则弹 Toast 提示“保存成功”,并刷新列表;若 fail 则弹 Alert 显示错误信息。
这个链路没有 JSON 解析、没有 HTTP 状态码判断、没有跨进程通信开销——所有环节都在 JVM 内完成,调试时断点可一路跟到底。
2.3 关键配置文件说明:避免“配置写对但不起作用”的黑匣子
spring-context.xml(核心容器配置)
<!-- 扫描 service 包,启用事务注解 --> <context:component-scan base-package="com.example.service"/> <tx:annotation-driven transaction-manager="transactionManager"/> <!-- 数据源配置(HikariCP 替代默认 DBCP,性能更稳) --> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="maximumPoolSize" value="20"/> </bean>mybatis-config.xml(MyBatis 全局配置)
<!-- 开启驼峰命名自动映射(user_name → userName) --> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 开发期必须开,看 SQL 是否拼对 --> </settings> <mappers> <!-- 扫描 mapper 接口包 --> <package name="com.example.dao.mapper"/> </mappers>applicationContext.xml(整合配置)
<!-- 引入 MyBatis SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- 扫描 Mapper 接口,生成代理对象 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.dao.mapper"/> </bean>这些配置不是复制粘贴就能用的。比如mapUnderscoreToCamelCase不开,你的user_name字段永远映射不到User.userName;logImpl不设为STDOUT_LOGGING,你根本看不到 MyBatis 实际执行的 SQL,排查“查不到数据”问题只能靠猜。
3. JavaFX 客户端实现:事件驱动、异步封装与 UI 线程安全
JavaFX 最反直觉的点在于:所有 UI 更新必须在 JavaFX Application Thread 执行,否则直接抛IllegalStateException。而网络请求(哪怕本地调用)默认在后台线程,这就要求你必须显式切回 UI 线程。本方案用Task封装所有耗时操作,并通过updateMessage()/updateValue()主动通知 UI,而非用Platform.runLater()手动切线程——后者容易漏写、难维护。
3.1 登录模块:从界面到会话保持的完整闭环
LoginController.java是第一个要写的控制器,它决定了整个项目的启动姿势:
public class LoginController { @FXML private TextField usernameField; @FXML private PasswordField passwordField; @FXML private Button loginBtn; @FXML public void initialize() { loginBtn.setOnAction(e -> doLogin()); } private void doLogin() { String username = usernameField.getText().trim(); String password = passwordField.getText(); // 1. 前端基础校验 if (username.isEmpty() || password.isEmpty()) { showAlert("提示", "用户名和密码不能为空"); return; } // 2. 封装为 Task,避免阻塞 UI 线程 Task<Map<String, Object>> loginTask = new Task<>() { @Override protected Map<String, Object> call() throws Exception { // 3. 调用后端 API(本地方法,非 HTTP) return ApiClient.login(username, password); // 返回 {code:200, data:{userId:1, token:"abc"}} } }; // 4. 登录成功后的 UI 更新(自动在 UI 线程执行) loginTask.setOnSucceeded(event -> { Map<String, Object> result = loginTask.getValue(); if ("200".equals(result.get("code"))) { Map<String, Object> userData = (Map<String, Object>) result.get("data"); SessionHolder.setUserId((Integer) userData.get("userId")); SessionHolder.setToken((String) userData.get("token")); // 跳转主界面 loadMainView(); } else { showAlert("登录失败", (String) result.get("msg")); } }); // 5. 登录失败或异常的 UI 反馈 loginTask.setOnFailed(event -> { Throwable ex = loginTask.getException(); showAlert("系统错误", "登录请求失败:" + ex.getMessage()); }); // 6. 启动任务(自动在线程池中执行) new Thread(loginTask).start(); } private void showAlert(String title, String content) { Alert alert = new Alert(Alert.AlertType.WARNING); alert.setTitle(title); alert.setHeaderText(null); alert.setContentText(content); alert.showAndWait(); } }这段代码的关键设计点:
Task是 JavaFX 官方推荐的异步模式,它天然支持onSucceeded/onFailed回调,且回调自动在 UI 线程执行,无需Platform.runLater();SessionHolder是一个静态工具类,用于跨 ViewController 共享登录态(private static Integer userId; private static String token;),比用Preferences或文件存储更轻量,符合课设需求;ApiClient.login()是一个普通 Java 方法,内部直接 newUserController()并调用其方法,无任何框架侵入——这意味着你可以把web-api模块打成 jar,fx-client直接依赖,完全绕过 Web 容器。
3.2 用户管理列表:分页加载、双击编辑、批量删除的实战写法
UserListViewController.java是第二个高频模块,它集中体现了 JavaFX 与 SSM 协作的典型模式:
public class UserListViewController { @FXML private TableView<User> userTable; @FXML private TableColumn<User, String> nameCol; @FXML private TableColumn<User, String> phoneCol; @FXML private TableColumn<User, String> deptCol; @FXML private Pagination pagination; private List<User> allUsers = new ArrayList<>(); @FXML public void initialize() { // 1. 初始化表格列(绑定属性名,非 getter 名) nameCol.setCellValueFactory(new PropertyValueFactory<>("name")); phoneCol.setCellValueFactory(new PropertyValueFactory<>("phone")); deptCol.setCellValueFactory(new PropertyValueFactory<>("deptName")); // 注意:deptName 是 User 类里的冗余字段,非 dept.id // 2. 分页控件初始化(每页 10 条) pagination.setPageCount(0); // 初始页数由数据决定 pagination.currentPageIndexProperty().addListener((obs, old, cur) -> loadPage(cur.intValue())); // 3. 双击行打开编辑窗口 userTable.setRowFactory(tv -> { TableRow<User> row = new TableRow<>(); row.setOnMouseClicked(event -> { if (event.getClickCount() == 2 && !row.isEmpty()) { User clickedUser = row.getItem(); openEditDialog(clickedUser); } }); return row; }); } private void loadPage(int pageIndex) { Task<PageResult<User>> task = new Task<>() { @Override protected PageResult<User> call() throws Exception { // 调用后端分页查询(参数:当前页、每页数) return ApiClient.listUsers(pageIndex + 1, 10); // PageHelper 从 1 开始计页 } }; task.setOnSucceeded(event -> { PageResult<User> page = task.getValue(); allUsers = page.getData(); // 缓存全量,供搜索用 userTable.setItems(FXCollections.observableArrayList(page.getData())); pagination.setPageCount((int) Math.ceil((double) page.getTotal() / 10)); }); new Thread(task).start(); } private void openEditDialog(User user) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/user_edit.fxml")); Parent root = loader.load(); UserEditViewController controller = loader.getController(); controller.initForUpdate(user); // 传入待编辑对象 Stage stage = new Stage(); stage.setScene(new Scene(root)); stage.setTitle("编辑用户"); stage.show(); } catch (IOException e) { e.printStackTrace(); } } }这里有两个易错点必须强调:
PropertyValueFactory<>("deptName")中的"deptName"必须与User.java中的字段名完全一致(大小写敏感),不是getDeptName()的方法名缩写;Pagination的currentPageIndex从 0 开始,但PageHelper.startPage(pageNum, pageSize)的pageNum从 1 开始,所以调用时必须pageIndex + 1,否则第一页永远查不到数据。
3.3 文件导出模块:Apache POI 生成 Excel 并触发本地保存对话框
报表导出是毕设高频加分项。本方案用 Apache POI 3.17(兼容 JDK8),不依赖模板,纯代码生成:
@FXML private void exportToExcel() { Task<Void> exportTask = new Task<>() { @Override protected Void call() throws Exception { // 1. 获取全部用户(非当前页,是全量) List<User> users = ApiClient.getAllUsers(); // 2. 创建工作簿和工作表 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("用户列表"); // 3. 写入表头 String[] headers = {"ID", "姓名", "手机号", "部门", "创建时间"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 4. 写入数据行 int rowNum = 1; for (User u : users) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(u.getId()); row.createCell(1).setCellValue(u.getName()); row.createCell(2).setCellValue(u.getPhone()); row.createCell(3).setCellValue(u.getDeptName()); row.createCell(4).setCellValue( u.getCreateTime() != null ? u.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : "" ); } // 5. 自动调整列宽 for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } // 6. 弹出保存对话框(UI 线程操作) FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("保存 Excel 文件"); fileChooser.setInitialFileName("用户列表_" + System.currentTimeMillis() + ".xlsx"); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter("Excel Files", "*.xlsx") ); File file = fileChooser.showSaveDialog(userTable.getScene().getWindow()); if (file != null) { try (FileOutputStream out = new FileOutputStream(file)) { workbook.write(out); } } return null; } }; exportTask.setOnSucceeded(e -> { showAlert("导出成功", "文件已保存至:" + file.getAbsolutePath()); }); new Thread(exportTask).start(); }注意:FileChooser.showSaveDialog()必须在 UI 线程调用,所以它放在onSucceeded里(自动在 UI 线程执行),而不是在call()里——否则会报Not on FX application thread错误。
4. SSM 后端实现:Controller 统一契约、Service 事务边界、Mapper 动态 SQL
SSM 层的难点不在语法,而在职责边界的清醒认知。很多同学把所有逻辑堆在 Controller,导致无法单元测试、事务失效、SQL 注入风险高。本方案强制约定:Controller 只做三件事——参数校验、调用 Service、包装 Result;Service 做业务编排和事务控制;Mapper 只负责单表 CRUD。
4.1 Controller 层:统一返回结构与全局异常处理
Result<T>是整个后端的契约基石,定义如下:
public class Result<T> { private int code; // 200 成功,500 系统异常,400 业务异常 private String msg; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "操作成功"; r.data = data; return r; } public static Result<Object> fail(String msg) { Result<Object> r = new Result<>(); r.code = 400; r.msg = msg; return r; } // getter/setter 省略 }UserController.java示例(精简版):
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result<Map<String, Object>> login(@RequestBody Map<String, Object> params) { String username = (String) params.get("username"); String password = (String) params.get("password"); // 1. 基础校验(空值、长度) if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { return Result.fail("用户名或密码不能为空"); } // 2. 调用 Service,不处理异常,交给全局处理器 return userService.login(username, password); } @PostMapping("/save") public Result<Integer> save(@RequestBody Map<String, Object> params) { // 参数转换(此处应使用 DTO,课设简化为 Map) User user = new User(); user.setName((String) params.get("name")); user.setPhone((String) params.get("phone")); user.setDeptId(Integer.parseInt((String) params.get("deptId"))); String rawPwd = (String) params.get("password"); if (StringUtils.isNotBlank(rawPwd)) { user.setPassword(EncryptUtil.encode(rawPwd)); // 密码必须加密 } return userService.saveUser(user); } @GetMapping("/list") public Result<PageResult<User>> list(@RequestParam Integer pageNum, @RequestParam Integer pageSize) { return userService.listUsers(pageNum, pageSize); } }注意:
@RequestBody Map<String, Object>是课设级的取巧写法,避免为每个接口写 DTO。实际项目必须用@RequestBody UserSaveDTO,但课设阶段,Map 足够清晰且省事。
4.2 Service 层:事务控制与业务逻辑隔离
UserServiceImpl.java是事务的守门人:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Override @Transactional(rollbackFor = Exception.class) // 关键:声明式事务 public Result<Map<String, Object>> login(String username, String password) { User user = userMapper.findByUsername(username); if (user == null) { return Result.fail("用户名不存在"); } // BCrypt 密码比对(明文密码 vs 加密后字符串) if (!BCryptPasswordEncoder.matches(password, user.getPassword())) { return Result.fail("密码错误"); } // 构建返回数据(含用户基本信息 + token) Map<String, Object> data = new HashMap<>(); data.put("userId", user.getId()); data.put("token", UUID.randomUUID().toString().replace("-", "")); // 简单 token,课设够用 data.put("role", user.getRoleId()); // 角色 ID,前端用于按钮显隐 return Result.success(data); } @Override @Transactional(rollbackFor = Exception.class) public Result<Integer> saveUser(User user) { // 1. 手机号唯一性校验(查库) User existing = userMapper.findByPhone(user.getPhone()); if (existing != null && !existing.getId().equals(user.getId())) { return Result.fail("手机号已存在"); } // 2. 新增 or 更新 if (user.getId() == null) { user.setCreateTime(LocalDateTime.now()); userMapper.insert(user); } else { user.setUpdateTime(LocalDateTime.now()); userMapper.updateById(user); } // 3. 处理角色关联(假设是多对多,中间表 user_role) if (user.getRoleId() != null) { // 先删旧关联 userMapper.deleteUserRole(user.getId()); // 再插入新关联 userMapper.insertUserRole(user.getId(), user.getRoleId()); } return Result.success(user.getId()); } }事务失效的三大雷区:
@Transactional方法被本类其他方法调用(内部调用不走代理,事务失效)→ 本方案所有 Service 方法均由 Controller 直接调用,杜绝此问题;@Transactional方法内try-catch吞掉异常 → 本方案所有异常都向上抛,由@RestControllerAdvice统一捕获;- 方法不是
public→@Transactional只对 public 方法生效,本方案严格遵守。
4.3 Mapper 层:XML 动态 SQL 与分页插件集成
UserMapper.xml是 SQL 的最终落脚点,必须体现 MyBatis 的核心优势——动态 SQL:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.dao.mapper.UserMapper"> <!-- 1. 根据用户名查用户(用于登录) --> <select id="findByUsername" resultType="User"> SELECT * FROM t_user WHERE username = #{username} </select> <!-- 2. 根据手机号查用户(用于唯一性校验) --> <select id="findByPhone" resultType="User"> SELECT * FROM t_user WHERE phone = #{phone} </select> <!-- 3. 分页查询(PageHelper 会自动拼 LIMIT) --> <select id="listPage" resultType="User"> SELECT u.*, d.dept_name as deptName FROM t_user u LEFT JOIN t_dept d ON u.dept_id = d.id <where> <if test="name != null and name != ''"> AND u.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND u.dept_id = #{deptId} </if> </where> ORDER BY u.create_time DESC </select> <!-- 4. 批量删除(ids 是 Long 数组) --> <delete id="deleteBatch"> DELETE FROM t_user WHERE id IN <foreach item="id" collection="ids" open="(" separator="," close=")"> #{id} </foreach> </delete> <!-- 5. 插入用户 + 返回主键 --> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user (name, phone, password, dept_id, create_time) VALUES (#{name}, #{phone}, #{password}, #{deptId}, #{createTime}) </insert> </mapper>关键点说明:
<where>标签自动处理AND开头的 SQL 拼接,避免WHERE AND ...语法错误;<foreach>用于IN查询和批量插入,collection="ids"中的ids必须与 Service 方法参数名一致;useGeneratedKeys="true"+keyProperty="id"是获取自增主键的唯一可靠方式,比SELECT LAST_INSERT_ID()更安全。
5. 避坑指南:SSM + JavaFX 项目里最常翻车的 5 个点
这些不是教科书里的“注意事项”,而是我在三次课设答辩现场、两次毕设预演中亲眼看到学生卡住、重装环境、甚至想重写的血泪经验。每一条都对应一个具体现象、根本原因和可立即执行的解决动作。
5.1 现象:JavaFX 界面点击按钮没反应,控制台无任何日志
原因:@FXML注解的控件字段名与 FXML 文件中的fx:id不一致,或initialize()方法未被调用(常见于忘记在 FXML 中设置fx:controller)。
解决:
- 检查 FXML 文件顶部是否声明
fx:controller="com.example.controller.LoginController"; - 检查
LoginController.java中@FXML private Button loginBtn;的loginBtn是否与 FXML 中<Button fx:id="loginBtn" ... />的fx:id完全一致(包括大小写); - 在
initialize()方法第一行加System.out.println("init called");,运行看是否打印——不打印说明控制器未绑定。
5.2 现象:MyBatis 查询返回 null,但数据库明明有数据
原因:resultType指向的 POJO 字段名与数据库列名不匹配,且未开启mapUnderscoreToCamelCase。例如数据库列user_name,POJO 字段userName,若没开启驼峰映射,MyBatis 不会自动赋值。
解决:
- 确认
mybatis-config.xml中<setting name="mapUnderscoreToCamelCase" value="true"/>已配置; - 查看控制台输出的 SQL(
logImpl="STDOUT_LOGGING"),确认 SQL 本身能查出数据; - 在
UserMapper.xml的<select>标签中,显式用AS别名:SELECT user_name AS userName FROM t_user,临时验证是否是映射问题。
5.3 现象:保存用户时报错org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.dao.mapper.UserMapper.insert
原因:Mapper 接口与 XML 文件未正确绑定。常见于:XML 文件名与接口名不一致(如接口UserMapper.java,XML 却叫UserDao.xml);或mapperLocations配置路径错误,XML 未被扫描到。
解决:
- 确保 XML 文件名与接口名完全一致(
UserMapper.java↔UserMapper.xml); - 检查
applicationContext.xml中<property name="mapperLocations" value="classpath:mapper/*.xml"/>的路径是否正确,mapper/目录是否在src/main/resources下; - 运行时查看
target/classes/mapper/目录下是否存在编译后的 XML 文件(Maven clean package 后检查)。
5.4 现象:分页查询总是只返回第一页,pageNum参数传过去没用
原因:PageHelper.startPage()未在 Service 方法开头调用,或调用位置错误(如在for循环里调用)。PageHelper 是基于ThreadLocal的,必须在执行Mapper.select()前一刻调用。
解决:
- 在
UserServiceImpl.listUsers()方法中,return userMapper.listPage(...)前,必须写PageHelper.startPage(pageNum, pageSize); - 确保
listPage()方法是 MyBatis 的select,不是自己写的for循环; - 检查
PageHelperConfig.java是否配置了@Configuration和@Bean,确保 PageHelper Bean 被 Spring 管理。
5.5 现象:JavaFX 打包后运行报错java.lang.NoClassDefFoundError: javafx/application/Application
原因:JDK 版本问题。Java 11+ 已将 JavaFX 从 JDK 中移除,需单独引入 JavaFX SDK 并配置模块路径。
解决:
- 下载 OpenJFX SDK(https://gluonhq.com/products/javafx/),解压;
- 在
pom.xml中添加 JavaFX Maven 依赖(以 Java 17 为例):
<dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>17.0.2</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> <version>17.0.2</version> </dependency>- 打包时用
mvn clean javafx:jlink(需配置javafx-maven-plugin),而非mvn clean package。
6. 答辩验收 checklist:从代码到演示的 7 个必过动作
答辩不是讲 PPT,是现场操作。老师最关心三件事:能不能跑起来?数据能不能增删改查?关键功能有没有明显 bug?我带过的项目里,90% 的“差点挂”都源于没做这七件事。以下是我现在每次交付前强制执行的 checklist,它不保证满分,但能确保不因低级失误丢分。
6.1 环境一致性检查:三台机器,一个结果
| 检查项 | 操作 | 为什么重要 |
|---|---|---|
| JDK 版本 | 在命令行执行java -version,确认是 JDK 8 或 JDK 17(与 pom.xml 中<java.version>一致) | JavaFX 11+ 需 JDK 11+,但 SSM 对 JDK 8 兼容性最好;混用版本会导致UnsupportedClassVersionError |
| MySQL 服务 | 执行netstat -ano | findstr :3306(Windows)或lsof -i :3306(Mac/Linux),确认 MySQL 进程在运行 | 课设环境常因 MySQL 未启动,导致Connection refused,学生以为代码错了,实则环境问题 |
| 数据库初始化 | 用 MySQL 客户端执行CREATE DATABASE demo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,再执行项目提供的init.sql | 表结构缺失、字符集不对(如用utf8而非utf8mb4)会导致中文乱码或插入失败 |
提示:把这三步写成
check_env.bat(Windows)或check_env.sh(Mac/Linux),答辩前双击运行,5 秒内确认绿灯。
6.2 代码级验收:5 个必须能通过的测试点
用 IDEA 打开项目,依次验证:
| 测试点 | 操作 | 预期结果 | 失败应对 |
|---|---|---|---|
| 1. 启动后端 | 运行WebApplication.java(含main方法的 Spring 启动类) | 控制台输出Started WebApplication in X seconds,无红色 ERROR | 检查applicationContext.xml中dataSource的jdbcUrl是否指向本地 MySQL,密码是否正确 |
| 2. 启动客户端 | 运行MainApp.java(JavaFX 入口) | 弹出登录窗口,无ClassNotFoundException | 检查pom.xml中javafx-controls依赖是否生效(IDEA 右侧 Maven 面板看依赖树) |
| 3. 登录流程 | 输入admin/123456(init.sql 预置账号),点击登录 | 跳转到主界面,地址栏显示http://localhost:8080/api/user/list(证明后端连通) | 若卡在登录页,F12 打开浏览器开发者工具(即使桌面应用,JavaFX WebView 也支持),看 Network 是否有 404 |
| 4. 用户新增 | 在主界面点击【新增】,填姓名/手机/部门,保存 | 表格第一行出现新用户,数据库t_user表中INSERT一条记录 | 若保存失败,看控制台UserServiceImpl.saveUser()是否执行到userMapper.insert(),确认事务未被吞异常 |
| 5. Excel 导出 | 点击【导出】,选择保存路径,打开生成的.xlsx文件 | Excel 中显示完整用户列表,中文不乱码 | 若乱码,检查XSSFCell.setCellValue()传入的是String而非byte[],且文件编码为 UTF-8 |
6.3 演示脚本:3 分钟讲清你的项目价值
答辩时老师不会听你讲“我用了 SSM”,他只想知道“这东西能干什么”。我
本文还有配套的精品资源,点击获取