Restlet and EE?
Restlet is now a quite "old" (in the good way) but a proven REST oriented framework. A bit like Resin server at its time, it kind of leads the web development in java providing before specifications and most of servers NIO and an advanced REST oriented API etc...
Let's see if EE filled the gap Restlet was surfing on at the beginning, and how to integrate both today.
If you dig in Restlet API you will quickly identify it is very trendy because you modelize an application then you add components (like a router) and resources (REST semantic). To deploy, you can use a servlet container where you will just define a servlet referencing your application. Then, in standalone you will create a Component with a server (more or less how to bind/open the port) and you will deploy (attach) your application on this Component that makes the link between your application, its exposure (server) and potentially its security (realm). Last note on this: there is of course a shortcut class to do it in one line called Server ;).
This looks very hype as it is really the way "microservices" servers tend to design their API - TomEE embedded, Meecrowave, Swarm .... The development of the resources themselves is pretty close to JAX-RS (but with a restlet design) so API is very accessible to java developpers.
Until there, you probably think that Spring or JavaEE does the same job and are more "mainstream". This is almost true so why should we use Restlet? In fact, it goes further on some areas:
- client API (almost) shares the server API (everyone wants an interface based on API in JAX-RS client API, as known as "proxy client API")
- routing is more advanced than JAX-RS or Servlet ones (did you already hit the list/detail mapping issue with JAX-RS? /users vs /user/{id} in the same resource for instance)
- some automatic services are available (range one is interesting coming from standard web applications for instance)
- reverse proxying is managed out of the box (a bit like tomcat rewrites implementation)
- it has a javascript API aligned on java (fullstack developpers will enjoy working in this environment)
- swagger integration
- security advanced features
- a consistent web oriented API (no need to mix Servlet, JAX-RS, OAuth2-libX (etc) API to build part of your application), all is design in Restlet model and integrates smoothly since it is a full stack
- etc ...
To be a bit fair with EE (same points would apply to spring by the way) here are few drawbacks:
- API itself looks old (probably because it needs to run on a bunch of environments): inheritance which is not really needed today, lots of shortcuts making the API not always intuitive (media type handling for instance). To be fair, this last point will not make java developpers (scripting guys) enjoy it too, so it may be a strategical choice.
- No full container integration out of the box (there is a spring integration but not sure it is that friendly and aligned with spring boot now)
- IoC is missing out of the box
- No scanning. Thus, a manual declaration is needed.
Restlet (the company) provides a full ecosystem around the framework (like a studio to design your resources, a test framework, and even a deployment solution) so if you don't only look for a web framework, it's probably worth it. In case you just need a web framework and you are not a java developper, it can be a good choice too. If you are a java developper you can be a bit disappointed but advanced features can still make you happy if you need them!
From now on, let's assume you are in a servlet container.
Auto registration
The first issue you will probably hit as a java developper is the lack of scanning. To solve it, an easy solution is to write a ServletContainerInitializer - if you run in standalone, the tip would be to use CDI directly or even xbean-finder. This initializer will be responsible for finding the classes we want to deploy and create an application. Concretely it will try to find an Application child, and if so, deploy it on the restlet ServerServlet. If not, it will create one from the set of ServerResource found, and create an application with a router delegating to these resources. The only issue in this last case is how to know the url to map when registering the resource? To make it simple, we'll define a @Mapping annotation holding the mapping:
// don't forget the resources/META-INF/services/javax.servlet.ServletContainerInitializer
// with the fully qualified name of this class in it
@HandlesTypes({Application.class, ServerResource.class})
public class RestletInitializer implements ServletContainerInitializer {
@Override
public void onStartup(final Set<Class<?>> classes, final ServletContext ctx) throws ServletException {
if (classes == null || classes.isEmpty()) {
return;
}
final Optional<Class<?>> app = classes.stream()
.filter(c -> Application.class.isAssignableFrom(c) && Modifier.isPublic(c.getModifiers()))
.findAny();
if (app.isPresent()) {
final ServletRegistration.Dynamic restlet = ctx.addServlet("restlet", ServerServlet.class);
ctx.setInitParameter("org.restlet.application", app.get().getName());
restlet.setLoadOnStartup(1);
restlet.addMapping("/*");
} else {
final ServletRegistration.Dynamic restlet = ctx.addServlet("restlet", new ServerServlet() {
@Override
protected Application createApplication(final Context parentContext) {
return new Application(parentContext.createChildContext()) {
@Override
public Restlet createInboundRoot() {
final Router router = new Router(getContext());
classes.stream().filter(c -> ServerResource.class.isAssignableFrom(c) && c.isAnnotationPresent(Mapping.class))
.forEach(c -> router.attach(c.getAnnotation(Mapping.class).value(), Class.class.cast(c)));
return router;
}
};
}
});
restlet.setLoadOnStartup(1);
restlet.addMapping("/*");
}
}
@Retention(RUNTIME)
@Target(TYPE)
public @interface Mapping {
String value();
}
}
With this initializer you can just write in your application this resource and it will automatically be deployed in restlet:
import org.restlet.resource.Get;
import org.restlet.resource.ServerResource;
@Mapping("/hello")
public class AResource extends ServerResource {
@Get
public String hello() {
return "hello";
}
}
CDI integration
Then you will start implementing the service and you will almost immediately miss a way to do injections.
Restlet uses Finder API to instantiate resources. By default it is done for each request (kind of prototype/@Dependent scope). But with CDI or spring this is not the way you control the scope so you shouldn't care and just declare the scope in your bean:
@Mapping("/hello")
@SessionScoped
public class AResource extends ServerResource {
@Get
public String hello() {
return "hello";
}
}
To do so, we will implement a CdiFinder which is just a CDI factory for restlet. The implementation is straight forward: when the finder is created - which corresponds to the moment the resource is attached to the router - it gets the resource class. We will lookup the associated bean at that moment from the BeanManager. Then, when restlet will request an instance we will lookup a new instance in CDI context instead of just doing a new.
Small optimization: for normal scoped beans we'll cache the instance when the finder is created. This is possible because normal scoped instances are proxies and instantiation is managed by CDI behind the proxies.
The last trick is to release the CreationalContext used for unnormal scoped proxies. This is needed to not leak any memory and correctly call@PreDestroy methods of unnormal scoped instances of the resource object graph. A simple solution is to proxy the resource delegating all method and calling the release one when doRelease() of the resource is called - restlet handles it for us.
Here is a potentially implementations using javassist for the proxying to keep it simple (a real implementation would probably directly use asm library):
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.resource.Finder;
import org.restlet.resource.ServerResource;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.Vetoed;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import java.util.function.Supplier;
@Vetoed
public class CdiFinder extends Finder {
private final BeanManager beanManager;
private final Supplier<ServerResource> provider;
public CdiFinder(final Context context, final Class<? extends ServerResource> targetClass) {
super(context, targetClass);
beanManager = CDI.current().getBeanManager();
final Bean<?> bean = beanManager.resolve(beanManager.getBeans(targetClass));
if (bean == null) {
provider = null;
return;
}
if (beanManager.isNormalScope(bean.getScope())) {
final ServerResource proxy = ServerResource.class.cast(beanManager.getReference(bean, ServerResource.class, beanManager.createCreationalContext(null)));
provider = () -> proxy;
} else {
final ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(targetClass);
final Class<?> proxyClass = factory.createClass();
provider = () -> {
final CreationalContext<? extends ServerResource> cc = beanManager.createCreationalContext(null);
final ServerResource instance = ServerResource.class.cast(beanManager.getReference(bean, ServerResource.class, cc));
try {
final ServerResource resource = ServerResource.class.cast(proxyClass.newInstance());
Proxy.class.cast(resource).setHandler((o, method, method1, objects) -> {
if (!method.isAccessible()) {
method.setAccessible(true);
}
try {
return method.invoke(instance, objects);
} finally {
if ("doRelease".equals(method.getName())) {
cc.release();
}
}
});
return resource;
} catch (final InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
};
}
}
@Override
public ServerResource create(final Class<? extends ServerResource> targetClass, final Request request, final Response response) {
return provider != null ? provider.get() : super.create(targetClass, request, response);
}
}
Then to activate it, we set this finder class on the router we create in our initializer:
@Override
public Restlet createInboundRoot() {
final Router router = new Router(getContext());
router.setFinderClass(CdiFinder.class); // the magic will happen
classes.stream()
// ...;
return router;
}
With such a setup our resource can now be:
@Mapping("/hello")
@SessionScoped
public class AResource extends ServerResource {
@Inject
private UserService users;
@Get
public User findByName() {
return users.findById(getAttribute("id"));
}
}
Going further
This implementation has a big pitfall: it only works for thread safe resources (@Dependent, @RequestScoped, @SessionScoped, ...). It already provides the IoC integration which is the most important thing we wanted to achieve but not generic enough to be suggested as a stack.
To go further, we need to enable users to use @ApplicationScoped or other scopes.
The issue is that the ServerResource holds the request/response so it needs to be bound to a single serving thread.
An easy way to achieve it is:
- to proxy all resources,
- to capture, by thread, the call to init()/release() on the resource
- and to propagate it accordingly ensuring that one single instance serves one single request at a time.
Technically it is quite hard. In fact, for @ApplicationScoped and @Singleton, we know that we need to serialize all requests (which would kill performances but ensure it behaves as expected). But for custom scopes we don't know if the normal scope is longer or shorter than a request or thread safe (thinking about the session which is generally thread safe but way longer than a request).
I'm not going further more on the CdiFinder implementation because it would be risky with a poor gain. So the constraint is to ensure the scope of the resources are controlled. However, if you need some application scoped instance you can still delegate to another bean which is not a resource, so no real blocker.
An alternative implementation is to simply inject in the resource instances and not consider the resource itself as a bean. This is easily feasible with Unmanaged helper of CDI but then you lose the optimization of the previous implementation which will make the runtime faster for @RequestScoped usages.
Conclusion
Even if JavaEE reduced a lot of gaps between its own API and Restlet, some features and usages can still just justify to bet on Restlet. The built-in API doesn't embrace EE but with few glue code (less than 150 lines for the snippets of this post) you can enable it and benefit from the whole power of both worlds.
From the same author:
In the same category: