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>
コントローラクラス
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 プロジェクトの様子
説明
起動
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
では、 FXMLLoader
の controllerFactory
を 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();
}
}
一応動いたけど、何が違うのだろう?