IntelliJ IDEAでGradleプロジェクトの実行がエラーになる!?

Eclipse で開発していた JavaFX アプリケーション Gradle プロジェクト形式 がありました。この Gradle プロジェクトを IntelliJ IDEA にインポートして実行すると IllegalStateException: Location is not set. というエラーが発生してアプリケーションの起動に失敗してしまいました。Eclipse では実行できていたのになぜ?

その原因は Eclipse IntelliJ IDEA のリソースの扱いの違いにありました。Gradle プロジェクトでリソースをどこに配置するのがよいか紹介したいと思います。

IntelliJ IDEA JavaFX アプリケーションを実行したときに発生した例外は以下の内容でした。

Exception in Application start method
java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:464)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1051)
Caused by: java.lang.RuntimeException: Exception in Application start method
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:900)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.IllegalStateException: Location is not set.
	at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2459)
	at javafx.fxml/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2435)
	at com.example.myapp.Main.start(Main.java:21)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$(LauncherImpl.java:846)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:455)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
	at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
	... 1 more

どうやら FXML のロードに失敗しているようです。

Gradle プロジェクトの構成

私は JavaFX アプリケーションを以下のような構成にしています。

  • myapp
    • build.gradle
    • settings.gradle
    • src
      • main
        • java
          • com
            • example
              • myapp
                • Main.java
                • Main.fxml
                • Main.css
        • resources
          • img
            • icon.png

FXML CSS をソースコード .java と同じ場所に配置しています。こうすることで 以下のように Main.java から Main.fxml をロードすることができます。

FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
Parent root = (Parent)loader.load();

Eclipse Buildship の場合

Eclipse からアプリケーションを実行すると bin ディレクトリーにビルドしたファイル一式が出力されます。

  • myapp
    • bin
      • main
        • com
          • example
            • myapp
              • Main.class
              • Main.fxml
              • Main.css
        • img
          • icon.png

Eclipse の場合はコンパイルされた .class だけでなく ソースディレクトリーに含まれている Main.fxml Main.css も出力ディレクトリーにコピーされています。これなら Main.class から Main.fxml がリソースとして参照できます。

IntelliJ IDEA の場合

IntelliJ IDEA からアプリケーションを実行すると out/production ディレクトリーにビルドしたファイル一式が出力されます。

  • myapp
    • out
      • production
        • classes
          • com
            • example
              • myapp
                • Main.class
        • resources
          • img
            • icon.png

おや? IntelliJ IDEA では Main.fxml Main.css が出力ディレクトリーにコピーされていません。これでは FXML のロードに失敗するのも当然ですよね…。

FXML をどこに置けばいいの?

Gradle としては IntelliJ IDEA の動作が正しいようです。Eclipse ではソースディレクトリーに含まれるリソースも出力ディレクトリーにコピーされるため 運よく動作していたということでした。

.fxml .css もリソースディレクトリーに配置するのが Gradle の正しい作法です。

私だって リソースディレクトリー resources の存在を知らなかったわけではありません。画像などのリソースは resources に配置していました。ただ Main.java と対になる Main.fxml は同じ場所に置いておいたほうが管理しやすいと思ったんです。

以下のように Main.fxml Main.css resources の配下に配置すれば IntelliJ IDEA でもアプリケーションの起動に成功しました。

  • myapp
    • build.gradle
    • settings.gradle
    • src
      • main
        • java
          • com
            • example
              • myapp
                • Main.java
        • resources
          • com
            • example
              • myapp
                • Main.fxml
                • Main.css
          • img
            • icon.png

うーん でもやっぱり このツリー構造は嫌です。

開発時には .java .fxml .css を相互に行ったり来たりしてコーディングをおこなうわけで これらのファイルが異なるディレクトリーに配置されているというのは扱いづらいです。

リソースディレクトリーを追加しよう!

この問題はリソースディレクトリーを追加することで解決することができます。

Gradle では既定で以下の構成になっています。

ソースディレクトリー
src/main/java
リソースディレクトリー
src/main/resources

これはあくまでも既定値であって build.gradle を書き換えればディレクトリーを変更したり追加したりすることもできます。そこで src/main/java をリソースディレクトリーに追加してみましょう。

ソースディレクトリー
src/main/java
リソースディレクトリー
src/main/resources
src/main/java ←追加

このような構成にするわけです。src/main/java はソースディレクトリーでもあり リソースディレクトリーでもあるということです。これなら src/main/java 配下のファイルも出力ディレクトリーにコピーされるはず。

具体的には build.gradle に以下の内容を追加します。

sourceSets.main {
    resources {
        srcDirs = ['src/main/resources', 'src/main/java']
    }
}

これで 当初のツリー構造を維持したまま 無事に IntelliJ IDEA でもアプリケーションを実行できるようになりました。

  • myapp
    • build.gradle
    • settings.gradle
    • src
      • main
        • java ← ソースディレクトリー兼リソースディレクトリー
          • com
            • example
              • myapp
                • Main.java
                • Main.fxml
                • Main.css
        • resources
          • img
            • icon.png

リソースディレクトリーに src/main/java を追加したのにあわせて .java が出力ディレクトリーにコピーされないように exclude 指定も必要かなと思ったのですが .java は出力ディレクトリーにコピーされていませんでした。特別扱いされているのかもしれません。

サンプルコード一式

最後に 動作確認に使用したコード一式です。

Main.java
package com.example.myapp; import java.io.IOException; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml")); loader.setController(this); Parent root = (Parent)loader.load(); Scene scene = new Scene(root); primaryStage.setScene(scene); primaryStage.show(); } }
Main.fxml
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <StackPane stylesheets="@Main.css" xmlns:fx="http://javafx.com/fxml" prefWidth="240" prefHeight="160"> <Button text="Hello!"/> </StackPane>
Main.css
.root { -fx-font-family: "Meiryo"; -fx-font-size: 12px; } Button { -fx-text-fill: blue; }
build.gradle
apply plugin: 'java' def defaultEncoding = 'UTF-8' tasks.withType(AbstractCompile).each { it.options.encoding = defaultEncoding } tasks.withType(GroovyCompile).each { it.groovyOptions.encoding = defaultEncoding } sourceSets.main { resources { srcDirs = ['src/main/resources', 'src/main/java'] } } repositories { jcenter() } dependencies { }

サンプルコードは以下のリンクからダウンロードできます。

この記事を共有しませんか?