简介:本资源是一个基于SpringBoot+MyBatis+JavaFX构建的轻量级档案扫描管理桌面应用,面向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 Controller;Controller调用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没上XML?
H2数据库:
application.yml中配置spring.datasource.url: jdbc:h2:file:./data/inventory;DB_CLOSE_ON_EXIT=FALSE,意味着数据文件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}") List<Product> 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 模块间依赖关系图:箭头方向即调用流向,虚线表示跨进程通信
| 调用方 | 被调用方 | 通信方式 | 是否跨进程 | 备注 |
|---|---|---|---|---|
ProductTableViewController | ProductController | HTTP POST/GET | 是 | JavaFX进程 → SpringBoot进程 |
ProductController | ProductServiceImpl | Spring Bean注入 | 否 | 同进程,IoC容器管理 |
ProductServiceImpl | ProductMapper | MyBatis代理对象 | 否 | 接口代理,底层JDBC执行 |
ProductMapper | H2数据库文件 | 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:id="productTable" prefHeight="400.0" prefWidth="700.0"> <columns> <TableColumn text="ID" fx:id="idColumn"/> <TableColumn text="名称" fx:id="nameColumn"/> <TableColumn text="分类" fx:id="categoryColumn"/> <TableColumn text="库存" fx:id="stockColumn"/> <TableColumn text="操作" fx:id="actionColumn"/> </columns> </TableView>对应ProductTableViewController.java中字段注入:
@FXML private TableView<Product> productTable; @FXML private TableColumn<Product, Long> idColumn; @FXML private TableColumn<Product, String> nameColumn; @FXML private TableColumn<Product, String> categoryColumn; @FXML private TableColumn<Product, Integer> stockColumn; @FXML private TableColumn<Product, 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 ResponseEntity<Product> 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") List<Product> 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仍是null;ProductQuery是专门用于查询条件的DTO,与Product实体分离,避免污染领域模型。
4. 运行与调试全流程:从零开始启动的6个必做步骤及3个致命陷阱
4.1 启动前检查清单:6个动作决定你能否看到第一个窗口
- 确认JDK版本:项目
pom.xml中<java.version>17</java.version>,必须用JDK 17+(JavaFX 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(关键!):运行配置中添加:
路径需替换为本地JavaFX SDK解压路径,否则启动报--module-path "C:\path\to\javafx-sdk-17.0.2\lib" --add-modules javafx.controls,javafx.fxmljava.lang.module.FindException: Module javafx.controls not found; - 首次运行前清空H2数据:删除项目根目录下的
data/文件夹,避免旧数据导致主键冲突。
4.2 启动命令与预期输出:终端里出现这三行才算成功
在项目根目录执行:
mvn clean compile exec:java -Dexec.mainClass="com.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
现象1:JavaFX窗口一闪而退,控制台无报错
原因: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 :8080(Windows)或lsof -i :8080(Mac/Linux)查PID; - 杀死进程后重试;
- 或临时改
server.port: 8081,同步修改JavaFX端URL字符串。
现象3:JavaFX表格始终空白,但Postman调GET /api/products能返回JSON数据
原因:TableView未设置ObservableList数据源,或setCellValueFactory()绑定字段名与实体getter不匹配。
解决:
- 在
ProductTableViewController.initialize()中必须调用:productTable.setItems(FXCollections.observableArrayList(productService.findAll())); - 检查
Product类是否有getId()方法(非getid()或getID()),IDEA可按Alt+Insert生成Getter确保命名规范。
5. 二次开发实战:如何快速接入MySQL、增加导出Excel功能、改造为多窗口架构
5.1 替换H2为MySQL:5步完成数据库迁移,不改一行业务代码
| 步骤 | 操作 | 配置文件位置 | 关键参数说明 |
|---|---|---|---|
| 1 | 添加MySQL驱动依赖 | pom.xml | <artifactId>mysql-connector-java</artifactId><version>8.0.33</version> |
| 2 | 修改数据库连接配置 | application.yml | spring.datasource.url: jdbc:mysql://localhost:3306/inventory?useSSL=false&serverTimezone=Asia/Shanghai |
| 3 | 创建MySQL数据库 | MySQL客户端 | CREATE DATABASE inventory CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| 4 | 初始化表结构 | schema.sql(新建文件) | 复制H2建表SQL,调整AUTO_INCREMENT和DATETIME类型 |
| 5 | 关闭H2控制台 | application.yml | spring.h2.console.enabled: false(避免端口冲突) |
注意:MySQL时区必须与JVM一致,否则
NOW()函数存入时间偏移8小时。serverTimezone=Asia/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 Task<Void> exportTask = new Task<Void>() { @Override protected Void call() throws Exception { List<Product> 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前三项。希望帮到你。
本文还有配套的精品资源,点击获取