In this article I’d like to explain two things. First, what to do when you want to restrict JAX-RS providers to be used on, for example, client-side only. And second, what are the issues (and how to solve them) with injecting providers when you create and register instances of them directly.
Constraining JAX-RS providers to particular runtime
Some of the JAX-RS providers can be used on the server-side as well as on the client-side. The reusability of providers was among the goals when JAX-RS 2.0 Client API was proposed and designed. Unquestionably this being a useful feature of JAX-RS 2.0 there are cases in which you want to constrain some of the available providers to either client or server. Constraining providers, or Features for that matter, is even more useful when writing a general purpose library that aims to support both sides but each in a slightly different manner.
Creators of JAX-RS 2.0 thought about needs like this and came up with two possible solutions:
Declarative – @ConstrainedTo
@ConstrainedTo annotation can be placed on any JAX-RS provider type (be it interceptor, filter, message body provider or feature) and in Jersey you can place it on your custom providers annotated with @Contract and registered within your application too. Annotated provider is restricted to be picked-up and used only in a specified run-time context, defined by RuntimeType enum value in the annotation.
For example, the Server-Sent Events in Jersey are supported on both, client and server. All you need to do is registering SseFeature and you’re good to go.
Usually, SSE in Jersey works like this: You create an OutboundEvent in your server application and broadcast the message to all connected clients. On the client you receive an InboundEvent and read the data. As you may guess, the SSE extension module contains message body writer used on the server and message body reader for the client. So, to make sure the reader is not accidentally registered on server we annotate the provider as follows and that’s it:
@ConstrainedTo(RuntimeType.CLIENT)
class InboundEventReader implements MessageBodyReader<InboundEvent> {
// ...
}
Programmatic – RuntimeType
Registering JAX-RS providers for a specified run-time programmatically is mainly used in Features. Basically you obtain the current RuntimeType from Configuration and act upon it. In case of the mentioned SseFeature the configure method looks like:
public class SseFeature implements Feature {
public boolean configure(final FeatureContext context) {
if (context.getConfiguration().isEnabled(this.getClass())) {
return false;
}
switch (context.getConfiguration().getRuntimeType()) {
case CLIENT:
context.register(EventInputReader.class);
context.register(InboundEventReader.class);
break;
case SERVER:
context.register(OutboundEventWriter.class);
break;
}
return true;
}
}
The code above does the same thing as placing @ConstrainedTo annotation on every provider listed in the switch block.
However, you’re able to determine the current run-time everywhere where you have access to Configuration. Imagine your provider behaves differently on client than it does on server. Also let’s assume you decide this difference is not that a big of an issue that would force you to create separate providers for both run-times. In this case you can take advantage of RuntimeType (and during runtime), e.g.:
class MyWriterInterceptor implements WriterInterceptor {
@Override
public void aroundWriteTo(final WriterInterceptorContext context) throws IOException {
// do something
if (RuntimeType.SERVER
== context.getConfiguration().getRuntimeType) {
// do something more
}
context.proceed();
}
}
How injection works for JAX-RS provider instances in Jersey
On server, you don’t need to do anything special to properly inject instances and classes of JAX-RS providers and resources (and basically everything managed by HK2 or CDI). Injection works as you would expect. There is no problem to inject anything because of the fact that there is only one server run-time (and only one ServiceLocator) for each application.
On client, you can still inject classes of JAX-RS providers without noticing a difference. However, the situation with provider instances is a little bit different.
Imagine you have a client request filter that you want to initialize with some data and inject it with an instance of MyInjectedService:
class MyLoggingFilter implements ClientRequestFilter {
private static final Logger LOGGER = Logger.getLogger(MyLoggingFilter.class.getName());
private final String loggingPrefix;
@Inject
private MyInjectedService service;
public MyLoggingFilter(final String prefix) {
this.loggingPrefix = prefix;
}
public void filter(final ClientRequestContext context) throws IOException {
final String name = service.getName();
LOGGER.info(prefix + name);
// ...
}
}
Now, you create a JAX-RS client, register your provider with the client and create two web targets:
final Client client = ClientBuilder.newClient()
.register(new MyLoggingFilter("JAX-RS Client #" + (i++) + ":"));
final WebTarget target1 = client.target("http://example.com");
final WebTarget target2 = client.target("http://example.com/json")
.register(JacksonFeature.class);
// Throws NPE.
target1.request().get();
// Throws NPE as well.
target2.request().get();
The second web target requires also JacksonFeature to be able to work with JSON. When you try to invoke a request from either one of these web targets the NullPointerException would be thrown. Why?
Well, service field in MyLoggingFilter would be null because the filter instance doesn’t get injected. The reason behind this is that you (possibly) have multiple client run-times which means you have multiple HK2’s _ServiceLocator_s. Jersey, of course, knows which one should be used but when you think about it we cannot inject the filter. Every web-target (client runtime) has reference to the same instance of MyLoggingFilter (it’s a singleton). In addition, provides ought to be thread-safe so you cannot inject the filter using one service locator and hope that the filter wouldn’t be, at the same time, used from another thread (from a different web-target). It just wouldn’t work ’cause you’d be using wrongly injected service.
To be fair, this would work in the case if Jersey injected a dynamic proxy into service field but we don’t do that. The reason – it’d be pretty slow.
There is another way to solve the injection introduced in Jersey 2.6. Use ServiceLocatorClientProvider to extract ServiceLocator from context in any JAX-RS provider and with locator you can obtain anything previously registered there. The following example shows how to utilize ServiceLocatorClientProvider in the rewritten version of MyLoggingFilter:
class MyLoggingFilter implements ClientRequestFilter {
private static final Logger LOGGER = Logger.getLogger(MyLoggingFilter.class.getName());
private final String loggingPrefix;
public MyLoggingFilter(final String prefix) {
this.loggingPrefix = prefix;
}
public void filter(final ClientRequestContext context) throws IOException {
// use ServiceLocatorClientProvider to extract HK2 ServiceLocator
// from request
ServiceLocator locator = ServiceLocatorClientProvider
.getServiceLocator(requestContext);
// and ask for MyInjectedService:
MyInjectedService service = locator.getService(MyInjectedService.class);
final String name = service.getName();
LOGGER.info(prefix + name);
// ...
}
}