JAX-RS のリソースから送出された例外の扱い
JAX-RS のリソースのメソッドから例外が送出された場合の挙動についてちゃんと把握していなかったので調べた。
前提知識 : JAX-RS について
JAX-RS は、RESTful Web API を提供するための Java の API。 もともとは 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
WebApplicationExceptionand its subclasses MUST be mapped to a response as follows. If theresponseproperty of the exception does not contain an entity and an exception mapping provider (see Section 4.4) is available forWebApplicationExceptionor the corresponding subclass, an implementation MUST use the provider to create a newResponseinstance, otherwise theresponseproperty is used directly. The resultingResponseinstance 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
Responseinstance that is then processed according to Section 3.3.3. If the exception mapping provider throws an exception while creating aResponsethen 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
ServletExceptionas the wrapper. JAX-WSProvider-based implementations MUST useWebServiceExceptionas 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 theExceptionMapper<T>interface and may be annotated with@Providerfor 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
Responseinstance. The resultingResponseis processed as if a web resource method had returned theResponse, see Section 3.3.3. In particular, a mappedResponseMUST 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 つめがそれである。
WebApplicationException の response プロパティがエンティティを持っていない場合で、対応する exception mapping provider が存在する場合には exception mapping provider による Response 生成がなされる。 それ以外の場合は WebApplicationException の response プロパティの値がそのままレスポンスとして使われる。
サンプルコード
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"
}