古いJavaライブラリ(crawler4j)の依存関係を整理してみた

# はじめに

[Crawler4jを利用した日本語コンテンツの収集](https://qiita.com/YasuhiroABE/items/4df272fcc0c2cccc4173)で利用しているCrawler4jについてです。

Javaの歴史も長くなってメンテナンスされていないライブラリを使い続けることが難しくなってきました。

ライブラリの変更は互換性のある後継ライブラリが存在しない限り、既存コードを大幅に書き換えることが必要となります。

今回はCrawler4jのオリジナルコードをGithubでForkし、依存関係にあるライブラリを最新版にしつつ再コンパイルしてみました。

ついでに作成したコードをMaven Centralにアップロードしたので、それらの顛末をまとめておきます。

## 目的

とりあえず次の2つが目標です。

1. HarborのScannerで重度の警告が出ない程度に依存パッケージを最新版にする
1. Java 21(LTS)を前提とする

## 成果物

* [https://github.com/YasuhiroABE/crawler4j](https://github.com/YasuhiroABE/crawler4j)

## Crawler4jについて

Javaで比較的規模の大きいWebサイトを巡回し、コンテンツを収集するためのライブラリがCrawler4jです。

他の言語も含めて特定のページから情報を抜き出すスクレイピングに特化したライブラリは比較的充実していますが、サイトの巡回はサーバーのリソースを枯渇させないように適切な間隔を保ったり、適切な規模の並行性を持ちつつバランスを取るための機能は自前で実装しなければいけないものが多い印象です。

あるいはApache Nutchのように機能が完璧すぎるあまりカスタマイズの余地があまりなかったり、複雑だったりする場合もあります。

Crawler4jは自前でクローリング・アプリケーションを開発する目的では良いバランスをもっていると思うのですが、人気があまりなくオリジナル版はメンテナンスがされないままとなっていました。

オリジナルは [https://github.com/yasserg/crawler4j](https://github.com/yasserg/crawler4j) で、多くの開発者がForkしています。

Forkされたコードはパッケージを分割する方向や独自機能の実装といった取り組みが多く、現在もメンテナンスされているものはあるのですが、ドキュメントなどはほとんどなく自分で使うには少し難しい状況となっています。

2020年まではほぼ互換性を持ったまま更新されていたライブラリがあったので、そちらを利用していたのですが、更新されなくなって久しいので自分で変更することにしました。

# 既存ライブラリを自分でビルドする

いろいろ考慮した結果、今回は自分で依存関係を整理して脆弱性などをできるだけ配慮したバージョンにする方法を選択しました。

基本的には単純に依存ライブライリのバージョンを上げてビルドすれば良いのですが、メジャーバージョンアップのタイミングで破壊的な変更が行われいてるとコードに手を入れる必要が出てきます。

## Gradleのバージョンアップ

GradleはMavenのようにJavaコードをビルドするために必要な基盤環境ですが、これもバージョンアップのタイミングで破壊的な変更が導入されています。

例えばGradleの設定ファイルである **crawler4j/build.gradle** について、次のような変更がまずコンパイルのために必要でした。

基本的にはキーワードの変更が中心ですが、Maven Centralにパッケージをアップロードするための署名などのためにはさらに追加での作業が必要でした。

```diff
diff --git a/crawler4j/build.gradle b/crawler4j/build.gradle
index 2378230..8bd728f 100644
--- a/crawler4j/build.gradle
+++ b/crawler4j/build.gradle
@@ -13,13 +13,15 @@ configurations.all {
     it.exclude group: 'org.apache.logging.log4j'
 }
 
+// https://stackoverflow.com/questions/23796404/could-not-find-method-compile-for-arguments-gradle
+
 dependencies {
-    compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.26'
-    compile group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.26'
-    compile group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.26'
-    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.7'
-    compile group: 'com.sleepycat', name: 'je', version: '18.3.12'
-    compile(group: 'org.apache.tika', name: 'tika-parsers', version: '1.20') {
+    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36'
+    implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.36'
+    implementation group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.36'
+    implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.14'
+    implementation group: 'com.sleepycat', name: 'je', version: '18.3.12'
+    implementation(group: 'org.apache.tika', name: 'tika-parsers', version: '1.28.5') {
         exclude(module: 'poi-ooxml')
         exclude(module: 'poi-scratchpad')
         exclude(module: 'poi-ooxml')
@@ -65,24 +67,24 @@ dependencies {
         exclude(module: 'json')
         exclude(module: 'sentiment-analysis-parser')
     }
-    compile group: 'io.github.pgalbraith', name: 'url-detector', version: '0.1.20'
-    compile group: 'com.google.guava', name: 'guava', version: '27.0.1-jre'
-    compile group: 'de.malkusch.whois-server-list', name: 'public-suffix-list', version: '2.2.0'
-    runtime group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
-
-    testCompile group: 'junit', name: 'junit', version: '4.2'
-    testCompile group: 'com.github.tomakehurst', name: 'wiremock', version: '2.21.0'
-    testCompile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.6'
-    testCompile group: 'org.spockframework', name: 'spock-core', version: '1.2-groovy-2.5'
+    implementation group: 'io.github.pgalbraith', name: 'url-detector', version: '0.1.20'
+    implementation group: 'com.google.guava', name: 'guava', version: '33.2.1-jre'
+    implementation group: 'de.malkusch.whois-server-list', name: 'public-suffix-list', version: '2.2.0'
+    runtimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6'
+
+    testImplementation group: 'junit', name: 'junit', version: '4.13.2'
+    testImplementation group: 'com.github.tomakehurst', name: 'wiremock', version: '3.0.1'
+    testImplementation group: 'org.codehaus.groovy', name: 'groovy-all', version: '3.0.22'
+    testImplementation group: 'org.spockframework', name: 'spock-core', version: '2.4-M4-groovy-3.0'
 }
 
 task sourcesJar(type: Jar, dependsOn: classes) {
-    classifier = 'sources'
+    archiveClassifier = 'sources'
     from sourceSets.main.allJava
 }
 
 task javadocJar(type: Jar, dependsOn: javadoc) {
-    classifier = 'javadoc'
+    archiveClassifier = 'javadoc'
     from javadoc.destinationDir
 }
```

実際の作業では **build.gradle** ファイルの変更が一番負荷のかかる作業でした。

コードの変更自体は、多少の調査やAPIドキュメントを検索する必要はありましたが、それほど大変ではありません。

## Maven Centralへのアップロード

自分だけが利用するのであれば、Maven Centralに登録する必要はありませんが、しばらくcrawler4jは利用するつもりなのと、従来似たようなコンセプトでメンテナンスされていたcom.goikosoft.crawler4jがメンテナンスを停止しているので登録することにしました。

オリジナルではmaven-publishプラグインを使って、oss.sonatype.org(OSSRH)を経由してmaven centralにアップロードしていましたが、OSSRHは2024年2月でサービスを止めたため、Maven Centralへ直接(?)アップロードする方法がMaven Centralの公式ガイドに掲載されています。

2024年8月時点では、**maven-publish**プラグインだけでMaven Centralへのアップロードを行う方法はないようで、公式ガイドでは3rd-partyプラグインのリストが掲載されています。

そのためbuild.gradleファイルのそれなりの部分を書き換える必要がありました。Singingのタイミングなども変更されているようで、問題なく動いているようですが冗長な処理をしていないか、いまいち自身がありません。

この部分は今後も情報が更新される可能性がありそうなので、都度確認した方がよさそうです。

## 依存関係にありライブラリに対する処理

バージョンを更新する以外にもいくつか考慮点があります。

1. メージャーバジョンアップの際の破壊的な変更について、対処しつつ動作を確認する
1. SLF4jのメンテナンスが低調なようなのでLog4J2への更新を検討する

これらを踏まえてコードにも手を加えなければいけなかった点がいろいろとありました。

### テストでのデータベース接続

Crawler4jはテストプロセスの中で、PostgreSQLにクローリング先の情報を格納するサンプルアプリケーションをビルドして動作を確認しています。正直なところデータのやりとりはRDBMSよりもMQのようなメッセージ渡しの方が現実的かなと思うのですが、せっかくなのでそのままにしています。

とはいえFlyway自体はメインのcrawler4jでは使っていないので、あまり労力は使いたくありません。

ここでのエラーに対応するためのコードの変更自体は軽微で、**org.flywaydb.core.Flyway** のインスタンス作成方法が変更になっているだけなので、それに合わせて変更しています。変更があった以上の具体的な情報はなかったのですが、エラーメッセージとAPIリファレンスを見比べて対応しました。

Flywayはv10になってから接続するDatabase毎にパッケージが分割されて設定方法も少し変更になっているので、将来v10に移行するための露払いとしてv9系列の最新版にしています。現状ではv9系列の最新版に脆弱性の情報はないようなので、v10系列の情報が集まるまではしばらくはこれで様子をみようと思います。

```diff
diff --git a/crawler4j-examples/crawler4j-examples-postgres/src/main/java/edu/uci/ics/crawler4j/examples/SampleLauncher.java 
b/crawler4j-examples/crawler4j-examples-postgres/src/main/java/edu/uci/ics/crawler4j/examples/SampleLauncher.java
index 79bac69..620890c 100644
--- a/crawler4j-examples/crawler4j-examples-postgres/src/main/java/edu/uci/ics/crawler4j/examples/SampleLauncher.java
+++ b/crawler4j-examples/crawler4j-examples-postgres/src/main/java/edu/uci/ics/crawler4j/examples/SampleLauncher.java
@@ -1,6 +1,7 @@
 package edu.uci.ics.crawler4j.examples;
 
 import org.flywaydb.core.Flyway;
+import org.flywaydb.core.api.configuration.ClassicConfiguration;
 
 import com.google.common.io.Files;
 import com.mchange.v2.c3p0.ComboPooledDataSource;
@@ -46,8 +47,9 @@ public class SampleLauncher {
         controller.addSeed("https://pt.wikipedia.org/wiki/Protocolo");
         controller.addSeed("https://de.wikipedia.org/wiki/Datenbank");
 
-        Flyway flyway = new Flyway();
-        flyway.setDataSource(args[1], "crawler4j", "crawler4j");
+        ClassicConfiguration flywayConfig = new ClassicConfiguration();
+        flywayConfig.setDataSource(args[1], "crawler4j", "crawler4j");
+        Flyway flyway = new Flyway(flywayConfig);
         flyway.migrate();
 
         ComboPooledDataSource pool = new ComboPooledDataSource();
```

### Apache Tikaのメジャーバージョンアップ

Crawler4jをforkしようとした理由のほとんどは、Tikaの依存関係で多くの脆弱性が報告されていたものの、v1.xからv2.xへの移行作業が必須だったためです。

Tikaは1.x系列から2.x系列に変更されて久しく、Tika自身というよりもこれに依存するコードの脆弱性に起因する問題がDocker Scoutなどのスキャナで報告されてしまうので、何とかしたいと思っていました。

pom.xmlでバージョンを2.x系列最新の2.9.2にしてみると、コードの変更自体は非常に軽微なものでした。

```diff
diff --git a/crawler4j/src/main/java/edu/uci/ics/crawler4j/parser/Parser.java b/crawler4j/src/main/java/edu/uci/ics/crawler4j
/parser/Parser.java
index b1aefad..d89f351 100644
--- a/crawler4j/src/main/java/edu/uci/ics/crawler4j/parser/Parser.java
+++ b/crawler4j/src/main/java/edu/uci/ics/crawler4j/parser/Parser.java
@@ -17,7 +17,7 @@
 
 package edu.uci.ics.crawler4j.parser;
 
-import org.apache.tika.language.LanguageIdentifier;
+import org.apache.tika.langdetect.tika.LanguageIdentifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
```

変更自体はコード自体よりも **build.gradle** の変更の方が大きかったです。

## SLF4jからLog4j2への変更について

この作業はまだcrawler4jには実施していませんが、自分で作成した他のコードでは既に実施しています。

変更のほとんどはimport文を変更するだけで済む事が多いので、負荷は低いものの、必要性も感じていないというのが本音です。

いまのところSLF4jが原因でScannerがアラートを出すこともないので、必要になれば変更するという対応で問題はないのかなと思っています。

# さいごに

古いcrawler4jはHarborのコンテナScannerが大量のアラートを報告していましたが、作業が完了した現在ではDockerHubでも脆弱性はレポートされなくなりました。

副次的な効果としてcrawler4jと自作クローリングアプリケーションのライブラリのバージョンがほぼ一緒になったので、依存しているライブラリの改善なども寄与してバイナリサイズは30%ほど削減できました。

自分で何でも管理するのは良い方法とはいえませんが、他に選択肢がなさそうであれば、こういう判断もありじゃないかなと思います。とはいえ開発者がなんでも自分で開発・管理しようとするのは悪い癖なので、注意して取り組むことが必要です。

またMaven Centralに登録したコードは取り下げができないので十分にテストしてからアップロードしましょう。

以上