JAX-RS Router (Jersey)

ServiceTalk offers JAX-RS support via its Jersey HTTP Router module. This module gives access to all JAX-RS features as well as ServiceTalk specific extensions. This document details these features and also delves deeper into the implementation.

Note: If using Jersey 3.X, replace all imports containing javax.ws with jakarta.ws, and replace the dependency with: * servicetalk-http-router-jersey3-jakarta9 for Jersey 3.0.X * servicetalk-http-router-jersey3-jakarta10 for Jersey 3.1.X

Features

In its simplest form, starting a ServiceTalk server with the Jersey router amounts to:

HttpServers.forPort(8080)
    .listenStreamingAndAwait(new HttpJerseyRouterBuilder().build(jaxrsApplication))
    .awaitShutdown()

where jaxrsApplication is any implementation of javax.ws.rs.core.Application, including sub-classes of org.glassfish.jersey.server.ResourceConfig if the intention is to use Jersey-specific features.

Standard Resources

The Jersey Router allows ServiceTalk to support a variety of JAX-RS features, which we detail in this section.

As expected, standard JAX-RS-annotated resource classes and methods can be used, like the following:

@Path("greetings")
public class HelloWorldJaxRsResource {
    @GET
    @Path("hello")
    @Produces(TEXT_PLAIN)
    public String hello(@DefaultValue("world") @QueryParam("who") final String who) {
        return "hello " + who;
    }
}

Any standard JAX-RS entity provider can be used directly. For example the following code works as expected if a JSON media type provider (such as Jersey’s jersey-media-json-jackson) is included as a dependency:

@POST
@Path("hello")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public Map<String, String> hello(final Map<String, String> salutation) {
    return singletonMap("hello", salutation.getOrDefault("who", "world"));
}

Asynchronous processing with CompletionStage and the older model based on AsyncResponse are both supported. Here is an example with CompletionStage:

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Path("hello")
@POST
public CompletionStage<Map<String, String>> hello(final Map<String, String> salutation) {
    return completedFuture(singletonMap("hello", salutation.getOrDefault("who", "world")));
}
Server-Sent Events are also fully supported.

ServiceTalk Aware Resources

In addition to standard JAX-RS features, the Jersey router also allows users to take full advantage of ServiceTalk asynchronous primitives.

The router supports a few ServiceTalk-specific optional features for users who want to use Reactive Streams concepts in their resources or want to use ServiceTalk types like Buffer.

Standard Jersey supports byte[] request/response entities whereas ServiceTalk commonly uses a higher level abstraction called Buffer. The router lets you use Buffer in your resources. Doing so has the main advantage of completely bypassing the Input/OutputStream Adaptation layer used internally between ServiceTalk streams and the blocking Input/OutputStream that are used pervasively in Jersey.

To be more specific, ServiceTalk allows consuming and producing the following entities (in addition to standard JAX-RS entities):

  • Buffer — the aggregated request or response body,

  • Single<Buffer> — the aggregated request or response body as an async source,

  • Publisher<Buffer> — the request or response body as an async stream of `Buffer`s.

Using Buffer and async payloads will prevent off-the-shelf payload-aware Filters and Interceptors from working properly. Moreover, such payloads can not currently be used with Server-Sent Events (SSE).

Let’s look at some examples. The first one is a rewrite of the previous hello example but, this time, returning Buffer instead of String:

@GET
@Path("hello")
@Produces(TEXT_PLAIN)
public Buffer hello(@DefaultValue("world") @QueryParam("who") final String who) {
    return DEFAULT_ALLOCATOR.fromUtf8("hello " + who);
}

Real benefits come when using async sources in and out, as shown here:

@POST
@Path("hello")
@Consumes(TEXT_PLAIN)
@Produces(TEXT_PLAIN)
public Single<Buffer> hello(final Single<Buffer> who,
                            @Context final ConnectionContext ctx) {
    final BufferAllocator allocator = ctx.executionContext().bufferAllocator();
    return who.map(b -> allocator.newCompositeBuffer()
            .addBuffer(allocator.fromAscii("hello, "))
            .addBuffer(b)
            .addBuffer(allocator.fromAscii("!")));
}

The advantage of this approach is that the resource method only takes care of assembling the response as an execution chain: ServiceTalk is fully in charge of executing it. This enables advanced features like executing on the I/O executor for non-blocking scenarios (discussed later in the Execution Strategies section).

Notice how the example above receives a ConnectionContext instance via a @Context-annotated method parameter. This is one of the two ServiceTalk-specific context objects that you can receive via @Context injection (on top of all the standard JAX-RS ones like HttpHeaders, UriInfo or SecurityContext):

  • ConnectionContext — gives access to connection meta information, executors and allocator,

  • StreamingHttpRequest — the ServiceTalk-specific representation of the in-flight HTTP request.

The @Context-provided objects are only available to the same thread that has called the resource method. This is why the allocator is captured in a variable for later use in the async execution chain in the above example.
ServiceTalk does not have a compatibility layer for Servlet, thus objects like ServletConfig or HttpServletRequest are not available via @Context injection.

Assembling complete responses in a single Buffer is not practical for large payloads. This is when streaming the response with a Publisher comes handy, as shown in the following examples that produces a streaming response of `Buffer`s:

@GET
@Path("hello")
@Produces(TEXT_PLAIN)
public Response hello(@DefaultValue("world") @QueryParam("who") final String who,
                      @Context final ConnectionContext ctx) {
    final BufferAllocator allocator = ctx.executionContext().bufferAllocator();
    return Response.accepted(
            // Wrap content Publisher to capture its generic type (i.e. Buffer)
            // so it is handled correctly
            new GenericEntity<Publisher<Buffer>>(
                Publisher.from(allocator.fromUtf8("hello "),
                               allocator.fromUtf8(who))) {}
    ).build();
}

Notice that in this example how the standard Response and GenericEntity helpers can be used as with any vanilla JAX-RS resource.

CompletionStage Alternative

It is also possible to use ServiceTalk’s primitives in lieu of CompletionStage, allowing users to use consistent semantics and behavior across their async code. As an example, the following illustrates how Completable can be used in place of CompletionStage<Void>:

@POST
@Path("start")
public Completable start(@QueryParam("id") final String id) {
    // Do something with id
    return Completable.completed();
}

Similarly, this example shows that Single<String> can replace CompletionStage<String>:

@GET
@Path("hello")
@Produces(TEXT_PLAIN)
public Single<String> hello(@DefaultValue("world") @QueryParam("who") final String who) {
    return Single.succeeded("hello " + who);
}

ServiceTalk JSON Provider

ServiceTalk provides a JSON Provider (servicetalk-data-jackson-jersey) that can be used as a drop-in replacement for Jersey’s jersey-media-json-jackson. This provider is based on Jackson’s non-blocking JSON parser and completely bypasses the blocking Input/OutputStream Adaptation layer that’s otherwise used with standard JAX-RS media-type providers. This can yield performance benefits when dealing with large body entities and is necessary for providing fully non-blocking routes.

The following example shows what this provider enables:

@POST
@Path("single-hello")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public Single<Map<String, String>> singleHello(final Single<Map<String, String>> salutation) {
    return salutation.map(m -> singletonMap("single hello", m.getOrDefault("who", "world")));
}

Jackson-serializable POJOs could be used in place of the Maps used in this example.

ServiceTalk’s JSON provider doesn’t support JAXB annotations nor JSONP. Use Jersey’s jersey-media-json-jackson in case you need these features

The Jackson ObjectMapper used behind the scene can be configured via a JAX-RS ContextResolver of type ContextResolver<JacksonSerializationProvider> that needs to be provided with the application. The ServiceTalkJacksonSerializerFeature class has helper methods for building such ContextResolver instances.

Because Jackson is used behind the scene for serialization and deserialization, it is possible to use its annotations (for example @JsonProperty).

Filters and Interceptors

Standard JAX-RS filters and interceptors can be used with ServiceTalk.

JAX-RS request filters and interceptors can only access the entity as an InputStream, which can be suboptimal if the intention is to avoid blocking I/O. If that is the case, consider using a ServiceTalk filter on front of the Jersey router.

Conversely, response entities are accessible to JAX-RS filters and interceptors, so it is possible to write filters that alter contents in a non-blocking fashion, as demonstrated here:

@Override
public void filter(final ContainerRequestContext requestCtx, final ContainerResponseContext responseCtx) {
    final Publisher<Buffer> modifiedContent =
            ((Publisher<Buffer>) responseCtx.getEntity()).map(b -> modifyBuffer(b));
    responseCtx.setEntity(new GenericEntity<Publisher<Buffer>>(modifiedContent) {});
}

In this example, it is assumed that the response entity is a Publisher<Buffer>: the Buffer it emits are altered via calls to the modifyBuffer function (omitted for brevity).

Security

By default, the Jersey router establishes an unauthenticated security context for all requests. Standard JAX-RS filters can be used to override this and set authenticated security contexts where appropriate. The following is an example of such filter, which could be used either globally, per resource class or method, using standard JAX-RS mapping techniques:

@Provider
@Priority(AUTHENTICATION)
public static class CustomSecurityFilter implements ContainerRequestFilter {
    @Override
    public void filter(final ContainerRequestContext requestCtx) {
        requestCtx.setSecurityContext(new CustomSecurityContext(requestCtx));
    }
}
ServiceTalk provides security filters that can be used with Basic authenticated requests. Refer to Basic Auth for Jersey Router for more information.

Exception Mappers

Standard JAX-RS exception mappers can be used with ServiceTalk. On top of this, it is possible to use ServiceTalk-specific response entities as error payloads, as shown here:

public static class ServiceTalkAwareExceptionMapper implements ExceptionMapper<Throwable> {
    @Context
    private ConnectionContext ctx;

    @Override
    public Response toResponse(final Throwable t) {
        final Buffer buf = ctx.executionContext().bufferAllocator().fromAscii(exception.getClass().getName());
        return status(555)
                .header(CONTENT_TYPE, TEXT_PLAIN)
                .header(CONTENT_LENGTH, buf.readableBytes())
                .entity(new GenericEntity<Single<Buffer>>(success(buf)) {})
                .build();
    }
}

Injection Management

The Jersey router doesn’t transitively require a particular Jersey Injection Manager dependency, it is up to the user to pick one of the available implementations by adding the relevant dependency to the application classpath.

Jersey provides two implementations:

  • jersey-hk2 — based on HK2, this is the most likely implementation that will be used with ServiceTalk,

  • jersey-inject-cdi2-se - relying upon CDI, this is to be used when running ServiceTalk in a Java EE application container.

ServiceTalk Features

The ServiceTalk JAX-RS Feature that enables the router functionalities is automatically registered with Jersey using its auto-discoverable features.

If this router is used in a context where Jersey’s auto-discovery has been disabled, users must manually register io.servicetalk.http.router.jersey.ServiceTalkFeature with the JAX-RS FeatureContext. Note that if the ServiceTalk JSON provider is used, its feature would have to be registered too: io.servicetalk.data.jackson.jersey.ServiceTalkJacksonSerializerFeature

Advanced Features

Service Composability

HttpJerseyRouterBuilder builds a standard ServiceTalk HTTP service so it can be composed with any other ServiceTalk services or filters.

The following example illustrates this be showing how a service built with HttpJerseyRouterBuilder can be seamlessly used alongside another regular ServiceTalk HTTP service, inside a single predicate based router:

ServerContext serverContext = HttpServers.forPort(8080)
        .listenStreamingAndAwait(
                new HttpPredicateRouterBuilder()
                        .whenPathStartsWith("/healthcheck")
                        .thenRouteTo(healthService)
                        .whenPathStartsWith("/api")
                        .thenRouteTo(new HttpJerseyRouterBuilder().build(jaxRsApplication))
                        .buildStreaming()
        );

Execution Strategies

It is important to have a good understanding of ServiceTalk’s threading model before considering tuning execution strategies. Refer to ServiceTalk’s main documentation to learn more about it.

By default, the Jersey router uses ServiceTalk’s global executor to handles requests, making it safe by default to use blocking code, either directly in user code or indirectly in third-party libraries and intermediaries like filters, interceptors and media-type providers.

It is possible to use a specific executor for all requests handled by the Jersey router, as demonstrated here:

HttpServers.forPort(8080)
    .executor(executor)
    .listenStreamingAndAwait(new HttpJerseyRouterBuilder().build(jaxrsApplication))
    .awaitShutdown();

where executor is the executor to use at the router level.

It is also possible to configure execution strategies in a finer grained manner, either at resource class or resource method level, using the @RouteExecutionStrategy annotation. For example, the following applies the execution strategy exec-1 to a specific resource method:

@RouteExecutionStrategy(id = "exec-1")
@POST
@Path("/do-work")
public void doWork() {
    // Work happens here
}

Now the question is where does the Jersey router find its execution strategies? The answer is in this HttpJerseyRouterBuilder method:

routeExecutionStrategyFactory(Function<String, HttpExecutionStrategy> routeStrategyFactory)

This allows you to provide a lambda that the router will use to resolve execution strategy IDs used in @RouteExecutionStrategy annotations (map::get can conveniently be used if you store your strategy mappings in a Map).

The router ensures it can resolve all execution strategy IDs at startup time.

By default ServiceTalk is "safe to block", which means that it takes care of ensuring that the application code doesn’t execute on the I/O threads (which shouldn’t be blocked). This applies to the Jersey router too: resources, filters, interceptors can perform blocking operations, like for example interacting with java.io streams, in a safe manner. This safety has a cost though: different executors are used at different levels, thread hops may occur while a request is in flight, etc… ServiceTalk gives advanced users the possibility to bypass this safety net and execute application code fully or partially on the I/O executor.

Before exploring this in details, let’s take a look at an example that completely runs on I/O threads. The following snippet shows the server bootstrap code and followed by one JAX-RS resource method:

HttpServers.forPort(8080)
    .executionStrategy(HttpExecutionStrategies.offloadNever())
    .listenStreamingAndAwait(new HttpJerseyRouterBuilder().build(jaxrsApplication))
    .awaitShutdown();

@NoOffloadsRouteExecutionStrategy
@Path("greetings")
public class HelloWorldJaxRsResource {
    @GET
    @Path("hello")
    @Produces(TEXT_PLAIN)
    public String hello(@DefaultValue("world") @QueryParam("who") final String who) {
        return "hello " + who;
    }
}

Notice how HttpExecutionStrategies.offloadNever() and @NoOffloadsRouteExecutionStrategy are used conjointly to ensure that offloading will be completely disabled and that the requests will be fully handled on I/O threads.

Disabling offloading should only be done when it is certain that no blocking code will be invoked. Request handling in Jersey follows a complicated and dynamic path, so unexpected blocking can occur in non-user code. Be sure to thoroughly test the routes for which you intend to disable offloading.
It is currently not possible to disable offloading if any JAX-RS resource uses @Suspended AsyncResponse, CompletionStage responses or Server-Sent Events. A workaround for this consists in using the Predicate router to selectively offload such resources, as shown in this test.

These different options combined together yield different effects at different level of the application code. The following table details what to expect when using them.

Route
Default
Route
Executor
Route
No Offloads

Router
Default

M: Global
C: Global
S: Global

M: Route
C: Route
S: Route

M: Route
C: Route
S: Global

Router
Executor

M: Router
C: Router
S: Router

M: Route
C: Route
S: Route

M: Route
C: Route
S: Global

Router
No Offloads

M: Global
C: Global
S: Global

M: Route
C: Route
S: Route

M: Server I/O
C: Server I/O
S: Server I/O

Where:

  • Route means either a resource class or resource method (where both @RouteExecutionStrategy and @NoOffloadsRouteExecutionStrategy can be used),

  • Router is the Jersey router,

  • Global refers to ServiceTalk’s GlobalExecutionContext (which provides among other things global I/O and standard executors),

  • Server I/O refers to the I/O executor configured on the HTTP server (which is the global I/O executor by default),

  • M stands for the executor used to call the resource Method,

  • C for the one provided via @Context ConnectionContext,

  • S refers to the executor used for stream events.

Users must exercise caution if they decide to execute on the I/O threads and must make sure they understand the caveats of doing so. Resources that only serve responses from memory or that interact with remote services via ServiceTalk are good potential candidates.

Be aware that route level execution strategy are applied after the JAX-RS filter chain has executed. If any of these JAX-RS filters perform blocking operations (for example blocking I/O) you must make sure that the Jersey router itself is not configured to use I/O threads, thus limiting the potential to run on I/O threads only to stream events (Router Default/Executor and Route No Offloads in the above table).

Implementation

This section details the design and implementation decisions taken for the Jersey router. It is intended for contributors and advanced users interested to know what’s under the hood.

The following diagram gives a lay of the land for request/response flows in the router, with a short description of the salient operations performed by ServiceTalk at the different spots in the flow.

st2 jersey flow

The upcoming sections expand more on some of the concepts depicted in this diagram.

As Jersey’s and ServiceTalk’s Jersey router codebases evolve, we expect some of the drawbacks listed hereafter to progressively disappear.

Input/OutputStream Adaptation

JAX-RS 2.1 depends heavily on java.io.Input/OutputStream for consuming request and producing response entity contents. The Jersey router uses ServiceTalk’s provided adapters to convert between its non-blocking sources and these blocking streams. This adaptation is what allows using all the Jersey-provided and third party media-type providers to work out of the box.

For request payloads, the Jersey router uses a special InputStream (namely BufferPublisherInputStream), which allows accessing the underlying Publisher<Buffer> for scenarios when the InputStream needs not be accessed.

For response payloads, the implementation is slightly more involved but in essence provides the same bypass capacity when OutputStream-writing isn’t required (users can look at `DefaultContainerResponseWriter’s source code for more information).

At this writing, all byte arrays written via the java.io.OutputStream adaptation layer have to be copied because of the way Jersey internally reuses buffers.

This adaptation is automatically bypassed when a resource method consumes or produces an entity type that can be directly handled by ServiceTalk, like for example Single<Buffer>.

There are subtle edge cases where filters or interceptors can affect this adaptation mechanism by replacing the entity body created by the router at the start of the request handling chain. Mitigation is in place to circumvent these issues: readers are invited to turn to the JavaDoc of io.servicetalk.http.router.jersey.internal.SourceWrappers for the gory details.

Endpoint Swapping

While most features of the Jersey router are implemented via JAX-RS constructs, some advanced features, like the ones listed here, needed to be wired deep in the insides of the Jersey handling chain:

  • Single and Completable response entity types (as alternative to CompletionStage),

  • per-route execution strategies.

This "deep wiring" is done by replacing the Endpoint that Jersey uses to invoke user code right at the end of its internal request process chain, with a custom Endpoint that wraps the original and intercepts the execution when one of the two above scenarios is in use.

This interception is achieved by using the suspend/resume mechanism that is normally used for JAX-RS async responses, which is why AsyncResponse and Server-Sent Events can’t be used with these advanced features (it’s also why CompletionStage can’t be used with per-route execution strategies).

The replacement of the original Endpoint is done via a JAX-RS filter whose priority is the lowest possible (Integer.MAX_VALUE because the lower the number, the higher priority) so it executes at the end of the filter chain.

A corollary of this approach is that this mechanism could fail in case other "lowest possible" filters are in use.

For response filters to work properly, the original Endpoint has to be swapped back into placed after the resource method has been invoked. This is because, although the router replacement endpoint implements Jersey’s Endpoint and ResourceInfo interface, Jersey’s infrastructure perform instance checks against ResourceMethodInvoker (its own Endpoint implementation) in the logic that applies response filters. This unfortunately leaves no option other than restoring the original ResourceMethodInvoker after ServiceTalk’s endpoint has served its purpose.