Vertx, Guice and Config Retriever: Dependency Injection in Vertx 4.x
In this post, we will review how to do dependency injection in Vertx with Google Guice and how to build a basic injection that has the ConfigRetriever component.
Join the DZone community and get the full member experience.
Join For FreeIn computer science, dependency injection is defined as a pattern, whereby one component gets other components (dependencies) from outside. Numerous posts were written about various implementations of dependency injection in Vertx using the Google Guice library. All of them are good and I do not want to reinvent a bicycle here and repeat the 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 dependency injection in Vertx with Google Guice and how 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 a very flexible and powerful solution. The 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 performed 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 constructor injection.
Take a look at 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 things simple, this component defines 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 avoided, 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 at 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 'outsource' 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 a 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 to 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 detail. 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 to 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 initialize 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 at 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 connecting 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 detail here. Basically, we have two outcomes:
onSuccess
= The verticle is successfully deployed and we complete theAppVerticle
starting process.onFailure
= Something went wrong! We fail theAppVerticle
starting process.
Let put everything 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 the full source code used in examples in this post, you can find it in this GitHub repository. Feel free to explore it!
Conclusion
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 at 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.
Published at DZone with permission of Yuri Mednikov. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments