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

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

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

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"
}