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

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

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

Java EE 技術から EE4J (Jakarta EE) 技術に移行する

2017 年に発表された Java EE の Eclipse Foundation への移管。 移管されたプロジェクトをまとめるルートプロジェクトの名前は Eclipse Enterprise for Java (EE4J) で、Java EE に相当するプラットフォームの名前は Jakarta EE となっている *1

EE4J には JAX-RSJSON Processing (JSON-P) などの Java EE の一部として使われていたプロジェクトも移管されている。 これらを単体で使う場合も今後は EE4J のものを使っていくべきだろう。 (古いリポジトリの方は更新されないだろうので。)

EE4J 傘下になってからの最初のリリースがぼちぼちなされていたりして、そろそろ EE4J の方に移行できるようになってきているので、移行について調べたことをまとめておく。

EE4J の各プロジェクト

EE4J 傘下のプロジェクトについては EE4J プロジェクトページ からリンクがあるので、それを辿ると見つけられる。 例えば JAX-RS のページや JSON-P のページは下記である。

EE4J 各プロジェクトの Maven リポジトリ

EE4J への移管に伴って、多くのプロジェクトの API や参照実装 (Reference Implementation; RI) の Maven リポジトリのグループ ID やアーティファクト ID が変更されている。 API については下記ページにまとめられている。

見た感じでは、javax という部分が jakarta に変更されている模様 *2

参照実装についてはプロジェクトごとに異なるようである。

例えば、JSON-P の参照実装については下記のように書かれており、org.glassfish:javax.json から org.glassfish:jakarta.json に変更されていることがわかる。

The main API jar file is now located at jakarta.json:jakarta.json-api and the main RI jar file is now located at org.glassfish:jakarta.json.

JSON Processing (JSON-P)

一方で、JAX-RS の参照実装である Jersery については、バージョン 2.28 が最初の Jakarta EE 実装としてリリースされているが、特にグループ ID やアーティファクト ID の変更はないようである。

ソースコードの変更は必要か?

Maven アーティファクトの名前が変わってはいつつも、API 自体に変更はない。 Java EE 時代の最後の API を使っているソースコードであれば、そのまま EE4J の最初のリリースに移行できるはず。

This is common for every Jakarta EE project this release: it was required not to provide any changes in API and functionality to ensure the compatibility between last Java EE and initial Jakarta EE releases.

Jersey 2.28 has been released | Jan's Blog

*1:EE4J と Jakarta EE の名前の使い方などは Jakarta EE の FAQ に書かれている。 が、これを読んでも正直なところ EE4J と Jakarta EE の使い分けがいまいちわからない……。

*2:正式な名前変更のルールについてはどこかで言及されているのかもわからないが、見つけられていない。

JAX-RS のリソースから送出された例外の扱い

JAX-RS のリソースのメソッドから例外が送出された場合の挙動についてちゃんと把握していなかったので調べた。

前提知識 : JAX-RS について

JAX-RS は、RESTful Web API を提供するための JavaAPI。 もともとは Java EE の一部。 現在は Eclipse Foundation に移管されて EE4J プロジェクトの一部 (Jakarta EE の一部) となっている。

JAX-RS のリソースから送出された例外の扱いについて

今回は JAX-RS 2.1 の仕様を参照する。

3.3.4 節に書かれている。

3.3.4 Exceptions

A resource method, sub-resource method or sub-resource locator may throw any checked or unchecked exception. An implementation MUST catch all exceptions and process them in the following order:

  • 1. Instances of WebApplicationException and its subclasses MUST be mapped to a response as follows. If the response property of the exception does not contain an entity and an exception mapping provider (see Section 4.4) is available for WebApplicationException or the corresponding subclass, an implementation MUST use the provider to create a new Response instance, otherwise the response property is used directly. The resulting Response instance is then processed according to Section 3.3.3.
  • 2. If an exception mapping provider (see Section 4.4) is available for the exception or one of its superclasses, an implementation MUST use the provider whose generic type is the nearest superclass of the exception to create a Response instance that is then processed according to Section 3.3.3. If the exception mapping provider throws an exception while creating a Response then return a server error (status code 500) response to the client.
  • 3. Unchecked exceptions and errors that have not been mapped MUST be re-thrown and allowed to propagate to the underlying container.
  • 4. Checked exceptions and throwables that have not been mapped and cannot be thrown directly MUST be wrapped in a container-specific exception that is then thrown and allowed to propagate to the underlying container. Servlet-based implementations MUST use ServletException as the wrapper. JAX-WS Provider-based implementations MUST use WebServiceException as the wrapper.

Note: Items 3 and 4 allow existing container facilities (e.g. a Servlet filter or error pages) to be used to handle the error if desired.

Exception mapping provider による変換

上記の説明の中に、「exception mapping provider」 というものが出てくる。 詳細は 4.4 節に書かれている。

4.4 Exception Mapping Providers

Exception mapping providers map a checked or runtime exception to an instance of Response. An exception mapping provider implements the ExceptionMapper<T> interface and may be annotated with @Provider for automatic discovery.

When a resource class or provider method throws an exception for which there is an exception mapping provider, the matching provider is used to obtain a Response instance. The resulting Response is processed as if a web resource method had returned the Response, see Section 3.3.3. In particular, a mapped Response MUST be processed using the ContainerResponse filter chain defined in Chapter 6.

When choosing an exception mapping provider to map an exception, an implementation MUST use the provider whose generic type is the nearest superclass of the exception. If two or more exception providers are applicable, the one with the highest priority MUST be chosen as described in Section 4.1.3.

To avoid a potentially infinite loop, a single exception mapper must be used during the processing of a request and its corresponding response. JAX-RS implementations MUST NOT attempt to map exceptions thrown while processing a response previously mapped from an exception. Instead, this exception MUST be processed as described in steps 3 and 4 in Section 3.3.4.

すなわち、リソースメソッドなどから送出された例外を変換して Response オブジェクトを生成する機能を提供するものである。

Exception mapping provider は複数登録することができる。 例外が送出されると、対応する exception mapping provider が選択され、Response が生成されて、それがレスポンスとして使用される。 対応する exception mapping provider がない場合、非検査例外だとそのまま再送出されるし、検査例外だとコンテナに応じた非検査例外にラップされて送出されるとのこと。

WebApplicationException について

基本的には上に書いた通りの例外ハンドリングがなされるが、リソースメソッドなどから送出された例外が WebApplicationException の場合は、exception mapping provider を通らないことがある。 3.3.4 節の箇条書きの 1 つめがそれである。

WebApplicationExceptionresponse プロパティがエンティティを持っていない場合で、対応する exception mapping provider が存在する場合には exception mapping provider による Response 生成がなされる。 それ以外の場合は WebApplicationExceptionresponse プロパティの値がそのままレスポンスとして使われる。

サンプルコード

JAX-RS の参照実装である Jersey を使った Kotlin のサンプルコード。

import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory
import org.glassfish.jersey.server.ResourceConfig
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.Response
import javax.ws.rs.core.UriBuilder
import javax.ws.rs.ext.ExceptionMapper

@Path("exceptions")
class MyResource {
    @GET
    @Path("re")
    fun re() {
        // RuntimeException なので対応する MyExceptionMapper で変換される。
        throw RuntimeException("RuntimeException")
    }

    @GET
    @Path("wae-without-entity")
    fun waeWithoutEntity() {
        // エンティティのない WebApplicationException なので MyExceptionMapper で変換される。
        throw WebApplicationException("WebApplicationException with response not containing entity",
            Response.status(Response.Status.BAD_REQUEST).build())
    }

    @GET
    @Path("wae-with-entity")
    fun waeWithEntity() {
        // エンティティを持つ WebApplicationException なので MyExceptionMapper で変換されない。
        throw WebApplicationException("WebApplicationException with response containing entity",
            Response.status(Response.Status.BAD_REQUEST).entity("Entity").build())
    }
}

class MyExceptionMapper : ExceptionMapper<Throwable> {
    override fun toResponse(exception: Throwable): Response {
        println(exception)
        return Response.status(Response.Status.BAD_REQUEST).entity("$exception").build()
    }
}

fun main() {
    val baseUri = UriBuilder.fromUri("http://localhost/").port(9998).build()
    val config = ResourceConfig(MyResource::class.java, MyExceptionMapper::class.java)
    JdkHttpServerFactory.createHttpServer(baseUri, config)
}

ビルドコードは以下のような感じで動くはず。 (Gradle。)

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.3.11'
}

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    compileOnly "javax.ws.rs:javax.ws.rs-api:2.1"

    compile "org.glassfish.jersey.containers:jersey-container-jdk-http:2.27"
    compile "org.glassfish.jersey.inject:jersey-hk2:2.27"

    // JDK 9 以降は下記が必要。
    // See : https://stackoverflow.com/questions/43574426/how-to-resolve-java-lang-noclassdeffounderror-javax-xml-bind-jaxbexception-in-j
    compile "javax.xml.bind:jaxb-api:2.2.11"
    compile "javax.activation:activation:1.1.1"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

Maven の Surefire プラグインと JMockit と JaCoCo プラグイン

ビルドツールとして Maven を使っている Java プロジェクトで JMockit と JaCoCo を使いたいときの話。 ユニットテストの実行には Maven Surefire プラグインを使用しているものとする。 また、JaCoCo の適用には JaCoCo Maven プラグインを使用するものとする。

JMockit と JaCoCo の -javaagent 指定を共存させる

JMockit も JaCoCo も、Java プログラミング言語エージェントの仕組みを利用している *1。 なので、どちらも基本的には -javaagent の指定が必要である。

JMockit-javaagent 指定

JMockit の方は、Surefire プラグインargLine パラメータで -javaagent を指定する。

<plugins>
   <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.21.0</version> <!-- or some other version -->
      <configuration>
         <argLine>
            -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
         </argLine>
         <!-- ... -->
      </configuration>
   <plugin>
   <!-- ... -->
<plugins>
JMockit - Tutorial - Introduction

JaCoCo の -javaagent 指定

JaCoCo については jacoco:prepare-agent ゴールにて argLine プロパティ *2-javaagent が追加される仕組みになっている。

Surefire プラグインargLine パラメータが指定されていないときには argLine プロパティの値が使われるので、Surefire プラグインargLine パラメータが指定されていない場合にはそれ以上の設定は必要ない。

一方で、Surefire プラグインargLine パラメータの指定がされている場合は (そのままでは) JaCoCo プラグインが設定した argLine プロパティの値が使われないため、別途設定が必要となる。 具体的には、Surefire プラグインlate property evaluation (late replacement) を使用する。 これは、プロパティ参照の ${...} の代わりに @{...} を使うというもの。 ${...} を使った場合はプラグインの実行前に評価されるため JaCoCo プラグインargLine プロパティを変更してもそれが反映されないが、@{...} を使うとプラグインの実行後の値が反映される。

One of the ways to do this in case of maven-surefire-plugin - is to use syntax for late property evaluation:

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
      <argLine>@{argLine} -your -extra -arguments</argLine>
    </configuration>
  </plugin>

IntelliJ IDEA での問題

さて、Maven でビルドを実行する際に JMockit と JaCoCo を共存させるだけならここまでで解決するが、さらにややこしいのが IntelliJ IDEA の存在である。

IntelliJ IDEA で Maven プロジェクトを扱う場合、デフォルトでは JUnit 実行時に Surefire プラグインargLine パラメータを読み取るのだが、IntelliJ IDEA は late replacement (@{...}) に対応していない。 そのため、下記のようなエラーが発生してしまう。

エラー: メイン・クラス@{argLine}が見つからなかったかロードできませんでした

回避策の一つは、IntelliJ IDEA でのユニットテスト実行時に Surefire プラグインargLine パラメータを参照する設定を無効にすること。 設定の中の 「Maven」 の 「Running Tests」 で Surefire プラグインargLine を参照するかどうか指定できる。

私が採った回避策は、argLine パラメータには空文字列として宣言したプロパティを含めておき、JaCoCo を有効にしたいときだけコマンドライン引数で @{argLine} を埋め込むというもの。

    <properties>
        <!--
        Surefire プラグインの `argLine` に含める文字列。 JaCoCo の Java Agent を有効にするためには `@{argLine}` を指定すること。
        IntelliJ IDEA が `@{...}` による late replacement に対応しておらず、直接 `@{argLine}` を指定すると IntelliJ IDEA
        でのテスト実行時にエラーが発生してしまうので、`@{argLine}` をコマンドラインオプションで指定できるように
        このようなプロパティを用意した。
        See : http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html (Surefire Plugin の `argLine` の説明)
        See : https://www.eclemma.org/jacoco/trunk/doc/prepare-agent-mojo.html (JaCoCo の Java Agent の指定について)
        See : https://youtrack.jetbrains.com/issue/IDEA-143187 (IntelliJ IDEA が argLine の `@{...}` を解釈しない件)
        -->
        <surefireArgLine/>
    </properties>
    <!-- (中略) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19.1</version>
                <configuration>
                    <argLine>
                        ${surefireArgLine}
                        <!-- See : http://jmockit.github.io/tutorial/Introduction.html#runningTests -->
                        -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
                    </argLine>

上のようにしておけば IntelliJ IDEA でのテスト実行にエラーが出ることはない。 そして Mavenコマンドラインから動かすときには、下記のように指定すれば JaCoCo が有効になる。

mvn package -D surefireArgLine="@{argLine}"

もっといい方法があれば知りたい……。

*1:JaCoCo についてはオフラインインストゥルメントの仕組みもあるが。

*2:デフォルトの場合。 変更可。

MockK と JMockit の組み合わせで AttachNotSupportedException 例外が発生することがあるっぽい

Kotlin で Mockito を使うのが辛くなってきた *1 ので、「よーしパパ MockK 入れちゃうぞー」 と言って MockK 1.8.12 を導入したのだけど、その結果テストを実行すると以下のような初期化エラーが発生するようになってしまった。

java.lang.ExceptionInInitializerError
	at MyTest.<init>(MyTest.kt:101)
Caused by: java.lang.IllegalStateException: Error during attachment using: net.bytebuddy.agent.ByteBuddyAgent$AttachmentProvider$Compound@718207
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:384)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:358)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:326)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:312)
	at io.mockk.proxy.jvm.JvmMockKAgentFactory.initInstrumentation(JvmMockKAgentFactory.kt:122)
	at io.mockk.proxy.jvm.JvmMockKAgentFactory.init(JvmMockKAgentFactory.kt:31)
	at io.mockk.impl.JvmMockKGateway.<init>(JvmMockKGateway.kt:45)
	at io.mockk.impl.JvmMockKGateway.<clinit>(JvmMockKGateway.kt:163)
	... 22 more
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at net.bytebuddy.agent.Attacher.install(Attacher.java:84)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:379)
	... 29 more
Caused by: com.sun.tools.attach.AttachNotSupportedException: no providers installed
	at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
	... 35 more

スタックトレースの一番下を見ると com.sun.tools.attach.VirtualMachine なので、JDK の内部でなんかなってるのかなー、と最初は思っていたのだけど、IDE で辿っていくと VirtualMachine クラスは JMockit に含まれているものだということがわかった。 使っていた JMockit が 1.22 と少し古いものだったので、最新の 1.43 を使うようにしたら無事エラーは発生しなくなった。

Mockito でも同様のエラーが発生することがある模様。

ちなみに com.sun.tools.attach.VirtualMachine は、JVM にアタッチするための API に含まれるものらしい。

JaCoCo の Java 10 対応はバージョン 0.8.1 から

Gradle 4.6 で Gradle の JaCoCo プラグインを使用しているプロジェクトをビルドすると下記のエラーが発生した。 Java 環境は OpenJDK 10。

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 java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:510)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:522)
Caused by: java.lang.RuntimeException: Class java/lang/UnknownError could not be instrumented.
        at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:139)
        at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:100)
        at org.jacoco.agent.rt.internal_290345e.PreMain.createRuntime(PreMain.java:55)
        at org.jacoco.agent.rt.internal_290345e.PreMain.premain(PreMain.java:47)
        ... 6 more
Caused by: java.lang.NoSuchFieldException: $jacocoAccess
        at java.base/java.lang.Class.getField(Class.java:1958)
        at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:137)
FATAL ERROR in native method: processing of -javaagent failed
        ... 9 more

java.lang.NoSuchFieldException: $jacocoAccess』 と出ているので JaCoCo 関連っぽいということで調べたところ、JaCoCo の JDK 10 対応は JaCoCo 0.8.1 からだった。

Gradle 4.6 時点での JaCoCo プラグインでは、デフォルトで JaCoCo 0.8.0 を使うようになっているので、JDK 10 で使うには JaCoCo のバージョンを指定してやる必要がある。

jacoco {
    toolVersion = '0.8.2'
}

ちなみに Gradle 4.10.2 時点ではデフォルトは JaCoCo 0.8.1 になっている。