A hurtless Play! router based on RoutingDSL
Play! router has got some nice enhancements with its RoutingDSL, becoming less static and opening new doors to new use cases. The two main usages I'm coming from to use the routing DSL are:
- tests
- and the ability to bind routes depending on the environment without maintaining a lot of configuration files, enabling me to interpolate the routes from some environment conditions and values.
When you start from the official documentation it looks quite easy to implement: you inject the dsl, implement a provider and return it. However, in practise it is not that smooth until you use some other extension point (probably a bug in play IoC since there is no real reason it needs more).
The workaround I chose was to use lombok - I'm coding in Java but scala can help as well - to generate the delegate part. So at the end I use the delegate pattern but implement and bind a Router. Here is a potential implementation:
@RequiredArgsConstructor(onConstructor_ = @Inject) // <1>
public class RouterImpl implements Router { // <2>
private final RoutingDsl routingDsl; // <3>
private final SimpleController controller; // <4>
@Delegate // <5>
@Getter(lazy = true) // <6>
private final Router delegate = get();
private Router get() { // <7>
return routingDsl.GET("/simple")
.routeTo(controller::test)
.build()
.asScala();
}
}
- Lombok will use that to generate a guice friendly constructor and initialize final fields,
- We directly implement Router instead of Provider<Router>,
- We inject the RoutingDsl enabling us to define the routes,
- We inject our controllers to be able to bind them into the routes as "handler",
- The lombok @Delegate annotation will ensure the Router API is implemented using delegate field,
- @Getter(lazy = true) is a nice hack to initialize the delegate field from get() private method after the constructor injection are done. You can replace it by a real constructor but make sure to keep @Delegate,
- Finally we implement the routes using RoutingDSL API. Note howe the controller is bound as handler.
If you don not use lombok, an alternative implementation will be:
@Singleton
public class AppRouterImpl implements Router {
private final Router delegate;
public AppRouterImpl(final RoutingDsl routingDsl,
final SimpleController controller) {
this.delegate = routingDsl.GET("/test/simple")
.routeTo(controller::test)
.build()
.asScala();
}
@Override
public PartialFunction<RequestHeader, Handler> routes() {
return delegate.routes();
}
@Override
public Seq<Tuple3<String, String, String>> documentation() {
return delegate.documentation();
}
@Override
public Router withPrefix(String s) {
return delegate.withPrefix(s);
}
@Override
public Option<Handler> handlerFor(RequestHeader requestHeader) {
return delegate.handlerFor(requestHeader);
}
@Override
public play.routing.Router asJava() {
return delegate.asJava();
}
}
This implementation is a bit more verbose but has one real advantage to not require to keep only the delegate router as reference in the router instead of all controllers and the routing dsl. With lombok it becomes even simpler:
?@Singleton
public class AppRouterImpl implements Router {
@Delegate
private final Router delegate;
public AppRouterImpl(final RoutingDsl routingDsl,
final SimpleController controller) {
this.delegate = routingDsl.GET("/test/simple")
.routeTo(controller::test)
.build()
.asScala();
}
}
Whether you pick this pattern or the first one, it enables you to bring back some hooks in the router of Play where you can put some custom logic and still use your IoC. With modern times where we do new things with old things (or recycling) you can easily use that to inject the Config into the constructor and generate routes from the configuration. It makes it very trivial to create a mediation layer even if Play will not be as powerful as a Camel deployed on top of a Servlet container. It will allow you to use your business code and combine that with akka to build a very customizable platform which is always interesting.
Finally, to make this work, you need to specify that this router must be looked up in the IoC setting play.http.router to the fully qualified name of the class. It can be done in system properties or in a reference.conf or play/reference-overrides.conf files.
Last note is that using this pattern in tests is really nice and allows you to very quickly test any Play! library without having to completely setup a test application structure.
From the same author:
In the same category: