Your JAX-RS APIs were not born equal: using dynamic features
This time we are going to talk a little bit about JAX-RS 2.0 APIs and touch on one very interesting aspect of the specification: dynamic features and how they are useful.
Traditionally, when JAX-RS 2.0 APIs are configured and deployed (using Application class, bootstrapped from servlet or created through RuntimeDelegate), there is an option to register additional providers and features. The great examples of those could be bean validation (JSR 349) or Java API for JSON processing (JSR-353) support. Those providers and features are going to be applied to all JAX-RS 2.0 resources and in most use cases this is a desired behavior. However, from time to time there is a need to enable a particular provider or feature only for some resources, leaving others unaffected. This is exactly the use case where dynamic features are going to help us a lot.
For this post we are going to use the latest version 3.1.5 of excellent Apache CXF framework but dynamic features are part of the JAX-RS 2.0 specification and are supported by most (if not all) of the implementations.
Let us consider a very simple JAX-RS 2.0 API to manage people, with a single method to handle HTTP GET requests. Let us assume this is a version 1 of the API and although the @Range annotation is specified for the count query parameter, its support was never implemented and it is present in the code for documentation purposes only.
@Path("/v1/people") public class PeopleRestService { @Produces( { MediaType.APPLICATION_JSON } ) @GET public List<Person> getAll(@Range(min = 1, max = 10) @QueryParam("count") int count) { return Collections.nCopies(count, new Person("a@b.com", "A", "B")); } }
In this case, passing an invalid value for the count query parameter is going to result in Internal Server Error. Let us make sure this is exactly what is happening:
$ curl -i http://localhost:8080/rest/api/v1/people?count=-1 HTTP/1.1 500 Server Error Cache-Control: must-revalidate,no-cache,no-store Content-Type: text/html;charset=iso-8859-1 Content-Length: 377 Connection: close Server: Jetty(9.3.7.v20160115)
After some time we realized the issues with this API and decided to implement the proper validation mechanism in place, using the Bean Validation 1.1 integration with JAX-RS 2.0. However, we made a decision to create version 2 of the API and to keep version 1 untouched as its clients do not expect any other HTTP status codes except 200 and 500 to be returned (unfortunately, in real life it happens more often than not).
There are couple of different approaches to implement such per-API customization, but probably the most simple one is by introducing a dedicated annotation, for example @EnableBeanValidation, and annotating JAX-RS 2.0 resource class with it:
@Path("/v2/people") @EnableBeanValidation public class ValidatingPeopleRestService { @Produces( { MediaType.APPLICATION_JSON } ) @GET public @Valid List<Person> getAll(@Range(min = 1, max = 10) @QueryParam("count") int count) { return Collections.nCopies(count, new Person("a@b.com", "A", "B")); } }
To enable Bean Validation 1.1 for all the JAX-RS 2.0 APIs annotated with @EnableBeanValidation we are going to create a dynamic feature class, BeanValidationDynamicFeature:
@Provider public class BeanValidationDynamicFeature implements DynamicFeature { private final JAXRSBeanValidationInInterceptor inInterceptor; private final JAXRSBeanValidationOutInterceptor outInterceptor; public BeanValidationDynamicFeature(final BeanValidationProvider provider) { this.inInterceptor = new JAXRSBeanValidationInInterceptor(); this.inInterceptor.setProvider(provider); this.outInterceptor = new JAXRSBeanValidationOutInterceptor(); this.outInterceptor.setProvider(provider); } @Override public void configure(final ResourceInfo resourceInfo, final FeatureContext context) { if (resourceInfo.getResourceClass().getAnnotation(EnableBeanValidation.class) != null) { context.register(inInterceptor); context.register(outInterceptor); } } }
Its job is pretty simple, just register JAXRSBeanValidationInInterceptor and JAXRSBeanValidationOutInterceptor interceptor instances as additional providers for JAX-RS 2.0 APIs in question. One minor but important note though: exception mappers are not supported by dynamic features, at least with respect to Apache CXF implementation, and should be registered as a regular providers (along with dynamic features themselves), for example:
@Bean @DependsOn("cxf") public Server jaxRsServer() { final JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication(), JAXRSServerFactoryBean.class ); factory.setServiceBean(validatingPeopleRestService()); factory.setServiceBean(peopleRestService()); factory.setProvider(new JacksonJsonProvider()); factory.setProvider(new BeanValidationDynamicFeature(new BeanValidationProvider())); factory.setProvider(new ValidationExceptionMapper()); return factory.create(); } @Bean public JaxRsApiApplication jaxRsApiApplication() { return new JaxRsApiApplication(); } @Bean public ValidatingPeopleRestService validatingPeopleRestService() { return new ValidatingPeopleRestService(); } @Bean public PeopleRestService peopleRestService() { return new PeopleRestService(); }
That is basically all we have to do. Once the BeanValidationDynamicFeature is registered (in this case using JAXRSServerFactoryBean), it is going to be applied to all matching service beans. Let us make sure that for version 2 of our people management API the proper out of the box validation is triggered:
$ curl -i http://localhost:8080/rest/api/v2/people?count=-1 HTTP/1.1 400 Bad Request Content-Length: 0 Server: Jetty(9.3.7.v20160115)
This time the response is different, indicating that invalid input has been submitted by the client (straight result of Bean Validation 1.1 in action): Bad Request.
Hopefully, dynamic features are going to be yet another useful tool in your toolbox. The example we have covered here is somewhat imaginary but it is very easy to use dynamic features with security, tracing, logging, profiling, … Moreover, dynamic features can be applied even on a particular resource methods, allowing fined-grained control over your APIs.
- The complete project source is available on Github.
Reference: | Your JAX-RS APIs were not born equal: using dynamic features from our JCG partner Andrey Redko at the Andriy Redko {devmind} blog. |