Vertx, Guice and Config Retriever: Dependency Injection in Vertx 4.x
Yuri Mednikov
Posted on June 23, 2021
In computer science, the dependency injection is defined as a pattern, whereby one component gets other components (dependencies) from outside. Numerous posts were written about various implementations of a dependency injection in Vertx using Google Guice library. All of them are good and I do not want to reinvent a bicycle here and to repeat same things again. However, in my opinion, it is a good idea to give a bit more systematic approach to this topic. In my experience, in most cases, for developers is not a big deal to implement a basic DI with Vertx, rather it seems hard to incorporate Vertx components into the DI pipeline. In this post we will review how to do a dependency injection in Vertx with Google Guice and how to to build a basic injection that has the ConfigRetriever component (Vertx's way to obtain an application configuration). Please note, that in this article we use Futures API, so it is focused on Vertx 4.x developers.
Basic DI with Google Guice
Google Guice is an established solution to implement a dependency injection technique in Java applications. Like most established libraries, it is quite simple to use, yet it does not mean that it is limited in any way. This is very flexible and powerful solution. Main building blocks of the framework are modules and injectors. The module is used to define how to get dependencies. The injector serves as a main entry point of an application and is used for an actual component initialization. Let have a quick example, that uses a constructor injection technique. Imagine, that you develop a verticle, that has two external dependencies - a client class (that performes HTTP calls) and a repository class (that does database operations). For sure, we will not do here their precise implementations, because it is out of the scope of the post. In order to specify these dependencies classes inside the verticle we need to use an @Inject annotation. If you are familiar with the Spring framework, you would find a process familiar. Basically, we need to create fields to keep references for components, define a constructor and annotate it with the @Inject, so Guice will know that we use a constructor injection.
Take a look on the following code snippet below:
class ProjectVerticle extends AbstractVerticle {
private ProjectClient client;
private ProjectRepository repository;
@Inject
ProjectVerticle(ProjectClient client, ProjectRepository repository){
this.client = client;
this.repository = repository;
}
// ...
Another step is to tell Guice how these classes are created. For that, we need to create a module. Keeping simple, this component define what is called bindings (again, if you are an experienced Spring developer, you call this wiring). Modules extend an AbstractModule class, provided by Guice, and override the configure() method, which is utilized for bindings. For our example, we will make it simple:
class ProjectVerticleModule extends AbstractModule {
@Override
protected void configure() {
bind(ProjectClient.class).to(ProjectClientImpl.class);
bind(ProjectRepository.class).to(ProjectRepositoryImpl.class);
}
}
You can note, that we bind types (interfaces) to instances. There are several types of bindings, that are available in Guice:
- Instance binding = we map a dependency to the specific instance; this is a good choice for simple dependencies, but for bigger it should be avoid, as it will slow down a start up time. We use it in the next section
- Linked binding = we map a dependency to its implementation, and Guice creates an instance for us; this is what we used here
- Provider binding = we provide an own implementation of the Provider interface, that is used to create dependencies; this is a case for complex dependencies; in this post we will omit it
Once we have created a module, we can use it in order to create components. For that we need to use an injector, which is defined as a class, that builds graphs of objects that make up our application. An injector takes one or many modules, and uses them to understand how actually to provide requested dependencies and to create an instance of the component. Take a look on the following example:
@Test
void injectTest(Vertx vertx, VertxTestContext context){
// create a module
ProjectVerticleModule module = new ProjectVerticleModule();
// create an injector
Injector injector = Guice.createInjector(module);
// get an instance of verticle
ProjectVerticle verticle = injector.getInstance(ProjectVerticle.class);
// deploy the ProjectVerticle
Future<String> deploy = vertx.deployVerticle(verticle);
deploy.onComplete(id -> {
context.verify(() -> {
String client = verticle.getProjectClientName();
String repository = verticle.getProjectRepositoryName();
Assertions.assertThat(client).isEqualTo("ProjectClientImpl");
Assertions.assertThat(repository).isEqualTo("ProjectRepositoryImpl");
context.completeNow();
});
}).onFailure(err -> context.failNow(err));
}
You can note, that we use an injector to initialize the ProjectVerticle component, instead of using the new keyword. In other words, we can say, that we "out source" the initialization process to Guice. We could verify that dependencies were created correctly, by validating their names. This is an easy example. Now, let move to more practical topic - how we can incorporate actual Vertx components in the DI pipeline.
Adding ConfigRetriever
In this subsection we will review a bit more complex (and real life) example. Often (if not always) you need to create components, that use configurations from the outer world. A naive example, is a repository, that listens database credentials, or an http client, that need requires API tokens. In the Vertx world, in order to read a configuration we use the vertx config library. We will not cover all aspects of its usage; I recommend you to review my ebook Principles of Vertx, where I cover it in details. For the purpose of this post, you just need to remember, that the key component that does this job is called ConfigRetriever. This class abstracts particular configuration types and provides a single entry point (and also it allows to listen of configuration updates; but this is out of the scope of this article). In order to initialize it, we use a static factory method, that requires a reference to the Vertx object:
private ConfigRetriever configRetriever;
AppVerticleModule(Vertx vertx){
configRetriever = ConfigRetriever.create(vertx);
}
@Override
protected void configure() {
bind(ConfigRetriever.class)
.annotatedWith(Names.named("ConfigRetriever"))
.toInstance(configRetriever);
}
Here we define a dependency using the familiar instance binding. Note, we also use an optional naming binding here: in the consumer class (verticle) we provide a @Named annotation with the constructor dependency injection.
@Inject
AppVerticle(@Named("ConfigRetriever") ConfigRetriever configRetriever){
this.configRetriever = configRetriever;
}
The config retriever component is used in order to get a configuration for the application. This is done using the getConfig method. In this post we use Futures API (Vertx 4.x) instead of callbacks (Vertx 3.x + Vertx 4.x):
Future<JsonObject> cr = configRetriever.getConfig();
Future<ProjectVerticle> vert = cr.map(c -> {
logger.info("Configuration obtained successfully");
Injector injector = Guice.createInjector(new ProjectVerticleModule(vertx, c));
ProjectVerticle verticle = injector.getInstance(ProjectVerticle.class);
return verticle;
}).onFailure(err -> {
logger.warning("Unable to obtain configuration");
startPromise.fail(err);
});
Let review this code snippet. The first step is to obtain a future of the configuration reading. Then we map the result in order to create an injector and to initailize the ProjectVerticle. We also provide config to the module in order to use them in dependency buildings (like credentials, tokens etc). If the config retriever fails to get a configuration, we fail the whole start( process of the AppVerticle.
The next stage is to deploy the project verticle. Take a look on the following implementation:
Future<String> dr = vert.compose(v -> vertx.deployVerticle(v));
dr.onSuccess(id -> {
logger.info("ProjectVerticle deployed");
startPromise.complete();
}).onFailure(err -> {
logger.warning("Unable to deploy ProjectVerticle");
logger.warning(err.getMessage());
startPromise.fail(err);
});
The future composition allows to connect two futures (configuration and deployment). The lambda parameter v here is the verticle, created inside the map() method in the previous step. The topic of using Futures API is out of the scope of this post, so we will not cover things in details here. Basically, we have two outcomes:
- onSuccess = the verticle is successfully deployed and we complete the AppVerticle starting process
- onFailure = something went wrong! We fail the AppVerticle starting process
Let put everyting together. Inside the main method of our fictional application we create a module and an injector for the AppVerticle and deploys it. Here we do not need to deploy other verticles, because the AppVerticle serves as an entry point of our app and does this job:
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
Injector injector = Guice.createInjector(new AppVerticleModule(vertx));
AppVerticle appVerticle = injector.getInstance(AppVerticle.class);
Future<String> dr = vertx.deployVerticle(appVerticle);
dr.onSuccess(id -> logger.info("AppVerticle started..."))
.onFailure(err -> {
logger.warning("Unable to start AppVerticle");
logger.warning(err.getMessage());
})
.onComplete(r -> {
vertx.close();
logger.info("Vertx closed");
});
}
Source code
If you would like to get full source code used in examples in this post, you can find it in this github repository. Feel free to explore it!
Conslusion
Simply speaking, the dependency injection is a pattern that allows to "outsource" the creation of components. There are several proven solutions to implement this architecture in Java apps and the Google Guice library is one of them. It is simple, but in the same time powerful. In this post we reviewed how to implement dependency injection in Vertx 4.x applications. We did two examples: one is a simple demonstration of essential Guice methods. The second uses an actual Vertx component - the ConfigRetriever, that reads a configuration and provides it to other components (that in their turn is also used to create other dependencies). If you have questions regarding this topic, please feel free to contact me or drop a comment below.
Posted on June 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.