[play1-deps] ui4jを使いたいのですが

  playframework1

うだうだ

とあるサイトをJsoupでスクレイピングして情報を収集していたのですが、ある日を境に収集に失敗するように。調べてみるとreact.jsというのを使って動的にページを作成するように変わったようです。

こうなると単なるhtmlをダウンロードするjsoupではお手上げ、ブラウザのように振舞いJavascriptを実行してページを読み込まないといけません。

playframework1ではテスト機能をtestrunnerというモジュールとして内蔵しています。その中でhtmlテスト用にhtmlUnitというライブラリを標準で使用するようになっています。いわゆるheadlessブラウザで簡単なWebページやJavascriptであれば表示(画面はないのでDOMとして読み込み)が可能です。

htmlUnitは確かにJavaScriptを実行できるのですが、JavaScriptエンジンにMozilla Rhihoというオープンソースのエンジンを採用していて、その仕様に制限を受けることが間々あります。Yahoo!JAPANのような皆が見るようなサイトでもJavaScriptエラーが出まくり例外スローを抑える設定をしないとページ読み込みさえ完遂できないことがあります。(WebClientを生成するときのバージョン指定で古いブラウザであるINTERNET_EXPLORERを指定すると相手方で手加減してくれるワンチャンあるようです)

自分でサイトを一から作成するときのテストツールとしては使えないこともないですが、一般に公開されているサイトのスクレイピング用には不向きと言わざるを得ません。

次なる候補としてui4jというのを使ってみることにしました。JavaFXに含まれるWebKitのラッパーライブラリということで使いやすそうだったからです。

導入…できない?

ui4jのgithubレポジトリ(webfolderio/ui4j: Web Automation for Java (github.com))を見ると、mavenレポジトリから取得できるよ、と書いてあります。headlessモードに対応させるためのMonocleも同じように取得できるので conf/dependencies.yml にて解決しそうです。

<dependency>
    <groupId>io.webfolder</groupId>
    <artifactId>ui4j-webkit</artifactId>
    <version>4.0.0</version>
</dependency>
<dependency>
    <groupId>org.testfx</groupId>
    <artifactId>openjfx-monocle</artifactId>
    <version>jdk-11+26</version>
    <scope>test</scope>
</dependency>
require:
    - play
    - play -> docviewer
    - io.webfolder -> ui4j-webkit 4.0.0
    - org.testfx -> openjfx-monocle jdk-11+26

こんな感じに記述して、play deps もエラーなく成功。でも実際に実行してみると、javafx.application.Platform というクラスが見つからないと例外が出てしまいました。

java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at play.modules.launcher.Launcher.main(Launcher.java:57)
Caused by: java.lang.NoClassDefFoundError: javafx/application/Platform
	at io.webfolder.ui4j.webkit.WebKitBrowser.<init>(WebKitBrowser.java:59)
	at io.webfolder.ui4j.webkit.WebKitBrowserProvider.create(WebKitBrowserProvider.java:27)
	at io.webfolder.ui4j.api.browser.BrowserFactory.getBrowser(BrowserFactory.java:115)
	at io.webfolder.ui4j.api.browser.BrowserFactory.getWebKit(BrowserFactory.java:151)
	at io.webfolder.ui4j.api.browser.BrowserFactory$getWebKit.call(Unknown Source)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:119)
	at test.run(test.groovy:13)
	at play.modules.launcher.GroovyLauncher.executeScript(GroovyLauncher.java:90)
	at jobs.LaunchScript.main(LaunchScript.java:33)
	... 4 more
Caused by: java.lang.ClassNotFoundException: javafx.application.Platform
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	... 15 more

な、なにゆえに。と調べてみるとui4jに依存するライブラリとしてダウンロードされた、javafx-base.jarjavafx-graphics.jarjavafx-web.jarjavafx-controls.jar の中身が空っぽでした。

さらに調べを進めると、そもそもそれらのライブラリは実行環境によって、win/linux/macを指定するものらしいです。(例:<classifier>win</classifier>

解決できた

で、どうやって解決したかというと、下記のようにui4jの定義の前に javafx関連の定義を書くことで解決できました。先勝ちのようですね。

require:
    - play
    - play -> docviewer
    - org.openjfx -> javafx-base 11.0.2 win
    - org.openjfx -> javafx-graphics 11.0.2 win
    - org.openjfx -> javafx-web 11.0.2 win
    - org.openjfx -> javafx-controls 11.0.2 win
    - io.webfolder -> ui4j-webkit 4.0.0 win
    - org.testfx -> openjfx-monocle jdk-11+26

ちなみに、いったん実行環境(プラットフォーム)の指定なし版をダウンロードしまうと、実行環境を指定した版を取得してくれないことがありました。そんなときは強制キャッシュクリアのオプション(--clearcache)を付けましょう。

BrowserEngine browser = BrowserFactory.getWebKit()
Page page = browser.navigate('about:blank') 
page.show()
page.getDocument().getBody().append('<h1>Hello, world!</h1>')

Thread.sleep(5000)

page.close()
browser.shutdown()

ひとまずブラウザの表示までの動きが確認できました。

しかし?

ブラウザが起動できることは確認できたので一般のサイトを表示させてみよう、ということでお馴染みの Yahoo! JAPAN を開いてみました。ところが、java.lang.NoClassDefFoundError: com/sun/media/jfxmedia/MediaManager という例外が発生して止まってしまいました。まだ何か足りないようです。

[main] INFO io.webfolder.ui4j.api.browser.BrowserFactory - Initializing WebKit
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [30%]
Exception in thread "JavaFX Application Thread" java.lang.NoClassDefFoundError: com/sun/media/jfxmedia/MediaManager
	at com.sun.javafx.webkit.prism.PrismGraphicsManager.getSupportedMediaTypes(PrismGraphicsManager.java:155)
	at com.sun.webkit.MainThread.twkScheduleDispatchFunctions(Native Method)
	at com.sun.webkit.MainThread.lambda$fwkScheduleDispatchFunctions$0(MainThread.java:35)
	at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
	at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
	at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
	at com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ClassNotFoundException: com.sun.media.jfxmedia.MediaManager
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	... 10 more

その後、さらにJavaFXについて調べた結果、OpenJDK11を利用している場合、jarの取得だけでは不十分ということが分かりました。OpenJDKではJavaFX部分が分離されており、実行時にJavaFXのモジュールを利用する場合はVM引数で明示しなければならないそうです。

具体的には下記のようなVM引数を付けることで、ブラウザで Yahoo! JAPAN を表示するところまで到達しました。(headlessの場合はjavafx.graphicsも?)

--module-path="C:\Tools\bin\javafx-sdk-11.0.2\lib"
--add-modules=javafx.controls,javafx.web
--add-exports javafx.web/com.sun.webkit=ALL-UNNAMED

OpenJDK 11にJavaFXを導入する
JDK に JavaFX が同梱されなくなったため、 JavaFX アプリケーションの開発には別途 OpenJFX の導入が必要になりました。

https://blogs.osdn.jp/2018/11/12/merge-openjfx.html

JavaFXライブラリのインストール – ソフトウェアエンジニアリング – Torutk
本ページは、Java SE 11以降でJavaFXライブラリを利用した開発をするためのインストール作業を記述します。

https://www.torutk.com/projects/swe/wiki/JavaFX%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

java – Package ‘com.sun.webkit.dom’ is declared in module ‘javafx.web’, which does not export it to module – Stack Overflow

https://stackoverflow.com/questions/47684470/package-com-sun-webkit-dom-is-declared-in-module-javafx-web-which-does-not

というわけでめでたくウェブページを表示するところまでたどり着きました。(まだ下記のようなエラーは出ますが)

[main] INFO io.webfolder.ui4j.api.browser.BrowserFactory - Initializing WebKit
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [30%]
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [50%]
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [55%]
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [100%]
Exception in thread "JavaFX Application Thread" java.lang.IllegalAccessError: class io.webfolder.ui4j.webkit.browser.WebKitPageContext (in unnamed module @0x30c93896) cannot access class com.sun.webkit.dom.DocumentImpl (in module javafx.web) because module javafx.web does not export com.sun.webkit.dom to unnamed module @0x30c93896
	at io.webfolder.ui4j.webkit.browser.WebKitPageContext.createDocument(WebKitPageContext.java:122)
	at io.webfolder.ui4j.webkit.WebKitBrowser$WorkerLoadListener.changed(WebKitBrowser.java:161)
	at io.webfolder.ui4j.webkit.WebKitBrowser$WorkerLoadListener.changed(WebKitBrowser.java:1)
	at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
	at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
	at javafx.base/javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
	at javafx.base/javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
	at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
	at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
	at javafx.web/javafx.scene.web.WebEngine$LoadWorker.updateState(WebEngine.java:1251)
	at javafx.web/javafx.scene.web.WebEngine$LoadWorker.dispatchLoadEvent(WebEngine.java:1366)
	at javafx.web/javafx.scene.web.WebEngine$LoadWorker.access$1200(WebEngine.java:1244)
	at javafx.web/javafx.scene.web.WebEngine$PageLoadListener.dispatchLoadEvent(WebEngine.java:1231)
	at javafx.web/com.sun.webkit.WebPage.fireLoadEvent(WebPage.java:2513)
	at javafx.web/com.sun.webkit.WebPage.fwkFireLoadEvent(WebPage.java:2358)
	at javafx.web/com.sun.webkit.network.URLLoader.twkDidFinishLoading(Native Method)
	at javafx.web/com.sun.webkit.network.URLLoader.notifyDidFinishLoading(URLLoader.java:871)
	at javafx.web/com.sun.webkit.network.URLLoader.lambda$didFinishLoading$5(URLLoader.java:862)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
	at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
	at java.base/java.lang.Thread.run(Thread.java:834)

そしてもう一つ問題が。思ったより表示にかかる時間が長く、気軽に使える感じじゃありません。もしかすると設定値とかを工夫すると何とかなるのかもしれませんが、ちょっとモチベーションがそこまで続きそうもないので、ui4jは一旦ここまでとしたいと思います。

唐突に終わってすみません。

LEAVE A COMMENT