Using Jersey model processor for supporting edge-service features
In this post I would like to show you how to add a resource programatically in Jersey container. We start from a business use case that needs to be implemented. What we are trying to achieve is to allow external clients to use some resources of internal microservices. I am aware of the fact that the solution we are going to discuss is not the best way to solve the problem. Choosing the best solution lies outside the scope of this post. What is covered in this post are the steps and solutions we tried to use in order to solve the problem.
Business use case #
At Allegro, we are constantly working on rebuilding our architecture to the Service Oriented Architecture (SOA). This means that a monolithic system is being replaced by microservices. Unfortunately, this architecture shift solves problems of one type but creates other problems instead. Let us take a look at a concrete example. Suppose we want to sell some clothes that our child has grown out of. We take our mobile phone, take some pictures and then list them on allegro.pl. Since the monolithic system is accessible from the Internet, the mobile application can access the system’s API. But when we use SOA, a microservice responsible for rendering a list of offers is accessible only from within the cloud environment. We need an edge service that serves as a proxy between mobile devices and the listing items microservice (and other microservices too).
PublicApiVersion #
Of course, not all resources provided by microservices are available for public access. We have decided that when a microservice wants to expose some of its resources, it has to set a specific supported media type for these resources. Since we use Jersey as our JAX-RS implementation, this means that a proper media type must be set for all methods that process HTTP requests:
@Path("/somePath")
public class SomeEndpoint {
@GET
@Produces("application/vnd.allegro.public.v1+json")
@Consumes("application/vnd.allegro.public.v1+json")
public Response getSomeResponse(@Valid @NotNull SomeRequest someRequest) {
...
}
}
The solution presented above works for sure, but we do not like to stick to it because it is
very error-prone. We want to get rid of hard-coded values because, for example, a typo may
force us to go through the build-release-deploy cycle again and again (imagine that multiplied
by the number of microservices that want to expose some part of their API to the public).
Moreover, the code presented above is more difficult to maintain. What we are trying to
achieve is to produce one solution in the form of an external dependency (a library) for all
microservices implemented in Java. We came to the conclusion that the optimal solution should,
first of all, not be a burden to the teams that develop and maintain their services. Our
solution is the @PublicApiVersion
annotation:
@Documented
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface PublicApiVersion {
long value() default 1;
}
@Path("/somePath")
public class SomeEndpoint {
@GET
@PublicApiVersion(1)
public Response getSomeResponse() {
...
}
}
Now, the proposed solution looks nice and clean to users of our public-api-version library.
They only have to include the library in the classpath and use the @PublicApiVersion
annotation. But how to configure Jersey to treat @PublicApiVersion(1)
as
@Produces("application/vnd.allegro.public.v1+json")
and
@Consumes("application/vnd.allegro.public.v1+json")
?
Filters #
There are two main kinds of filters in Jersey: client filters and server filters. Of course, implementing a client filter that would change a request media type is not an option because our clients may use REST clients other than the client provided by Jersey. As a result, we do not want to modify and maintain any REST client, and consequently, we focus on server filters.
ContainerRequestFilter
is a server filter which allows us to manipulate requests. According
to filter and interceptor execution order,
we are only interested in pre-matching filters. Post-matching filters are not an option
because the matching process has been already completed before they are fired, and a client
request with the application/vnd.allegro.public.v1+json
media type does not match any
resource method. Consequently, we cannot use the
@NameBinding
annotation because
name-bound filters are also fired after the matching process is completed.
In order to create a pre-matching filter we should annotate our class with
the @PreMatching
annotation and implement the
ContainerRequestFilter interface:
@PreMatching
@Provider
public class PreMatchingFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
...
}
}
ContainerRequestContext
provides lots of useful information. For example, in order to find
acceptable media types of a request we should call the
requestContext.getAcceptableMediaTypes()
method that returns an immutable List<MediaType>
.
ContainerRequestContext
has only a few setter methods (setEntityStream()
, setMethod()
,
setProperty()
, setRequestUri()
, setSecurityContext()
) none of which serves our purpose.
This class has a method that could be useful, i.e. getHeaders()
that returns a mutable
MultivaluedMap
.
But what should we put in that map? The problem is how can we access the
metainformation about the class (i.e. SomeEndpoint.class
) to find resource methods that
handle HTTP requests? Please keep in mind that we are not allowed to hardcode anything and we
cannot assume anything about the microservice implementation. Our library clients just
include it in their classpath, annotate some methods with @PublicApiVersion
— and that is
all what they need to do. In order to handle @PublicApiVersion
properly we must be able to
get information about all endpoints and their methods, but unfortunately, we have no such
information in a @PreMatching
filter. Therefore, we cannot tell methods annotated with
@PublicApiVersion
from other ones and we cannot fetch metainformation provided by the
annotation.
Dynamic Feature #
Jersey
DynamicFeature
allows us to register providers that may be applied to a particular
resource class or method. Let us take a look at an example from the
StackOverflow website:
@Provider
public class MyDynamicFeature implements DynamicFeature {
@Override
public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
if ("HelloWorldResource".equals(resourceInfo.getResourceClass().getSimpleName())
&& "getHello".equals(resourceInfo.getResourceMethod().getName())) {
context.register(MyContainerRequestFilter.class);
}
}
}
@Provider
public class MyContainerRequestFilter implements ContainerRequestFilter {
...
}
As you can see, MyContainerRequestFilter
will be applied to the
HelloWorldResource.getHello
method. But still MyContainerRequestFilter
is an instance of
ContainerRequestFilter
and hence, it is not possible to use DynamicFeature
to implement
@PublicApiVersion
.
Interceptors #
Jersey interceptors are intended to modify entities by manipulating entity input/output streams. Unfortunately, they are useless in this case.
ResourceConfig #
The ResourceConfig
API allows us to create Jersey resources programatically. According to
the Jersey documentation,
this can be useful when the creation of a REST service depends on lots of configuration
parameters or other things such as database structure. This sounds very useful but,
unfortunately, the API does not work in our case. What we are trying to achieve is to alter or
replace existing resources with resources for which their media types in
@Produces
and
@Consumes
annotations have been set properly according to the @PublicApiVersion
annotation, and not to add new resources.
ModelProcessor for the win! #
Having spent some time researching the subject, we came across
ModelProcessor
.
According to the documentation, this is a part of API for constructing or altering resources
programmatically. Every resource that can be designed using the standard JAX-RS approach (e.g.
SomeEndpoint
) via annotated resource classes can be also modeled using the Jersey
programmatic API. In the documentation we read: “the standard use case is to enhance the
current resource model by additional methods and resources” and this is exactly what we want
to achieve.
Before we dive into PublicApiVersionProcessor
, let us visit some helper methods first:
private boolean isPublicApiVersionAnnotationPresent(final Method method) {
return method.isAnnotationPresent(PublicApiVersion.class);
}
private Method getClassMethod(final ResourceMethod resourceMethod) {
return resourceMethod.getInvocable().getDefinitionMethod();
}
private String getPublicApiVersionMediaType(final Method method) {
PublicApiVersion annotation = method.getAnnotation(PublicApiVersion.class);
return String.format("application/vnd.allegro.public.v%s+json", annotation.value());
}
getPublicApiVersionMediaType()
– given aMethod
, this method returns a String representation of thePublicApiVersion
media type,getClassMethod()
– given aResourceMethod
, this method returnsjava.lang.reflect.Method
implementing theResourceMethod
. For example, assuming that theResourceMethod
object describesgetSomeResponse()
,getClassMethod()
returnsgetSomeResponse()
’sjava.lang.reflect.Method
,isPublicApiVersionAnnotationPresent()
– checks whether or not the method is annotated with@PublicApiVersion
.
Here is our solution:
package pl.allegro.tech.api.version.server;
import org.glassfish.jersey.server.model.ModelProcessor;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import pl.allegro.tech.api.version.annotation.PublicApiVersion;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.ext.Provider;
import java.lang.reflect.Method;
@Provider
public class PublicApiModelProcessor implements ModelProcessor {
@Override
public ResourceModel processSubResource(ResourceModel subResourceModel, Configuration configuration) {
return subResourceModel;
}
@Override
public ResourceModel processResourceModel(ResourceModel resourceModel, Configuration configuration) {
final ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false);
resourceModel.getResources().stream().forEach(resource -> newResourceModelBuilder.addResource(createResource(resource)));
return newResourceModelBuilder.build();
}
private Resource createResource(Resource resource) {
final Resource.Builder resourceBuilder = Resource.builder()
.path(resource.getPath())
.name(resource.getName());
resource.getChildResources().stream().forEach(childResources ->
resourceBuilder.addChildResource(createResource(childResources)));
for (final ResourceMethod resourceMethod : resource.getResourceMethods()) {
Method classMethod = getClassMethod(resourceMethod);
if (isPublicApiVersionAnnotationPresent(classMethod)) {
final String publicApiVersionMediaType = getPublicApiVersionMediaType(classMethod);
resourceBuilder.addMethod(resourceMethod)
.consumes(publicApiVersionMediaType)
.produces(publicApiVersionMediaType);
} else {
resourceBuilder.addMethod(resourceMethod);
}
}
return resourceBuilder.build();
}
private boolean isPublicApiVersionAnnotationPresent(final Method method) {
return method.isAnnotationPresent(PublicApiVersion.class);
}
private Method getClassMethod(final ResourceMethod resourceMethod) {
return resourceMethod.getInvocable().getDefinitionMethod();
}
private String getPublicApiVersionMediaType(Method method) {
PublicApiVersion annotation = method.getAnnotation(PublicApiVersion.class);
return String.format("application/vnd.allegro.public.v%s+json", annotation.value());
}
}
We annotate PublicApiVersionModelProcessor
with @Provider
because we want Jersey to
register it automatically. Subresources are not processed so in processSubResource()
we
leave subResourceModel
unchanged. The real action takes place in processResourceModel()
and createResource()
methods. The main idea is simple: leave all the methods that are not
annotated with @PublicApiVersion
unchanged. On the other hand, we add
@Produces("application/vnd.allegro.public.vX+json")
and
@Consumes("application/vnd.allegro.public.vX+json")
for all methods that are annotated with
@PublicApiVersion
. In order to implement this, we start by creating a new
ResourceModel.Builder
that will be filled up with resource methods. Next, we create a new
Resource
in the createResource()
method for each resource in ResourceModel
.
The first step in the createResource()
method is to create a Resource.Builder
object. We
set its path
and name
to values from the original resource. Then, we process each
subresource recursively by invoking the createResource()
method.
The third step involves processing resource methods. If a method is annotated with
@PublicApiVersion
, we create a copy of the method with consumes()
and produces()
values
equal to the media type provided by the annotation. But if there is no @PublicApiVersion
annotation present on the method, we just copy the method as it is.
Finally, we build and return the new resource.
Testing #
At Allegro, we believe that skilled software engineers never leave their code untested. Apart
from unit tests, we wanted to create an integration/context test to make sure that
PublicApiModelProcessor
works properly inside a Jersey container. Fortunately, there is no
need to create a full-blown end-to-end test, where we would set up the server and then send
some requests to server endpoints. Our test case simply extends
JerseyTest
which does all of the configuration of the Jersey container for us, but we still
need an endpoint:
@Path("/publicApiVersion")
public class PublicApiVersionEndpoint {
@GET
@PublicApiVersion
public String publicApiVersion() {
return "ok";
}
}
Then, we have to configure Jersey to use the endpoint and the PublicApiModelProcessor
— the
setup is done in the configure()
method:
public class PublicApiModelProcessorContextTest extends JerseyTest {
@Override
protected Application configure() {
// Log request and response
enable(TestProperties.LOG_TRAFFIC);
ResourceConfig resourceConfig = new ResourceConfig(PublicApiModelProcessor.class);
resourceConfig.register(PublicApiVersionEndpoint.class);
return resourceConfig;
}
@Test
public void shouldServeRequestToPublicApiVersion() {
// given
final String path = "/publicApiVersion";
final String mediaType = "application/vnd.allegro.public.v1+json";
// when
final Response response = target(path).request(mediaType).get(Response.class);
// then
assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode());
assertThat(response.getMediaType.toString()).isEqualTo(mediaType);
}
}
Acknowledgments #
I would like to thank Łukasz Przybyła for his hard work and commitment while working on this
task and the first implementation of PublicApiModelProcessor
.
Summary #
The Jersey framework is very flexible and really simplifies RESTful service development. Thanks to its API you can easily configure it to fit your needs. Moreover, model processors can be executed in the chain so that each model processor will be executed with resource model processed by the previous model processor. As a result, you can go far beyond the standard use case (e.g. adding OPTIONS HTTP methods for every URI endpoint) and completely change the resource model.