SpringBoot+JavaFX+MyBatis本地桌面应用实战

发布时间:2026/10/24 4:21:05
SpringBoot+JavaFX+MyBatis本地桌面应用实战 简介本资源是一个基于SpringBootMyBatisJavaFX构建的轻量级档案扫描管理桌面应用面向Java全栈初学者与桌面端项目实践者聚焦于解决传统档案数字化过程中服务端逻辑、数据库交互与本地GUI界面协同开发的学习痛点。压缩包共130个文件含24个核心Java源码如LoginStageController、BusDocumentInfo等、65个XML配置/映射文件支撑Spring自动装配与MyBatis SQL绑定、4个FXML界面定义文件承载JavaFX视图层结构以及CSS样式、Properties配置等整体仅146KB结构紧凑、模块职责清晰。已有4208人学习下载适合通过小而完整的项目理解三层架构落地用SpringBoot快速搭建内嵌Web服务与依赖管理借助MyBatis实现Oracle数据库的增删改查与事务控制并依托JavaFX构建跨平台、响应式的扫描操作界面。1. 这不是个“Java桌面程序”Demo而是一套能跑通完整业务闭环的本地化管理系统SpringBoot做后端API、MyBatis管数据持久化、JavaFX搭交互界面三者在单机环境里真正拧成一股绳你见过多少所谓“SpringBoot JavaFX”项目点开一看要么是JavaFX窗体里硬塞了个RestTemplate调测试接口后端连RestController都没写全要么是MyBatis只配了application.yml连Mapper.xml都空着更别说事务和分页。这个项目不一样——它把一个真实可运行的「本地库存管理」场景从头到尾串起来了JavaFX主界面点击“新增商品”触发HTTP请求打到本机8080端口的SpringBoot ControllerController调用Service层Service用MyBatis的SelectProvider动态拼SQL查数据库H2嵌入式新增成功后JavaFX表格实时刷新连带库存预警颜色自动变红/绿。整个链路不依赖外部服务、不走公网、不启动Tomcat外置容器打包成JAR双击就能用。适合正在做课程设计、毕设选题或想落地“前后端一体化桌面应用”的开发者——尤其当你被导师问“你这JavaFX怎么跟数据库联动的”时能直接打开ProductController.java和ProductTableViewController.java两份文件指着代码说清楚每一跳。2. 项目结构与技术栈拆解三层物理隔离逻辑紧耦合的设计真相2.1 目录结构src/main下三个平行世界但package命名暴露了强绑定意图项目采用标准Maven多模块结构但实际是单模块聚合无pom.xml子模块声明目录树如下src/ ├── main/ │ ├── java/ │ │ └── com.example.inventory/ │ │ ├── config/ # SpringBoot配置类DataSourceConfig、MyBatisConfig │ │ ├── controller/ # REST接口ProductController、CategoryController │ │ ├── dao/ # MyBatis Mapper接口ProductMapper.java │ │ ├── entity/ # JPA风格实体Product.java、Category.java含Lombok注解 │ │ ├── service/ # 业务逻辑ProductServiceImpl.java含Transactional │ │ └── javafx/ # JavaFX专属包AppLauncher.java、main.fxml、controllers/ │ ├── resources/ │ │ ├── application.yml # 端口8080、H2数据库路径、MyBatis日志级别 │ │ └── static/ # 空目录预留Web资源位当前未启用 │ └── webapp/ # 空目录传统WAR部署位本项目不用 └── test/ └── java/ # JUnit5测试ProductServiceTest.java含Sql注解初始化H2数据提示javafx/包不在Spring Boot默认扫描路径内所以AppLauncher.java里必须显式调用SpringApplication.run()并传入InventoryApplication.class否则JavaFX线程无法注入Spring Bean。这是新手最容易卡住的点——不是FXML加载失败而是Autowired ProductController为null。2.2 技术栈选型逻辑为什么用H2而不是MySQL为什么MyBatis没上XMLH2数据库application.yml中配置spring.datasource.url: jdbc:h2:file:./data/inventory;DB_CLOSE_ON_EXITFALSE意味着数据文件inventory.mv.db会生成在项目根目录。好处是零安装、免运维关机重启数据不丢坏处是并发写入能力弱本项目单用户场景够用。若要换MySQL只需改三处url、username、password并确保h2依赖换成mysql-connector-java。MyBatis纯注解驱动所有Mapper接口方法均用Select、Insert、Update等注解定义SQL例如Mapper public interface ProductMapper { Select(SELECT * FROM product WHERE category_id #{categoryId} AND stock #{lowStockThreshold}) ListProduct findLowStockProducts(Param(categoryId) Long categoryId, Param(lowStockThreshold) Integer lowStockThreshold); }这种写法省去XML文件维护成本且IDE能直接跳转SQL适合中小型CRUD。但复杂动态SQL如多条件组合查询会变得臃肿——本项目用SelectProvider解决见3.2节。JavaFX与Spring Boot通信机制JavaFX线程Platform.runLater不直接调用Spring Bean而是通过HttpURLConnection发HTTP请求非RestTemplate避免引入额外依赖。ProductTableViewController.java中新增商品代码片段private void handleAddProduct() { String urlStr http://localhost:8080/api/products; try { URL url new URL(urlStr); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Content-Type, application/json; utf-8); conn.setDoOutput(true); String jsonPayload new ObjectMapper().writeValueAsString(product); try (OutputStream os conn.getOutputStream()) { os.write(jsonPayload.getBytes(StandardCharsets.UTF_8)); } int responseCode conn.getResponseCode(); if (responseCode 201) { // CREATED refreshTable(); // 刷新JavaFX表格 } } catch (Exception e) { showErrorAlert(保存失败 e.getMessage()); } }关键点conn.getResponseCode()必须判断201而非200因为Controller返回的是ResponseEntity.created()refreshTable()需在JavaFX主线程执行否则抛IllegalStateException。2.3 模块间依赖关系图箭头方向即调用流向虚线表示跨进程通信调用方被调用方通信方式是否跨进程备注ProductTableViewControllerProductControllerHTTP POST/GET是JavaFX进程 → SpringBoot进程ProductControllerProductServiceImplSpring Bean注入否同进程IoC容器管理ProductServiceImplProductMapperMyBatis代理对象否接口代理底层JDBC执行ProductMapperH2数据库文件JDBC Driver否文件IO非网络连接注意JavaFX和SpringBoot虽在同一JVM启动AppLauncher.main()先启SpringBoot再启JavaFX Stage但HTTP通信本质是进程间调用——这意味着你能用Postman直接访问http://localhost:8080/api/products验证后端是否独立可用。这是调试分层问题的黄金法则先确认后端API能跑通再查JavaFX前端调用逻辑。3. 核心模块代码逐行解析从JavaFX按钮点击到H2数据库落盘的7个关键节点3.1 JavaFX界面层FXML绑定与事件处理的隐式契约main.fxml中商品列表表格定义TableView fx:idproductTable prefHeight400.0 prefWidth700.0 columns TableColumn textID fx:ididColumn/ TableColumn text名称 fx:idnameColumn/ TableColumn text分类 fx:idcategoryColumn/ TableColumn text库存 fx:idstockColumn/ TableColumn text操作 fx:idactionColumn/ /columns /TableView对应ProductTableViewController.java中字段注入FXML private TableViewProduct productTable; FXML private TableColumnProduct, Long idColumn; FXML private TableColumnProduct, String nameColumn; FXML private TableColumnProduct, String categoryColumn; FXML private TableColumnProduct, Integer stockColumn; FXML private TableColumnProduct, Void actionColumn; // Void类型用于放置按钮列关键细节TableColumn泛型必须与TableView泛型一致Product否则setCellValueFactory()会编译报错actionColumn用Void是因为该列只放按钮不绑定数据属性。setCellValueFactory()设置单元格值来源idColumn.setCellValueFactory(new PropertyValueFactory(id)); nameColumn.setCellValueFactory(new PropertyValueFactory(name)); // ...其他列同理这里id是Product类的getter方法名getId()不是字段名——若写成Id或ID运行时抛NoSuchMethodException且IDE不会提示。3.2 SpringBoot后端Controller接收JSON并校验的防御性写法ProductController.java中新增接口PostMapping(/api/products) public ResponseEntityProduct createProduct(Valid RequestBody Product product, BindingResult result) { if (result.hasErrors()) { throw new IllegalArgumentException(参数校验失败 result.getFieldErrors().stream() .map(e - e.getField() : e.getDefaultMessage()) .collect(Collectors.joining(; ))); } Product saved productService.save(product); return ResponseEntity.status(HttpStatus.CREATED) .header(Location, /api/products/ saved.getId()) .body(saved); }Valid触发JSR-303校验Product实体类需加注解public class Product { NotNull(message ID不能为空) private Long id; NotBlank(message 名称不能为空) Size(max 50, message 名称长度不能超过50字符) private String name; Min(value 0, message 库存不能为负数) private Integer stock; // ...getter/setter }BindingResult result必须紧跟RequestBody参数后否则Spring无法注入校验结果ResponseEntity.created()返回201 Created状态码并设置Location头指向新资源URI符合RESTful规范若校验失败手动抛IllegalArgumentException而非返回400 Bad Request因全局异常处理器GlobalExceptionHandler.java会捕获并转为统一JSON错误格式。3.3 MyBatis持久层动态SQL与事务边界的精确控制ProductServiceImpl.java中保存逻辑Service public class ProductServiceImpl implements ProductService { Autowired private ProductMapper productMapper; Override Transactional(rollbackFor Exception.class) // 显式声明回滚异常类型 public Product save(Product product) { if (product.getId() null) { productMapper.insert(product); // 新增 } else { productMapper.updateById(product); // 更新 } return product; } }ProductMapper.java中动态SQL实现Mapper public interface ProductMapper { Insert(INSERT INTO product (name, category_id, stock, created_time) VALUES (#{name}, #{categoryId}, #{stock}, NOW())) Options(useGeneratedKeys true, keyProperty id) void insert(Product product); SelectProvider(type ProductSqlProvider.class, method selectByCondition) ListProduct selectByCondition(ProductQuery query); } // 动态SQL提供类 public class ProductSqlProvider { public String selectByCondition(ProductQuery query) { return new SQL(){{ SELECT(*); FROM(product); if (query.getCategoryId() ! null) { WHERE(category_id #{categoryId}); } if (query.getLowStockThreshold() ! null) { WHERE(stock #{lowStockThreshold}); } if (StringUtils.hasText(query.getSearchKeyword())) { WHERE(name LIKE CONCAT(%, #{searchKeyword}, %)); } }}.toString(); } }注意Options(useGeneratedKeys true, keyProperty id)让MyBatis将H2生成的自增ID回填到product.id字段否则JavaFX端拿到的id仍是nullProductQuery是专门用于查询条件的DTO与Product实体分离避免污染领域模型。4. 运行与调试全流程从零开始启动的6个必做步骤及3个致命陷阱4.1 启动前检查清单6个动作决定你能否看到第一个窗口确认JDK版本项目pom.xml中java.version17/java.version必须用JDK 17JavaFX 17已集成进JDK无需额外下载检查H2数据库路径权限application.yml中./data/inventory目录需有读写权限Windows建议放在非系统盘如D:/inventory/data避免UAC拦截验证Maven依赖完整性执行mvn dependency:tree | grep -E (spring-boot|mybatis|javafx)应看到[INFO] - org.springframework.boot:spring-boot-starter-web:jar:3.1.0:compile [INFO] - org.mybatis.spring.boot:mybatis-spring-boot-starter:jar:3.0.2:compile [INFO] - org.openjfx:javafx-controls:jar:17.0.2:compile编译JavaFX模块若用IntelliJ IDEA需在Project Structure → Modules → Dependencies中添加javafx.controls和javafx.fxml库并勾选Export设置VM Options关键运行配置中添加--module-path C:\path\to\javafx-sdk-17.0.2\lib --add-modules javafx.controls,javafx.fxml路径需替换为本地JavaFX SDK解压路径否则启动报java.lang.module.FindException: Module javafx.controls not found首次运行前清空H2数据删除项目根目录下的data/文件夹避免旧数据导致主键冲突。4.2 启动命令与预期输出终端里出现这三行才算成功在项目根目录执行mvn clean compile exec:java -Dexec.mainClasscom.example.inventory.javafx.AppLauncher终端应依次出现[INFO] Starting AppLauncher using Java 17.0.2 on DESKTOP-XXXX with PID 12345 [INFO] Started AppLauncher in 3.2 seconds (process running for 3.5) [INFO] JavaFX Application started. Server running at http://localhost:8080此时浏览器访问http://localhost:8080/actuator/health应返回{status:UP}JavaFX主窗口弹出表格显示“暂无数据”。4.3 常见问题排查3条血泪经验专治启动失败、数据不更新、HTTP 404现象1JavaFX窗口一闪而退控制台无报错原因AppLauncher.java中launch()调用位置错误。常见误写// ❌ 错误在SpringApplication.run()前调用launch() public static void main(String[] args) { launch(args); // 此时JavaFX Toolkit未初始化 SpringApplication.run(InventoryApplication.class, args); }解决必须在Spring Boot完全启动后再启动JavaFX线程// ✅ 正确用ApplicationRunner延迟执行 Component public class JavaFXStarter implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { Platform.startup(() - { // JavaFX初始化代码 AppLauncher.launch(AppLauncher.class, args.getSourceArgs()); }); } }现象2点击“新增商品”按钮JavaFX报错java.net.ConnectException: Connection refused原因SpringBoot服务未启动成功或端口被占用。检查application.yml中server.port: 8080是否与其他程序冲突如Chrome DevTools、旧版IDEA内置服务器。解决执行netstat -ano | findstr :8080Windows或lsof -i :8080Mac/Linux查PID杀死进程后重试或临时改server.port: 8081同步修改JavaFX端URL字符串。现象3JavaFX表格始终空白但Postman调GET /api/products能返回JSON数据原因TableView未设置ObservableList数据源或setCellValueFactory()绑定字段名与实体getter不匹配。解决在ProductTableViewController.initialize()中必须调用productTable.setItems(FXCollections.observableArrayList(productService.findAll()));检查Product类是否有getId()方法非getid()或getID()IDEA可按AltInsert生成Getter确保命名规范。5. 二次开发实战如何快速接入MySQL、增加导出Excel功能、改造为多窗口架构5.1 替换H2为MySQL5步完成数据库迁移不改一行业务代码步骤操作配置文件位置关键参数说明1添加MySQL驱动依赖pom.xmlartifactIdmysql-connector-java/artifactIdversion8.0.33/version2修改数据库连接配置application.ymlspring.datasource.url: jdbc:mysql://localhost:3306/inventory?useSSLfalseserverTimezoneAsia/Shanghai3创建MySQL数据库MySQL客户端CREATE DATABASE inventory CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4初始化表结构schema.sql新建文件复制H2建表SQL调整AUTO_INCREMENT和DATETIME类型5关闭H2控制台application.ymlspring.h2.console.enabled: false避免端口冲突注意MySQL时区必须与JVM一致否则NOW()函数存入时间偏移8小时。serverTimezoneAsia/Shanghai是刚需不是可选项。5.2 增加Excel导出功能Apache POI集成与JavaFX进度条联动在ProductTableViewController.java中新增导出按钮事件FXML private void handleExportToExcel() { FileChooser fileChooser new FileChooser(); fileChooser.setTitle(导出Excel); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter(Excel Files, *.xlsx)); File file fileChooser.showSaveDialog(productTable.getScene().getWindow()); if (file ! null) { // 启动后台任务避免阻塞UI TaskVoid exportTask new TaskVoid() { Override protected Void call() throws Exception { ListProduct products productService.findAll(); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(商品列表); // 写入表头 Row headerRow sheet.createRow(0); String[] headers {ID, 名称, 分类, 库存, 创建时间}; for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); } // 写入数据 for (int i 0; i products.size(); i) { Row row sheet.createRow(i 1); Product p products.get(i); row.createCell(0).setCellValue(p.getId()); row.createCell(1).setCellValue(p.getName()); row.createCell(2).setCellValue(p.getCategoryName()); // 假设已关联分类名 row.createCell(3).setCellValue(p.getStock()); row.createCell(4).setCellValue(p.getCreatedTime().toString()); } try (FileOutputStream out new FileOutputStream(file)) { workbook.write(out); } return null; } }; // 绑定进度条 ProgressBar progressBar new ProgressBar(); progressBar.progressProperty().bind(exportTask.progressProperty()); exportTask.setOnSucceeded(e - { showAlert(导出成功, 文件已保存至 file.getAbsolutePath()); }); exportTask.setOnFailed(e - { showAlert(导出失败, exportTask.getException().getMessage()); }); new Thread(exportTask).start(); } }关键点必须用Task封装耗时操作否则Excel生成过程冻结JavaFX界面progressBar.progressProperty().bind()实现进度实时更新XSSFWorkbook支持.xlsx格式比HSSFWorkbook.xls更现代。5.3 改造为多窗口架构主窗口编辑弹窗的父子级联控制当前项目所有操作在单窗口完成扩展性差。改造思路主窗口MainViewController.java只负责列表展示和新增/编辑按钮编辑弹窗EditProductDialogController.java独立FXML通过Stage模态显示弹窗关闭后主窗口监听onHidden事件刷新表格。MainViewController.java中打开弹窗逻辑FXML private void handleEditProduct() { try { FXMLLoader loader new FXMLLoader( getClass().getResource(/fxml/edit_product_dialog.fxml)); Parent root loader.load(); EditProductDialogController controller loader.getController(); controller.setProduct(selectedProduct); // 传入待编辑对象 Stage dialogStage new Stage(); dialogStage.setTitle(编辑商品); dialogStage.initModality(Modality.WINDOW_MODAL); dialogStage.initOwner(productTable.getScene().getWindow()); dialogStage.setScene(new Scene(root)); // 监听弹窗关闭事件 dialogStage.setOnHidden(e - { if (controller.isSaved()) { // controller内设标志位 refreshTable(); // 刷新主表格 } }); dialogStage.showAndWait(); // 阻塞直到弹窗关闭 } catch (IOException e) { showErrorAlert(打开编辑窗口失败 e.getMessage()); } }优势职责分离清晰编辑逻辑不污染主窗口模态窗口Modality.WINDOW_MODAL防止用户切换到主窗口操作showAndWait()保证弹窗关闭后才执行后续代码避免竞态。从那以后我每次接到“桌面后端”需求第一件事就是检查三点JavaFX线程是否绕过Spring IoC直接new对象、HTTP通信是否用HttpURLConnection而非RestTemplate减少依赖、H2路径是否写死相对路径。这三条踩过坑现在成了我新建项目的checklist前三项。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询