日々常々

ふつうのプログラマがあたりまえにしたいこと。

Apache POIのバージョンあげたらサイズ跳ね上がったので調べたログ

2023-03-04追記: Apache POI 5.1.0で解消しているのでご安心ください。 Apache POI 5.1.0出てた - 日々常々

Apache POI の久々のメジャーバージョンアップである 5.0.02021-01-20 に出ました。 テストも通るしいっかーと、雑に更新したらexecutable jarのサイズが26MB増えてウケました。(笑いごとじゃない

多少のJarサイズはそれほど問題にならないことが多いのですが、今回はもともと40MB程度のJarで、それでも「ちょっと大きいなー」って思っていたのがいきなり60MBを超えただけに、ちょっと待ってくれって気分です。

そんなわけでちょっとみてみようかーと。見ていった手順をだらだら書きます。参考になるかどうかは知りません。

ちなみにPOIのソース管理は Subversion で、GitHubにあるのはミラーです。 今回ソースは読んでないけど。ライブラリのソースは本体のリポジトリじゃなく、Mavenリポジトリ-sources.jar で読むことの方が多いと思います。でも本体の場所を知ってると、ごく稀にコミットログとか追いたくなったときに便利です。

空のjarを作るプロジェクトを作る

調べるのに他のライブラリが混ざってるとかったるいので、ピュアなプロジェクトを作ります。 ビルドにいちいち時間かかるのも嫌だし。

雑にcatbuild.gradle を書きます。

% cat>build.gradle
plugins { id 'java' }
^C

一つもクラスを作らずにビルドします。

% gradle build

BUILD SUCCESSFUL in 537ms
1 actionable task: 1 up-to-date

できました。 中身は もはや人間が実行することもなくなった jar コマンドでみれます。

% ls -l build/libs
total 8
-rw-r--r--  1 irof  staff  261  2  3 22:43 poi5.jar
% jar -tf build/libs/poi5.jar
META-INF/
META-INF/MANIFEST.MF

MANIFEST.MF だけですね。この手順は「Gradleが動いてる」とかその辺の確認です。すぐ確認できるんだからすぐ確認する。あ、ディレクトリ名を poi5にしたんでその名前になってますね。

jar コマンドがよくわからないなら拡張子を .zip に変えて適当に開いてやればよいです。昔はよくやってました。jarってzipなんで。

poiを入れる

引き続きcatで追記します。

% cat>>build.gradle
repositories.jcenter()
dependencies.implementation 'org.apache.poi:poi:5.0.0', 'org.apache.poi:poi-ooxml:5.0.0'
^C

repositoriesmavenCentralでもいいです。Gradleの次のアップデート以降で非推奨になったりするんだろな。

追記: てか jcenter なくなります。タイムリーすぎてウケた(笑い事じゃない

implementationとかは実務では1artifactずつ並べるんですが、今回は cat で書いてるから滅多に使わない複数列挙をします。 repositoriesとかdependencies とかは普通 {} で書きますが、一つだったらそのまま . で繋げられます。滅多に役に立たない知識。 この書き方は実務ではお勧めしません。

ooxmlとかは ブログの過去ログを検索すれば……10年以上前か。当時使ってたのが3.5なので、長いこと中身読んでないなぁ。。

build.gradle全体

plugins { id 'java' }
repositories.jcenter()
dependencies.implementation 'org.apache.poi:poi:5.0.0', 'org.apache.poi:poi-ooxml:5.0.0'

しんぷる。

依存ツリーを確認

gradle dependencies でみれます。ようするにMavendependency:tree ……なんだけど、IDE上でGUIで見ることの方が多いかなと思います。コマンド知らなくても特に問題ないんじゃないかな。

--configuration 無しだと compileClasspath とか testRuntimeClasspath とか色々でてノイズなので、眺めるときはだいたい runtimeClasspath を使ってます。

% gradle dependencies --configuration runtimeClasspath

> Task :dependencies

------------------------------------------------------------
Root project 'poi5'
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.apache.poi:poi:5.0.0
|    +--- org.slf4j:slf4j-api:1.7.30
|    +--- org.slf4j:jcl-over-slf4j:1.7.30
|    +--- commons-codec:commons-codec:1.15
|    +--- org.apache.commons:commons-collections4:4.4
|    +--- org.apache.commons:commons-math3:3.6.1
|    \--- com.zaxxer:SparseBitSet:1.2
\--- org.apache.poi:poi-ooxml:5.0.0
     +--- org.apache.poi:poi:5.0.0 (*)
     +--- org.apache.poi:poi-ooxml-lite:5.0.0
     |    \--- org.apache.xmlbeans:xmlbeans:4.0.0
     +--- org.apache.commons:commons-compress:1.20
     +--- com.github.virtuald:curvesapi:1.06
     +--- org.bouncycastle:bcpkix-jdk15on:1.68
     |    \--- org.bouncycastle:bcprov-jdk15on:1.68
     +--- org.bouncycastle:bcprov-jdk15on:1.68
     +--- org.apache.santuario:xmlsec:2.2.1
... 略 ...
     +--- org.apache.xmlgraphics:batik-all:1.13
... 略 ...
     \--- de.rototor.pdfbox:graphics2d:0.30
          \--- org.apache.pdfbox:pdfbox:2.0.22
               \--- org.apache.pdfbox:fontbox:2.0.22

削ってるからあれだけど、めっちゃいっぱい出てました。poi-ooxml由来で189個。

詳細は省略しますが、org.apache.xmlgraphics:batik-all で約13MB、 org.bouncycastle で約7MB、 de.rototor.pdfbox:graphics2d で約5MB。これらで約23MBになります。増加分とほぼ合いますね。

Apache BatikSVGを扱うもの。Bouncy Castleは暗号化。最後のはApache PDFBoxのアダプタなんでPDFを扱うものです。 なるほどこれらに対応したのかーと言う感じですね。POI使う時に制約になった記憶がうっすらあります。うっすら。(でも今回はシンプルな xlsx 扱うだけなんで使わない……)

リリースノートを確認

最初にやれ←

なるほど(わからん

ちなみに poi-ooxml-lite とかがあって、これ使うといいんじゃ?と思いましたが、こちらは元 ooxml-schemas と書いてるようにスキーマだけ。使いたい XSSFWorkbook とかは入っておらずでした。

ちなみに前バージョン

% gradle dependencies --configuration runtimeClasspath

> Task :dependencies

------------------------------------------------------------
Root project 'poi5'
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.apache.poi:poi:4.1.2
|    +--- commons-codec:commons-codec:1.13
|    +--- org.apache.commons:commons-collections4:4.4
|    +--- org.apache.commons:commons-math3:3.6.1
|    \--- com.zaxxer:SparseBitSet:1.2
\--- org.apache.poi:poi-ooxml:4.1.2
     +--- org.apache.poi:poi:4.1.2 (*)
     +--- org.apache.poi:poi-ooxml-schemas:4.1.2
     |    \--- org.apache.xmlbeans:xmlbeans:3.1.0
     +--- org.apache.commons:commons-compress:1.19
     \--- com.github.virtuald:curvesapi:1.06

こっちは省略なしです。シンプルなものですね。

上でサイズの大半を占めてるって書いてたライブラリはこっちでも使ってないし exclude しちゃっていい気がする。 ってことで外して、テスト通って、OK。と言う感じ。

注意と昔話

Mavencompile スコープで宣言されるものを外すのは危険です。自信とそれを裏付ける自動テストがなければやめましょう。(とか言いながら自信はなかったりする。) ライブラリの依存ライブラリがないと、実行時にクラスねーよって落ち方します。コンパイルチェックが効かない。せっかくコンパイル言語使ってるのにこれが起こるのはダサい。

Maven以前は自身でJarを各ライブラリの配布サイトからかき集めてクラスパスに追加する作業が必要で、この時に集めるのが漏れて実行時エラーとかは結構よくある話でした。 でも現代ではこの苦労をする必要はありません。それがMavenが変えたJavaの世界です。

自動で入る依存ライブラリを外すのは、解決済みの問題のトリガーを自身で引く行為です。 必要な場合に、ある程度(完全ではない)安全に行うには自動テストが必須です。完全じゃないってのは肝に銘じた上で。

後続エントリ 2021-11-26追記

irof.hateblo.jp

5.0.0のときにこうなっててくれたら驚かなかったんだけどね。

SpringBootアプリケーションのProfileで制御したいとき

Short Answer

  • @Profile でBeanを変える
  • どうしても評価するなら Environment#acceptsProfiles(Profiles) を使う
  • 文字列では読まない
  • 文字列では読まない
  • 文字列では読まない

説明

SpringBootではProfileを使って設定ファイル( application-{profile}.properties とか)の読み替えを行うことが多いかと思います。 Profileは設定ファイルの切り替えだけじゃなく、と言うか、主にはBeanを切り替える機能です。 SpringBootネイティブな人だと「設定ファイルを変えるためのもの」と認識しているかもしれません。 あながち間違いじゃないです。

さて、掲題のProfileで何かを制御したいとき。処理を変えたい時。 基本的には先にあげたように @Profile を使ってのBean切り替えです。 @Profile@Conditional の仲間で、条件によってBean生成したりしなかったりする子です。

@Profile には基本的にProfile名を書きます。 @Profile("hoge") とかですね。 ですがそれだけではなく、色々な条件、たとえば !hoge & fuga とかが書けます。 詳しくはProfileのドキュメントProfiles#of(String...)のドキュメントを参照ください。 @Profile と他の @Conditional を組み合わせてのBean制御は、頑張ってなんとかできなくはないんですけど、素直に @Profile に寄せるのが吉です。

この @Profile はBeanを制御するアノテーションなんで、@Component なクラスを作るか @Bean なメソッドを作る必要があります。 「ちょっと分岐したいだけなのにいちいちクラス作るなんてとんでもない!」ってのがあるかもしれません。(賛同できませんが。)

そのような、どうしても実行時に評価したいときは、 Environmentをインジェクションするなりなんなりして acceptsProfiles メソッドで評価できます。 名前の通り引数で渡したProfileが有効かを見てくれるメソッドです。 コンテキスト起動時に決まってるものを、どうしてもリクエストごと等の実行時に常に評価したいなら、これを使いましょう。こんな表現してながら私もたまにやってたりします。

背景

こう言うことを書いているのも、よろしくない別解があって。

@Value("${hoge.fuga}") とかでプロパティが読めるのと、実行時に指定する -Dspring.profiles.active=hoge とか環境変数 SPRING_PROFILES_ACTIVE (大文字である必要はないけどなんとなく)から、こんな感じで取れるのが想像できちゃうわけで、これを稀によく見るんですよね……。

@Value("${spring.profiles.active}")
String activeProfile;

if ("hoge".equals(activeProfile)) ...

これはやっちゃいけないです。

Profileが1つだったら期待通り動くとは思います。

でもProfileって複数指定できるんですよね。 "spring.profiles.active" だし。 後からProfile追加したりとかするかもしれない。そうなった時に動かなくなるかもしれない。 String#containsは流石にないよね。カンマでsplitする?正規表現でマッチさせる? ……それっぽくはできるかもしれないけれど、Springがその通り解釈してるかは怪しいところです。 Springが解釈して実際に動いてるProfileと完全に同じでなければ、バグの温床になります。

と言うことで、先頭に書いた通り。 Profile自体の評価はSpringにお任せして、文字列として比較とか独自の評価方法は避けましょう。

誰に言われたでもないけど追記: 2021-01-20T13:00

「Profile1つしか設定しないし別にいいじゃん」みたいなのは、短期的にはその通りなんですけど、いくつか理由があって。

  • @Value("${spring.profiles.active}") と書くと、何もProfileを指定しない(default が設定される)時に「そんなプロパティないよ」ってエラーで起動しなくなります。
    • 空プロパティを定義するとか ${spring.profiles.active:} みたいな書き方したら回避できるけど、ダサいよね。
  • 継続開発の中で「Profileを増やして対応しよう」と言う手段が取れなくなる
    • Springの経験のある人が後から参画した際に「Springならこれが使えるはず」が通じなくなってしまいます。
    • こう言うのが増えると「Springを使っている」じゃなく、独自フレームワークを使ってるのと同義になってしまいます。つらい。

後者が大きいかな。「よく使われているフレームワークを使用する」のメリットを活かせる開発をしてくほうが、きっといいことあります。

余談

先頭には書きませんでしたが、Environment#acceptsProfiles(Profiles) で分岐するのではなく、 application-{Profile}.properties とかに何かしらの制御用のプロパティを定義して、Profileではなくそっちで制御するとかもあります。 Environment のようなSpringの低レベルなAPI使うより、こっちの方がいい場合もあります。 プロパティファイルの構成とか、アプリケーションの作りとかに合わせていい感じにいきましょう。

蛇足

実際にProfileがどう解決されてるかは追ってみると楽しいかもしれません。 追加されたりマージされたりしていて、おそらく想像以上のことをやってます。 たとえば default がどこで入ってるか、とか。楽しいですよね。(賛同が得られるとは思っていない

SpringBootのプロジェクトを作成する

2020-12-29 時点で私がどうやっているかって言うの。 色々やり方あるし、他でも書いた記憶あるけど、現時点のスナップショットを書いておきます。

必要なもの

以下が実行できること

  • curl
  • gradle
    • 私は SDKMAN で入れてます
    • gradle の実行にJDKいるけど、JDKは入ってるでしょ←
  • idea

やること

curl -O https://start.spring.io/build.gradle
gradle wrapper
idea .

こんだけ。以下は解説とかおまけとか。

やってること

curl で叩いてるのは Spring Initializr です。 SpringBootの雛形を作成してくれるWebサービス。必要なライブラリとかを -d dependencies=web,actuator とかで指定できるんだけど、それはあまり使わなかったり。 build.gradleの最終形までSpring Initializrでできるわけではないし、ここでコマンドで入力するより build.gradle を後で直接編集するほうが早いし、まぁ色々。 用途が限定的なデモの手順やWebUIでやるなら選んでいいと思います。

普通(?)に使うとプロジェクトの一式が入ったzipファイルを作ってくれるんだけど、私が欲しいのは build.gradle だけなので、パスで指定してます。正直このくらいは自分で書いてもいいし、前はそうしてたけど、SpringBootの最新バージョンいくつだっけーとか確認するのも面倒になったし、そこそこいい感じ(例えば SpringBoot2.3.xだと SpringBoot2.2でJUnit5がデフォルトになったのでbuild.gradleを書き換える で書いたようなspring-boot-testからjunit-vintageのexcludeをやってくれる。2.4で要らなくなったけど。)のbuild.gradleにしてくれるので使ってます。

次にGradle Wrapperの生成。ローカルにインストールしているバージョンのGradleで作ります。 Spring Initializrからダウンロードしたらgradlewが入ってるんだけど、バージョンの更新がワンテンポ遅れたりとか、SpringにGradleのバージョンが依存するのは私的に依存が逆なので、コントロールできる手元のGradleでやってます。 バージョンズレでSpring Initializrの作ってくれるbuild.gradleが読めないとかだと使えないのだけど、Gradleも後方互換かなり強いので大丈夫かなって。 Spring Initializrからダウンロードしようと思ったらzipダウンロードからの展開になるし、入ってるののバージョン確認しなきゃだし、まぁ色々。用途が限定的なデモの手順や(以下略

最後にIDEAを起動して、IDEAにGradleを使ってjarのダウンロードとか、プロジェクトの準備をしてもらいます。

そのあとやること

IDEAが起動したら @SpringBootApplication なメインクラスを作ります。 これもSpring Initializrが作ってくれるのとほぼ同じになりますが、コーディングの肩慣らし的な意味もあります。 IDEが元気に動いてるのかとか、メインクラスの名前をどうするのかとか、パッケージをどうするのかとか。この辺りもSpring Initializrで指定して生成できますが、コマンドラインとかWebUIとかで入力する気にはならないんだ。用途が限定的な(以下略

作ったらとりあえず起動。動くかの確認は確実に動くと確信しているところで行います。 このタイミングで「なぜか起動できない」とかも稀によく起こります。JDKいろんなバージョン入れてるもんだから、たまにIDEAが使うのを消しちゃってたり。起こったことがない人は、運がいいか、きっちりしてるか。私の適当さはこれくらいです。

起動したら @SpringBootTest なテストクラスと空っぽの @Test メソッドを作ります。 Spring Initializrで生成できるのと一緒。まぁメインクラスからテストクラスをIDEAに作ってアノテーションつけるくらいなんで、3秒くらいです。 これは他のテストが動き次第、消したり消さなかったり。この手のテストを自動生成すると消せなくなりがちなので、何のためのテストかを認識しながら作るのがいいと思ってます。

やったりやらなかったり、だいたいやること

Spring Initializrで生成するbuild.gradleはGradleプラグインio.spring.dependency-management で依存ライブラリのバージョンを制御しています。 これをGradle5.2以降で使える platform を使うように変更します。 そのままでも害はないのですが、SpringBoot以外のBOMを使うこともあります。そう言う時に複数の制御方法があるのは避けたいところ。 混在したことによる問題に遭遇したことはないんですが、もし遭遇したらすっごい面倒なことになると思うんですよね……。 意図しないバージョンのライブラリが入った時に解析する方法は少ないほうがいいと思っています。

build.gradle の片付けを終えたら、必要に応じて依存関係を足していきます。 この辺は追加したいライブラリの公式サイトを見たり見なかったりしたあと、なんだかんだでMaven Centralを検索してます。

こんな感じ。