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 で公開しています。

FXPlugin