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 Map
s 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 |
M: Global |
M: Route |
M: Route |
Router |
M: Router |
M: Route |
M: Route |
Router |
M: Global |
M: Route |
M: 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’sGlobalExecutionContext
(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 resourceMethod
, -
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.
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
andCompletable
response entity types (as alternative toCompletionStage
), -
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.