Dependency Injection with Vert.x

One of my favorite things about Vert.x is its flexibility. The Vert.x toolkit is not an opinionated framework. It is designed to allow developers freedom to connect components in ways that make sense for their applications.

Dependency Injection

Dependency Injection is a technique used widely in applications built on the JVM. If you search for how to use dependency injection with Vert.x you are going to see a number of different approaches. I would like to share the approach that has worked well for both small and large applications I have developed using Vert.x.

Vert.x and Guice

In this example I am going to use Google Guice but you could use your dependency injection library of choice. The technique I am going to show here can work with Dagger, Dagger 2, Spring, HK2 or any other DI framework.

Dependency Injection is a significant topic by itself that I am not going to cover in this article but may in the future. Let me know if you would like to see an article in the future about this on Twitter.

The first thing we need to do is create our Guice modules that will provide our components. We will need two modules. The first is a configuration module that exposes the environment variables we need to the container - this allows us to request configuration using the @Named annotation. The second module provides an http server and a router with a root endpoint handler.

internal class ConfigModule : AbstractModule() {
    override fun configure() {
        val properties = Properties()
        properties.setProperty("HOST", System.getenv("HOST"))
        properties.setProperty("PORT", System.getenv("PORT"))
        Names.bindProperties(binder(), properties)
    }
}

internal class RouterModule(private val vertx: Vertx) : PrivateModule() {
    override fun configure() { }

    @Provides
    @Singleton
    @Exposed
    fun httpServer(): HttpServer {
        val options = HttpServerOptions()
        options.isCompressionSupported = true
        return vertx.createHttpServer(options)
    }

    @Provides
    @Singleton
    @Exposed
    fun router(): Router {
        val router = Router.router(vertx)

        router.route("/").handler { ctx ->
            LoggerFactory.getLogger("Router").debug("Received request on /")
            ctx.response()
                .setStatusCode(200)
                .end("Dependency Injection with Vert.x")
        }

        return router
    }
}

Our verticle will need an http server, router, and host and port to listen on. These will be provided by Guice.

class HttpVerticle @Inject constructor(private val httpServer: HttpServer,
                                       private val router: Router,
                                       @Named("HOST") private val host: String,
                                       @Named("PORT") private val port: Int) : CoroutineVerticle() {

    @Throws(Exception::class)
    override suspend fun start() {
        awaitResult<HttpServer> {
            httpServer
                .requestHandler(router)
                .listen(port, host, it)

            LoggerFactory.getLogger(HttpVerticle::class.java).info("Server listening on $host:$port")
        }
    }
}

Launching a Verticle

With our modules in place we can now create an instance of Guice and ask the container for an instance of our verticle. We will then ask Vertx to deploy it.

fun main(args: Array<String>) {

    val vertx = Vertx.vertx(options())

    val injector = Guice.createInjector(Stage.PRODUCTION,
                                        ConfigModule(),
                                        RouterModule(vertx))

    val httpVerticle = injector.getInstance(HttpVerticle::class.java)

    vertx.deployVerticle(httpVerticle)
}

On startup if everything has been wired up correctly you should see the server listening and if you go to localhost:8080 in your browser you should get a response.

Jul 03, 2019 9:05:17 AM com.zsiegel.api.HttpVerticle
INFO: Server listening on 0.0.0.0:8080

Vert.x Verticle Factory

In the example above we deployed a verticle with Guice by first creating the instance and asking Vertx to deploy it. We can improve this by hooking into the Vertx lifecycle so it can deploy all verticles within our dependency injection container on demand. In order to achieve this we will create our own VerticleFactory.

class GuiceVerticleFactory(private val injector: Injector) : VerticleFactory {

    companion object {
        private const val prefix: String = "com.zsiegel"
    }

    override fun prefix(): String = prefix

    override fun createVerticle(verticleName: String, classLoader: ClassLoader): Verticle {
        val verticle = VerticleFactory.removePrefix(verticleName)
        return injector.getInstance(classLoader.loadClass(verticle)) as Verticle
    }
}

The VerticleFactory is responsible for creating verticles. We will register this factory with Vertx and anytime a call to deployVerticle is made it will go through this factory.

Using the VerticleFactory

We can now refactor the previous example and delegate the work of creating verticles to the factory. Whenever a new verticle is deployed using this instance of Vertx they will be routed through the custom factory and use the Google Guice container.

fun main(args: Array<String>) {


    val vertx = Vertx.vertx(options())

    val injector = Guice.createInjector(Stage.PRODUCTION,
                                        ConfigModule(),
                                        RouterModule(vertx))

    val factory = GuiceVerticleFactory(injector)
    
    vertx.registerVerticleFactory(factory)
    vertx.deployVerticle("${factory.prefix()}:${HttpVerticle::class.java.name}")
}
Jul 03, 2019 9:11:21 AM com.zsiegel.api.HttpVerticle
INFO: Server listening on 0.0.0.0:8080

Using the VerticleFactory is a great way to simplify dependency injection with Vert.x. It allows you to launch any number of verticles without needing to manage or pass around the dependency injection container.

A sample gradle project with the full code above can be found here