Java
JavaFX
Weld
CDI

JavaFXでCDI(Weld)を使用する方法メモ

More than 1 year has passed since last update.

JavaFXの実装でCDI(Weld)を利用する方法のメモ。

環境

Java

1.7.0_25

Weld

1.1.10

実装

Mainクラス

Main.java
package fx;

import javafx.application.Application;
import javafx.stage.Stage;

import javax.enterprise.util.AnnotationLiteral;

import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    private Weld weld;

    @SuppressWarnings("serial")
    @Override
    public void start(Stage stage) throws Exception {
        this.weld = new Weld();
        WeldContainer container = this.weld.initialize();

        container.event()
                 .select(Stage.class, new AnnotationLiteral<MyQualifier>() {})
                 .fire(stage);
    }

    @Override
    public void stop() {
        this.weld.shutdown();
    }
}

イベント通知を絞り込むためのカスタム限定子

MyQualifier.java
package fx;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Qualifier
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyQualifier {
}

JavaFXの画面表示を開始するクラス

ApplicationStarter.java
package fx;

import java.io.IOException;
import java.io.InputStream;

import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import javax.enterprise.event.Observes;
import javax.inject.Inject;

public class ApplicationStarter {

    @Inject private FXMLLoader loader;

    public void start(@Observes @MyQualifier Stage stage) throws IOException {
        try (InputStream fxml = MyController.getFxmlStream()) {
            Parent root = (Parent)this.loader.load(fxml);
            Scene scene = new Scene(root);

            stage.setScene(scene);
            stage.show();
        }
    }
}

FXMLLoader の Producer クラス

FXMLLoaderProducer.java
package fx;

import javafx.fxml.FXMLLoader;
import javafx.util.Callback;

import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;

public class FXMLLoaderProducer {

    @Inject Instance<Object> instance;

    @Produces
    public FXMLLoader createLoader() {
        FXMLLoader loader = new FXMLLoader();

        loader.setControllerFactory(new Callback<Class<?>, Object>() {
            @Override
            public Object call(Class<?> param) {
                return instance.select(param).get();
            }
        });

        return loader;
    }
}

FXML

cdi.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.*?>

<AnchorPane fx:id="base" minHeight="158.0" prefHeight="158.0" prefWidth="266.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="fx.MyController">
  <!-- TODO Add Nodes -->
  <children>
    <Button fx:id="button" layoutX="75.0" layoutY="25.0" mnemonicParsing="false" onAction="#onButtonClick" prefHeight="37.0" prefWidth="118.0" text="Button">
      <font>
        <Font size="20.0" fx:id="x1" />
      </font>
    </Button>
    <Label id="text" fx:id="label" alignment="CENTER" contentDisplay="CENTER" font="$x1" layoutX="15.0" layoutY="79.0" minHeight="12.0" prefHeight="65.0" prefWidth="237.0" text="Label" textAlignment="CENTER" />
  </children>
</AnchorPane>

Scene Builder で見た様子。
cdi.fxml

コントローラクラス

MyController.java
package fx;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

import javax.inject.Inject;

public class MyController {

    public static InputStream getFxmlStream() {
        return MyController.class.getResourceAsStream("/cdi.fxml");
    }

    @FXML   private Label     label;
    @Inject private MyService service;

    @FXML
    public void onButtonClick() {
        label.setText("cnt : " + this.service.nextCount());
    }
}

CDIでインジェクションするクラス

MyService.java
package fx;

import javax.inject.Singleton;

@Singleton
public class MyService {

    private int cnt;

    public int nextCount() {
        return ++cnt;
    }
}

実行結果

実行結果
クリックする度にカウントが加算される。

Eclipse プロジェクトの様子

Eclipse プロジェクトの様子

説明

起動

Main.java
public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @SuppressWarnings("serial")
    @Override
    public void start(Stage stage) throws Exception {
        // ....
        container.event()
                 .select(Stage.class, new AnnotationLiteral<MyQualifier>() {})
                 .fire(stage);
        // ....
    }
}

まず、 CDI のイベント通知機能を利用して、JavaFX 画面起動用のメソッド(ApplicationStarter#start(Stage))を呼び出している。

CDI 用に改造した FXMLLoader をインジェクションする

ApplicationStarter.java
public class ApplicationStarter {

    @Inject private FXMLLoader loader;

    public void start(@Observes @MyQualifier Stage stage) throws IOException {
        // ....       
        Parent root = (Parent)this.loader.load(fxml);
        // ....
    }
}

JavaFX 画面起動用クラスには、 FXMLLoader をインジェクションしている。この FXMLLoader は、 FXMLLoaderProducer というインスタンス提供用クラスを作成して、そこから取得している。

FXMLLoaderProducer.java
public class FXMLLoaderProducer {

    @Inject Instance<Object> instance;

    @Produces
    public FXMLLoader createLoader() {
        FXMLLoader loader = new FXMLLoader();

        loader.setControllerFactory(new Callback<Class<?>, Object>() {
            @Override
            public Object call(Class<?> param) {
                return instance.select(param).get();
            }
        });

        return loader;
    }
}

FXMLLoaderProducer では、 FXMLLoadercontrollerFactory を CDI のコンテナからインスタンスを取得する実装に差し替えたものを提供するようにしている。

こうすることで、 JavaFX が FXML とコントローラクラスを結びつけるときに、コントローラクラスのインスタンスを CDI コンテナから取得するようになる。

結果、コントローラクラスでも CDI の機能が使用できるようになる。

参考

書いた後に気づいたこと

参考に挙げているサイトの実装をそのまま利用しているので、 ApplicationStarter#start(Stage) の実行にはイベントを使用しているけど、よく考えたら普通に ApplicationStarter のインスタンスをコンテナから取得して実行しても問題ない気がする。

Main.java
package fx;

import javafx.application.Application;
import javafx.stage.Stage;

import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        Weld weld = new Weld();
        WeldContainer container = weld.initialize();

        // コンテナからインスタンスを取得して実行
        ApplicationStarter starter = container.instance().select(ApplicationStarter.class).get();
        starter.start(stage);

        weld.shutdown();
    }
}

一応動いたけど、何が違うのだろう?