JavaFX+SSM架构实战:桌面客户端与数据服务端的高效整合

简介:这是一份融合 JavaFX 与 SSM 框架的 ERP 系统完整项目,面向需要将桌面 GUI 与企业级后端整合开发的 Java 学习者、毕业设计或项目实战人群,既能学习界面设计,也能掌握经典后端框架的落地方式。资源共包含 2 个文件:一个主压缩包为 7z 格式,内含工程源码及 FXML、CSS 等界面资源;另附一份 SQL 数据库脚本,用于快速初始化业务表,整体约 75.26MB,已有 3415 人学习/下载。项目以典型 ERP 业务为背景,覆盖 Spring 的依赖注入与事务管理、SpringMVC 的请求调度、Mybatis 的注解与 XML 映射,以及 JavaFX 的 FXML 布局、CSS 美化、TableView 实时刷新与表单双向绑定等关键技术点,能直观看到各层之间的调用关系。7z 压缩包内工程目录组织清晰,SQL 脚本包含初始化数据,导入 IDE 后即可还原演示环境;通过实际代码可以学到桌面端与后端服务如何分层协作,界面设计与持久层映射也能直接复用,是一份适合进阶开发或课题设计的完整参考。

1. 这到底是个什么项目:为什么有人会用 JavaFX 去套 SSM

第一次见到这个组合的时候我也愣了一下,JavaFX 是桌面客户端技术,SSM 是 Web 后端三板斧,这俩怎么能凑到一个项目里?跑起来才明白,这个项目把 JavaFX 当成「界面渲染器」,把 SSM 当成「数据服务层」,两者通过 HTTP 接口通信。换句话说,这是一个典型的「瘦客户端 + 数据服务端」架构——界面负责展示和交互,业务逻辑和数据访问全部下沉到 SpringMVC 的 Controller 和 Mybatis 的 Mapper 里。

这种结构最适合谁?一类是刚学完 SSM 想做点东西却不知道往哪放的同学,另一类是想把老 Web 系统改造成桌面工具的内部开发者。JavaFX 负责把数据变成表格、表单、图表,SSM 负责把 MySQL 里的数据洗干净再吐出来,分工干净,测试也方便。如果你正愁「SSM 学完了只会写网页」,这个项目能给你另一种落地姿势。

2. 架构与技术栈:JavaFX 和 SSM 是怎么各司其职的

2.1 分层设计:从 FXML 到 Mapper 的数据流

整个项目按「两端三层」的方式组织。两端指 JavaFX 客户端和 SSM 服务端,三层指服务端的 Controller、Service、Mapper 三层。客户端不直接连数据库,所有数据操作都通过 HTTP 请求打到 SpringMVC 的接口上,返回 JSON 再渲染到界面。

这种设计有一个明显的好处:JavaFX 端的代码只关心「怎么把数据画出来」,服务端只关心「数据从哪来、怎么算」。职责边界清楚了,两个人同时改同一个功能也不会打架。我在二次开发时甚至能把 JavaFX 端整个扔掉,换成 Flutter 或 Vue 的 Web 页面,后端一行不用动——接口定了,前端随便换。

服务端的目录结构是这样的:

src/main/java/com/example/ssm/ ├── controller/ # SpringMVC 控制器,唯一对外的入口 │ ├── UserController.java │ ├── OrderController.java │ └── StatsController.java ├── service/ │ ├── UserService.java │ ├── OrderService.java │ └── impl/ │ ├── UserServiceImpl.java │ └── OrderServiceImpl.java ├── mapper/ # Mybatis 的 Mapper 接口 │ ├── UserMapper.java │ └── OrderMapper.java ├── entity/ # 数据库实体类 │ ├── User.java │ └── Order.java └── config/ ├── SpringConfig.java └── SpringMvcConfig.java

服务端是标准的 SSM 三层。客户端这边,JavaFX 按界面拆分成 FXML 文件和对应的 Controller 类,一个 FXML 对应一个界面模板,一个 Controller 负责一个界面的交互逻辑。

2.2 技术选型:为什么用 Mybatis 而不是 JPA

这个项目选了 Mybatis 而不是 JPA,老开发一看就懂——Mybatis 对 SQL 的控制是粒度的,尤其是统计类报表,写起来比 JPA 的 Criteria 舒服得多。项目中有一个订单统计模块,要吃聚合函数、日期分组、多表关联,Mybatis 里几行 XML 就搞定了,JPA 那边要折腾半天。

Spring 的容器负责把所有 Bean 串起来。SpringMvc 的 DispatcherServlet 拦截 /api 路径的请求,分发到 Controller;Service 层做事务管理;Mapper 层通过 Mybatis 的 SqlSession 操作数据库。集成方式用的是全注解 + 少量 XML,数据源交给 Druid 管理,连接池参数在 jdbc.properties 里调。

客户端到服务端的通信,项目里用的是 Spring 自带的 RestTemplate。每个 JavaFX Controller 在初始化时注入一个 RestTemplate 实例,调用后端接口拿数据。这里有个小设计值得学:他把所有接口地址集中放到了一个 ApiUrls 常量类里,前端代码里不会散落一堆字符串 URL,后期接口地址调整一处改完。

2.3 关键取舍:JavaFX 端的线程模型处理

JavaFX 的界面线程(FX Application Thread)是不能被阻塞的,否则界面直接卡死。这个项目在客户端封装了一个简单的异步工具类,把每一条 HTTP 调用丢到线程池里执行,拿到结果后再通过 Platform.runLater() 切回界面线程刷新数据。这个封装虽然简单,但是是 JavaFX 客户端能不能「用起来」的关键,后面核心代码部分会专门拆开讲。

3. 核心模块拆解:一条订单数据从前端到数据库的完整链路

3.1 入口与配置:Spring 容器如何与 JavaFX 共存

这个项目的启动方式比较特殊,它不是 Web 项目那种丢进 Tomcat 就能跑,也不是纯 JavaFX 的 main 方法直接 launch。它的启动分两步:先初始化 Spring 容器,再启动 JavaFX 的 Application 类。

public class MainLauncher { public static void main(String[] args) { // 第一步:初始化 Spring 容器,加载所有 Bean ApplicationContext context = new AnnotationConfigApplicationContext( SpringConfig.class, SpringMvcConfig.class); // 第二步:把 Spring 容器挂到全局,供 JavaFX 的 Controller 使用 SpringContextHolder.setContext(context); // 第三步:启动 JavaFX 界面 Application.launch(FxApplication.class, args); } }

这里的逻辑说明两点。第一,Spring 容器必须先生成,否则 JavaFX 界面加载时拿不到 Service 层的 Bean;第二,用 SpringContextHolder 这个静态类做全局桥接,让 FXML 对应的 Controller 类可以在界面的 initialize() 方法里直接取需要的 Bean,这是一种常见的「非 Spring 管理的类里使用 Spring Bean」的做法,在不引入额外框架的前提下最干净。

参数说明:SpringConfig.class 是纯注解配置类,负责扫描 service 和 mapper 包,配置数据源与事务管理器;SpringMvcConfig.class 负责启用 SpringMVC 注解驱动和设置 JSON 序列化器。如果你的项目还需要集成其他组件,往这两个 Configuration 类里继续添加 @Bean 即可。

3.2 Controller 层设计:REST 接口是前后端的分界线

Controller 层是两面胶。对前端来说,它是数据接口;对后端来说,它是忙碌的入口。这个项目里 Controller 写得很克制,每个方法都很薄,主要做三件事:收参数、调 Service、封装返回结果。

@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @GetMapping("/list") public Result list(@RequestParam(name = "page", defaultValue = "1") int page, @RequestParam(name = "limit", defaultValue = "10") int limit, @RequestParam(name = "keyword", required = false) String keyword) { // 分页查询订单,keyword 是模糊搜索条件 PageResult<Order> pageResult = orderService.queryPage(page, limit, keyword); return Result.success(pageResult); } @GetMapping("/detail/{id}") public Result detail(@PathVariable("id") Integer id) { // 查询订单详情,包含订单项和关联的用户信息 OrderDetailVO detail = orderService.getDetailById(id); return Result.success(detail); } @PostMapping("/update") public Result update(@RequestBody Order order) { // 更新订单,参数直接是 JSON 体,SpringMVC 自动反序列化 orderService.update(order); return Result.success(null); } }

Result 是项目里封装的一个统一返回体,包含 code、message、data 三个字段。这个设计非常有用:JavaFX 端只用判断 code 是否为 200,不用每次解析不同的返回结构。接口路径上的 /api 前缀让前端一眼就知道哪些请求走服务端,如果需要做权限拦截,在 SpringMvcConfig 里配置拦截器只拦 /api/** 就行。

这里有个细节值得二次开发时注意:update 方法用的是 @RequestBody,JavaFX 端发请求时必须设置 Content-Type 为 application/json,而且 Order 实体类必须有对应的 setter 方法,Jackson 才能把 JSON 字段塞进去。很多新手在这里踩坑,明明接口签名没问题就是 400,多半是实体类缺了无参构造器或 setter。

3.3 Service 层与事务管理:别把业务逻辑写进 Controller

Controller 薄,Service 层就得厚一点。这个项目的 Service 层做得比较讲究,几个关键场景都有体现,比如订单下单时同时扣库存、写流水,这些操作必须放在同一个事务里。

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private StockMapper stockMapper; @Autowired private FlowMapper flowMapper; @Override @Transactional(rollbackFor = Exception.class) public void createOrder(Order order, List<OrderItem> items) { // 1. 插入订单主表 orderMapper.insertOrder(order); // 2. 插入订单明细 for (OrderItem item : items) { item.setOrderId(order.getId()); orderMapper.insertItem(item); // 3. 扣减库存,注意这里要开启行锁 int rows = stockMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new RuntimeException("库存不足:" + item.getProductId()); } } // 4. 写资金流水 flowMapper.insertFlow(buildFlowFromOrder(order)); } }

扣库存那步是关键。stockMapper.deductStock 的 SQL 用了 UPDATE ... SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity},返回值是受影响的行数,如果为 0 说明库存不够,直接抛出异常触发事务回滚。这种「乐观锁 + 原子扣减」的写法在库存类业务里是标准做法,避免先查再改带来的并发超卖问题——这也是 SSM 三件套做业务系统时最有含金量的部分,面试时能把这套链路讲清楚,比背一百道八股文管用。

事务注解用了 rollbackFor = Exception.class,把运行时异常和受检异常都纳入回滚。我见过不少项目只写 @Transactional 不写 rollbackFor,结果业务抛出 IOException 时事务静默提交,数据就花了。

3.4 Mapper 层与动态 SQL:Mybatis 的 XML 怎么写才不乱

Mapper 接口和 XML 文件是这个项目的持久层核心。接口定义方法签名,XML 写 SQL 和结果映射。看一段订单分页查询的动态 SQL:

<select id="queryPage" resultType="com.example.ssm.entity.Order"> SELECT id, order_no, user_id, total_amount, status, create_time FROM t_order <where> <if test="keyword != null and keyword != ''"> AND (order_no LIKE CONCAT('%', #{keyword}, '%') OR user_id IN (SELECT id FROM t_user WHERE name LIKE CONCAT('%', #{keyword}, '%'))) </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{limit} </select>

标签是 Mybatis 动态 SQL 的常用技巧,它会自动处理条件前缀,如果关键字为空就只查全部,不为空才会追加 AND 子句,省去手动写 WHERE 1=1 这种不那么体面的写法。LIMIT #{offset}, #{limit} 是 MySQL 的分页语法,#{} 是预编译占位符,Mybatis 会生成 PreparedStatement,能有效防止 SQL 注入。

这里有一个项目里没用但值得补的建议:如果数据量上来了,不要用 LIMIT 大偏移量分页,改成基于上一页最后一条订单时间或 ID 的键集分页(Keyset Pagination),性能会好一个量级。这属于二次开发时的优化方向,后面会再提到。

4. JavaFX 客户端核心逻辑:界面怎么与服务端稳定对接

4.1 FXML 与 Controller 绑定:画界面和写逻辑分离

JavaFX 端的界面文件用 FXML 编写,类似 HTML 之于网页。FXML 里写控件布局,Controller 类里写交互逻辑,通过 fx:id 和 @FXML 注解关联起来。

看一个订单列表界面的 FXML 片段:

<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.fx.controller.OrderListController"> <HBox spacing="10" padding="10"> <TextField fx:id="keywordField" promptText="输入订单号或客户姓名"/> <Button text="查询" onAction="#handleSearch"/> <Button text="刷新" onAction="#handleRefresh"/> </HBox> <TableView fx:id="orderTable"> <columns> <TableColumn text="订单号" fx:id="orderNoCol"/> <TableColumn text="金额" fx:id="amountCol"/> <TableColumn text="状态" fx:id="statusCol"/> <TableColumn text="下单时间" fx:id="timeCol"/> </columns> </TableView> </VBox>

fx:controller 属性把 FXML 文件绑定到 OrderListController 类,fx:id 和 Controller 里标注 @FXML 的字段一一对应。onAction="#handleSearch" 表示查询按钮点击后调用 Controller 类里的 handleSearch 方法。这样设计的好处是,美工调样式改 FXML,开发调逻辑改 Controller,互不干扰。

4.2 异步 HTTP 调用:不让界面线程卡死的封装

JavaFX 单一界面线程的约束是所有桌面端项目最难受的点,也是这个项目的核心处理逻辑所在。我见过有人直接用 HttpURLConnection 同步调用,一点查询按钮整个界面冻住好几秒,像是程序崩溃了,其实只是网络请求阻塞了 UI 线程。

这个项目封装了一个 AsyncHttp 工具类,思路值得借鉴:

public class AsyncHttp { private static final RestTemplate restTemplate = new RestTemplate(); // 在后台线程中发送 GET 请求,结束后回调 onSuccess 方法 public static <T> void get(String url, ParameterizedTypeReference<T> type, Consumer<T> onSuccess, Consumer<Exception> onError) { executor.execute(() -> { try { ResponseEntity<T> response = restTemplate.exchange( url, HttpMethod.GET, null, type); // 切回 JavaFX 界面线程执行回调 Platform.runLater(() -> { if (response.getStatusCode() == HttpStatus.OK) { onSuccess.accept(response.getBody()); } }); } catch (Exception e) { Platform.runLater(() -> onError.accept(e)); } }); } private static final ExecutorService executor = Executors.newFixedThreadPool(4); }

这个工具类的逻辑说明:executor 是固定大小为 4 的线程池,所有 HTTP 请求在线程池里执行,不占用 JavaFX 的界面线程;拿到响应后用 Platform.runLater() 把回调丢回界面线程,保证 UI 操作安全。RestTemplate.exchange 方法接收一个 ParameterizedTypeReference,这是为了处理泛型返回类型——比如 Result<PageResult > 这种嵌套泛型,如果不传这个参数,Jackson 反序列化时会把泛型信息丢掉,这是很多同学遇到「接口返回正常但前端一直拿不到 List 数据」的根因之一。

在多线程代码里有一个常见的坑是:在回调函数里直接更新 TableView 的 items。JavaFX 要求对界面控件的修改必须在 FX 线程上执行,所以项目把 onSuccess 包装在 Platform.runLater() 里再调用。这个封装跨项目通用,我在做其他 JavaFX 项目时也直接复制过。

4.3 TableView 数据绑定:把 Service 拿到的列表灌进表格

TableView 是 JavaFX 中最常用的数据表格控件,但它的数据绑定方式比 Web 端的表格要绕一些。TableView 本身不直接接收 List,而是接收 ObservableList,这样才能监听数据变化并自动刷新界面。

@FXML private TableView<OrderVO> orderTable; @FXML private TableColumn<OrderVO, String> orderNoCol; @FXML private TableColumn<OrderVO, BigDecimal> amountCol; // 数据集合必须是 ObservableList,TableView 会自动监听并刷新 private final ObservableList<OrderVO> dataList = FXCollections.observableArrayList(); @FXML private void handleSearch() { String keyword = keywordField.getText(); String url = ApiUrls.ORDER_LIST + "?page=1&limit=10&keyword=" + URLEncoder.encode(keyword, StandardCharsets.UTF_8); // 清空旧数据,避免界面残留上一条查询结果 dataList.clear(); AsyncHttp.get(url, new ParameterizedTypeReference<Result<PageResult<OrderVO>>>() {}, result -> { // 从服务端返回的分页对象中取出列表数据 dataList.addAll(result.getData().getRecords()); }, error -> { error.printStackTrace(); // 生产环境应该在这里弹出 Alert 提示用户 }); } @FXML private void initialize() { // 把每个列绑定到 OrderVO 的属性名 orderNoCol.setCellValueFactory(new PropertyValueFactory<>("orderNo")); amountCol.setCellValueFactory(new PropertyValueFactory<>("totalAmount")); statusCol.setCellValueFactory(new PropertyValueFactory<>("status")); orderTable.setItems(dataList); // 启动后自动加载一次数据 handleRefresh(); }

这段代码有两个需要留意的节点。第一,PropertyValueFactory 通过反射调用 OrderVO 的 getOrderNo() 等方法取数,所以 OrderVO 属性名要和 FXML 里的字符串对应,而且必须有 getter,否则表格列会一直空白不报错,这种情况排查起来很头疼,从这入手能很快定位;第二,URL 中的中文关键字必须先 URLEncoder.encode,否则服务端接收到的参数是乱码,有时候能查到值有时候查不到,就是因为编码不一致,这是个经典翻车点。

5. 运行环境与避坑:把这套项目拉起来需要踩过哪些坑

5.1 从零启动:数据库初始化到界面弹窗的完整步骤

先把运行环境讲清楚。JDK 推荐 8 或 11,JavaFX 在 JDK 8 里是自带组件,到 JDK 11 之后变成了独立模块,需要额外引入 org.openjfx:javafx-controls 依赖。Maven 项目直接在 pom.xml 里声明即可。数据库用 MySQL 5.7 以上版本,项目带了一个 init.sql 初始化脚本,建库建表,另外配好 Druid 连接池的连接信息。

# 第一步:初始化数据库 mysql -uroot -p123456 < init.sql # 第二步:修改数据库连接配置 vim src/main/resources/jdbc.properties # 第三步:启动服务端(先启动 Spring 容器) mvn spring-boot:run # 第四步:启动客户端(Launch MainLauncher 即可) mvn javafx:run

服务端如果用的是内嵌 Tomcat,启动后默认监听 8080 端口。JavaFX 客户端的 ApiUrls 常量类里 BaseUrl 默认是 http://localhost:8080,如果服务端部署在远程机器,只需要改这一处常量,不用动任何其他代码。这是我比较喜欢这个项目的原因之一——接口集中定义了,运行环境的差异被压到了最小,改动成本变得很低。

5.2 常见问题排查:从端口冲突到中文乱码到泛型丢失

这个项目能跑通不算难,真正恼人的是那些「环境没问题但就是跑不出数据」的场景,我把在调试中遇到过的问题整理成了排查清单。

现象一:服务端日志显示请求进来了,但 JavaFX 界面一直转圈没有数据。原因多半是 RestTemplate 请求线程抛了异常,但异常被吞掉没有反馈到界面。解决方法是先看后台线程的 error 回调有没有被调用,在 onError 里加日志输出。这是我见过最多的一个隐蔽问题,排查时要强制走通这条回调链路再谈其他。

现象二:窗口打开了,Table 列表数据加载不出来,但后端 Postman 测接口有返回。这个现象让人摸不着头脑。原因基本出在泛型反序列化丢失上。如果直接用 RestTemplate.getForObject(url, Result.class) 接收返回值,Result 里的 data 字段会被转成 LinkedHashMap,取不到 Order 对象。解决方法是必须用 ParameterizedTypeReference 带完整的泛型信息去请求,或者对 Result 的 data 字段做手动 TypeReference 转换。写 RestTemplate 的时候要养成「带泛型信息请求」的习惯,可以避免很多类似问题。

现象三:前端提交中文数据,服务端接收乱码。原因有两层:第一层是 JavaFX 端 URL 参数没有编码,第二层是 SpringMVC 的字符集过滤器没有配置。解决方法是前端用 URLEncoder.encode 编码,后端在 web.xml 或 SpringBoot 的配置类里加 CharacterEncodingFilter,强制 UTF-8。我遇到过前后端都检查了还是乱码的情况,最后发现是 MySQL 表字段的 collation 是 latin1 开头,建表时指定 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 才能彻底解决。

现象四:Maven 编译报错 JavaFX 类找不到。原因是在 JDK 11 及以上版本没有引入 JavaFX 依赖时,代码里 import javafx.* 会直接编译失败。解决办法是 pom.xml 增加 org.openjfx 依赖并配置 maven-compiler-plugin,同时注意 Windows/Mac/Linux 的 classifier 不同。当然,有的项目为了省事直接退回 JDK 8,倒也是个可行的方案,但长期维护还是建议按标准方式配好依赖。

5.3 开发阶段的调试技巧:日志和接口文档两手都要硬

开发这套项目时,建立一个简单的调试习惯能省不少时间。服务端接口用 Postman 先测过一轮,确认返回 JSON 结构再动前端;前端调不通的时候,不要盲目猜,先看服务端控制台有没有报错,再看返回的 JSON 结构是否符合前端 parse 的预期,最后才怀疑是不是界面线程的问题。

6. 二次开发思路:围绕这套骨架扩展新功能的具体做法

6.1 从订单模块扩展到报表模块

这个项目最值得改的切入点是把现有订单数据变成统计报表。服务端新增一个报表 Controller,JavaFX 端使用 BarChart 控件展示柱状图,整条链路走的是项目里已经跑通的分层套路,不需要引入额外框架。

@RestController @RequestMapping("/api/stats") public class StatsController { @Autowired private StatsService statsService; @GetMapping("/monthly") public Result monthly(@RequestParam("year") Integer year) { // 返回 1 到 12 月订单总额,供前端图表展示 List<Map<String, Object>> data = statsService.monthlySales(year); return Result.success(data); } }

Service 层的实现用一句 SQL 就能聚合出月度销售数据,比 Java 内存里循环统计高效得多,这正是 Mybatis 适合干的事。JavaFX 端把 AsyncHttp.get() 请求的结果灌进 XYChart.Series,再把 Series 加到 BarChart 上,十几行代码就能做出一张月度营收趋势图。如果业务上还需要导出 Excel,可以在服务端集成一个 EasyExcel 依赖,新增一个导出接口,复用现成的查询逻辑。

6.2 给服务端加上权限控制

项目当前的接口是裸奔的,谁拿到地址就能调。如果要把这套骨架用于内部工具或生产环境,建议第一件事就是加登录认证。技术路线上最轻量的是拦截器加 Token。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头中取 Token,校验通过才放行 String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token) || !TokenStore.validate(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } return true; } }

TokenStore 可以是 Redis 的封装或内存 Map,取决于部署规模。JavaFX 客户端那边,只需要在发送请求时从一个全局登录会话里取 Token,塞进请求头。两个端改动各集中在一处,因为这属于跨切面的横切关注点,可以放到已有的工具类中集中处理,不需要每个界面改一遍,这也体现了项目分层合理的优势。

6.3 替换 RestTemplate 为轻量级 HttpClient

如果客户端请求量比较大,或者需要做连接复用、超时控制,可以考虑把 RestTemplate 换成 java.net.http.HttpClient 或 OkHttp。项目里所有 HTTP 调用都走了 AsyncHttp 工具类,所以替换时只需要改这一个类——这也说明当初的封装给二次开发留了后路。

private static final java.net.http.HttpClient httpClient = java.net.http.HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); private static <T> void getHttpClient(String url, TypeReference<T> type, Consumer<T> onSuccess, Consumer<Exception> onError) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(15)) .GET() .build(); httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenApply(json -> JsonUtils.parse(json, type)) .whenComplete((data, throwable) -> { Platform.runLater(() -> { if (throwable != null) { onError.accept(throwable); } else { onSuccess.accept(data); } }); }); }

JDK 自带的 HttpClient 在 JDK 11 以后 API 已经是稳定的了,配合同步转异步的 CompletableFuture,代码甚至比 RestTemplate 还要干净一些。注意 JavaFX 的界面刷新仍然要包在 Platform.runLater() 里,这是绕不开的原则。换完以后,原有的调用方代码一行都不用动,全链路也就十分钟的事。

6.4 改造后的验证方法

改动完成后,先按「查询订单 → 打开详情 → 修改金额 → 刷新列表」这条主链路回归一遍。如果改了通信层,用抓包工具看一眼请求格式,确认 Content-Type 和参数名没变。如果改了数据库表结构,重点看 Mapper XML 里 SQL 的字段引用是否同步更新了。

这套项目骨架的可贵之处在于,服务端和客户端之间隔了一条清晰的 HTTP 接口线。我后来再做桌面工具项目时,始终保持着这条线的纪律——接口先定、双边各自 mock、联调阶段再对齐边界。从那以后我每次启动新功能都会强制走一遍「Controller 接口签名先行,Service 实现跟进,JavaFX 端最后填充」的流程,翻车次数就肉眼可见地少了。希望这次的拆解也能帮你把这套代码顺顺当当跑起来,改出你自己想要的效果。

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

相关新闻

Python与API文本分析实战:从文本清洗到工程化部署

Python与API文本分析实战:从文本清洗到工程化部署

AI应用的潜力&#xff0c;有一大半藏在文本里。文本分析这个方向&#xff0c;我自己前前后后折腾过不少方案&#xff0c;从早期的正则规则、词频统计&#xff0c;到开源模型微调&#xff0c;再到现在用 Python 接各类大模型 API 做文本分析&#xff0c;最大的感受是&#xff1a…

2026/10/20 6:27:33 阅读更多 →
IPv4报文头部详解:字段、分片与网络排查核心知识

IPv4报文头部详解:字段、分片与网络排查核心知识

做网络排查做得久了&#xff0c;你会发现一个特别有意思的现象&#xff1a;很多人能把TCP的三次握手、四次挥手讲得天花乱坠&#xff0c;可一旦你把抓包工具里的一帧数据展开&#xff0c;让他逐字节解释一下IPv4头部&#xff0c;他反而会卡壳。TCP、UDP这些传输层协议太出名&am…

2026/10/20 6:27:33 阅读更多 →
STM32 bxCAN实战:从物理层到过滤器配置与调试排障

STM32 bxCAN实战:从物理层到过滤器配置与调试排障

做嵌入式这几年&#xff0c;UART、I2C、SPI都是老朋友&#xff0c;但真正让我觉得“像个通信工程师”的&#xff0c;是开始啃STM32的CAN外设之后。原因很简单&#xff1a;在汽车、机器人和工业现场&#xff0c;CAN总线是默认的“老大哥”&#xff0c;而它在微控制器上的硬件实现…

2026/10/20 6:26:33 阅读更多 →

最新新闻

投机推理:突破大模型自回归瓶颈的并行验证加速技术

投机推理:突破大模型自回归瓶颈的并行验证加速技术

1. 大模型推理的“车堵一条道”困局老做推理优化的朋友都有过这种体验&#xff1a;GPU 明明算力爆炸强&#xff0c;但每次生成一个 token 都得几毫秒甚至几十毫秒&#xff0c;batch 稍微大一点&#xff0c;延时就像坐火箭一样往上蹿。一开始我们都骂“模型太大了”&#xff0c;…

2026/10/20 7:13:55 阅读更多 →
Spring Boot+微信小程序汽车用品商城开发:从车型适配到支付回调

Spring Boot+微信小程序汽车用品商城开发:从车型适配到支付回调

先说句实在话&#xff1a;汽车用品这个品类&#xff0c;看着和普通电商差不多&#xff0c;但等你真把“Spring Boot 微信小程序”这套销售系统落地时&#xff0c;会发现一堆让人挠头的细节——车型适配怎么做、库存怎么拆、微信支付回调怎么验签、审核类目怎么过。这篇文章不聊…

2026/10/20 7:13:55 阅读更多 →
Delta机器人正逆运动学C#实现:高精度实时解算模块

Delta机器人正逆运动学C#实现:高精度实时解算模块

简介&#xff1a;本资源是一套面向机器人控制开发者与自动化专业学习者的Delta并联机器人运动学算法C#实现工程&#xff0c;聚焦高速拾放场景下的正向/逆向运动学求解这一核心难点。项目以Visual Studio解决方案形式组织&#xff0c;含41个文件&#xff0c;涵盖6个核心C#源码&a…

2026/10/20 7:13:55 阅读更多 →
无语言基础模型如何实现通用视觉异常检测?原理与落地

无语言基础模型如何实现通用视觉异常检测?原理与落地

这两年做计算机视觉落地&#xff0c;特别是工业视觉相关的项目时&#xff0c;我几乎绕不开一个词&#xff1a;异常检测。不管是手机外壳上的划痕、布料上的断纱&#xff0c;还是电路板上的漏焊&#xff0c;本质都是同一件事——在绝大多数正常样本中&#xff0c;把分布外的那一…

2026/10/20 7:13:55 阅读更多 →
Chatterbox TTS实战:开源语音合成全家桶部署与调优指南

Chatterbox TTS实战:开源语音合成全家桶部署与调优指南

去年底我就在关注开源TTS圈子的动向&#xff0c;当时看到Chatterbox TTS这个项目时&#xff0c;第一反应是"又一个语音合成模型"&#xff0c;但真正部署完、跑了几天推理之后&#xff0c;我改变了看法——这玩意儿不是单个模型&#xff0c;而是一整套开箱即用的语音合…

2026/10/20 7:13:55 阅读更多 →
微服务商城对接支付宝支付:独立支付服务与异步回调实战

微服务商城对接支付宝支付:独立支付服务与异步回调实战

1. 支付服务在微服务架构里的定位做电商项目&#xff0c;前面把商品、库存、订单、用户这些服务都搭起来之后&#xff0c;早晚要面对一个绕不开的环节——支付。我这次在基于Vue和SpringCloudAlibaba的微服务商城项目里&#xff0c;把支付单独拆成了一个平台服务&#xff0c;专…

2026/10/20 7:12:54 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/19 2:54:50 阅读更多 →
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/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练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/18 12:16:16 阅读更多 →