Creating a JAX-RS Compliant Stub for RESTTemplate

8:15 AM , 0 Comments

I really like RESTTemplate, but I have a small beef with the lack of JAX-RS support (go to Arjen Poutsuma's comments below for Spring's rationale for not building it in). So, if you are like me and want to use JAX-RS annotations and RESTTemplate together, here is what I did.

First, a little bit of background on what JAX-RS looks like. JAX-RS is the result of JSR 311 which includes a set of method and class annotations. These annotations are interpreted by a JAX-RS REST provider like Jersey or CXF. A typical class might be annotated in the following way:


@Path("/myResource")
@Consumes("application/json")
public interface MyEndpoint {
@GET
@Path("/myAction/{resourceId}")
@Produces("application/json")
MyClass getResource(@PathParam("resourceId") String resourceId);
}


Then, with some configuration, CXF and the like will create the client that translates method invocations into the appropriate HTTP request.

So, the goal is to create some sort of proxy or instrumentation piece that will take this interface and translate invocations to it into the correct RESTTemplate method invocation.

I only needed a handful of the features from the JAX-RS spec which basically included:
  • Support for GET, POST, PUT, and DELETE rest headers.

  • Support for url templates via PathParam, QueryParam, and FormParam.

  • Support for Path annotations at the class and method level.
One possibly important thing for other people that I DIDN'T tackle:
  • Support for one method parameter having no JAX-RS annotation.
(Note: I decided to create a java InvocationHandler, but there are several ways to go about this, including Spring AOP.)

In the case of a JAX-RS client, the HTTP methods actually translate into different RESTTemplate method calls. Since I wasn't doing location requests, I have a one-to-one mapping from JAX-RS HTTP method annotations and RESTTemplate method calls:


if ( httpMethod instanceof POST ) {
return restTemplate.postForObject(url + queryParamExtractor.getExtracted(), formParamExtractor.getExtracted(), method.getReturnType(), pathParamExtractor.getExtracted());
} else if ( httpMethod instanceof GET ) {
return restTemplate.getForObject(url + queryParamExtractor.getExtracted(), method.getReturnType(), pathParamExtractor.getExtracted());
} else if ( httpMethod instanceof DELETE ) {
restTemplate.delete(url + queryParamExtractor.getExtracted(), pathParamExtractor.getExtracted());
} else if ( httpMethod instanceof PUT ) {
restTemplate.put(url + queryParamExtractor.getExtracted(), formParamExtractor.getExtracted(), pathParamExtractor.getExtracted());
}


The more involved part is checking each method parameter and appropriately interpreting the JAX-RS method parameter annotations. I just needed PathParam, QueryParam, and FormParam:


for ( int i = 0; i < allParameterAnnotations.length; i++ ) {
Annotation[] parameterAnnotations = allParameterAnnotations[i];
Object arg = args[i];
for ( Annotation parameterAnnotation : parameterAnnotations ) {
if ( parameterAnnotation instanceof PathParam ) {
pathParamExtractor.extractFrom(((PathParam)parameterAnnotation).value(), arg);
} else if ( parameterAnnotation instanceof QueryParam ) {
queryParamExtractor.extractFrom(((QueryParam)parameterAnnotation).value(), arg);
} else if ( parameterAnnotation instanceof FormParam ) {
formParamExtractor.extractFrom(((FormParam)parameterAnnotation).value(), arg);
}
}
}


Everything else is just helper classes. Pay no attention, for example, to the variables 'pathParamExtractor', etc. They are simply helpers for accumulating the values of those parameters in a way to satisfy the url template.

Hope this gets you started, too.

Josh Cummings

"I love to teach, as a painter loves to paint, as a singer loves to sing, as a musician loves to play" - William Lyon Phelps

0 comments: