JavaFXでCDI（Weld）を使用する方法メモ

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


#環境
##Java
1.7.0_25

##Weld
1.1.10

#実装
##Mainクラス
```java: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();
    }
}
```

##イベント通知を絞り込むためのカスタム限定子
```java: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の画面表示を開始するクラス
```java: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 クラス
```java: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
```xml: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](https://qiita-image-store.s3.amazonaws.com/0/28302/6b6ca6ab-048d-5c93-0f15-2c436a951e08.jpeg)

##コントローラクラス
```java: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でインジェクションするクラス
```java:MyService.java
package fx;

import javax.inject.Singleton;

@Singleton
public class MyService {
    
    private int cnt;
    
    public int nextCount() {
        return ++cnt;
    }
}
```

##実行結果
![実行結果](https://qiita-image-store.s3.amazonaws.com/0/28302/b1762cc3-15e6-2599-7d8f-268063c8d88e.jpeg)
クリックする度にカウントが加算される。

##Eclipse プロジェクトの様子
![Eclipse プロジェクトの様子](https://qiita-image-store.s3.amazonaws.com/0/28302/0a21f1c0-e035-08bd-b3d3-b72eb74f0a14.jpeg)

#説明
##起動
```java: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 をインジェクションする
```java: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` というインスタンス提供用クラスを作成して、そこから取得している。

```java: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 の機能が使用できるようになる。

#参考
+ [FXML &amp; JavaFX—Fueled by CDI &amp; JBoss Weld | Javalobby](http://java.dzone.com/articles/fxml-javafx-powered-cdi-jboss)

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

```java: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();
    }
}
```

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