Contents
1. 役割分担と統合のメリット/デメリット
このセクションでは、Spring Boot と JavaFX がそれぞれどの層を担当するかを整理し、導入時に注意すべきポイントを示します。全体像を把握したうえで、実装計画を立てやすくなることが目的です。
1.1 メリット
Spring Boot がバックエンドロジック・設定・外部サービス連携を一元管理し、JavaFX がフロントエンドの UI を担当することで得られる利点を列挙します。
-
テスト容易性
Spring のコンポーネントはユニットテストが標準化されているため、ビジネスロジックだけを高速に検証できます。 -
関心分離(Separation of Concerns)
UI 開発者は JavaFX/FXML に集中でき、バックエンド開発者は Spring Boot の機能だけで実装可能です。 -
設定の共有
application.yml等の設定ファイルを UI 側でも参照できるため、環境依存情報(API エンドポイントや認証トークン)を一元管理できます。
1.2 デメリット
統合に伴う技術的ハードルと運用上の注意点を整理し、事前対策を検討できるようにします。
-
スレッド制御
JavaFX の UI はJavaFX Application Thread上でしか操作できません。Spring コンテキストから取得した Bean がバックグラウンドで実行される場合は、Platform.runLater等で UI スレッドへ切り替える必要があります。 -
ビルド構成の衝突
OpenJFX のモジュール化と Spring Boot の fat‑jar(executable jar)生成が同時に行われると、モジュールパスやクラスローダーの競合が起きやすくなります。適切なプラグイン設定で回避しましょう。
2. プロジェクト構成例(Maven/Gradle)
ここでは、依存関係の衝突を防ぎつつテスト可能なマルチモジュール構造を提案します。実際に手元の環境でビルドできる設定サンプルも併せて示します。
2.1 ディレクトリ構成
以下は Maven を前提とした典型的なレイアウトです。core が Spring Boot、ui が JavaFX 用モジュールとなります。
|
1 2 3 4 5 6 7 |
my-app/ ├─ pom.xml ← 親 POM ├─ core/ ← Spring Boot(サービス層・リポジトリ) │ └─ pom.xml └─ ui/ ← JavaFX(FXML・コントローラ) └─ pom.xml |
2.2 親 POM の設定(共通 BOM)
Spring Boot と OpenJFX のバージョン管理は dependencyManagement にまとめ、サブモジュールでのバージョン指定を省略できるようにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<project xmlns="http://maven.apache.org/POM/4.0.0" ...> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>my-app</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <modules> <module>core</module> <module>ui</module> </modules> <dependencyManagement> <dependencies> <!-- Spring Boot BOM --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.1</version> <type>pom</type> <scope>import</scope> </dependency> <!-- OpenJFX BOM (Java 21) --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-bom</artifactId> <version>21.0.2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 省略: プラグイン設定 --> </project> |
2.3 core モジュール(Spring Boot)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<project ...> <parent> <groupId>com.example</groupId> <artifactId>my-app</artifactId> <version>1.0.0</version> </parent> <artifactId>core</artifactId> <dependencies> <!-- Spring Boot のスタータ --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JPA 例(必要に応じて) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> </dependencies> </project> |
2.4 ui モジュール(JavaFX)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<project ...> <parent> <groupId>com.example</groupId> <artifactId>my-app</artifactId> <version>1.0.0</version> </parent> <artifactId>ui</artifactId> <dependencies> <!-- JavaFX モジュール --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> </dependency> <!-- Spring コンテキスト取得用 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> <build> <plugins> <!-- JavaFX 用プラグイン例(exec-maven-plugin) --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.1.0</version> <configuration> <mainClass>com.example.ui.MainApp</mainClass> </configuration> </plugin> </plugins> </build> </project> |
2.5 Gradle(Kotlin DSL)で同等設定
Gradle を使用する場合のサンプルです。マルチプロジェクト構成を想定しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
plugins { id("org.springframework.boot") version "3.2.1" apply false kotlin("jvm") version "1.9.0" } subprojects { apply(plugin = "java") repositories { mavenCentral() } dependencies { implementation(platform("org.openjfx:javafx-bom:21.0.2")) implementation("org.openjfx:javafx-controls") implementation("org.openjfx:javafx-fxml") // UI プロジェクトだけが Spring を使用 if (project.name == "ui") { implementation("org.springframework.boot:spring-boot-starter") } } tasks.withType<JavaCompile> { options.encoding = "UTF-8" javaToolchain.languageVersion.set(JavaLanguageVersion.of(21)) } } |
3. 起動フローと DI の実装手順
Spring コンテキストを先行起動し、FXMLLoader に Spring の BeanFactory を注入することで、JavaFX コントローラでも @Autowired が利用可能になります。以下では具体的なコード例とポイント解説を示します。
3.1 メインクラス(Spring 起動+JavaFX 起動)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package com.example.ui; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; /** * JavaFX アプリケーションのエントリポイント。 * Spring コンテキストを先に起動し、FXML ローダーへ注入します。 */ public class MainApp extends Application { private static ConfigurableApplicationContext springContext; public static void main(String[] args) { // 1️⃣ Spring Boot 起動(ヘッドレス無効化) springContext = new SpringApplicationBuilder(AppConfig.class) .headless(false) .run(args); // 2️⃣ JavaFX アプリケーション起動 launch(args); } @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader( getClass().getResource("/fxml/MainView.fxml")); // Spring の BeanFactory をコントローラ生成に使用 loader.setControllerFactory(springContext::getBean); Parent root = loader.load(); primaryStage.setTitle("画像管理ツール(サンプル)"); primaryStage.setScene(new Scene(root)); primaryStage.show(); } @Override public void stop() throws Exception { // アプリ終了時にコンテキストをクローズしてリソース解放 springContext.close(); } } |
ポイント
headless(false)が無いと JavaFX の UI スレッドが起動できません。loader.setControllerFactory(springContext::getBean)により、@Componentで定義したコントローラが Spring 管理下に置かれます。
3.2 コントローラ例(DI 対応)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package com.example.ui.controller; import com.example.core.service.ImageService; import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.TableView; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * FXML に紐付くコントローラ。Spring の Service を DI しています。 */ @Component public class MainController { private final ImageService imageService; @FXML private TableView<?> tableView; // 実際の型は適宜置き換えてください @Autowired public MainController(ImageService imageService) { this.imageService = imageService; } /** FXML の initialize メソッド。UI 初期化とデータバインドを行う */ @FXML private void initialize() { // バックエンドから取得したデータを ObservableList に変換 tableView.setItems(FXCollections.observableArrayList( imageService.findAll())); } /** UI ボタン等から呼び出す例 */ @FXML private void onRefresh() { tableView.getItems().setAll(imageService.findAll()); } } |
注意点
@Componentが付与されていることで、springContext::getBeanがインスタンス化時に依存注入を実行します。- UI 更新は必ず JavaFX スレッド上で行う必要があるため、バックエンドスレッドから呼び出す場合は
Platform.runLater(...)を使用してください。
3.3 ライフサイクルとクリーンアップ
Spring の @PreDestroy メソッドや ApplicationListener<ContextClosedEvent> を活用すると、アプリ終了時のリソース解放が容易になります。
|
1 2 3 4 5 6 7 8 9 10 |
@Component public class ShutdownHook implements ApplicationListener<ContextClosedEvent> { @Override public void onApplicationEvent(ContextClosedEvent event) { // 例: DB コネクションプールやキャッシュのクリア処理 System.out.println("Spring コンテキストが閉じられました。"); } } |
4. テスト・パッケージング戦略
実務でリリース品質を担保するために、ユニットテスト・統合テストの構成と、配布用ランタイム生成手順をまとめます。
4.1 UI とバックエンドの統合テスト(JUnit 5 + TestFX)
TestFX は JavaFX アプリケーションの操作自動化に特化したフレームワークです。Spring のコンテキストと組み合わせることで、DI が正しく機能しているかも同時に検証できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.example.ui; import com.example.core.service.ImageService; import javafx.scene.control.TableView; import javafx.stage.Stage; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.testfx.api.FxRobot; import org.testfx.framework.junit5.ApplicationExtension; import org.testfx.framework.junit5.Start; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Spring コンテキストと JavaFX UI の統合テスト例。 */ @SpringBootTest @ExtendWith(ApplicationExtension.class) class MainControllerIT { @Autowired private ImageService imageService; @Start void start(Stage stage) throws Exception { // 本物の UI を起動(MainApp の start メソッドを流用) new MainApp().start(stage); } @Test void tableShowsInsertedImage(FxRobot robot) throws Exception { // テストデータ投入 imageService.save(MockFiles.samplePng()); // TableView が自動で更新されることを確認 TableView<?> table = robot.lookup("#tableView").queryAs(TableView.class); assertEquals(1, table.getItems().size()); } } |
参考: TestFX 公式ドキュメントは https://github.com/TestFX/TestFX を参照してください。
4.2 カスタムランタイムの作成(jlink + jpackage)
配布サイズ削減と OS 毎のインストーラ生成のため、JDK のモジュールシステムを利用した手順です。以下は Gradle (Kotlin DSL) 用スクリプト例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import org.gradle.internal.os.OperatingSystem tasks.register<Exec>("jlink") { dependsOn("installDist") val jmods = "${System.getProperty("java.home")}/jmods" val runtimePath = "$buildDir/image-tool-runtime" commandLine( "jlink", "--module-path", "$jmods:${project.configurations.runtimeClasspath.asPath}", "--add-modules", "java.base,javafx.controls,javafx.fxml,com.example.ui", "--output", runtimePath, "--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages" ) } tasks.register<Exec>("jpackage") { dependsOn("jlink") val runtimePath = "$buildDir/image-tool-runtime" commandLine( "jpackage", "--input", "$buildDir/libs", "--name", "ImageTool", "--main-jar", "ui.jar", "--runtime-image", runtimePath, "--type", if (OperatingSystem.current().isWindows) "msi" else "dmg" ) } |
実行手順
./gradlew clean buildで JAR を生成。./gradlew jlink→ カスタムランタイムが$buildDir/image-tool-runtimeに作成。./gradlew jpackage→ OS 向けインストーラ(Windows は.msi、macOS は.dmg)が生成されます。
4.3 よくある落とし穴と対策
| 症状 | 主な原因 | 推奨対策 |
|---|---|---|
FXMLLoader が FXML を見つけられない |
パスをクラスパス基準で相対指定している | 先頭に / を付けた絶対パス(例: getResource("/fxml/MainView.fxml"))に変更 |
java.lang.IllegalStateException: Not on FX application thread |
バックエンドのコールバックが直接 UI 更新を行っている | Platform.runLater(() -> {/* UI 操作 */}); でスレッド切り替え |
| ビルド時に「Missing required module」エラー | jlink --add-modules に自作モジュールが含まれていない |
自作 JAR に module-info.java があり、exports が正しく記述されているか確認 |
| アプリ終了後のメモリリーク(画像ハンドル残存) | ImageView に設定した Image を解放していない |
画面遷移時やウィンドウ閉鎖時に imageView.setImage(null); を呼び出す |
5. まとめ
- 役割分担
- Spring Boot → DI、設定、ビジネスロジック、トランザクション管理
-
JavaFX → FXML/CSS によるリッチ UI、ユーザー操作のハンドリング
-
プロジェクト構成は
core(Spring)+ui(JavaFX)のマルチモジュール化が推奨。BOM を用いたバージョン統一で依存衝突を防げます。 -
起動フローは
main()で Spring コンテキストを先行起動し、Application.launch後にFXMLLoader.setControllerFactory(springContext::getBean)を設定すれば、コントローラでも@Autowiredが利用可能です。 -
テスト戦略は JUnit 5 と TestFX の組み合わせで UI‑バックエンド統合テストを自動化し、リグレッション防止に役立ちます。
-
パッケージングは
jlink+jpackageによるカスタムランタイム生成が主流。インストーラ作成で社内配布がシンプルになります。
本稿の手順とサンプルをベースに、貴社プロジェクトでも Spring Boot と JavaFX の統合を迅速かつ安全に導入し、保守性とユーザー体験の高いデスクトップアプリケーション開発を実現してください。