Overview
A RESTful API service may throw exception in many cases. It’s important to handle them properly, so that we can provide a right HTTP response to the client, in particular a right status code (4xx or 5xx errors) and a correct entity. In this article, we will focus on « Exception Mapper », understand it’s mechanism in regards to exceptions.
After reading this article, you will understand:
- What is an exception mapper?
- How to declare an exception mapper in JAX-RS application?
- Exceptions matching mechanism
- When exception mapper throws an exception…
As usual, the source code is available for free on GitHub as mincong-h/jaxrs-2.x-demo. You can install and run the demo as following:
~ $ git clone https://github.com/mincong-h/jaxrs-2.x-demo.git ~ $ cd jaxrs-2.x-demo/exception exception $ mvn clean install exception $ java -jar target/exception-1.0-SNAPSHOT-jar-with-dependencies.jar Exception Mapper
Before talking about exception mapper, we first need to understand the concept of provider. Providers in JAX-RS are responsible for various cross-cutting concerns such as filtering requests, converting representations into Java objects, mapping exceptions to responses, etc. By default, a single instance of each provider class is instantiated for each JAX-RS application, aka singletons.
Interface ExceptionMapper<E extends Throwable> defines a contract for a provider that maps Java exceptions E to Response. Same as other providers, exception mappers can be either pre-packaged in the JAX-RS runtime or supplied by an application. In order to create your own exception mapper, you need to create a class which implements interface ExceptionMapper. Here’s an example for mapping ExceptionA in your application:
package io.mincong.demo; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; public class MapperA implements ExceptionMapper<ExceptionA> { @Override public Response toResponse(ExceptionA ex) { return Response.status(400) .entity("Mapper A: " + ex.getMessage()) .build(); } }When an exception of type ExceptionA thrown by a JAX-RS resource, this exception mapper can catch the exception and transform it into a HTTP 400 response, with the origin exception message as entity.
Exercise time: given the following JAX-RS resource method newExceptionA1(), use command line tool cUrl to observe the HTTP response and verify the exception mapping.
@GET @Path("a") public Response newExceptionA() { throw new ExceptionA("Exception A"); }Command cUrl in terminal:
$ curl -i http://localhost:8080/a HTTP/1.1 400 Bad Request Content-Type: text/plain Connection: close Content-Length: 21 Mapper A: Exception A So we successfully verified that MapperA is used for handling ExceptionA coming from resource method.
Declare Exception Mapper in JAX-RS Application
Providers implementing ExceptionMapper contract must be either programmatically registered in a JAX-RS runtime or must be annotated with @Provider annotation to be automatically discovered by the JAX-RS runtime during a provider scanning phase.
Programmatically registered example. In JAX-RS application “MyApplication”, add exception mapper “MapperA” as a singleton. Now, Mapper A is programmatically registered in a JAX-RS runtime.
package io.mincong.demo; import java.util.HashSet; import java.util.Set; import javax.ws.rs.core.Application; public class MyApplication extends Application { @Override public Set<Object> getSingletons() { Set<Object> set = new HashSet<>(); set.add(new MapperA()); return set; } ... }Automatic discovery example. Add @Provider annotation to Mapper A, so that it can be automatically discovered by the JAX-RS runtime during a provider scanning phase:
import javax.ws.rs.ext.Provider; @Provider public class MapperA implements ExceptionMapper<ExceptionA> { ... }Exception Mapping Mechanism
Exception mapping providers map a checked or runtime exception to an instance of Response. When choosing an exception mapping provider to map an exception, JAX-RS implementation (e.g. Jersey) 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 will be chosen. (Spec §4.4)
Mapping = nearest superclass + highest priority
For example, in our demo, 2 exception mappers are available:
MapperAfor mapping all exceptions of classExceptionAor its sub-classesMapperA1for mapping all exceptions of classExceptionA1or its sub-classes, whereExceptionA1is a child class ofExceptionA
and the following exceptions:
java.lang.Exception └── java.lang.RuntimeException └── io.mincong.demo.ExceptionA ├── io.mincong.demo.ExceptionA1 └── io.mincong.demo.ExceptionA2 The mapping will behave as the table below:
| Exception | Mapper A1 | Mapper A | General Mapper |
|---|---|---|---|
ExceptionA1 | x | ||
ExceptionA2 | x | ||
ExceptionA | x | ||
RuntimeException | x | ||
Exception | x |
Is is possible to have infinite loop when handling exception?
According to Spec §4.4 Exception Mapping Providers, JAX-RS implementations use a single exception mapper during the processing of a request and its corresponding response. So this should never happen.
A Failing Exception Mapper
What will happen if exception mapper throws an exception?
If an exception mapping provider throws an exception while creating a Response then, then a server error (status code 500) response is returned to the client (Spec §3.3.4 Exceptions).
We can verify it using the following resource method and exception mapper:
@GET @Path("failing") public Response newFooException() { throw new FooException(); }public class FailingExceptionMapper implements ExceptionMapper<FooException> { @Override public Response toResponse(FooException exception) { throw new IllegalStateException(); } }Verify using cUrl in terminal:
$ curl -i http://localhost:8080/failing HTTP/1.1 500 Internal Server Error Connection: close Content-Length: 0 We can see the response is 500. And the following stack trace can be observed in the server log:
Dec 03, 2018 9:46:47 PM org.glassfish.jersey.server.ServerRuntime$Responder mapException SEVERE: An exception was not mapped due to exception mapper failure. The HTTP 500 response will be returned. io.mincong.demo.FooException at io.mincong.demo.DemoResource.newFooException(DemoResource.java:38) 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:498) ... Conclusion
In this article, we learnt the definition of exception mapper, how to register it in JAX-RS application (programmatically or via annotation). We’ve also seen the matching mechanism (nearest-superclass). At the end, we verified that a failing provider is handled by JAX-RS implementation.
As usual, the source code is available for free on GitHub as mincong-h/jaxrs-2.x-demo. Feel free to download it and give it a try. Hope you enjoy this article, see you the next time!