JavaFX でプラグインを使う
このエントリーは JavaFX Advent Calendar 2014 の最終日です。
昨日は @orekyuu さんの 24 日目なのに、タイトルは 25 日目になっている JavaFX Advent Calendar25日目 ~ JavaFXで夢のCanvasライフ でした。
いつも、アニメーションなどの描画ネタが多いのですが、今日は趣を変えて JavaFX のアプリケーションでプラグインを作ろうと思います。
意外に知られていないと思いますが、Java SE にはプラグインを作るのに便利なクラスがあります。
そのクラスは java.util.ServiceLoader クラスです。
ServiceLoader クラスについては、ずいぶん前に ITpro の Java 技術最前線に記事を書いたので、そちらをぜひご参照ください。
「Java SE 6完全攻略」第11回 コンポーネントのロードを行うServiceLoader
ようするに ServiceLoader クラスはプラグインを検索して、ロードしてくれるクラスです。
たとえば、プラグインを表すインタフェースを foo.Bar インタフェースとしましょう。
そして、プラグインごとに Jar ファイルを作成しますが、その時に META-INF/services/foo.Bar というファイルを作成します。その foo.Bar ファイルには foo.Bar インタフェースを実装したクラス名を記述します。
これだけでその Jar ファイルをクラスパスに含めておけば、プラグインとしてロードすることができます。
ここでは、プラグインの仕組みだけを示すために、簡単なアプリケーションを作ることにします。アプリケーションには複数のタブがあり、そのタブの中身をプラグインで表示させるということにします。
こんな感じ。
プラグインは、ファクトリとプラグインの本体という構造にしたいと思います。
まずファクトリは PluginFactory インタフェースとします。
package net.javainthebox.fxplugin.plugin; import java.util.Optional; public interface PluginFactory { String getName(); Optional<Plugin> createPlugin(); }
getName メソッドはタブの名前を返すためのメソッド、プラグインの本体は createPlugin メソッドで生成します。
プラグインの JAR ファイルは、このインタフェース名と同じファイルを作成します。つまり、META-INF/services/net.javainthebox.fxplugin.plugin.PluginFactory ファイルです。
そして、プラグインの本体は Plugin インタフェースです。
public interface Plugin { Node getContent(); }
そして、このプラグインをロードするためのアプリケーションはすごい簡単なものにしました。
public class Main extends Application { @Override public void start(Stage stage) { StackPane root = new StackPane(); TabPane tabs = new TabPane(); root.getChildren().add(tabs); loadPlugins(tabs); Scene scene = new Scene(root, 300, 250); stage.setTitle("FXPlugin"); stage.setScene(scene); stage.show(); } private void loadPlugins(TabPane tabs) { ServiceLoader<PluginFactory> loader = ServiceLoader.load(PluginFactory.class); loader.forEach(factory -> { Tab tab = new Tab(factory.getName()); factory.createPlugin().ifPresent(plugin -> tab.setContent(plugin.getContent())); tabs.getTabs().add(tab); }); } public static void main(String... args) { launch(args); } }
ここで、プラグインをロードしているのは loadPlugins メソッドです。
ServiceLoader オブジェクトは load メソッドで生成できます。load メソッドの引数はロードするプラグインのインタフェースです。
ServiceLoader クラスは Iterable インタフェースを実装しているので、forEach メソッドを使用できます。
まず、ファクトリの getName メソッドを使用して、Tab オブジェクトを作成します。
そして、プラグインの本体は createPlugin メソッドで生成します。この返り値は Optional クラスなので、値があるときだけ、Tab オブジェクトのコンテンツをセットするようにしました。
これでプラグインをロードする部分はできました。
プラグインの作成
では次にプラグインを作成してみましょう。
ここではシンプルなプラグインということで、ボタンが 1 つだけあるプラグインを作成します。
まずはファクトリクラスです。
public class ButtonPluginFactory implements PluginFactory { @Override public String getName() { return "Button"; } @Override public Optional<Plugin> createPlugin() { try { return Optional.of(new ButtonPlugin()); } catch (IOException ex) { return Optional.empty(); } } }
getName メソッドは Button を返すだけです。
createPlugin メソッドは IOException 例外が発生したら、空の Optional オブジェクトを返します。それ以外は ButtonPlugin オブジェクトを使用します。
では ButtonPlugin クラスを見てみましょう。
public class ButtonPlugin implements Plugin { private AnchorPane content; private ButtonPluginViewController controller; public ButtonPlugin() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("ButtonPluginView.fxml")); content =loader.load(); controller = loader.getController(); } @Override public Node getContent() { return content; } }
ここでは、FXML をロードしています。
FXMLLoader オブジェクトを生成しているのは、コントローラクラスを取得するためです。ここではコントローラクラスを直接アクセスしていませんが、大規模なアプリケーションの場合はコントロールクラスからモデルへアクセスするなど、コントローラクラスの取得が必要な場合が多くあります。
そういうときのためにも、FXMLLoader オブジェクトを生成しておく方がいいと思います。
いちおう FXML ファイルも示しておきましょう。
<?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import java.util.*?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <AnchorPane prefHeight="200.0" prefWidth="300.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="netjavainthebox.fxplugin.buttonplugin.ButtonPluginViewController"> <children> <Button layoutX="117.0" layoutY="75.0" mnemonicParsing="false" onAction="#action" style="-fx-font-size: 24;" text="OK" AnchorPane.bottomAnchor="75.0" AnchorPane.leftAnchor="117.0" AnchorPane.rightAnchor="116.0" AnchorPane.topAnchor="75.0" /> </children> </AnchorPane>
コントローラクラスは、ボタンがクリックされたらダイアログを表示するだけです。
ダイアログを使用しているので、このサンプルをビルド、実行するには、JDK 8u40 が必要です。
public class ButtonPluginViewController implements Initializable { @FXML private void action(ActionEvent event) { Alert alert = new Alert(AlertType.INFORMATION); alert.setTitle("Button Plugin"); alert.getDialogPane().setHeaderText("Button Plugin"); alert.getDialogPane().setContentText("Button Plugin"); alert.show(); } @Override public void initialize(URL url, ResourceBundle rb) { } }
そして、忘れてはいけないのが META-INF/services/net.javainthebox.fxplugin.plugin.PluginFactory ファイルです。
ファイルの中身は PluginFactory インタフェースの実装クラス名です。
netjavainthebox.fxplugin.buttonplugin.ButtonPluginFactory
もう 1 つ、同じように ListPlugin というのも作成しました。
そして、アプリケーションの実行時には、2 つのプラグインの JAR ファイルをクラスパスに付け加えます。Windows だったら、こんな感じで実行します。
java -cp PluginContainer.jar;ButtonPlugin.jar;ListPlugin.jar net.javainthebox.fxplugin.container.Main
実行すると、タブが 2 つ表示されます。
ちゃんとプラグインがロードできました。
ここでは、簡単なアプリケーションですが、これを応用すれば、複雑なアプリケーションもできるはずです!
サンプルのコードは GitHub で公開しています。