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
- myapp
- example
- com
- resources
- img
- icon.png
- img
- java
- main
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
- myapp
- example
- img
- icon.png
- com
- main
- bin
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
- myapp
- example
- com
- resources
- img
- icon.png
- img
- classes
- production
- out
おや? 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
- myapp
- example
- com
- resources
- com
- example
- myapp
- Main.fxml
- Main.css
- myapp
- example
- img
- icon.png
- com
- java
- main
うーん、 でもやっぱり、 このツリー構造は嫌です。
開発時には .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
- myapp
- example
- com
- resources
- img
- icon.png
- img
- java ← ソースディレクトリー兼リソースディレクトリー
- main
リソースディレクトリーに src/main/java
を追加したのにあわせて、 .java
が出力ディレクトリーにコピーされないように exclude
指定も必要かなと思ったのですが、 .java
は出力ディレクトリーにコピーされていませんでした。特別扱いされているのかもしれません。
サンプルコード一式
最後に、 動作確認に使用したコード一式です。
Main.javapackage 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.gradleapply 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 {
}
サンプルコードは以下のリンクからダウンロードできます。