:啟動配置與分頁避坑)
簡介一份面向Java開發(fā)者的SpringBoot3JavaFxMyBatisPlus整合示例項目適合需要快速構(gòu)建桌面應(yīng)用并集成數(shù)據(jù)庫持久層的場景可有效降低配置成本并提升開發(fā)效率。壓縮包約110.76MB共79個文件涵蓋java源碼、fxml界面布局、xml映射與配置、yml配置文件、SQL腳本、jar依賴以及class編譯產(chǎn)物同時保留完整Maven工程結(jié)構(gòu)便于直接運行、調(diào)試或二次開發(fā)。該項目已有2415人學(xué)習下載實例演示了SpringBoot3自動配置、JavaFx現(xiàn)代化富客戶端界面以及MyBatisPlus通用CRUD、條件構(gòu)造器、分頁插件等核心功能的完整整合鏈路。除代碼外包內(nèi)還提供操作錄屏、界面截圖和項目配置說明能幫助開發(fā)者避開集成過程中的常見坑點適合從入門到進階的Java開發(fā)者作為工程化參考。1. 為什么偏要在桌面上跑一個SpringBoot——這次整合的動機先說結(jié)論SpringBoot3整合JavaFx并不是什么炫技而是被業(yè)務(wù)逼出來的。我手上有一個老舊的桌面客戶端數(shù)據(jù)訪問層是手寫JDBC界面是JavaFx 8那套老代碼。業(yè)務(wù)越加越多之后問題就非常明顯數(shù)據(jù)庫連接沒人管事務(wù)散落在各個Service里參數(shù)校驗靠復(fù)制粘貼想要加一個簡單的分頁查詢得寫小一百行樣板代碼。最難受的是任何一次數(shù)據(jù)庫結(jié)構(gòu)變更都要手動去改DAO層的SQL和結(jié)果集映射改錯一個字段名就是運行時異常排查起來特別費勁。后來我決定用SpringBoot3做底座把JavaFx當純視圖層數(shù)據(jù)訪問統(tǒng)一交給MyBatisPlus。這樣一來依賴注入、事務(wù)管理、連接池、配置中心這些Spring生態(tài)的東西全都能用上而JavaFx只需要負責把界面渲染出來就行。整個項目從“一個傳統(tǒng)的桌面程序”變成了“一個跑在桌面上的SpringBoot應(yīng)用”這本質(zhì)上改變了項目的架構(gòu)級別。這套整合方案適合誰如果你也在維護JavaFx桌面程序并且遇到下面這些問題數(shù)據(jù)訪問代碼混亂、事務(wù)管理靠手動、想用MyBatisPlus的代碼生成器和分頁插件、或者想把Service層抽出來復(fù)用給Web端那這篇文章就是寫給你的。我會把整個整合過程的依賴選型、啟動方式、配置文件、常見坑都講一遍盡量做到照著走就能跑通。2. 依賴與版本選型SpringBoot3時代的硬性門檻2.1 JDK版本與JavaFx的模塊化問題SpringBoot3要求JDK 17起步這是第一個硬門檻。如果你還在用JDK 8那不用往下看了要么升級JDK要么繼續(xù)用SpringBoot2.x。JDK 11以后JavaFx就從JDK中剝離出來了需要單獨引入依賴。這一點很多人第一次接觸會踩坑以為引入spring-boot-starter就能自動帶上JavaFx實際完全不是一回事。JavaFx現(xiàn)在走的是OpenJFX的獨立坐標。dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version17.0.8/version /dependency dependency groupIdorg.openjfx/groupId artifactIdjavafx-fxml/artifactId version17.0.8/version /dependency這里有一個關(guān)鍵點如果項目用了模塊化module-info.java那需要在module-info里聲明requires javafx.controls和requires javafx.fxml同時還要處理Spring的模塊訪問問題。我的建議是非必要不要用模塊化直接打成普通jar包用Classpath方式運行會省掉很多煩惱。2.2 SpringBoot3與MyBatisPlus的版本對應(yīng)關(guān)系MyBatisPlus從3.5.3開始正式支持SpringBoot3之前版本的mybatis-plus-boot-starter是基于javax包的在SpringBoot3下會直接啟動失敗報一些關(guān)于jakarta包的ClassNotFoundException。SpringBoot3把javax.servlet換成了jakarta.servlet這是個大變化。所以依賴坐標也要換成對應(yīng)的starterdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency注意artifactId里的spring-boot3這個不能寫錯。我見過有人直接把舊項目的mybatis-plus-boot-starter復(fù)制過來結(jié)果啟動時一堆莫名其妙的報錯排查半天才發(fā)現(xiàn)是依賴坐標不對。數(shù)據(jù)庫驅(qū)動方面MySQL 8以上版本建議直接用mysql-connector-j用起來就是這樣dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果用了Druid連接池也注意要引入適配SpringBoot3的druid-spring-boot-3-starter與舊的druid-spring-boot-starter是兩個不同坐標。3. 啟動入口設(shè)計把JavaFx Application線程與Spring IoC容器橋接起來3.1 最常見的錯誤寫法我見過很多人在JavaFx里用Spring方式是先手動創(chuàng)建Spring上下文然后在Controller里通過ApplicationContext.getBean()去拿Service。這種方式能跑但問題很大Controller和Spring容器耦合嚴重任何脫離容器的操作都會變成定時炸彈而且你根本沒法使用Autowired界面類完全游離在Spring管理之外。如果你只是想“能用”那沒問題。但如果想讓整個項目結(jié)構(gòu)夠干凈Controller、Service、Mapper都脫離手動管理標準做法是把JavaFx的Application啟動入口做成一個獨立的啟動類讓Spring在Application的init()階段初始化然后在start()階段從Spring容器里拿已經(jīng)管理好的對象。3.2 正確的啟動方式SpringBootApplication public class FxLauncher { public static void main(String[] args) { // JavaFx入口不啟動Spring容器 Application.launch(FxApplication.class, args); } }public class FxApplication extends Application { private ConfigurableApplicationContext springContext; Override public void init() { // 在JavaFx初始化階段啟動Spring容器 springContext new SpringApplicationBuilder(FxLauncher.class) .headless(false) .run(getParameters().getRaw().toArray(new String[0])); } Override public void start(Stage primaryStage) throws Exception { // 從Spring容器中獲取FxmlLoader這樣FXML中的Controller才能被Spring管理 SpringFxmlLoader loader springContext.getBean(SpringFxmlLoader.class); Parent root loader.load(/view/MainView.fxml); Scene scene new Scene(root, 1280, 800); primaryStage.setTitle(SpringBoot3 JavaFx MyBatisPlus); primaryStage.setScene(scene); primaryStage.show(); } Override public void stop() { springContext.close(); Platform.exit(); } }這里有個非常重要的細節(jié)SpringApplicationBuilder.run()默認會把非Web應(yīng)用當作無界面應(yīng)用如果你在JavaFx里啟動SpringBoot會有概率出現(xiàn)“HeadlessException”因為你沒有圖形環(huán)境。所以必須加.headless(false)告訴SpringBoot這個世界是有屏幕的。3.3 讓FXML的Controller被Spring管理FXML文件里Controller的實例化默認是由JavaFx的FXMLLoader完成的它不認識Spring容器。解決辦法是自定義一個loader讓FXMLLoader在加載Controller時從Spring容器里查找BeanComponent public class SpringFxmlLoader { Autowired private ApplicationContext context; public Parent load(String fxmlPath) throws IOException { FXMLLoader loader new FXMLLoader(getClass().getResource(fxmlPath)); loader.setControllerFactory(context::getBean); return loader.load(); } }這樣寫之后FXML里的Controller就可以放心使用Autowired注入Service、Mapper等Spring Bean了。這個ControllerFactory機制是JavaFx專門留出來給外部IoC框架做集成的口子把它接上JavaFx和Spring的整合就算真正完成了。4. MyBatisPlus分頁配置與500條限制的真實成因4.1 分頁插件的配置方式MyBatisPlus的分頁不是默認開啟的不配置分頁攔截器的話selectPage方法會直接查全表再內(nèi)存分頁這在小數(shù)據(jù)量下看不出問題一旦表里幾萬條數(shù)據(jù)就炸了。配置分頁攔截器的方式如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(pagination); return interceptor; } }4.2 單頁500條限制到底是怎么回事很多人在線上遇到“MyBatisPlus單頁只能查500條”的問題查下一頁就報錯或者在日志里看到類似“Illegal SQL, the limit is 500”這樣的提示。首先要說明MyBatisPlus本身并沒有寫死500條。這個限制來自PaginationInnerInterceptor的maxLimit屬性。當你顯式設(shè)置這個值或者所在團隊的基礎(chǔ)架構(gòu)配置里設(shè)置了它那么任何單次查詢的pageSize超過這個值都會被攔截器直接攔截。PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L);設(shè)置了500之后如果你調(diào)用selectPage(new Page(1, 1000), queryWrapper)MyBatisPlus會在SQL執(zhí)行前把limit改掉或者直接拋異常這是官方給的一種防止手滑查爆數(shù)據(jù)庫的保護機制。所以“500條限制”本質(zhì)上不是框架缺陷而是一個保護閥。網(wǎng)絡(luò)熱搜里提到的“接觸mybatisplus單頁500條限制”大概率是兩種場景一是項目里有人配置了maxLimit500但后來忘了二是某次版本升級后框架默認行為變化導(dǎo)致分頁被限制。如果確實需要放開有兩種方式// 方式一Java配置中設(shè)為一個更大的值或null pagination.setMaxLimit(10000L); // 方式二YAML中統(tǒng)一配置 mybatis-plus: interceptor: pagination: max-limit: 10000我個人的建議是不要直接改成無限制而是根據(jù)業(yè)務(wù)合理調(diào)大。比如報表導(dǎo)出這種場景確實可能單次超過500條但普通列表頁真正需要一次顯示超過500條的幾乎沒有。保留這個保護閥至少能讓那些寫錯參數(shù)的隊友得到一次明確的報錯而不是超時。4.3 MyBatisPlus核心配置的YAML寫法mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0分頁相關(guān)的YAML配置注意SpringBoot3 MyBatisPlus 3.5.x版本里分頁攔截器還是推薦用Java配置類創(chuàng)建BeanYAML里的interceptor配置在不同版本中支持情況不一致。你在網(wǎng)上搜到的mybatis-plus.interceptor.pagination.max-limit這種配置在3.5.5之前的版本不一定生效。最穩(wěn)的方式是把分頁攔截器寫成Bean不要依賴YAML去隱式配置。5. 字段關(guān)鍵字與保留字沖突MyBatisPlus最隱蔽的坑5.1 一個例子引發(fā)的血案有一張訂單表設(shè)計的時候有個字段叫order用來存排序權(quán)重。結(jié)果用MyBatisPlus查詢時生成的SQL是SELECT id, order FROM ...在MySQL里直接報語法錯誤因為order是保留字。類似的字段還有desc、group、index、key、lock、rank這些都是SQL保留字或MySQL函數(shù)名。問題在MyBatisPlus里尤其隱蔽因為實體類字段叫order生成的Mapper方法根本看不出任何問題直到運行時SQL解析報錯。5.2 解決方案TableField注解與反引號Data TableName(t_order) public class OrderEntity { TableId(type IdType.AUTO) private Long id; TableField(value order) private Integer order; }關(guān)鍵是TableField(value order)手動給字段加上反引號這樣MyBatisPlus生成SQL時會保留這個反引號MySQL就能正確識別列名。5.3 從源頭避免關(guān)鍵字問題我踩過這個坑之后的經(jīng)驗是建表時就避開保留字字段命名盡量用業(yè)務(wù)語義更完整的詞。比如order改成sort_orderdesc改成descriptiongroup改成group_code從源頭就切斷沖突的可能。如果你的項目已經(jīng)大量存在這種字段還有一種辦法在MyBatisPlus全局配置中開啟關(guān)鍵字自動轉(zhuǎn)義但這需要手動維護一個關(guān)鍵字列表而且不同數(shù)據(jù)庫的保留字還不完全一樣維護成本不低。最推薦的還是改表字段名和實體字段名一勞永逸。5.4 復(fù)習一下怎么快速排查是不是關(guān)鍵字問題遇到SQL語法錯誤時第一步把MyBatisPlus的SQL日志打開mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl然后把打印出來的SQL復(fù)制到數(shù)據(jù)庫客戶端里直接執(zhí)行。如果直接在客戶端報錯就復(fù)制SQL字段名看哪些是保留字。這里有個技巧把字段逐個用反引號包起來再執(zhí)行如果包了某個字段后SQL就正常了那問題就鎖定了。6. 整合項目中的其它實戰(zhàn)細節(jié)6.1 事務(wù)管理SpringBoot管理事務(wù)在Service方法上加Transactional注解即可這一點在桌面應(yīng)用里和在Web應(yīng)用里完全一樣。但要注意一個區(qū)別JavaFx的界面事件處理線程和Spring事務(wù)管理器的線程綁定機制。默認情況下Spring的事務(wù)是基于ThreadLocal的同一個事務(wù)必須在同一個線程內(nèi)完成。JavaFx的按鈕點擊事件運行在JavaFx Application線程上如果在這個線程里直接調(diào)用一個帶Transactional的Service方法事務(wù)是生效的因為Service方法在同一線程內(nèi)執(zhí)行。但如果你在事件回調(diào)里開了新線程去處理數(shù)據(jù)比如用CompletableFuture或ThreadPoolExecutor做異步處理那新線程里的事務(wù)是獨立的不共享外層事務(wù)。這是一個非常容易踩的坑。典型場景是點擊“批量導(dǎo)出”按鈕界面線程提交任務(wù)到線程池線程池里的方法使用Transactional結(jié)果發(fā)現(xiàn)事務(wù)根本沒生效因為Spring默認只攔截代理對象上的事務(wù)方法而從線程池調(diào)用的并不是被代理的對象入口。解決辦法是讓線程池內(nèi)部的調(diào)用直接注入Service代理不要手動new Service。6.2 數(shù)據(jù)源與連接池配置桌面應(yīng)用和Web應(yīng)用的數(shù)據(jù)庫連接模式很不相同。Web應(yīng)用是高并發(fā)、短連接、連接數(shù)大桌面應(yīng)用是單用戶、低并發(fā)、連接時間短但頻率高。所以我建議在桌面端把連接池的maximum-pool-size調(diào)小一點比如5到10就夠用了。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 3000還有一個細節(jié)桌面應(yīng)用經(jīng)常需要手動關(guān)閉數(shù)據(jù)庫連接。雖然HikariCP會自動管理連接池但如果你在Service里不小心持有Connection對象不釋放連接池會被耗盡。MyBatisPlus的Mapper調(diào)用本身不需要手動管理連接但如果你混用了JdbcTemplate或原生JDBC務(wù)必用try-with-resources保證連接歸還。6.3 界面線程與數(shù)據(jù)庫操作的性能考量JavaFx的UI線程不能做耗時操作否則界面卡死。但很多人不知道MyBatisPlus的Mapper查詢?nèi)绻麛?shù)據(jù)量大即使是正常查詢也可能占用幾百毫秒這在UI線程上就是明顯的卡頓。解決方案很簡單查詢操作放到獨立線程查詢完成后通過Platform.runLater回到UI線程更新界面。TaskListOrderEntity task new Task() { Override protected ListOrderEntity call() { return orderService.list(new LambdaQueryWrapperOrderEntity().eq(OrderEntity::getStatus, 1)); } }; task.setOnSucceeded(event - { ListOrderEntity result task.getValue(); // 更新TableView }); new Thread(task).start();JavaFx的Task類可以和WritableValue完美配合這是官方推薦的線程方案比直接new Thread更安全它會自動處理異常傳遞和界面更新。集成SpringBoot之后Task內(nèi)部可以直接調(diào)用注入好的Service完全不影響Spring的代理機制。6.4 打包發(fā)布的注意事項桌面應(yīng)用最終要打成可執(zhí)行jar或exe這里有個SpringBoot和JavaFx的經(jīng)典矛盾SpringBoot默認把依賴打進fat jar而JavaFx的native library在fat jar里可能會因為路徑問題加載失敗。我實測可行的方案是用maven-shade-plugin或者jpackage打包成可執(zhí)行文件。其中最簡單有效的是用jlink和jpackage做自定義運行時這樣JavaFx的native庫會以模塊方式安裝不會出現(xiàn)路徑問題。如果是團隊內(nèi)部工具不追求一鍵安裝直接用maven打包成fat jar配合java --module-path指定JavaFx模塊路徑運行也行。注意啟動命令要帶JavaFx的模塊java --module-path /path/to/javafx-sdk-17/lib --add-modules javafx.controls,javafx.fxml -jar app.jar如果打了真機運行的exe用jpackage是正式方案jpackage --name MyApp --input target/ --main-jar app.jar --main-class com.example.FxLauncher --type app-image打包過程中最常遇到的問題是SpringBoot的反射機制無法識別JavaFx的Controller類導(dǎo)致某些場景下Controller實例化失敗。解決辦法是在SpringBoot啟動類里手動掃描或者用ComponentScan顯式指定controller包。7. 這套架構(gòu)后續(xù)還能怎么擴展整合完成之后你會發(fā)現(xiàn)項目突然有了很多可能性。因為Service層已經(jīng)完全獨立于界面層你可以把同樣的Service抽出來作為本地RPC服務(wù)或者直接加一個SpringBootWeb模塊讓同一個服務(wù)同時支持桌面端和Web端調(diào)用。數(shù)據(jù)訪問層也更規(guī)范了代碼生成器一鍵生成Mapper和Entity分頁、邏輯刪除、自動填充這些需求全部通過配置完成不再需要手寫SQL。我在實際開發(fā)中體會最深的一點是把SpringBoot引入桌面應(yīng)用這件事表面上是加了一個框架實際上是改變了自己對桌面項目結(jié)構(gòu)的思考方式。以前寫JavaFx打開一個窗口就要想著怎么初始化數(shù)據(jù)庫、怎么管理連接、怎么在多個窗口間共享數(shù)據(jù)現(xiàn)在這些都變成了Spring的配置問題界面代碼只需要關(guān)心渲染和事件。如果你正在考慮給自己的JavaFx項目做一次架構(gòu)升級我建議先拿一個非核心模塊試點把啟動入口和分頁配置跑通再逐步遷移業(yè)務(wù)代碼。這套整合方案的收益不是立竿見影的但一旦跑順了后續(xù)加功能、改表結(jié)構(gòu)、加報表效率提升會非常明顯。本文還有配套的精品資源點擊獲取