One thing that I think is very important when implementing a RESTful service is to use the appropriate HTTP response codes for error statuses. JAX-RS provides a few ready-made exceptions that do this, so my
getCustomerById
method from the previous post can throw a
NotFoundException
if the customer does not exist.
But I'm using an underlying API that throws an exception if it receives too many requests in a short space of time. JAX-RS doesn't provide a
TooManyRequestsException
so I'll have to make one of my own.
public class TooManyRequestsException extends WebApplicationException {
public TooManyRequestsException() {
super(429);
}
}
And here's a test for it:
@Test(expected=TooManyRequestsException.class)
public void testGetCustomerById_tooManyRequests() {
int id = random.nextInt();
when(customerDao.getById(id)).thenThrow(new TooManyRequestsException());
customerService.getCustomerById(id);
}
The test above works as a unit test, but as an integration test using the client the exception caught is
ClientErrorException
. The CXF client doesn't know how to deal with the 429 status code and, frustratingly, CXF doesn't give us any mechanism to register a custom exception on the client side.
Fortunately, a little digging around in the source code reveals a workaround. The class
JAXRSUtils
contains a static map of exception classes mapped to HTTP status codes. When it receives an HTTP response with an error status it attempts to instantiate one of these exceptions using a constructor that takes a JAX-RS
Response
object. So I'll add that constructor to my
TooManyRequestsException
.
public TooManyRequestsException(Response response) {
super(response);
}
The map is private, so we have to do some reflection hackery to get it in there. You can put this code anywhere it will be run by the client, such as a static block, but I decided the best thing for me was to create my own custom
JAXRSClientFactory
.
public class CustomJAXRSClientFactory {
private final String url;
private final List<Object> providers = new ArrayList<>();
public VCommsManJAXRSClientFactory(String url) {
registerExceptions();
this.url = url;
providers.add(new JacksonJsonProvider());
}
/** This is where we register the exceptions with the CXF client */
private void registerExceptions() {
try {
Field f = JAXRSUtils.class.getDeclaredField("EXCEPTIONS_MAP");
f.setAccessible(true);
Map<Integer, Class<?>> m = (Map<Integer, Class<?>>) f.get(null);
m.put(429, TooManyRequestsException.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public <T> T create(Class<T> serviceInterface) {
JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
bean.setProviders(providers);
bean.setAddress(url);
bean.setServiceClass(serviceInterface);
return bean.create(serviceInterface);
}
}
Now I replace the client code in my test with this:
CustomJAXRSClientFactoryBean clientFactory = new CustomJAXRSClientFactoryBean("http://localhost:9090");
customerService = clientFactory.create(CustomerService.class);
And my integration test passes :)