torutkのブログ

ソフトウェア・エンジニアのブログ

mavenリポジトリからjarファイルの取得

はじめに

インターネットと常時接続が維持できない環境でプログラミングをする場合、ローカルにライブラリファイル等を保持し、ビルドスクリプトからはローカルのライブラリを参照すれば作業が可能になります。

NetBeans等のIDEでライブラリを利用して開発するときは、そのライブラリのクラスファイルの入ったJARファイル以外にもソースファイルのJARファイルとJavadocのJARファイルがあるとよい感じとなります。

手作業で1つ1つ取得することはできますが、依存関係が複数あると結構な手間となるので、ローカルに保持するライブラリのJARファイル、ソースJARファイル、JavadocのJARファイルをコマンドでまとめてmavenリポジトリから取得する方法がないかを調べました。

pomの作成

取得するライブラリを依存関係として記述したpom.xmlを作成します。

<?xml version="1.0"?>
<project>
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.torutk</groupId>
  <artifactId>download</artifactId>
  <version>1</version>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.6.1</version>
    </dependency>
  </dependencies>
</project>
  • project要素の子要素であるmodelVersion、groupId、artifactID、versionは必須のため適当に記載

このpom.xmlのあるディレクトリでmavenを実行します。

D:\work> mvn dependency:copy-dependencies
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< com.torutk:download >-------------------------
[INFO] Building download 1
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:copy-dependencies (default-cli) @ download ---
[INFO] Copying junit-jupiter-5.6.1.jar to D:\work\target\dependency\junit-jupiter-5.6.1.jar
  :
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.950 s
[INFO] Finished at: 2020-04-05T21:38:29+09:00
[INFO] ------------------------------------------------------------------------

ダウンロードしたファイルは次です。

D:\work> dir target\dependency
apiguardian-api-1.1.0.jar
junit-jupiter-5.6.1.jar
junit-jupiter-api-5.6.1.jar
junit-jupiter-engine-5.6.1.jar
junit-jupiter-params-5.6.1.jar
junit-platform-commons-1.6.1.jar
junit-platform-engine-1.6.1.jar
opentest4j-1.2.0.jar

D:\work>

ソースJARファイルをダウンロードする場合は、オプション -Dclassifier=sourcesを指定

D:\work> mvn dependency:copy-dependencies -Dclassifier=sources
  :

ダウンロードしたファイルは次です。

apiguardian-api-1.1.0-sources.jar
junit-jupiter-5.6.1-sources.jar
junit-jupiter-api-5.6.1-sources.jar
junit-jupiter-engine-5.6.1-sources.jar
junit-jupiter-params-5.6.1-sources.jar
junit-platform-commons-1.6.1-sources.jar
junit-platform-engine-1.6.1-sources.jar
opentest4j-1.2.0-sources.jar

JavadocのJARファイルをダウンロードする場合は、オプション-Dclassifier=javadocを指定

D:\work> mvn dependency:copy-dependencies -Dclassifier=javadoc
:

ダウンロードしたファイルは次です。

apiguardian-api-1.1.0-javadoc.jar
junit-jupiter-5.6.1-javadoc.jar
junit-jupiter-api-5.6.1-javadoc.jar
junit-jupiter-engine-5.6.1-javadoc.jar
junit-jupiter-params-5.6.1-javadoc.jar
junit-platform-commons-1.6.1-javadoc.jar
junit-platform-engine-1.6.1-javadoc.jar
opentest4j-1.2.0-javadoc.jar

ダウンロードファイルのディレクトリ指定

デフォルトでは、pom.xmlを置いたディレクトリの下にtarget\dependencyディレクトリが作られ、その中にダウンロードしたJARファイルが保存されます。このディレクトリは、-DoutputDirectory=libs のようにオプション指定で変更することができます。

Java SE 8の安定感

はじめに

これからソフトウェア開発を始めるプロジェクトでプログラミング言語Javaを使う場合に、Javaのバージョンをどうしようかと調べたことを書き付けます。

開発するソフトウェアは、5年から10年、場合によってはそれ以上使われることが想定されます*1

Javaのバージョンとライフサイクル

OracleRed Hat、Azul Systems、および Bell softwareが提供するJava SE Development Kitのサポート期限を調べてみました。

製品 サポート期限
Oracle Java SE 8 2030年12月*2
Oracle Java SE 11 2026年9月*3
Red Hat OpenJDK 8 2026年5月
Red Hat OpenJDK 11 2024年10月
Azul Zulu Enterprise 8 2030年12月
Azul Zulu Enterprise 11 2027年9月
Azul Zulu Community 8 2026年3月
Azul Zulu Community 11 2027年9月
Liberica JDK 8 2031年3月
Liberica JDK 11 2027年3月

Oracleのサポートは3段階あるうちの2段階目のExtended Supportの期限を記載しています。製品リリース後所定の期間がPremier Support期間で、続いてサポート料金が上済みされるがほぼ同等のサービスを提供するExtended Support期間となります。

参照情報

ビルドツールとの相性

JDK 8環境では、各種ツールとの相性はよかったのですが、JDK 11環境となるとまだまだ整っていない部分があります。

NetBeans IDE

NetBeans IDE 8.2はJava SE8対応ですが、Java SE 11を使うには、Apache NetBeans 10以降を使うことになります。しかしながら、NetBeans は8.2のリリースの後、OracleからApacheへ移管し、Apache incubatorのフェーズを経て2019年4月にApache正式プロジェクトへ昇格しました。

Java SE 8の開発であれば、NetBeans IDE 8.2が使えますが、Java SE 11の開発となると、現時点ではApache NetBeans 11.3 になります。ただ、OracleからApacheへの移行に伴い、プラグインサイトの更新が追い付いていない感があり、安定して利用するには少々時間がかかりそうと思います。

JPMS対応

Apache Ant、Gradleは、JPMS(Java Platform Module Systems)対応が遅れており、Apache Mavenがやっと、というところです。

FindBugs

FindBugsは開発が停滞しており、JDK 11への対応が厳しいかなという状況です。SpotBugsに差し替えるのが有効な解決策の1つですが、NetBeans IDEに組み込むプラグインがまだない模様です。Eclipse用はありますが。

javapackagerの喪失

JDK 11ではjavapackagerが削除され、WindowsインストーラーやLinuxインストーラー(RPMパッケージ他)を作るのが面倒になりました。

外部ライブラリとの相性

Endorsed Standards Override Mechanismの廃止

クラスパスの設定にEndorsed Standards Override Mechanismを使っているライブラリはJDK 11では動かなくなります。(バッチファイル、シェルスクリプトファイルなので、クラスパス指定する等の修正をすれば動作) 例)JacORB 3.9

まとめ

Java SE 11がリリースされてから2年半が経過していますが、まだ周辺ツールが追い付いておらず、開発に使う場合の安定感はJava SE 8の方が高いです。

JDKのライフサイクルを見ると、Azul SystemsとBellSoftwareが提供するJDKについては、2021年9月リリース予定のJDK 17ですらJDK 8よりライフサイクルが短く、2024年9月リリース予定のJDK 23でようやくJDK 8のライフサイクルを超えるサポート期限となります。

もちろん、モジュールシステムや新しいAPI、新しい言語仕様、新しいGCなどの機能がどんどん取り込まれているので使いたい気持ちは山々なのですが、上述のとおりライフサイクルや開発環境(IDEだけでなく、品質チェック、ビルド、リリース等)を考慮すると、Java SE 11に移行すべきと言い切れないというのが現段階です。

*1:ちなみに、12年前に開発に携わったシステムは今も使われています

*2:Extended Support期限

*3:Extended Support期限

Java Module Systemのmain classを持つJARファイル作成の落とし穴

発生した事象

IntelliJ IDEA で作成していたJavaFXアプリケーションがあります。ビルド成果物でJARを作成し、Main Classを指定すると、実行可能JARファイルが生成されます。

このアプリケーションのプロジェクトにmodule-info.javaを追加してJava Module System対応をしました。ビルドでJARファイルを生成し、そのJARファイルをコマンドラインから実行すると、−mオプションに明示的にmain classを指定しないとエラーとなってしまいます。実行可能JAR(-jarオプション)としての実行はできているのですが・・・

D:\work\myapp> java -p out\artifacts\myapp_jar -m com.torutk.myapp
モジュールcom.torutk.myappにModuleMainClass属性がありません。-m <module>/<main-class>を使用してください

生成されたJARファイルのMANIFEST.MFを見ると、Main-Class属性は記述されています。

ここで、エラーメッセージには「ModuleMainClass属性がありません」と出ているのがミソで、実行可能JARファイルのMANIFEST.MFに記述する「Main-Class属性」とは名前が似ているが異なっていた点に注意が向かなかったのが痛かったです。

調査

簡単なモジュールのサンプルを作り、次の2つの方法でJARファイルを生成します。

  1. MANIFEST.MFの雛形にMain-Class属性を記述し、jarコマンドの-mオプションで雛形を指定
  2. jarコマンドの--main-classオプションで実行するメインクラス名を指定
D:\work\myapp> jar --create --file out1\myapp.jar --manifest src\META-INF\MANIFEST.MF -C classes .
D:\work\myapp> jar --create --file out2\myapp.jar --main-class com.torutk.myapp.Main -C classes .

1つ目のjarファイル(out1\myapp.jar)は、エラーとなります。

D:\work\myapp> java -p out1 -m com.torutk.myapp
モジュールcom.torutk.myappにModuleMainClass属性がありません。-m <module>/<main-class>を使用してください

2つ目のjarファイル(out2\myapp.jar)は実行できます。

D:\work\myapp> java -p out2 -m com.torutk.myapp
Hello, Java Module System

この両者の違いを追うべく、2つのJARファイルの違いをwinmergeを使って確認したところ、MANIFEST.MFとmodule-info.classに差異が検出されました。

MANIFEST.MFは属性の記述順が異なるだけでした。 module-info.classは、javacでコンパイルした出力結果をjarコマンドでアーカイブしているだけと思い込んでいたので、jarファイルに含めるときに変更が行われていたとは少々驚きました。

javacでコンパイルしたあとに、jarコマンドでオプションを変えて2種類のjarを生成しています。これらのjarに含まれるmodule-info.classは、もとのjavacでコンパイルしたmodule-info.classとはサイズが異なっており、さらにそれぞれのJARファイルに格納されたmodule-info.classにも違いがありました。

今回の事象の原因となっている違いは、module-info.classファイルの中にModuleMainClass情報が含まれているか否かの違いです。

javapコマンドでmodule-info.classを調べる

jarコマンドの--describe-moduleオプションを指定すると、モジュール情報を確認できます。

D:\work\myapp> jar --describe-module --file out1\myapp.jar
com.torutk.myapp jar:file:///D:/work/myapp/out1/myapp.jar/!module-info.clas
requires java.base mandated
contains com.torutk.myapp
D:\work\myapp> jar --describe-module --file out2\myapp.jar
com.torutk.myapp jar:file:///D:/work/myapp/out2/myapp.jar/!module-info.clas
requires java.base mandated
contains com.torutk.myapp
main-calss com.torutk.myapp.Main

となり、--main-classオプションを指定して生成したJARファイルの中に含まれるmodule-info.classには、メインクラスに関する情報が追加されています。

--main-classオプションを指定して生成したJARファイルの中にあるmodule-info.classを取り出して、バイナリエディタで覗くと「ModuleMainClass」の文字列が確認できます。

また、このJARファイルから取り出したmodule-info.classをjavapにかけてみたところ、次の様にクラスファイルの中にModuleMainClassという情報が追加されていることが分かります。

D:\work\myapp> javap -v module-info.class
Classfile /D:/work/myapp/module-info.class
  Last modified 2020/03/10; size 266 bytes
  MD5 checksum 865c4ebb10b566b887dd80d3783de624
  Compiled from "module-info.java"
module com.torutk.myapp
  minor version: 0
  major version: 55
  flags: (0x8000) ACC_MODULE
  this_class: #2                          // "module-info"
  super_class: #0
  interfaces: 0, fields: 0, methods: 0, attributes: 4
Constant pool:
   #1 = Utf8               module-info
   #2 = Class              #1             // "module-info"
   #3 = Utf8               module-info.java
   #4 = Utf8               com.torutk.myapp
   #5 = Module             #4             // "com.torutk.myapp"
   #6 = Utf8               ModuleMainClass
   #7 = Utf8               com/torutk/myapp/Main
   #8 = Class              #7             // com/torutk/myapp/Main
   #9 = Utf8               ModulePackages
  #10 = Utf8               com/torutk/myapp
  #11 = Package            #10            // com/torutk/myapp
  #12 = Utf8               java.base
  #13 = Module             #12            // "java.base"
  #14 = Utf8               11.0.6
  #15 = Utf8               SourceFile
  #16 = Utf8               Module
{
}
SourceFile: "module-info.java"
Module:
  #5,0                                    // "com.torutk.myapp"
  #0
  1                                       // requires
    #13,8000                                // "java.base" ACC_MANDATED
    #14                                     // 11.0.6
  0                                       // exports
  0                                       // opens
  0                                       // uses
  0                                       // provides
ModuleMainClass: #8                     // com.torutk.myapp.Main
ModulePackages:
  #11                                     // com.torutk.myapp

結論

mainクラスの指定を省略して実行できるモジュールJARを生成するときは、jarコマンドの--main-classオプションを使って生成する必要があります。

この仕組みに言及している文献等

現在Java読書会BOFで主催しているJava読書会で読んでいる書籍「The Java Module System」には、4.5.3項 Defining an entry point に記載がありました(p.101 上4行目からの段落を抜粋)。

When jar is used to package class files into an archive, you can define a main class with --main-class ${class}, where ${class} is the fully qualified name of the class with the main method. It will recorded in the module descriptor and used by defaults as the main class when the module is the initial module for launching an application.

粗訳

jarコマンドを使ってクラスファイル群をアーカイブする際、--main-class ${class}を指定してメインクラスを定義することができます。${class}には、mainメソッドを持つクラスの完全限定クラス名(FQCN)を指定します。そして、これはモジュール記述子に記録され、モジュールがアプリケーション起動時の初期モジュールとなるときのデフォルトのメインクラスとして適用されます。

この箇所は、前回(2月1日)の読書会で読んだ範囲にあるのですが、悲しいかな全く記憶にありませんでした。

この仕組みへのビルドツールの対応状況

NetBeans 8.2のFindBugs Integrationプラグインインストールがエラーに

発生した事象

4,5年前に立ち上げ時期だけ参加していたプロジェクトがありました。そのプロジェクトはもうクローズしていますが、その資産を引き継ぎ新たに開始したプロジェクトがあり、Javaに詳しい開発者がいないとうことで、そのプロジェクトを(本業に差し支えのない範囲で)支援することになりました。

支援先の開発環境が、NetBeans IDE 8.2(とOpenJDK 1.8.0)を使っているので、 それに合わせて開発環境の確認をするため、開発で使用するプラグインNetBeans IDEに順次入れて動作確認をしている中で、FindBugs Integrationプラグインのインストール時にエラーが発生してしまいました。

f:id:torutk:20200301193554p:plain
NetBeans 8.2にFindBugs Integrationプラグインをインストールしようとしてエラー

以前インストールしたときは問題なかったのですが・・・

ということで、どうしてエラーになったのか原因を調べてみました。

FindBugs Integration プラグインの構成

NetBeans IDEプラグインFindBugs Integrationを選択すると、次の2つのプラグインをインストールしようとします。

  1. FindBugs Library Wrapper
  2. FindBugs Integration

ここで、FindBugs Library Wrapper は、FindBugsが依存するライブラリ(ASM、BCEL、Commons Lang、DOM4JFindBugs、JAXEN、jFormatString、JSR305)をインストールするのですが、これらはプラグインのファイルには含まれておらず、インストールを実施するときに外部(mavenリポジトリ)からダウンロードする仕組みとなっています。

ここで、mavenリポジトリから依存するライブラリをダウンロードする際のURLが、ASMの場合次の様に記載されています。

CRC:3011554661
SIZE:380313
URL:http://repo1.maven.org/maven2/org/ow2/asm/asm-debug-all/5.0.2/asm-debug-all-5.0.2.jar
URL:m2:/org:ow2:asm:asm-debug-all:5.0.2:jar

試しにこのURLをWebブラウザで指定してアクセスしてみました。すると、

501 HTTPS Required. 
Use https://repo1.maven.org/maven2/
More information at https://links.sonatype.com/central/501-https-required

とエラーになっています。

mavenリポジトリにアクセスできなくなったのは?

上述でWebブラウザからURLにアクセスしたときのエラーメッセージにある"More information"を見てみます。

  • 2020年1月15日から、The Central Repositoryは、素のHTTPは受け付けず、HTTPSでアクセスする必要がある
  • URLのhttpをhttpsに書き換える
  • どうしてもHTTPでアクセスする必要がある場合、http://insecure.repo1.maven.org/maven2/ へアクセスする

mavenリポジトリがHTTPを受け付けなくなった背景は次に述べられています。

blog.sonatype.com

mavenリポジトリに依存したビルドが、2020年1月15日を境に壊れるという事象が発生しているところがあちらこちらにありそうです。

FindBugs Integrationプラグインをインストールするにはどうすればよいか

理想は、FindBugs IntegrationプラグインHTTPSを使って依存ライブラリをダウンロードするように設定を変更することです。

しかし、アプリケーション開発者の制御範囲外のことですから、プラグイン側が対応するまでの間、回避策が必要です。

回避策1 URLを変更する

まず、FindBugs Integrationプラグインのファイル(nbm: NetBeans Moduleファイル)をダウンロードします。本日時点でNetBeans 8.2でインストールする場合のnbmファイルのURLは次です。

このURLの導出方法を知りたい方は、以前NetBeans 8.0のときに以下のWikiに記載しましたので参照ください。 www.torutk.com

次に、ダウンロードしたorg-netbeans-libs-findbugs.nbmをzipで開き、次のファイルを取り出します。

+-- Info
+-- META-INF
+-- netbeans
      +-- config
      +-- modules
            +-- ext
            |     +-- asm-debug-all-5.0.2.jar.external
            |     +-- bcel-6.0-SNAPSHOT.jar.external
            |     +-- commons-lang-2.6.jar.external
            |     +-- dom4j-1.6.1.jar.external
            |     +-- findbugs.jar.external
            |     +-- jaxen-1.1.6.jar.external
            |     +-- jFormatString.jar.external
            |     +-- jsr305.jar.external

この、externalで終わるファイル名には、jarファイルをmavenリポジトリからダウンロードするURLが記述されています。このURLを、httpからhttpsに書き換えます。

例えば、asm-debug-all-5.0.2.jar.externalは次のテキストが記述されたファイルです。

CRC:3011554661
SIZE:380313
URL:http://repo1.maven.org/maven2/org/ow2/asm/asm-debug-all/5.0.2/asm-debug-all-5.0.2.jar
URL:m2:/org:ow2:asm:asm-debug-all:5.0.2:jar

これを次の様に書き換えます。

CRC:3011554661
SIZE:380313
URL:https://repo1.maven.org/maven2/org/ow2/asm/asm-debug-all/5.0.2/asm-debug-all-5.0.2.jar
URL:m2:/org:ow2:asm:asm-debug-all:5.0.2:jar

同じ要領で、残りのファイル(名前がexternalで終わるもの)についてもhttpをhttpsに書き換えます。

Windows上で7zipツールを使う場合、org-netbeans-libs-findbugs.nbmを7zipで開き、netbeans\modules\extディレクトリを開き、ファイル名がexternalで終わるファイルを右クリックし、[編集]を選択、開いたメモ帳でhttpをhttpsに変更して保存、アーカイブを更新していきます。

次に、このorg-netbeans-libs-findbugs.nbmは署名付きJARファイルとなっているので、内容を変更した結果署名と合わなくなりエラーとなってしまいます。そこで、META-INFの下にある次の署名ファイルを削除します。

+-- META-INF
      +-- ORACLE_C.SF
      +-- ORACLE_C.RSA

すべてのexternalファイルの変更と、署名ファイルの削除が終わったら、NetBeans IDE 8.2上で、[ツール]メニュー > [プラグイン]で「プラグイン」画面を開き、[ダウンロード済]タブを選択し、[プラグインの追加]ボタンを押し、「プラグインの追加」ファイル選択ダイアログが表示されるので修正済みのorg-netbeans-libs-findbugs.nbmと、ダウンロードしたorg-netbeans-modules-findbugs.nbmの2つを選択して[開く]をクリック、「プラグイン」画面に戻るので[インストール]ボタンをクリックしてインストールします。

回避策2 externalを実体に差し替える

org-netbeans-libs-findbugs.nbmの中のexternalファイルを、実際のJARファイルに置き換えます。 NetBeans 8.0のときの作業を先のWikiページに記載しているので、それに倣って対応します(結果は未確認)。

教訓

mavenは一見便利ですが、mavenリポジトリという外部組織が管理するサイトに依存することになるため、ビルドはいつ壊れるか分かりません。

特に、長期間保守をするシステムでは、安易に外部のmavenリポジトリを利用するのではなく、外部組織のサイト管理を常にウォッチしその変更に迅速に対応できる体制を構築して維持するとか(多分無理)、組織内に利用するライブラリ群を保持するmavenミラーリポジトリを用意して管理するとか、依存ライブラリをビルド環境に保持しローカル参照するようなビルドにするといったことが必要です。

ソフトウェア開発環境が含む範囲

ソフトウェア開発環境が含む範囲

本記事は、製品としてソフトウェアを作る場合で、複数の開発者が共同して作業する、企業でのビジネスを想定しています。プログラミング言語は、コンパイラ型でオブジェクト指向プログラミング機能を想定しています。

逆に、サービスの提供が目的でソフトウェア自体が製品ではない場合、開発規模が小さい(ごく少人数の開発で阿吽の呼吸が通じる)場合、既にある製品やサービスを活用し、ちょっとしたアドオンやカスタマイズで実現する場合、などは本記事の想定外となります。

開発環境と一言でいうとどこまでを含むのか

ソフトウェアの開発環境を整備しようとしたら、何を含めるのかについて同じ職場でもコンセンサスがなかなか得られていません。そんなときは両極端な思考をしてみて検討の幅を持たせて適切なところを探ります。

最小限の開発環境

まず、片方の極端な思考として、もっとも少ない開発環境を考えます。ミニマムではソースコードを実行体にコンパイル(ビルド)するのに必要なコンパイラがあればいいでしょう。作業効率も問わなけらば、ソースコードの記述は何でもよくて(メモ帳でもWordPadでも)、ソースファイルは日時のフォルダに整理して保存し、ソースファイルをコンパイルして実行体を生成します。

最大限の開発環境

続いて、もう片方の極端な思考として、もっとも多い開発環境を考えます。

プログラミング作業が用意になるよう統合開発環境IDE:Integrated Develop Environment)を用意します。開発者がそれぞれバラバラなソースコードを記述しないようコーディング規約を策定し、IDEに可能な限り規約に合わせた設定を行い、それを全員に配布し適用します。プロジェクトで定めたファイル先頭コメント(ライセンス記述やプロジェクト記述等)やコメントの雛形等をファイル新規作成時に自動で展開できるようにIDEに設定しておきます。設定を展開するのに必要であればIDEプラグインを作って全員に配布します。

ソースコードは構成管理ツールの下に集約し、ソースコードの作成、修正、履歴管理、リリース管理の方法を構成管理規約を策定し全員に配布します。要件(仕様)からのトレーサビリティを確保するため、要件から展開したタスクをタスク管理システムに登録し、タスク番号をブランチ名としてソースコードをコミットし管理するなども構成管理規約に含めます。

ソースコードの品質を評価する作業も開発の一部として開発環境に組み込みます。品質の代表的な評価手段としてレビュー(インスペクション)とテストがあります。レビューはプロセスやライブラリの外部インタフェース(プロセス間通信、ファイル等のリソースアクセス、APIなど)、プログラムの構造(パッケージ/クラス構造、ライブラリ構成)、ドキュメントコメントから生成する設計情報、各段階のテスト仕様、ソースコードを対象とするので、レビューの実施体制、レビュー基準、レビュー記録などの手順、計画、管理方法などを策定します。

ソースコードに対して静的解析ツールを適用して、予め定義した検査ルールに逸脱する箇所を検出します。検査ルールは、命名規約からネストの深さ、パッケージやクラスの構成(クラス数、メソッド数、ファンイン/ファンアウト数、他)、循環複雑度、凝集性、APIの呼び出し方法、バグパターンなどを定義しておきます。組み込み方も、ある程度開発が進んでから一括して検証するだけでなく、開発者がコードを書いている最中に検証をしておくのが望ましいので、IDEへ組み込めるようにします。

ユニットテストツールを適用して、小さいモジュール単位(クラス/メソッド等)で機能の確認をします。その際、カバレッジツールや動的解析ツールを併用してテストの検証範囲や計算機リソース(CPU、メモリ、ディスク、ネットワーク等)の使用状況を評価します。ユニットテストで不可欠なモックの作成をツールを使って効率化するならモックツールも適用します。

開発されたソースコードをまとめてビルドしテスト環境や納品用のリリース媒体を作るビルド・リリース環境も用意します。ビルドしたソースコードのレビジョンとリリースバージョンの対応付け、バグ管理ツールとの対応(そのリリースで修正されたバグ、既知のバグなど)などを行って、リリースノートへの記載などができるようにします。このビルド環境にも静的解析ツールを組み込んでの評価、ドキュメントコメントからのドキュメント生成などができるようにしておきます。

テストや運用中に発生したバグを管理し、バグの修正コードを記述するために、バグ管理ツールを導入し、バグ管理規約を作成します。バグ管理ツールは構成管理ツールとの紐づけが最低限必要です。

  • IDEとその設定(プラグイン含む)
  • コーディング規約
  • 構成管理ツール
  • 構成管理規約
  • 要件管理ツール(またはタスク管理ツール)
  • レビュー管理ツール
  • レビュー規約
  • 静的解析ツール
  • ユニットテストツール
  • カバレッジツール
  • 動的解析ツール
  • テスト規約(ユニットテスト
  • ビルドツール(自動ビルド)
  • リリースツール(必要ならインストール媒体作成を含む)
  • ドキュメント生成ツール
  • バグ管理ツール
  • バグ管理規約

ソフトウェア開発環境が対象とするもの

ソフトウェア開発環境が対象とするものをソースコードとするのか、製品の媒体(バイナリ、インストーラー等)とするのかもブレがありそうです。

ソースコードまでを範囲とする場合、各開発者が自分の環境でソースコードの作成をすることが主作業で、作業の必要上コンパイルする、ユニットレベルのテストをする、デバッグをする、といった範囲が開発環境となります。ソースコードをテスト環境や運用環境に展開する、あるいはインストール用媒体を生成する、などはおざなりに付く程度です。

実行体までを範囲とする場合は、テスト環境や運用環境に展開する、あるいはインストール用媒体を生成するまでが開発環境となります。

品質活動をどこまで開発環境に取り込むかによって、対象が増減します。

規約とツール

IDEEclipseを使います。ソースコード管理にGitLabを使います。テストはJUnitを使います。ビルドにはJenkinsを使います。バグ管理にはJIRAを使います。とツールだけを決めても開発現場は混乱します。ファイルの改行コード、エンコーディングがバラバラ、ビルドツールはIDE固有の人、Ant使う人、maven使う人などでビルド困難、命名規約がバラバラ、パッケージ単位もバラバラ(やたら細かい人、数十クラスを1つのパッケージにする人)、コメント記述バラバラ、ユニットテストの書き方、メンテナンスがバラバラ、などなど。

しまらないまとめ

ソフトウェア開発環境はピンキリですが、品質保証活動を取り入れた製品の開発を行う場合は、言わば開発対象のビジネス業務を丸ごと一つ業務設計するような決め事が必要になります。これを、ソフトウェアをエンジニアリングすることと言いたいのですが、伝わらないですねぇ。

でも、それをしなないとカオスの渦に飲み込まれてしまいます。

JDK 9のjavac "--release"オプションについて

はじめに

The Java Module System 2月1日開催の Java読書会「The Java Module Sysmem」を読む会(第4回)において、コラム枠にJDK 9で導入されたjavacコマンドのオプション--releaseの記載がありました。 --releaseオプションは、開発(コンパイル)に使用するJDKのバージョンよりも古いJava SEバージョンで実行可能なクラスファイルをコンパイルするために指定します。そして、JDK 8までの-source、-targetおよび-bootclasspathオプションを置き換えるものです。

JDK 8までの指定

開発(コンパイル)に使用するJDKバージョンより古いバージョンのJavaで実行可能なバイトコード(クラスファイル)を生成するために、javacには以前から次のオプションが用意されています。

  • -source
  • -target

コンパイルする際に適用するJava言語仕様のバージョンとコンパイルしたクラスファイルを実行可能なバージョンを指定することができます。例えば、JDK 11を開発環境に使っていて、Java SE 8で実行可能なクラスファイルを生成したい場合などに使用します。

ただし、この2つのオプションだけでは、-targetで指定したバージョンより新しいバージョンで追加されたAPIを使ったソースコードコンパイルしてもエラー、警告等は出ません。ですから、Java SE 8より後に追加されたAPIを呼び出すコードを-source 8 -target 8とオプションを指定してJDK 11のjavacコマンドでコンパイルしてもエラーにはならず、生成されたクラスファイルをJava SE 8で実行するとAPIが存在しないため実行時エラーとなってしまいます。

D:\work> javac -source 8 -target 8 Hello.java
警告:[options] ブートストラップ・クラスパスが-source 8と一緒に設定されていません
警告1個

D:\work> dir /b
Hello.class
Hello.java

D:\work>

後で述べる-bootclasspathが指定されていないと警告が出ますが、コンパイルは通ってクラスファイルは生成されています。

  • JDK 8で実行するとエラー
D:\work> java Hello
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Runtime.version()Ljava/lang/Runtime$Version;
        at Hello.main(Hello.java:3)

そこで、新しいバージョンのJDKを使用して-sourceオプションと-targetオプションを指定し、それより古いバージョンのJava SEで実行できるクラスファイルを生成するには、実行時エラーとなる新しいAPIの使用をコンパイル時にチェックできるよう-bootclasspathオプションを追加します。-bootclasspathには、実行したいバージョンのJDKに含まれるAPIのクラスファイル群(rt.jar)を指定します。

D:\work> javac -source 8 -target 8 -bootclasspath "C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar"
ShowRuntimeVersion.java:3: エラー: シンボルを見つけられません
        System.out.println("Runtime Version = " + Runtime.version());
                                                         ^
  シンボル:   メソッド version()
  場所: クラス Runtime
エラー1個

-bootclasspathで実行したいバージョンのJDKAPI(rt.jar)を指定することにより、そのrt.jarに含まれないAPIを使用したソースコードコンパイルエラーとなります。

-source、-target、-bootclasspath の問題点

  • 指定するオプションが3つもあり、それぞれ整合した指定を記述しなくてはならない
  • 開発に使用するJDKバージョンの他に実行時のJDKバージョンをインストールしなくてはならない
  • -bootclasspathの指定で絶対パスが登場するので扱いが煩雑に(開発環境に制約)

JDK 9からの--releaseオプション(JEP 247)

JDK 9からは、新たな--releaseオプションが追加されました。

openjdk.java.net

これは、従来の-source、-target、および-bootclasspathの3つのオプションに替わり、一括で指定できるようにしたとともに、開発に使用するJDKバージョンだけあればよく、実行したいバージョンのJDKはインストール不要(したがって絶対パス指定も不要)という優れもののオプションです。

D:\work> javac --release 8 Hello.java
ShowRuntimeVersion.java:3: エラー: シンボルを見つけられません
        System.out.println("Runtime Version = " + Runtime.version());
                                                         ^
  シンボル:   メソッド version()
  場所: クラス Runtime
エラー1個

D:\work>

なぜ指定したバージョンのJavaにはないAPIがエラーとできるのか

--releaseオプションの仕様を定義したJEP 247によると、ct.symファイルに情報があるとのことです。

JDK 11の場合次の場所にct.symがありました。

<JDK 11インストール基点ディレクトリ>
  +-- bin
  +-- include
  +-- jre
  +-- lib
  |     +-- ct. sym

ct.symはZIP形式アーカイブなので中を見ると次の様にフォルダが並んでいます。

META-INF
6
678
7
76
78
8
87
876
8769
8769A
879
879A
89
89A
9
9-modules
9A
A
A-modules
A789
A9
B

このフォルダ名は、JDKのバージョンを示しているようです。Aは10、Bは11に対応していると思われます。いくつかフォルダの下を探ってみます。

  • 6 フォルダの下のファイル構成(一部のみ展開)
6
+-- org
+-- javax
+-- java
|     +-- util
|     +-- sql
|     +-- security
|     +-- nio
|     |     +-- channels
|     |     |     +-- spi
|     |     |     +-- SocketChannel.sig
|     |     |     :
|     |     +-- CharBuffer.sig
|     +-- net
|     +-- lang
|     +-- io
|     +-- beans
|     +-- awt
|     +-- applet
+--com

全てではないですが、パッケージ名に対応したフォルダとクラス名に拡張子.sigを付けたファイルがツリー構造で存在しています。この拡張子.sigのファイルには、クラス名とメソッドシグニチャが含まれています。javacは、このct.symファイルを参照してコンパイルをしています。

  • 9-modulesフォルダの下のファイル構成(一部のみ展開)
9-modules
+-- java.activation
+-- java.base
|     +-- module-info.sig
+-- java.compiler
+-- java.corba
+-- java.datatransfer
:

こちらには、モジュール定義が格納されています。

  • 9フォルダの下のファイル構成(一部のみ展開)
9
+-- com
+-- java
|     +-- awt
|     +-- io
|     +-- lang
|     |     +-- Math.sig
|     |     +-- Module.sig
|     |     +-- Runtime$Version.sig
|     |     +-- Runtime.sig
|     |     +-- SecurityManager.sig
|     |     +-- StackWalker$StackFrame.sig
|     +-- security
|     +-- time
|     +-- util
+-- javafx
+-- javax
+--jdk
+--sun

JDK 11のct.symには、バージョンとシグニチャの関係が含まれているので、--releaseオプションで指定したバージョンで使用可能なAPIがチェックできるようです。

なお、ct.symの内部構造については仕様が公開されていないので実装依存情報となっている模様です。

「The Java Module System」を読む会これまでのまとめ(その2) #javareading

Java読書会「The Java Module System」を読む会(第1~3回)のまとめ(その2)

torutk.hatenablog.jp

の続きです。

第2章 Anatomy of a modular application

モジュール化したアプリケーションのディレクトリ構成の例
  +- libs    (サードパーティの依存物)
  +- mods    (コンパイル&パッケージ化したモジュール)
  +- monitor   (アプリケーションモジュールの1つ)
  |     +- src
  |     |    +- main
  |     |         +- java  (モジュールのソースコードを含む)
  |     |              +- monitor
  |     |              +- module-info.java  (モジュールの宣言)
  |     +- target
  |          +- classes         (モジュールのコンパイル結果を格納)
  +- monitor.observer (モジュール)
  |     +- src
  • 慣例で、modsフォルダーに生成したモジュールを格納
モジュール化するアプリケーションの作成の流れ
  1. アプリケーションをモジュールに分解する(関心事の分離戦略がよい)
  2. ディレクトリ構造の作成
  3. モジュール宣言
  4. 別モジュールへの依存関係の宣言
  5. モジュール公開APIの定義
  6. モジュールグラフの可視化
  7. コンパイルとパッケージ化
他のモジュールに依存のないモジュールのビルド
$ java -d monitor.observer/target/classes ${source-files}
$ jar --create --file mods/monitor.observer.jar
 -C monitor.observer/target/classes .
他のモジュールに依存のあるモジュールのビルド
$ java --module-path mods
 -d monitor.observer.alpha/target/classes ${source-files}
$ jar --create --file mods/monitor.observer.alpha.jar
 -C monitor.observer.alpha/target/classes .

依存するモジュールのあるフォルダを --module-pathオプションで指定します。

2つのフォルダにあるモジュールに依存のあるモジュールのビルド

アプリケーションのモジュールとサードパーティのモジュールとに依存のあるモジュールをビルドします。 --module-pathオプションで複数のフォルダを指定可能です。

$ javac --module-path mods:libs
 -d monitor.rest/target/classes ${source-files}
$ jar --create --file mods/monitor.rest.jar
 -C monitor.rest/target/classes .
メインメソッドを持つクラスを含むモジュールのビルド
$ javac --module-path mods
 -d monitor/target/classes ${source-files}
$ jar --create --file mods/monitor.jar
 --main-class monitor.Monitor
 -C monitor/target/classes .
アプリケーションの実行
$ java --module-path mods:libs --module monitor

モジュールパスでモジュールを収容するフォルダを指定、メインを持つモジュールの名前を指定して実行します。

第3章 Defining modules and their properties

2つのモジュールファイルフォーマット
  • JMOD
    JDKを構成するモジュールはJMODフォーマットで提供されています。アプリケーション開発者にはJMODファイルを生成する機能は提供されていません
  • モジュラーJAR 通常のJARファイル内のルートディレクトリにモジュール記述子module-info.classが含まれているものです。

モジュラーJARをクラスパスに置くと、モジュール定義が無視され従来のJARファイルとして扱われます。

モジュール宣言(module-info.java
module モジュール名 {
    requires モジュール名;
    exports パッケージ名;
}

モジュール名とパッケージ名の両方が登場し、命名規則が類似しているので混乱しがちです。

ルートモジュールからの依存性解析結果に含まれないモジュールは使用できません。 そこで、javaコマンドのオプション --add-modules や --add-reads で補うことができます。

  • --add-modulesは、指定したモジュール群を、ルートモジュールとして扱う
  • --add-readsは、指定したモジュールから、指定したターゲットモジュール群へrequiresなしでもアクセス可能とする

モジュール宣言(module-info.java)をコンパイルして生成されるクラスmodule-info.classは、モジュール記述子と呼びます。

モジュール名を付けるには次が重要です。

  • グローバルユニーク
  • 安定

ベストな命名は、ドメイン名の逆順です。これはパッケージ名の命名と同じルールです。そのため、モジュールに含まれるパッケージのパッケージ名のプレフィックスをモジュール名にするとよいです。

モジュールの種類
  • アプリケーションモジュール
    JDK以外のモジュール。モジュールパス上に置き、モジュラーJAR形式
  • イニシャルモジュール
    コンパイル開始対象モジュール、またはmainメソッドを持つモジュール
  • ルートモジュール
    JPMSが依存関係解決にとりかかる対象モジュールで、イニシャルモジュールはルートモジュールでもある
  • プラットフォームモジュール
    JDKを構成するモジュール。JMODファイルで提供
  • インキュベーターモジュール
    非標準なプラットフォームモジュール。jdk.incubator.*
  • システムモジュール
    プラットフォームモジュールのサブセットで実行イメージ用に抜粋したものと、jlinkが追加したアプリケーションモジュールから構成。
    java --list-modules で一覧されるもの。
  • オブザーバブルモジュール
    現在のランタイムにあるすべてのプラットフォームモジュールと、コマンドラインで指定したアプリケーションモジュール
  • ベースモジュール
    java.base
  • 明示的なモジュール
    プラットフォームモジュールと、モジュール記述子(module-info.class)を持つアプリケーションモジュール
  • 自動モジュール
    モジュール記述子を持たない、名前が指定されたモジュール(モジュールパスに置いたプレーンJAR)
  • 名前付きモジュール
    明示的なモジュールと自動モジュールの総称。モジュール記述子で定義した名前またはJPMSが推論した名前
  • 名前なしモジュール
    クラスパス上にあるコンテンツ

このうち、自動モジュールと名前なしモジュールはマイグレーションの過程で使用します。

requiresするのは直接利用しているモジュールのみでよい
monitor.persistence
        ↓
monitor.statistics
        ↓
monitor.observer

このモジュール依存関係の場合、monitor.persistenceはmonitor.statisticsをrequiresすればよく、monitor.observerのrequiresは不要です。 また、monitor.persistenceをコンパイルする場合、monitor.statisticsモジュールが存在すればよく、monitor.observerモジュールは存在しなくても構いません。ただし、実行時にはすべてのモジュールが必要です。

循環参照が生じるモジュール依存関係はエラー