現象
以下のようなPerson
データ
class Person(name: String, birthDay: LocalDate) {
var nameProperty: StringProperty = SimpleStringProperty(name)
var birthdayProperty: ObjectProperty<LocalDate> = SimpleObjectProperty(birthDay)
}
これをTableView
にバインディングする状況を考える。
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()
}
}
これを実行すると、以下のように望んだ結果となる。
次に年齢を出すためにPerson
型を
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
に年齢列を追加する。
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
であれば、
application {
mainClass = 'example.AppKt'
}
の箇所を
application {
applicationDefaultJvmArgs += ["--add-opens", "javafx.base/com.sun.javafx.binding=ALL-UNNAMED"]
mainClass = 'example.AppKt'
}
として、明示的にエクスポートするようコンパイラに伝える1。
この設定を行った上で、実行したところ例外が送出されることなく実行できた。
補足事項
Learning RxJava with JavaFXのサンプルコードでは、tornadefx
を使用している。
tornadefx
においても過去にTornadoFX uses internal APIs that are encapsulated in Java 9 #276なissueが上がっている(今は対処済みかもしれないが)
環境
- kotlin - version 1.4.31
- JVM - version 16.0.1
- Gradle - version 7.0.2
- JavaFX - version 16
- RxJava - version 2.2.212
- RxJavaFX - version 2.2.2