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
WebApplicationException
and its subclasses MUST be mapped to a response as follows. If theresponse
property of the exception does not contain an entity and an exception mapping provider (see Section 4.4) is available forWebApplicationException
or the corresponding subclass, an implementation MUST use the provider to create a newResponse
instance, otherwise theresponse
property is used directly. The resultingResponse
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 aResponse
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-WSProvider
-based implementations MUST useWebServiceException
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 theExceptionMapper<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 resultingResponse
is processed as if a web resource method had returned theResponse
, see Section 3.3.3. In particular, a mappedResponse
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 つめがそれである。
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" }