ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Java (JVM 言語) におけるコードカバレッジの計測方法

Java と Kotlin で書かれたアプリケーションのコードテストのカバレッジを取りたいなーと思って、コードカバレッジ計測ツールについて調べてみてる。

JVM 言語におけるコードカバレッジ計測の方法

JaCoCo のドキュメントにわかりやすくまとまっている。

Coverage information has to be collected at runtime. For this purpose JaCoCo creates instrumented versions of the original class definitions. The instrumentation process happens on-the-fly during class loading using so called Java agents.

There are several different approaches to collect coverage information. For each approach different implementation techniques are known. The following diagram gives an overview with the techniques used by JaCoCo highlighted:

f:id:nobuoka:20180228230512p:plain

JaCoCo - Implementation Design

カバレッジ情報は実行時に収集されるものなので、情報収集のための仕組みが必要。

大きく分けると Runtime Profiling と Instrumentation の 2 つの仕組みがある。 前者は JVM の仕組み (JVMTI や JVMPI) を使うもの。 後者は実行対象のプログラムの方に収集用の仕組みを搭載する (これを instrumentation というらしい?) もの。

Runtime Profiling

JVM TI は Java Virtual Machine Tool Interface の略。 Java SE 5 で導入された。

JVM TI は JVM 上で動くアプリケーションの状態を検査したり、実行を制御したりするためのプログラミングインターフェイスらしい。

JVMPI は JVM TI よりも古くからある同じようなインターフェイスらしい。 Java SE 5 で (JVM TI ができたことで) 非推奨になり、Java SE 6 で廃止された模様。

これらを使ったコードカバレッジは、アプリケーション側に何も手を入れなくて良いことが利点だと思われる。 が、詳細はわからない。

Instrumentation

上でも説明したように、計測対象のアプリケーションコードに情報収集のための仕組みを搭載する方法が、この Instrumentation である。 Runtime Profiling と異なり、Android アプリの実行環境のような非 JVM 環境でも使用できるという利点がありそう *1

上の図を見るとわかるように、様々な方法がある。

  • ソースコードを変更するもの。
  • バイトコードを変更するもの。
    • オフライン (offline) で変更するもの。 (= JVM に読み込まれる前の状態、例えばクラスファイルそのものを変更する。)
      • Replace と Inject って書かれてるけどそれぞれの意味はわからない。
    • オンザフライ (on-the-fly) で変更するもの。 (= JVM 読み込まれる際などにオンメモリで変更する。)
      • クラスローダで変更する方法と、Java Agent を用いる方法がある。

JaCoCo は、Java Agent を用いてオンザフライでバイトコードを変更する方式である。 *2

Java Agent について

Java Agent については java.lang.instrument パッケージの Javadoc に書かれている。

-javaagent:[=] というコマンドラインオプションで指定して使用できるものである。 次のページも参考になる。

エージェントの背景にある基本概念は、「JVM がクラスをロードする場合、エージェントはそのクラスのバイトコードを修正できる」 という考え方です。

それぞれの特徴

現時点で自分がわかっている範囲で特徴を書いておく。

  • ソースコードを変更する方式は、言語によって使用できるかどうかが変わる。 例えば Java 言語に対応しているツールでも Kotlin には対応していなかったりする。
    • 一方でバイトコードを変更する方式は、JVM 言語であればどれにでも対応できるはず。
  • オフラインで変更する方式は、ビルド時にクラスファイルが書き換えられてしまうので、それをそのまま本番アーティファクトのビルド時に使用できない。 (使用したらダメというわけではないが、パフォーマンスが落ちるなどの問題が起こる。)

様々なコードカバレッジツール

JVM 言語用の様々なコードカバレッジツールについて、Clover のブログで比較紹介してくれている。 (最終更新が 2017 年春なので情報はちょっと古いかもしれない。)

有名どころとしては JaCoCoOpenClover (Atlassian Clover がオープンソース化されたもの)、JCov といったところだと思う。

Instrumentation 方式に着目すると、JaCoCo と JCov はオフラインおよびオンザフライのバイトコード instrumentation に対応しており、Clover はソースファイル instrumentation に対応している。 なので JaCoCo や JCov は (JVM 言語なら何でも対応できるので) Kotlin にも対応するが、Clover は (対応言語に入っていない) Kotlin には対応しない。

Clover と JaCoCo を軽く使ってみたところ、設定の簡単さはどちらも同じ。 出力される HTML を見ると JaCoCo は単純な内容で、Clover の方はプロジェクトリスクの高いものを表示したり、視覚的だったりと、結果表示については高機能さを感じた。

今回は Kotlin でのカバレッジも取りたいので、(対応していない) Clover は選外で、JaCoCo か JCov のどっちかを使うことになりそう。

*1:憶測です

*2:オプションでオフラインでの instrumentation も可能。

Gradle のマルチモジュールプロジェクトで maven-publish プラグインを使う場合の依存関係

Gradle でビルド成果物 (build artifact) を Maven リポジトリに公開するためのプラグインとして、maven-publish プラグインがある。

maven-publish プラグインとマルチプロジェクトの依存関係

簡単な例

Java ライブラリのプロジェクトがあったとして、JAR ファイルを Maven リポジトリに公開するための build.gradle の記述は以下のようになる。

// Java ライブラリのビルド用の設定などは省略

apply plugin: 'maven-publish'

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
}

これで publishToMavenLocal タスクなどが使えるようになるので、例えばローカルホスト上の Maven リポジトリに公開するには ./gradlew publishToMavenLocal コマンドを実行すればよい。

依存関係

上の例では、ソフトウェアコンポーネントとして java コンポーネントを指定している。 このとき、依存関係としては runtime コンフィギュレーションのものが使われる。

Name Provided By Artifacts Dependencies
java The Java Plugin Generated jar file Dependencies from 'runtime' configuration
web The War Plugin Generated war file No dependencies
Maven Publishing (new) - Gradle User Manual

マルチモジュールプロジェクトでの依存関係

例えば :foo プロジェクトと :bar プロジェクトからなるマルチモジュールプロジェクトで、:bar プロジェクトから :foo プロジェクトに依存している場合に、出力される pom.xml に記載される依存関係はどうなるのか?? 具体的には bar/build.gradle に以下のように書かれている状況である。

dependencies {
    compile project(':foo')
}

これはドキュメントには書かれていないが、maven-publish プラグインソースコードを確認したところ、依存先プロジェクトの設定に応じて自動的に依存先ライブラリの指定がなされるようになっていた。

例えば、依存先プロジェクトで maven-publish プラグインが使われていない場合は、プロジェクトのグループやプロジェクト名、バージョンが使われるようである。 依存先プロジェクトで maven-publish プラグインが使われており、特定の条件を満たす publication が存在する場合は、その publication で指定されているグループ・artifactId・バージョンが使われる。

publishing {
    publications {
        maven(MavenPublication) {
            groupId 'org.example'
            artifactId 'foo-sample'
            version '1.1'

            from components.java
        }
    }
}

foo/build.gradle に上のように書かれていたら、./gradlew :bar:publishToMavenLocal タスクで公開される成果物の pom ファイルには以下の内容が含まれることになる。

    <dependency>
      <groupId>org.example</groupId>
      <artifactId>foo-sample</artifactId>
      <version>1.1</version>
      <scope>compile</scope>
    </dependency>

気を付ける必要があること

依存先プロジェクトの中で maven-publish プラグインが使用されて、publication が定義されている必要がある。

下記のように、親プロジェクトで子プロジェクトの publication をまとめて定義することもできるのだが、そうすると依存関係の解決時に 「依存先プロジェクトでは publication がない」 という扱いになってしまって、デフォルト値が使用される。

publishing {
    publications {
        mavenFoo(MavenPublication) {
            groupId 'org.example'
            artifactId 'foo-sample'
            version '1.1'

            from findProject(':foo').components.java
        }

        mavenBar(MavenPublication) {
            groupId 'org.example'
            artifactId 'bar-sample'
            version '1.1'

            from findProject(':bar').components.java
        }
    }
}

私はこれでハマってしまった。 (Exposed の build.gradle を見て、親の build.gradle で子プロジェクトの publication を宣言していたので、それを参考にした、という。) 注意されたし。

Java Persistence API (JPA) の JPQL で集計処理を書く (Spring Data JPA)

Spring Data JPA を使っていて、集計処理をどのように書くのが良いかわからずに調べた記録。 (例は Spring Data JPA 固有の話ではあるが、JPQL の知識は Spring Data JPA 以外での JPA でも使える。)

Spring Data JPA での集計の例

リポジトリにメソッドを定義して @Query アノテーション使うことで、JPQL (Java Persistence query language) によるクエリを記述できる。 これを用いて、例えば FooRepositoryFoo に関する集計処理を記述できる。

    @Query(value = """
        SELECT NEW com.example.AggregationResult(
          FUNCTION('year', foo.date), COUNT(foo)
        )
        FROM Foo foo LEFT JOIN foo.bar bar LEFT JOIN foo.baz baz
        WHERE foo.targetId = ?1
          AND (bar.content IS NOT NULL OR baz.content IS NOT NULL)
        GROUP BY FUNCTION('year', foo.date)
        """)
    fun aggregateCountPerYear(targetId: Long): List<AggregationResult>

上のコードは Kotlin で記述されたメソッド定義の例である。 (FUNCTION('year', foo.date) が 2 箇所にあるのが微妙だし仕様上正しく動くものなのかわからないので何とかしたかったけど、どう書くのがいいかわからなかったのでこの形になっている *1。 誰か詳しい人が居たら教えてください><)

上記の例に含まれる JPQL の知識を下記に述べる。

集計に使える JPQL の知識

参照した JPA 仕様

学び

SELECT 句内のコンストラクタ式 (JPA 2.2 仕様の 4.8.2 節)

JPQL の SELECT 句の項目として Java クラスのコンストラクタを記述できる。 集計結果をオブジェクトで返したいときに便利。 ちなみに対象のクラス名は完全修飾名で指定する必要がある。

データベース関数の実行 (JPA 2.2 仕様の 4.6.17.3 節)

データベースの関数を FUNCTION(function_name, arg1, arg2, ...) の形で記述できる。 JPQL で定義されていない集計関数を用いたい場合や、ユーザー定義の関数を利用したい場合などに便利。

LEFT JOIN (JPA 2.2 仕様の 4.4.5.2 節)

LEFT JOINLEFT OUTER JOIN はシノニムである。 JPA においては、エンティティ定義でエンティティ間の関連を定義して、JPQL では 単純に下記のようにプロパティ名の記述だけで JOIN を記述できる。

        FROM Foo foo JOIN foo.bar bar

ただし、上記のコードでは foo.barnull となるような Foo は処理対象から外れてしまう。 foo.barnull でも処理対象とするために LEFT JOIN を使用する必要がある。 普通に SQL を書く場合には LEFT JOIN を使うのは自然だと思うが、JPQL だと JOIN の条件を書かなかったり、Foo のエンティティを普通に引っ張ってくるときは barnull でも取得できたりするので、LEFT JOIN が必要なことをうっかり忘れがちになりがちな気がする。 注意が必要。

GROUP BY (JPA 2.2 仕様の 4.7 節)

SQL と同様 GROUP BYHAVING を使用できる。 SELECT 句では集約関数を使用できる。 (上の例では COUNT 関数。)

参考

*1:テストは書いてあるのでまあ大丈夫かなという……。

Java SE 9 の javac で過去バージョンをターゲットにするときには --release オプションを使用すると良い

JDKjavac コマンドを使って過去バージョンの Java 言語で書かれたソースコードコンパイルする際には、-source オプションや -target オプションを使用してきました。 (JDK 8 までの話。) このとき、適切なブートストラップクラスパスを設定しなければ、対象バージョンに存在しない Java API を使用していてもビルドが通ってしまう、という問題がありました。

JDK 9 の javac コマンドには --release オプションが追加されました。 今後は (基本的には) このオプションを使用するようにすると良さそうです。

  • ドキュメント : javac

Java SE 9 (Oracle JDK 9) のマイグレーションガイドより

マイグレーションガイドには以下のように書かれています。

If you use the -source and -target options with javac, then check the values that you use. In JDK 9, javac uses a "one plus three back" policy of supporting -source and -target options.

The supported -source/-target values are 9 (the default), 8, 7, and 6 (6 is deprecated, and a warning is displayed when this value is used).

In JDK 8, -source and -target values of 1.5/5 and earlier were deprecated and caused a warning to be generated. In JDK 9, those values cause an error.

>javac -source 5 -target 5 Sample.java 
warning: [options] bootstrap class path not set in conjunction with -source 1.5 
error: Source option 1.5 is no longer supported. Use 1.6 or later. 
error: Target option 1.5 is no longer supported. Use 1.6 or later.

If possible, use the new --release flag instead of the -source and -target options. The --release N flag is conceptually a macro for:

-source N -target N -bootclasspath $PATH_TO_rt.jar_FOR_RELEASE_N

The valid arguments for the --release flag follow the same policy as for -source and -target, one plus three back.

javac can recognize and process class files of all previous JDKs, going all the way back to JDK 1.0.2 class files.

See JEP 182: Policy for Retiring javac -source and -target Options.

Java Platform, Standard Edition Oracle JDK 9 Migration Guide, Release 9

つまり、--release フラグを指定すると、-source オプションと -target オプションを指定し、さらに適切なブートクラスパスの指定も行った状態になります。 これまで、過去バージョンのブートクラスパスを設定するには、過去バージョンの Java 実行環境を取得してその中の rt.jar を指定してやる必要があったので、格段に便利になりました。

試してみた

実際に、以下のようなコードを含む Java のソースファイルをコンパイルしてみました。

public class Main {
    public static void main(String[] args) {
        String test = String.join("", ""); // Java SE 8 で Java API に導入されたメソッド。
    }
}

--release 8 を指定した場合は、問題なくコンパイルされます。

~\java-project> javac --release 8 .\Main.java

--release 7 を指定した場合は、下記のようにエラーになります。

~\java-project> javac --release 7 .\Main.java
.\Main.java:3: エラー: シンボルを見つけられません
        String test = String.join("", "");
                            ^
  シンボル:   メソッド join(String,String)
  場所: クラス String
エラー1個

Java 7 向けにビルドしたけど Java 7 の実行環境で動かすと (Java 7 にない API を使っていて) 例外が発生する、というようなミスが減りそうで最高ですね!

Gradle での使い方

Gradle 4.2.1 現在、JavaCompile タスクjavac--release オプションを指定する方法として特別なメソッドは提供されていません。 通常のコンパイラオプションを指定する方法は使用できるので、通常のコンパイラオプションの指定と同じ方法で --release オプションを指定します。

具体的には CompileOptions#compilerArgs プロパティを使用します。 このドキュメントを読むと、『For example, it is possible to pass the --release option of JDK 9』 とか 『Note that if --release is added then -target and -source are ignored.』 とか書かれています。 --release オプションにもばっちり対応されていますね。

超単純な build.gradle の例を書いておきます。

apply plugin: 'java'

tasks.withType(JavaCompile) {
    options.compilerArgs.addAll(['--release', '8'])
}

Java 9 のモジュールシステム (JPMS; JSR 376) の概要

Java SE 9 がリリースされましたね! めでたい!

さてさて、Java SE 9 の目玉といえばやはり The Java Platform Module System (JPMS; JSR 376) ですよね! Project Jigsaw の心臓部です。

この記事では JSR 376 をさらっと読んで、JPMS の全体像をまとめておきます。 (実際の使い方などはこの記事の範囲外です。) モジュールシステムの理解への取っ掛かりとして皆さんの一助となれば幸いです。

仕様 (JSR 376) 概要

The Java Platform Module System (JPMS) の目的とその手段

JSR 376 に書かれているとおりの内容を日本語にしてます。

目標

JPMS の目標は、親しみやすく、それでいてスケーラブルなモジュールシステムを定義すること。

  • 開発者がライブラリやフレームワーク、そしてアプリケーションを開発したり使用したりする際に JPMS を使用できるように、親しみやすいもの――すなわち学び易く使い易いものでなければならない。
  • Java SE プラットフォーム自身、およびその実装をモジュール化することができるように、スケーラブルでなければならない。
手段

その目標を達成するために、2 つの基本的な機能が備わっている :

  • 信頼性の高い構成の仕組み : プログラム部品間の明確な依存関係を宣言できる (脆弱で、エラーが発生しやすいクラスパスのメカニズムを置き換える)
  • 強力なカプセル化 : どの型を他の部品からアクセスできるように公開し、どの型を公開しないか、ということをプログラム部品が宣言できる

これらの機能は、新たなプログラム部品であるモジュールによって実現される。 モジュールは Java プログラミング言語の構成物として定義され、コンパイル時と実行時の両方で一様に解釈されるものである。

JPMS の全体像

JPMS の詳細は Java 言語仕様や JVM 仕様などの中に含まれています。 JSR 376 は、それらの仕様のどこに JPMS が影響しているのかを分かりやすく表現しています。

多くのアプリケーションエンジニアが気にするのは、特に Java 言語仕様や Java API 仕様、JAR ファイル仕様といったところだと思います。 ここら辺の内容は後で少し触れます。

JSR 376 の一部ではありませんが、次のような文書も紹介されています。

また、今回のリリースに含めなかった機能等についての説明や、変更履歴についても JSR 376 には書かれています。

Java 言語仕様における JPMS

Java 言語仕様の中の JPMS に関わる部分をさらっと読んだのでメモ程度にまとめておきます。

  • 7.7 節 Module Declarations より
    • モジュール宣言は、新しい名前付きモジュール (named module) を記述する。
    • 名前付きモジュールは、他のモジュールへの依存や、他のモジュールに公開するものを記述する。
    • モジュール宣言により、モジュール名が導入される。 モジュール名は、他のモジュールとの関係を記述するのに使用される。
      • モジュール名は、1 個以上の Java 識別子をドットで連結したもの。
    • モジュールにはノーマルモジュール (normal module) とオープンモジュール (open module) の 2 種類がある。
      • ノーマルモジュールは、コンパイル時にも実行時にも export されたものしか外部コードからは扱えない。
      • オープンモジュールは、コンパイル時には export されたものしか外部コードからは扱えないが、実行時には export されていない型も外部コードから扱える。
      • リフレクションでの操作時に結構影響するっぽい。
    • モジュール宣言では、java.util.ServiceLoader によるサービスの提供あるいは利用の宣言も可能。
    • 名前付きモジュールに関連付けられていないクラスなどは、無名モジュール (unnamed module) に関連付けられる。
      • Java SE Platform は、必ず 1 つは無名モジュールをサポートしなければならない。 2 個以上サポートしても良い。
      • 無名モジュールは互換性のために存在するぽい。 (『Unnamed modules are provided by the Java SE Platform in recognition of the fact that programs developed prior to Java SE 9 could not declare named modules.』)
  • 6.1 節 より
    • モジュール名は、モジュールが export する本質的なパッケージ名に合わせるのが良い。 それが難しい場合は、著者が持つドメイン名を逆さに並べたもので始めると良い。

JAR ファイル仕様における JPMS

こちらもメモ程度に。

  • クラスパス上ではなくモジュールパス上に配置された JAR ファイルはモジュールである。
  • トップレベルに module-info.class ファイルを持つ JAR ファイルは、モジュール式 JAR ファイルである。
  • そうでない JAR ファイルは、自然発生的なモジュール (automatic module) とみなされる。
    • その場合のモジュール名は JAR ファイル名から決められ、export されるパッケージは .class ファイルから決められる。

さあ、始めましょう