☰
JavaFX+SSM混合架构实战:内嵌Jetty跑通SpringMVC+MyBatis完整流程
2026/10/16 3:48:00 网站建设 项目流程

简介:本资源是一个基于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”玩具:它真能跑通完整MVC流程,连MyBatis动态SQL和Spring事务回滚都配好了

你肯定见过那种“JavaFX + SSM”的Demo——界面是JavaFX写的,后端硬塞进一个Spring Boot启动类,Controller里new一个Service,Service里new一个Mapper,连@Autowired都懒得加,更别说事务管理。这种项目点开就报NullPointerException,改两行代码就崩,纯属教学幻觉。但这个“JavaFX完整版项目(Spring+SpringMVC+Mybatis)”不是。它用JavaFX做前端壳,内嵌Jetty启动一个标准的SpringMVC Web容器,所有请求走DispatcherServlet,Controller调Service,Service走MyBatis接口代理,Mapper XML里写了<foreach>和<choose>,事务注解@Transactional生效,异常时数据库真回滚。我拿它给某高校实训班学生搭毕设框架,3天内80%的人跑通了带用户登录、订单提交、库存扣减的闭环流程。适合需要真实分层架构、又不想被Tomcat部署和前端构建工具劝退的Java初学者;也适合想快速验证SSM整合细节的老手——比如你想确认SqlSessionFactoryBean怎么注入到SqlSessionTemplate,或者@RequestMapping在JavaFX线程里怎么被正确拦截,这个项目里全有现成答案。


2. 架构设计:为什么用JavaFX当壳?三层结构如何穿透Web容器边界

这个项目没走“JavaFX直接连数据库”的野路子,也没用WebView加载HTML假装是桌面应用。它采用一种更务实、更可控的混合架构:JavaFX作为独立GUI进程,通过HTTP与内嵌Web服务通信。整个系统实际是两个进程:一个是JavaFX主窗口(MainApp.java),另一个是内嵌的Jetty服务器(EmbeddedJettyServer.java),后者启动标准SpringMVC上下文。这种设计规避了JavaFX线程模型与Spring Bean生命周期的冲突,也让调试变得清晰——你可以用Postman直接调后端接口,确认逻辑没问题后再连前端。

2.1 目录结构:看清SSM三件套在JavaFX项目里怎么落位

项目目录不是把Web项目拖进JavaFX工程就完事。它做了明确分层:

src/ ├── main/ │ ├── java/ │ │ └── com/example/demo/ │ │ ├── config/ # Spring配置类(非XML!) │ │ │ ├── AppConfig.java # @Configuration,定义DataSource、SqlSessionFactory等 │ │ │ ├── WebConfig.java # @EnableWebMvc,注册ViewResolver、静态资源处理器 │ │ │ └── TransactionConfig.java # @EnableTransactionManagement,配置PlatformTransactionManager │ │ ├── controller/ # SpringMVC Controller │ │ │ └── UserController.java # @RestController,返回JSON │ │ ├── service/ # Service层,含@Transactional │ │ │ └── UserServiceImpl.java │ │ ├── dao/ # MyBatis Mapper接口(无XML!全注解) │ │ │ └── UserMapper.java # @Mapper,@Select/@Update等注解写SQL │ │ ├── entity/ # POJO,对应数据库表 │ │ │ └── User.java │ │ ├── fx/ # JavaFX专属包 │ │ │ ├── MainApp.java # JavaFX Application入口 │ │ │ ├── controller/ # JavaFX Controller(非Spring!) │ │ │ │ └── MainViewController.java # FXML绑定的事件处理器 │ │ │ └── util/ # HTTP工具类 │ │ │ └── HttpClientUtil.java # 封装HttpURLConnection,发JSON请求 │ │ └── EmbeddedJettyServer.java # 启动Jetty,加载Spring WebApplicationContext │ └── resources/ │ ├── application.properties # 数据库连接、日志级别等 │ └── logback-spring.xml # 日志配置 └── pom.xml # 关键依赖:javafx-maven-plugin、jetty-server、spring-webmvc、mybatis-spring

提示:pom.xml里没有spring-boot-starter-web,因为不用Spring Boot自动装配。而是手动引入spring-webmvc+jetty-server,这样你能看清DispatcherServlet怎么注册、ContextLoaderListener怎么加载根上下文——这是理解SSM整合本质的关键。

2.2 核心配置链:从JavaFX启动到SpringMVC容器就绪的5个关键节点

JavaFX程序启动后,并不直接加载Spring。它走了一条显式、可调试的初始化链:

  1. MainApp.start():JavaFX主线程启动,加载main.fxml,设置MainViewController为控制器;
  2. MainViewController.initialize():FXML加载完成后触发,这里调用EmbeddedJettyServer.start();
  3. EmbeddedJettyServer.start():创建JettyServer实例,配置ServletContextHandler,关键代码如下:
// EmbeddedJettyServer.java public void start() throws Exception { Server server = new Server(8080); ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/"); // 关键:注册Spring MVC的DispatcherServlet,并传入WebApplicationContext DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class); dispatcherServlet.setContextConfigLocation("com.example.demo.config.WebConfig"); ServletHolder servletHolder = new ServletHolder(dispatcherServlet); context.addServlet(servletHolder, "/*"); server.setHandler(context); server.start(); server.join(); // 阻塞,保持Jetty运行 }
  1. WebConfig加载:AnnotationConfigWebApplicationContext扫描@Configuration类,触发AppConfig创建DataSource、SqlSessionFactoryBean,再由MyBatisConfig(隐含在AppConfig中)注册MapperScannerConfigurer,自动扫描dao包下的@Mapper接口;
  2. UserController就绪:@RestController被Spring MVC的RequestMappingHandlerMapping注册,URL/api/user/login绑定到具体方法。

这条链路里,EmbeddedJettyServer是桥梁,WebConfig是粘合剂,AppConfig是地基。少任何一个环节,你的JavaFX按钮点下去就是404或500。

2.3 请求链路实测:一个登录请求如何穿越JavaFX→HTTP→SpringMVC→MyBatis

我们以MainViewController.loginButton.setOnAction()为例,看数据怎么流动:

// MainViewController.java @FXML private void handleLogin() { String username = usernameField.getText(); String password = passwordField.getText(); // 步骤1:JavaFX线程构造JSON请求体 Map<String, String> loginData = new HashMap<>(); loginData.put("username", username); loginData.put("password", password); // 步骤2:HttpClientUtil发POST请求(阻塞式,简单起见未用异步) try { String response = HttpClientUtil.post("http://localhost:8080/api/user/login", loginData); // 步骤3:解析JSON响应,更新UI JsonObject json = JsonParser.parseString(response).getAsJsonObject(); if (json.get("success").getAsBoolean()) { statusLabel.setText("登录成功"); } else { statusLabel.setText("用户名或密码错误"); } } catch (Exception e) { statusLabel.setText("网络错误:" + e.getMessage()); } }

后端UserController.login()接收请求:

// UserController.java @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; // Spring注入,非new @PostMapping("/login") public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials) { Map<String, Object> result = new HashMap<>(); try { // 步骤4:调Service,事务在此开启 User user = userService.login(credentials.get("username"), credentials.get("password")); result.put("success", true); result.put("user", user); return ResponseEntity.ok(result); } catch (AuthenticationException e) { result.put("success", false); result.put("message", e.getMessage()); return ResponseEntity.badRequest().body(result); } } }

UserServiceImpl.login()执行业务逻辑:

// UserServiceImpl.java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; // MyBatis动态代理对象 @Override @Transactional // 关键:事务注解生效 public User login(String username, String password) throws AuthenticationException { // 步骤5:MyBatis执行SQL,查数据库 User user = userMapper.findByUsername(username); if (user == null || !password.equals(user.getPassword())) { throw new AuthenticationException("用户名或密码错误"); } // 假设此处有更新登录时间的操作,若抛异常,此操作会回滚 user.setLastLoginTime(new Date()); userMapper.update(user); // 此update在同一个事务中 return user; } }

整个链路:JavaFX UI →HttpClientUtil发HTTP → Jetty接收 → DispatcherServlet路由 →UserController→UserService(事务开启) →UserMapper(MyBatis代理执行SQL)。每一步都可断点调试,没有黑匣子。


3. 运行指南:从零开始跑通,避开JDK版本、模块化、FXML路径三大玄学坑

这个项目不是“下载解压双击运行”,它对JDK版本、JavaFX模块路径、FXML资源定位有明确要求。我踩过所有坑,下面是你必须按顺序做的6件事,漏一步就卡在ClassNotFoundException或Location is required。

3.1 环境准备:JDK 11 + JavaFX SDK 17 是唯一验证组合

  • JDK必须用11:项目pom.xml中maven-compiler-plugin指定<source>11</source><target>11</target>。JDK 17+会因模块化报错java.lang.module.ResolutionException: Modules javafx.controls and javafx.fxml export package javafx.scene.control to module ...;JDK 8则缺少var关键字和HttpClient,编译不过。
  • JavaFX SDK必须用17:pom.xml中javafx-maven-plugin的<version>是0.0.8,它只兼容JavaFX 17 SDK。下载地址:https://gluonhq.com/products/javafx/ (选JavaFX SDK 17,非JRE)。解压后记下路径,如D:\javafx-sdk-17.0.1。
  • IDE配置:IntelliJ IDEA中,Project Structure → Project → Project SDK选JDK 11;Project Settings → Modules → Dependencies里,点击+ → JARs or directories,添加D:\javafx-sdk-17.0.1\lib下所有.jar文件。

注意:不要用--add-modules javafx.controls,javafx.fxml这种VM参数!javafx-maven-plugin已自动处理模块路径。手动加反而导致重复加载报错。

3.2 Maven构建:跳过测试、强制刷新依赖、检查生成的jar包结构

在项目根目录执行:

# 步骤1:清理并跳过测试(测试用例可能未写全,跳过避免失败) mvn clean package -Dmaven.test.skip=true # 步骤2:检查target目录下是否生成了fat jar(含所有依赖) ls target/*.jar # 应看到:demo-1.0-SNAPSHOT.jar(约25MB,含jetty、spring、mybatis等) # 步骤3:验证jar包是否包含JavaFX类(关键!) jar -tf target/demo-1.0-SNAPSHOT.jar | grep "javafx/scene/control" # 必须有输出,否则JavaFX控件加载失败

如果jar -tf没输出,说明javafx-maven-plugin没生效。检查pom.xml中该插件是否在<build><plugins>下,且<configuration><modules>包含javafx.controls和javafx.fxml。

3.3 启动与验证:先启后端再启前端,用curl确认API可用性

项目启动分两步,必须先确保后端API通了,再启动JavaFX界面:

# 步骤1:命令行启动jar(后台运行,方便查看日志) java -jar target/demo-1.0-SNAPSHOT.jar & # 步骤2:等待控制台输出"Started ServerConnector"和"Started o.e.j.s.h.ContextHandler@..."后,用curl测试 curl -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 应返回:{"success":true,"user":{"id":1,"username":"admin",...}} # 步骤3:此时再双击运行jar,或IDE中右键`MainApp.java → Run`,JavaFX窗口才会正常加载

如果curl返回404,检查EmbeddedJettyServer中server.setHandler(context)是否在server.start()前调用;如果返回500,看日志里是否有NoSuchBeanDefinitionException,大概率是AppConfig没被WebConfig正确import。

3.4 常见问题排查:3条血泪经验,专治启动失败、界面空白、请求超时

现象1:JavaFX窗口弹出但主界面空白,控制台无报错

原因:MainApp.java中FXMLLoader.setLocation()路径错误,或main.fxml不在src/main/resources/fx/下。项目约定FXML必须放在resources/fx/,且MainApp中写死路径:

// MainApp.java fxmlLoader.setLocation(getClass().getResource("/fx/main.fxml")); // 注意开头的"/"

解决:确认src/main/resources/fx/main.fxml存在,且文件编码是UTF-8(Windows记事本保存易出错,用IDEA另存为UTF-8)。

现象2:点击登录按钮,控制台报java.net.ConnectException: Connection refused

原因:Jetty没启动成功,或端口被占用。EmbeddedJettyServer中server.start()抛异常但被静默吞掉。
解决:在EmbeddedJettyServer.start()方法末尾加日志:

System.out.println("Jetty started on port 8080, context path: " + context.getContextPath());

如果这行没打印,说明server.start()失败。检查application.properties中spring.datasource.url是否正确(默认jdbc:h2:mem:testdb,需H2驱动在classpath)。

现象3:登录成功后,JavaFX界面上statusLabel不更新,仍显示空

原因:HttpClientUtil.post()在JavaFX主线程中执行了耗时的HTTP请求,导致UI线程阻塞,setText()被挂起。
解决:必须将HTTP调用移到后台线程。修改handleLogin():

@FXML private void handleLogin() { // ... 构造loginData // 改为后台线程执行 new Thread(() -> { try { String response = HttpClientUtil.post("http://localhost:8080/api/user/login", loginData); // 回到JavaFX线程更新UI Platform.runLater(() -> { JsonObject json = JsonParser.parseString(response).getAsJsonObject(); statusLabel.setText(json.get("success").getAsBoolean() ? "登录成功" : "失败"); }); } catch (Exception e) { Platform.runLater(() -> statusLabel.setText("网络错误:" + e.getMessage())); } }).start(); }

提示:这是JavaFX开发铁律——任何耗时操作(IO、计算)都不能在Platform.runLater外执行,否则UI假死。我第一次没加Platform.runLater,调了2小时才意识到是线程模型问题。


4. 核心模块拆解:UserMapper动态SQL与UserService事务回滚的完整实现

很多教程讲MyBatis只教@Select("select * from user"),这个项目却用了真实场景的动态SQL;讲事务只说@Transactional,这里却演示了异常类型、回滚边界、以及如何验证回滚生效。下面拆解最核心的UserMapper和UserServiceImpl。

4.1 UserMapper:注解式动态SQL,比XML更轻量,支持if/foreach

项目放弃XML配置,全部用@SelectProvider+@UpdateProvider实现动态SQL。UserMapper.java如下:

// UserMapper.java @Mapper public interface UserMapper { // 根据用户名查询,支持模糊匹配 @SelectProvider(type = UserSqlProvider.class, method = "findByUsername") User findByUsername(@Param("username") String username); // 批量插入用户,用foreach遍历List @InsertProvider(type = UserSqlProvider.class, method = "batchInsert") int batchInsert(@Param("users") List<User> users); // 条件更新,用@UpdateProvider实现动态SET @UpdateProvider(type = UserSqlProvider.class, method = "updateSelective") int updateSelective(@Param("user") User user, @Param("fields") String[] fields); }

UserSqlProvider是SQL生成器,用Java代码拼接SQL,类型安全:

// UserSqlProvider.java public class UserSqlProvider { // 动态WHERE:只有username不为空才加条件 public String findByUsername(Map<String, Object> params) { String username = (String) params.get("username"); return new SQL(){{ SELECT("*"); FROM("user"); if (username != null && !username.trim().isEmpty()) { WHERE("username = #{username}"); } }}.toString(); } // foreach批量插入 public String batchInsert(Map<String, Object> params) { List<User> users = (List<User>) params.get("users"); return new SQL(){{ INSERT_INTO("user"); INTO_COLUMNS("username", "password", "email", "created_time"); for (int i = 0; i < users.size(); i++) { User u = users.get(i); INTO_VALUES("#{users[" + i + "].username}", "#{users[" + i + "].password}", "#{users[" + i + "].email}", "#{users[" + i + "].createdTime}"); } }}.toString(); } // 动态SET:只更新传入的字段 public String updateSelective(Map<String, Object> params) { User user = (User) params.get("user"); String[] fields = (String[]) params.get("fields"); return new SQL(){{ UPDATE("user"); if (fields != null) { for (String field : fields) { switch (field) { case "username": SET("username = #{user.username}"); break; case "email": SET("email = #{user.email}"); break; case "last_login_time": SET("last_login_time = #{user.lastLoginTime}"); break; } } } WHERE("id = #{user.id}"); }}.toString(); } }

逻辑说明:@SelectProvider让MyBatis在运行时调用UserSqlProvider.findByUsername()生成SQL字符串,再执行。params是@Param传递的Map,#{}语法保证SQL注入防护。new SQL(){}是MyBatis内置的SQL构建器,比字符串拼接更安全。

4.2 UserServiceImpl:事务传播行为与回滚验证的实战写法

UserService的事务不是简单加个注解。它处理了两种典型场景:读操作不开启事务、写操作强制新事务,并用自定义异常触发回滚:

// UserServiceImpl.java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; // Spring Security的BCrypt // 查询方法:readOnly=true,不开启事务,提升性能 @Override @Transactional(readOnly = true) public User findByUsername(String username) { return userMapper.findByUsername(username); } // 登录方法:需要事务,但只读,所以也设readOnly @Override @Transactional(readOnly = true) public User login(String username, String password) throws AuthenticationException { User user = userMapper.findByUsername(username); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { throw new AuthenticationException("用户名或密码错误"); // 自定义运行时异常 } return user; } // 注册方法:写操作,需要完整事务 @Override @Transactional public User register(User user) throws RegistrationException { // 步骤1:检查用户名是否已存在(SELECT) if (userMapper.findByUsername(user.getUsername()) != null) { throw new RegistrationException("用户名已存在"); } // 步骤2:加密密码(内存操作,不涉及DB) user.setPassword(passwordEncoder.encode(user.getPassword())); user.setCreatedTime(new Date()); // 步骤3:插入用户(INSERT) userMapper.insert(user); // 步骤4:发送欢迎邮件(模拟外部调用,假设此处抛异常) try { sendWelcomeEmail(user.getEmail()); } catch (Exception e) { // 关键:此处抛出运行时异常,事务会回滚,user插入操作撤销 throw new RegistrationException("邮件发送失败,注册已回滚", e); } return user; } // 模拟邮件发送,故意抛异常用于验证回滚 private void sendWelcomeEmail(String email) { if ("fail@example.com".equals(email)) { throw new RuntimeException("SMTP connection timeout"); } System.out.println("Welcome email sent to " + email); } }

如何验证事务回滚生效?

  1. 启动项目,用Postman调POST /api/user/register,传{"username":"test","password":"123","email":"fail@example.com"};
  2. 观察控制台:RegistrationException抛出,且userMapper.insert(user)的SQL日志显示insert into user ...被执行;
  3. 再调GET /api/user?username=test,返回空——证明插入被回滚;
  4. 改邮箱为success@example.com重试,这次能查到用户。

参数说明:@Transactional(readOnly = true)告诉Spring这是只读事务,数据库连接可设为只读,某些数据库(如MySQL)会优化锁策略;@Transactional无参数时,默认rollbackFor = RuntimeException.class,所以RegistrationException(继承RuntimeException)会触发回滚。如果抛Exception(检查型异常),必须显式写rollbackFor = Exception.class。

4.3 异常体系设计:为什么用自定义异常而非通用Exception

项目定义了AuthenticationException和RegistrationException,都继承RuntimeException:

// exception/AuthenticationException.java public class AuthenticationException extends RuntimeException { public AuthenticationException(String message) { super(message); } public AuthenticationException(String message, Throwable cause) { super(message, cause); } }

好处有三:

  • 事务回滚精准:@Transactional默认只对RuntimeException及其子类回滚。如果用Exception,需额外配置,易遗漏;
  • Controller分层处理:UserController可捕获特定异常,返回不同HTTP状态码:
    @ExceptionHandler(AuthenticationException.class) public ResponseEntity<Map<String, Object>> handleAuthError(AuthenticationException e) { Map<String, Object> error = Map.of("success", false, "message", e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); }
  • 日志可追溯:Logback配置中可为AuthenticationException单独设日志级别,便于监控登录失败率。

5. 二次开发指南:3个高频改造场景与验证技巧,附可直接复用的代码片段

拿到这个项目,90%的开发者第一件事是换数据库、加新表、改界面。下面三个场景我反复实践过,每个都给出最小改动步骤、必改配置项、验证是否成功的标志,以及防翻车提示。

5.1 场景1:把H2内存数据库换成MySQL,5步搞定不丢数据

H2适合演示,但生产必须换MySQL。这不是改个URL就行,涉及驱动、连接池、SQL方言三处。

步骤操作验证标志防翻车提示
1. 加MySQL驱动pom.xml中删掉com.h2database:h2,加:
xml<br><dependency><br> <groupId>mysql</groupId><br> <artifactId>mysql-connector-java</artifactId><br> <version>8.0.33</version><br></dependency><br>
mvn compile成功,无ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL 8+驱动类名是com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver
2. 改数据库配置application.properties中:
spring.datasource.url=jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=your_password
启动时控制台出现Loaded JDBC driver: com.mysql.cj.jdbc.DriverURL必须加serverTimezone=UTC,否则MySQL 8+报时区错
3. 创建数据库与表在MySQL中执行:
CREATE DATABASE demo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
然后运行项目自带的schema-h2.sql(删掉H2特有语法如AUTO_INCREMENT,改为AUTO_INCREMENT)
EmbeddedJettyServer启动后,日志出现Creating tables和Inserting sample data表引擎必须用InnoDB(支持事务),别用MyISAM
4. 调整SQL方言UserSqlProvider.batchInsert()中,MySQL的INSERT ... VALUES (), ()语法与H2一致,无需改;但若用ON DUPLICATE KEY UPDATE,需在@InsertProvider中加判断UserMapper.batchInsert()调用成功,数据库有数据MySQL批量插入上限默认1000行,大数据量要分批
5. 验证事务用register接口注册用户,故意让邮件发送失败,然后查MySQL表,确认无记录控制台日志有Rolling back transaction,且SELECT * FROM user WHERE username='test'返回空启动时看日志是否有Using DataSource [HikariDataSource],证明连接池生效

我的血泪经验:第3步建库时,字符集必须用utf8mb4,否则存emoji报错。曾经有个学生用utf8,存微信昵称时报Incorrect string value,折腾半天才发现是MySQL的utf8其实是utf8mb3,不支持4字节Unicode。

5.2 场景2:新增“订单”模块,从建表到JavaFX界面联动

加新功能最怕改坏旧逻辑。按“数据库→Mapper→Service→Controller→JavaFX”顺序推进,每步验证。

步骤1:建表(MySQL)

CREATE TABLE `order` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `amount` decimal(10,2) NOT NULL, `status` varchar(20) DEFAULT 'PENDING', `created_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

步骤2:写Mapper接口与Provider

// dao/OrderMapper.java @Mapper public interface OrderMapper { @InsertProvider(type = OrderSqlProvider.class, method = "insert") long insert(@Param("order") Order order); @Select("SELECT * FROM `order` WHERE user_id = #{userId}") List<Order> findByUserId(@Param("userId") long userId); } // sqlprovider/OrderSqlProvider.java public class OrderSqlProvider { public String insert(Map<String, Object> params) { Order order = (Order) params.get("order"); return new SQL(){{ INSERT_INTO("`order`"); INTO_COLUMNS("user_id", "amount", "status", "created_time"); INTO_VALUES("#{order.userId}", "#{order.amount}", "#{order.status}", "#{order.createdTime}"); }}.toString(); } }

步骤3:写Service与事务

// service/OrderService.java @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private UserService userService; // 复用已有Service @Transactional public Order createOrder(long userId, BigDecimal amount) { // 先查用户是否存在(事务内) User user = userService.findById(userId); if (user == null) { throw new IllegalArgumentException("用户不存在"); } // 创建订单 Order order = new Order(); order.setUserId(userId); order.setAmount(amount); order.setStatus("PENDING"); order.setCreatedTime(new Date()); orderMapper.insert(order); return order; } }

步骤4:JavaFX界面联动(最小改动)
在MainViewController.java中加一个按钮和事件:

@FXML private Button createOrderButton; @FXML private void handleCreateOrder() { try { // 获取当前登录用户ID(假设已存于全局变量) long userId = CurrentUser.getId(); BigDecimal amount = new BigDecimal("99.99"); // 调用新接口 String response = HttpClientUtil.post( "http://localhost:8080/api/order/create", Map.of("userId", String.valueOf(userId), "amount", "99.99") ); JsonObject json = JsonParser.parseString(response).getAsJsonObject(); if (json.get("success").getAsBoolean()) { statusLabel.setText("订单创建成功,ID:" + json.get("orderId").getAsString()); } } catch (Exception e) { statusLabel.setText("创建失败:" + e.getMessage()); } }

验证标志:点击按钮后,MySQL的order表有新记录,且status为PENDING。如果userService.findById()查不到用户,应抛异常且订单不插入——证明事务跨Service生效。

5.3 场景3:JavaFX界面国际化(中英文切换),不重启应用

JavaFX原生支持ResourceBundle,但需配合Locale动态切换。项目已预留messages_en.properties和messages_zh.properties,只需3处改动:

1. 在MainViewController中加语言切换逻辑

// MainViewController.java private ResourceBundle bundle; private Locale currentLocale = Locale.getDefault(); @FXML private void switchToEnglish() { currentLocale = Locale.ENGLISH; reloadLabels(); } @FXML private void switchToChinese() { currentLocale = Locale.CHINA; reloadLabels(); } private void reloadLabels() { bundle = ResourceBundle.getBundle("messages", currentLocale); loginButton.setText(bundle.getString("login.button.text")); usernameLabel.setText(bundle.getString("login.username.label")); // ... 其他label }

2. 在initialize()中初始化

public void initialize() { reloadLabels(); // 首次加载 }

3. 在messages_zh.properties中补全键值

login.button.text=登录 login.username.label=用户名 login.password.label=密码

验证标志:点击“切换英文”按钮,所有文字实时变为英文,无需重启JavaFX应用。这是JavaFXResourceBundle的特性——只要setText()重新调用,就会取新Locale的值。

从那以后我每次加新模块,都强制走一遍“建表→写Mapper→写Service→写Controller→JavaFX调用→curl验证→日志确认”的闭环。哪怕只是改个按钮文字,也要确保HTTP请求能打到后端、后端能返回JSON、前端能解析并更新UI。这套肌肉记忆让我在客户现场改需求时,30分钟内就能交付可演示的版本。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询