JavaFX+SSM混合架构实战:内嵌Jetty跑通SpringMVC+MyBatis完整流程
简介本资源是一个基于JavaFX与SSMSpringSpringMVCMyBatis框架融合开发的企业级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协同开发模式、双向数据绑定实践、FXMLController解耦设计及ERP典型模块如员工/订单管理的实现逻辑。1. 这不是个“JavaFX桌面版SSM”玩具它真能跑通完整MVC流程连MyBatis动态SQL和Spring事务回滚都配好了你肯定见过那种“JavaFX SSM”的Demo——界面是JavaFX写的后端硬塞进一个Spring Boot启动类Controller里new一个ServiceService里new一个Mapper连Autowired都懒得加更别说事务管理。这种项目点开就报NullPointerException改两行代码就崩纯属教学幻觉。但这个“JavaFX完整版项目SpringSpringMVCMybatis”不是。它用JavaFX做前端壳内嵌Jetty启动一个标准的SpringMVC Web容器所有请求走DispatcherServletController调ServiceService走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 # MapperSelect/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-webmvcjetty-server这样你能看清DispatcherServlet怎么注册、ContextLoaderListener怎么加载根上下文——这是理解SSM整合本质的关键。2.2 核心配置链从JavaFX启动到SpringMVC容器就绪的5个关键节点JavaFX程序启动后并不直接加载Spring。它走了一条显式、可调试的初始化链MainApp.start()JavaFX主线程启动加载main.fxml设置MainViewController为控制器MainViewController.initialize()FXML加载完成后触发这里调用EmbeddedJettyServer.start()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运行 }WebConfig加载AnnotationConfigWebApplicationContext扫描Configuration类触发AppConfig创建DataSource、SqlSessionFactoryBean再由MyBatisConfig隐含在AppConfig中注册MapperScannerConfigurer自动扫描dao包下的Mapper接口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(); // 步骤1JavaFX线程构造JSON请求体 MapString, String loginData new HashMap(); loginData.put(username, username); loginData.put(password, password); // 步骤2HttpClientUtil发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 ResponseEntityMapString, Object login(RequestBody MapString, String credentials) { MapString, 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 { // 步骤5MyBatis执行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事务开启 →UserMapperMyBatis代理执行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指定source11/sourcetarget11/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必须用17pom.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 11Project 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.skiptrue # 步骤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中该插件是否在buildplugins下且configurationmodules包含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 → RunJavaFX窗口才会正常加载如果curl返回404检查EmbeddedJettyServer中server.setHandler(context)是否在server.start()前调用如果返回500看日志里是否有NoSuchBeanDefinitionException大概率是AppConfig没被WebConfig正确import。3.4 常见问题排查3条血泪经验专治启动失败、界面空白、请求超时现象1JavaFX窗口弹出但主界面空白控制台无报错原因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-8Windows记事本保存易出错用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配置全部用SelectProviderUpdateProvider实现动态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) ListUser 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(MapString, 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(MapString, Object params) { ListUser users (ListUser) 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(MapString, 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 // 查询方法readOnlytrue不开启事务提升性能 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 (failexample.com.equals(email)) { throw new RuntimeException(SMTP connection timeout); } System.out.println(Welcome email sent to email); } }如何验证事务回滚生效启动项目用Postman调POST /api/user/register传{username:test,password:123,email:failexample.com}观察控制台RegistrationException抛出且userMapper.insert(user)的SQL日志显示insert into user ...被执行再调GET /api/user?usernametest返回空——证明插入被回滚改邮箱为successexample.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 ResponseEntityMapString, Object handleAuthError(AuthenticationException e) { MapString, 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内存数据库换成MySQL5步搞定不丢数据H2适合演示但生产必须换MySQL。这不是改个URL就行涉及驱动、连接池、SQL方言三处。步骤操作验证标志防翻车提示1. 加MySQL驱动pom.xml中删掉com.h2database:h2加xmlbrdependencybr groupIdmysql/groupIdbr artifactIdmysql-connector-java/artifactIdbr version8.0.33/versionbr/dependencybrmvn compile成功无ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL 8驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver2. 改数据库配置application.properties中spring.datasource.urljdbc:mysql://localhost:3306/demo_db?useSSLfalseserverTimezoneUTCspring.datasource.driver-class-namecom.mysql.cj.jdbc.Driverspring.datasource.usernamerootspring.datasource.passwordyour_password启动时控制台出现Loaded JDBC driver: com.mysql.cj.jdbc.DriverURL必须加serverTimezoneUTC否则MySQL 8报时区错3. 创建数据库与表在MySQL中执行CREATE DATABASE demo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后运行项目自带的schema-h2.sql删掉H2特有语法如AUTO_INCREMENT改为AUTO_INCREMENTEmbeddedJettyServer启动后日志出现Creating tables和Inserting sample data表引擎必须用InnoDB支持事务别用MyISAM4. 调整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 usernametest返回空启动时看日志是否有Using DataSource [HikariDataSource]证明连接池生效我的血泪经验第3步建库时字符集必须用utf8mb4否则存emoji报错。曾经有个学生用utf8存微信昵称时报Incorrect string value折腾半天才发现是MySQL的utf8其实是utf8mb3不支持4字节Unicode。5.2 场景2新增“订单”模块从建表到JavaFX界面联动加新功能最怕改坏旧逻辑。按“数据库→Mapper→Service→Controller→JavaFX”顺序推进每步验证。步骤1建表MySQLCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;步骤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}) ListOrder findByUserId(Param(userId) long userId); } // sqlprovider/OrderSqlProvider.java public class OrderSqlProvider { public String insert(MapString, 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; } }步骤4JavaFX界面联动最小改动在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 场景3JavaFX界面国际化中英文切换不重启应用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分钟内就能交付可演示的版本。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Linux RPC报错errno 113排查:clnt_create与port mapper连接失败详解

Linux RPC报错errno 113排查:clnt_create与port mapper连接失败详解

1. 从一个报错说起:为什么这个RPC连接问题值得单独聊clnt_create: RPC: Port mapper failure - Unable to receive: errno 113 (No route to host)——这行报错,凡是在Linux环境下折腾过NFS挂载、做过RPC服务调试、或者维护过分布式存储集群的人&#xf…

2026/10/16 3:48:09 阅读更多 →
STM32读取多摩川TS5668N21绝对式编码器:RS485驱动实现

STM32读取多摩川TS5668N21绝对式编码器:RS485驱动实现

简介:这套针对STM32读取多摩川TS5668N21编码器的工程资料,适合嵌入式开发者、电机控制与工业自动化方向学习者使用。内容基于STM32F429平台,完整展示ADM485协议下UART收发配置、A/B/Z相脉冲捕获、位置增量与零位处理,以及Hall信号…

2026/10/16 3:47:08 阅读更多 →
OpenClaw实战:AI代理+长尾SEO自动化内容站月流量15000拆解

OpenClaw实战:AI代理+长尾SEO自动化内容站月流量15000拆解

如果有人告诉你,一个人靠AI代理加上长尾SEO,在不露脸、不投广告的情况下,把一个垂直内容站从0做到15000的月搜索流量,还拿到了每月2800美元左右的经常性收入,你大概率会先怀疑这是不是标题党。但站在我这几年的观察角度…

2026/10/16 3:47:08 阅读更多 →

最新新闻

汇编子程序与数制转换:深入8086栈机制与寄存器契约

汇编子程序与数制转换:深入8086栈机制与寄存器契约

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:29:20 阅读更多 →
Android温湿度监控APP开发实战:串口蓝牙数据采集与Room存储告警

Android温湿度监控APP开发实战:串口蓝牙数据采集与Room存储告警

简介:这份资源是面向物联网与移动应用开发学习者的温湿度监控手机APP完整代码包,适合具备一定Android基础、希望打通传感器采集到手机端展示全链路的开发者参考。内容围绕DHT11/DHT22等传感器数据采集、微控制器处理、无线模块上传云端,以及A…

2026/10/16 4:29:20 阅读更多 →
融合物理先验与域自适应的恶劣天气目标检测方法

融合物理先验与域自适应的恶劣天气目标检测方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:29:20 阅读更多 →
ONVIF设备支持H265完整开发指南:能力上报、RTSP与RTP打包改造

ONVIF设备支持H265完整开发指南:能力上报、RTSP与RTP打包改造

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:29:20 阅读更多 →
深入AQS:doReleaseShared如何用PROPAGATE守住共享锁唤醒链

深入AQS:doReleaseShared如何用PROPAGATE守住共享锁唤醒链

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:29:20 阅读更多 →
FPGA高速安全状态机设计:编码、时序优化与安全机制

FPGA高速安全状态机设计:编码、时序优化与安全机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:28:20 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →