[kotlin] RxJavaFxによるTableViewの計算列へのバンディングでjava.lang.IllegalAccessError出る場合の対処

# 現象

以下のような`Person`データ

```kotlin
class Person(name: String, birthDay: LocalDate) {
    var nameProperty: StringProperty = SimpleStringProperty(name)
    var birthdayProperty: ObjectProperty<LocalDate> = SimpleObjectProperty(birthDay) 
}
```

これを`TableView`にバインディングする状況を考える。

```kotlin
typealias TableCellFactory<T> = javafx.util.Callback<TableColumn.CellDataFeatures<Person,T>,ObservableValue<T>> 

class AppMain: Application() {
    override fun start(stage: Stage) {
        val table = TableView<Person>()        
        table.setEditable(true)
        table.getItems().setAll(
            Person("A", LocalDate.of(1989,1,18)),
            Person("B",LocalDate.of(1980,5,12)),
            Person("C",LocalDate.of(1975,3,8))        
        )       

        val nameCol = TableColumn<Person,String>("Name")  
        nameCol.cellValueFactory = TableCellFactory<String>{ v -> v.value.nameProperty } 
        
        val birthdayCol = TableColumn<Person,LocalDate>("Birthday")
        birthdayCol.cellValueFactory = TableCellFactory<LocalDate>{ v -> v.value.birthdayProperty }
        birthdayCol.cellFactory = TextFieldTableCell.forTableColumn(LocalDateStringConverter())
        
        table.getColumns().addAll(nameCol, birthdayCol)
        stage.setScene(Scene(table));
        stage.title = "Table View binding"
        stage.width = 300.0
        stage.height = 200.0
        stage.show()
    }
}
```

これを実行すると、以下のように望んだ結果となる。

![スクリーンショット 2021-07-25 22.51.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11233/dfc44e35-3a77-5bb0-c816-fb37efebd23c.png)

次に年齢を出すために`Person`型を

```kotlin
class Person(name: String, birthDay: LocalDate) {
    // (snip)
    var ageProperty: Binding<Long> = 
        JavaFxObservable.valuesOf(birthdayProperty)
        .map { d -> ChronoUnit.YEARS.between(d, LocalDate.now()) }
        .to(JavaFxObserver::toBinding)    
}
```

のように生年月日から年齢を導出するバインディングプロパティを用意する。

また`TableView`に年齢列を追加する。

```kotlin
class AppMain: Application() {
    override fun start(stage: Stage) {
        // (snip)        
        val ageCol = TableColumn<Person,Long>("Age")   
        ageCol.cellValueFactory = TableCellFactory<Long>{ v -> v.value.ageProperty }

        table.getColumns().addAll(nameCol, birthdayCol)
        // (snip)
    }
}
```

これを実行すると、`java.lang.IllegalAccessError`の例外が送出される。

その際のスタックトレースが以下

```
Exception in Application start method
Exception in thread "main" java.lang.RuntimeException: Exception in Application start method
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:900)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
        at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: java.lang.IllegalAccessError: class io.reactivex.rxjavafx.observers.BindingObserver (in unnamed module @0xeb3ba90) cannot access class com.sun.javafx.binding.ExpressionHelper (in module javafx.base) because module javafx.base does not export com.sun.javafx.binding to unnamed module @0xeb3ba90
        at ...
```

# 発生源

おそらく、`Person`型で追加した`Binding`型の`ageproperty`を構成する際の

> to(JavaFxObserver::toBinding)

で引っかかっている模様。

# 対処法

上記のスタックトレースで

> module javafx.base does not export com.sun.javafx.binding

に注目する。

`RxJavaFx`がエクスポートされていない`com.sun.javafx.binding`に依存していることが問題となっている。

したがって`com.sun.javafx.binding`をエクスポートしてやれば解決すると思われる。

`gradle`であれば、

```gradle
application {
    mainClass = 'example.AppKt'
}
```

の箇所を

```gradle
application {
    applicationDefaultJvmArgs += ["--add-opens", "javafx.base/com.sun.javafx.binding=ALL-UNNAMED"]
    mainClass = 'example.AppKt'
}
```

として、明示的にエクスポートするようコンパイラに伝える[^1]。

[^1]: gradleしか触ってないので他のビルド環境の指定方法はわかりません。

この設定を行った上で、実行したところ例外が送出されることなく実行できた。

![スクリーンショット 2021-07-25 23.16.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11233/d65b3d2a-0a45-7a77-7046-14f3426f4e15.png)

# 補足事項

*Learning RxJava with JavaFX*のサンプルコードでは、`tornadefx`を使用している。
`tornadefx`においても過去に[TornadoFX uses internal APIs that are encapsulated in Java 9 #276](https://github.com/edvin/tornadofx/issues/276)なissueが上がっている（今は対処済みかもしれないが）

# 環境

* kotlin - version 1.4.31
* JVM - version 16.0.1
* Gradle - version 7.0.2
* JavaFX - version 16
* RxJava - version 2.2.21[^2]
* RxJavaFX - version 2.2.2

[^2]: RxJavaFXがRxJava2にぶら下がっているため2系を使用した。