PolarSPARC |
Introduction to Vert.x - Part 6
Bhaskar S | 06/15/2019 |
Overview
In Part-5 of this series, we finally pulled together all the foundational concepts of Vert.x to implement and demonstrate a loosely-coupled, distributed REST based Contacts Management microservice with a simple memory based persistence layer (JVM heap) and a REST based service layer (using HTTP), which communicated with each other via message passing using the EventBus.
In this final part, instead of using a simple memory based persistence layer (JVM heap) for the Contacts Management microservice, we will implement and demonstrate the use of a database based persistence layer for the microservice.
Hands-on with Vert.x - 6
The Vert.x JDBC Client extension (vertx-jdbc-client) allows one to interact with any JDBC compliant database using an asynchronous callback based API. For this demonstration, we will use the H2 database (written in Java).
The following is the modified listing of the Maven project file pom.xml that includes the additional libraries vertx-jdbc-client and h2 as dependencies:
Performing a Maven update will download the H2 database jar file h2-1.4.199.jar.
Assuming the H2 database jar is in the lib directory, open a new Terminal window and execute the following command to start the database server:
java -cp ./lib/h2-1.4.199.jar org.h2.tools.Server -tcp -ifNotExists -baseDir ./data
We will use DBeaver (community edition) as our SQL client tool for creating and managing the contacts database.
Launch DBeaver and proceed to create a new database. Select H2 Server as the database and click on Next> as shown in the illustration below:
Next, select ./lib/h2-1.4.199.jar as the database driver, and create the database contacts with the credentials (user id admin and password s3cr3t) as shown in the illustration below:
Next, click on Test Connection to verify we are able to connect to the contacts database as shown in the illustration below:
Finally, create a database table CONTACTS with the table columns ( FIRST_NAME, LAST_NAME, EMAIL_ID, and MOBILE) as shown in the illustration below:
The Vert.x JDBC Client (vertx-jdbc-client) expects the following configuration parameters to be specified:
By default, the Vert.x JDBC Client under-the-hood uses the C3P0 connection pool. The default pool size is 15.
Rather than hard-coding the Vert.x JDBC Client configuration parameters, we will specify these parameters and values in the Vert.x config file (conf/config.json).
The following is the listing for the config file conf/config.json in the JSON format:
The following is the listing for the contacts management JDBC compliant database based persistence layer Sample12.java:
Let us explain and understand the code from Sample12 listed above.
This is just a *REMINDER* as it is very critical for the understanding - the interface io.vertx.core.Future is not the same as the one from core Java java.util.concurrent.Future. The Future from Vert.x has a non-blocking behavior (handled through asynchronous callback into a handler) unlike the Java Future which has a blocking semantics (calling the get() method will block the caller). The Future interface from Vert.x represents the result of an action that may not have completed yet and calls into a registered io.vertx.core.Handler<T> on completion (either success or failures).
The interface io.vertx.ext.jdbc.JDBCClient represents the JDBC client API for interacting with any JDBC compliant database. The static method createShared(Vertx, JsonObject, String) creates an instance of JDBCClient for the database whose config parameters are specified in the second argument as a JsonObject.
The method getConnection(Handler<T>) (where <T> is of type AsyncResult<U>) returns a SQL connection (from the connection pool) to the underlying database.
The interface io.vertx.ext.jdbc.SQLConnection represents a SQL connection to the database and an instance of SQLConnection is used to perform the CRUD (insert, query, update, delete) operations on the database table(s).
The method updateWithParams(String, JsonArray, Handler<T>) (where <T> is of type AsyncResult<U>) allows one to execute the specified prepared SQL statement which can be a DELETE, or an INSERT, or an UPDATE statement.
The method query(String, Handler<T>) (where <T> is of type AsyncResult<U>) allows one to execute the specified SQL SELECT statement. The database table rows are returned as a Java java.util.List of JsonObjects.
The method queryWithParams(String, JsonArray, Handler<T>) (where <T> is of type AsyncResult<U>) allows one to execute the specified prepared SQL SELECT statement. The database table rows are returned as a Java java.util.List of JsonObjects.
Notice that we call the close() method on the instance of SQLConnection after every database operation. This will return the database connection back to the pool.
Since we have added support for jdbc compliant database, we need to tweak the shell script called run.sh as shown below:
#!/bin/sh
JARS=""
for f in `ls ./lib/jackson*`
do
JARS=$JARS:$f
done
for f in `ls ./lib/netty*`
do
JARS=$JARS:$f
done
JARS=$JARS:./lib/vertx-core-3.7.0.jar:./lib/vertx-config-3.7.0.jar:./lib/hazelcast-3.10.5.jar:./lib/vertx-hazelcast-3.7.0.jar:./lib/vertx-web-3.7.0.jar:./lib/vertx-jdbc-client-3.7.0.jar:./lib/vertx-sql-common-3.7.0.jar:./lib/c3p0-0.9.5.2.jar:./lib/mchange-commons-java-0.2.11.jar:./lib/h2-1.4.199.jar
echo $JARS
java -Dvertx.hazelcast.config=./resources/my-cluster.xml -cp ./classes:./resources:$JARS com.polarsparc.Vertx.$1 $2
To start the contacts management database based persistence layer, open a new Terminal window and execute the following command:
./bin/run.sh Sample12
The following would be the typical output:
:./lib/jackson-annotations-2.9.0.jar:./lib/jackson-core-2.9.8.jar:./lib/jackson-databind-2.9.8.jar:./lib/netty-buffer-4.1.30.Final.jar:./lib/netty-codec-4.1.30.Final.jar:./lib/netty-codec-dns-4.1.30.Final.jar:./lib/netty-codec-http2-4.1.30.Final.jar:./lib/netty-codec-http-4.1.30.Final.jar:./lib/netty-codec-socks-4.1.30.Final.jar:./lib/netty-common-4.1.30.Final.jar:./lib/netty-handler-4.1.30.Final.jar:./lib/netty-handler-proxy-4.1.30.Final.jar:./lib/netty-resolver-4.1.30.Final.jar:./lib/netty-resolver-dns-4.1.30.Final.jar:./lib/netty-transport-4.1.30.Final.jar:./lib/vertx-core-3.7.0.jar:./lib/vertx-config-3.7.0.jar:./lib/hazelcast-3.10.5.jar:./lib/vertx-hazelcast-3.7.0.jar:./lib/vertx-web-3.7.0.jar:./lib/vertx-jdbc-client-3.7.0.jar:./lib/vertx-sql-common-3.7.0.jar:./lib/c3p0-0.9.5.2.jar:./lib/mchange-commons-java-0.2.11.jar:./lib/h2-1.4.199.jar Jun 15, 2019 3:27:48 PM com.hazelcast.instance.AddressPicker INFO: [LOCAL] [polarsparc] [3.10.5] Interfaces is enabled, trying to pick one address matching to one of: [127.0.0.1] Jun 15, 2019 3:27:48 PM com.hazelcast.instance.AddressPicker INFO: [LOCAL] [polarsparc] [3.10.5] Picked [127.0.0.1]:5701, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5701], bind any local is true Jun 15, 2019 3:27:48 PM com.hazelcast.system INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Hazelcast 3.10.5 (20180913 - 6ffa2ee) starting at [127.0.0.1]:5701 Jun 15, 2019 3:27:48 PM com.hazelcast.system INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved. Jun 15, 2019 3:27:48 PM com.hazelcast.system INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Configured Hazelcast Serialization version: 1 Jun 15, 2019 3:27:48 PM com.hazelcast.instance.Node INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] A non-empty group password is configured for the Hazelcast member. Starting with Hazelcast version 3.8.2, members with the same group name, but with different group passwords (that do not use authentication) form a cluster. The group password configuration will be removed completely in a future release. Jun 15, 2019 3:27:48 PM com.hazelcast.spi.impl.operationservice.impl.BackpressureRegulator INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Backpressure is disabled Jun 15, 2019 3:27:48 PM com.hazelcast.spi.impl.operationservice.impl.InboundResponseHandlerSupplier INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Running with 2 response threads Jun 15, 2019 3:27:48 PM com.hazelcast.instance.Node INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Creating TcpIpJoiner Jun 15, 2019 3:27:48 PM com.hazelcast.spi.impl.operationexecutor.impl.OperationExecutorImpl INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Starting 16 partition threads and 9 generic threads (1 dedicated for priority tasks) Jun 15, 2019 3:27:48 PM com.hazelcast.internal.diagnostics.Diagnostics INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Diagnostics disabled. To enable add -Dhazelcast.diagnostics.enabled=true to the JVM arguments. Jun 15, 2019 3:27:48 PM com.hazelcast.core.LifecycleService INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] [127.0.0.1]:5701 is STARTING WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by com.hazelcast.internal.networking.nio.SelectorOptimizer (file:/home/bswamina/Downloads/TTT/lib/hazelcast-3.10.5.jar) to field sun.nio.ch.SelectorImpl.selectedKeys WARNING: Please consider reporting this to the maintainers of com.hazelcast.internal.networking.nio.SelectorOptimizer WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release Jun 15, 2019 3:27:48 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Connecting to /127.0.0.1:5703, timeout: 0, bind-any: true Jun 15, 2019 3:27:48 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Connecting to /127.0.0.1:5702, timeout: 0, bind-any: true Jun 15, 2019 3:27:48 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Could not connect to: /127.0.0.1:5703. Reason: SocketException[Connection refused to address /127.0.0.1:5703] Jun 15, 2019 3:27:48 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Could not connect to: /127.0.0.1:5702. Reason: SocketException[Connection refused to address /127.0.0.1:5702] Jun 15, 2019 3:27:48 PM com.hazelcast.cluster.impl.TcpIpJoiner INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] [127.0.0.1]:5703 is added to the blacklist. Jun 15, 2019 3:27:48 PM com.hazelcast.cluster.impl.TcpIpJoiner INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] [127.0.0.1]:5702 is added to the blacklist. Jun 15, 2019 3:27:49 PM com.hazelcast.system INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Cluster version set to 3.10 Jun 15, 2019 3:27:49 PM com.hazelcast.internal.cluster.ClusterService INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Members {size:1, ver:1} [ Member [127.0.0.1]:5701 - 466d89c7-87af-4636-9fa5-5221329981be this ] Jun 15, 2019 3:27:49 PM com.hazelcast.core.LifecycleService INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] [127.0.0.1]:5701 is STARTED Jun 15, 2019 3:27:50 PM com.hazelcast.internal.partition.impl.PartitionStateManager INFO: [127.0.0.1]:5701 [polarsparc] [3.10.5] Initializing cluster partition table arrangement... Jun 15, 2019 3:27:50 PM io.vertx.config.impl.ConfigRetrieverImpl INFO: Config file path: conf/config.json, format:json Jun 15, 2019 3:27:50 PM com.mchange.v2.log.MLog INFO: MLog clients using java 1.4+ standard logging. Jun 15, 2019 3:27:50 PM com.mchange.v2.c3p0.C3P0Registry INFO: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10] Jun 15, 2019 3:27:50 PM com.polarsparc.Vertx.Sample12$DbCommandConsumerVerticle lambda$0 INFO: Eventbus commands address: contact.commands Jun 15, 2019 3:27:50 PM com.polarsparc.Vertx.Sample12 lambda$17 INFO: Deployed command consumer instance ID: adfbb1bd-024c-4bbb-a24e-ddef48d43fde
To start the contacts management service layer, open a new Terminal window and execute the following command:
./bin/run.sh Sample11
The following would be the typical output:
:./lib/jackson-annotations-2.9.0.jar:./lib/jackson-core-2.9.8.jar:./lib/jackson-databind-2.9.8.jar:./lib/netty-buffer-4.1.30.Final.jar:./lib/netty-codec-4.1.30.Final.jar:./lib/netty-codec-dns-4.1.30.Final.jar:./lib/netty-codec-http2-4.1.30.Final.jar:./lib/netty-codec-http-4.1.30.Final.jar:./lib/netty-codec-socks-4.1.30.Final.jar:./lib/netty-common-4.1.30.Final.jar:./lib/netty-handler-4.1.30.Final.jar:./lib/netty-handler-proxy-4.1.30.Final.jar:./lib/netty-resolver-4.1.30.Final.jar:./lib/netty-resolver-dns-4.1.30.Final.jar:./lib/netty-transport-4.1.30.Final.jar:./lib/vertx-core-3.7.0.jar:./lib/vertx-config-3.7.0.jar:./lib/hazelcast-3.10.5.jar:./lib/vertx-hazelcast-3.7.0.jar:./lib/vertx-web-3.7.0.jar:./lib/vertx-jdbc-client-3.7.0.jar:./lib/vertx-sql-common-3.7.0.jar:./lib/c3p0-0.9.5.2.jar:./lib/mchange-commons-java-0.2.11.jar:./lib/h2-1.4.199.jar Jun 15, 2019 3:28:31 PM com.hazelcast.instance.AddressPicker INFO: [LOCAL] [polarsparc] [3.10.5] Interfaces is enabled, trying to pick one address matching to one of: [127.0.0.1] Jun 15, 2019 3:28:31 PM com.hazelcast.instance.AddressPicker INFO: [LOCAL] [polarsparc] [3.10.5] Picked [127.0.0.1]:5702, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5702], bind any local is true Jun 15, 2019 3:28:31 PM com.hazelcast.system INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Hazelcast 3.10.5 (20180913 - 6ffa2ee) starting at [127.0.0.1]:5702 Jun 15, 2019 3:28:31 PM com.hazelcast.system INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved. Jun 15, 2019 3:28:31 PM com.hazelcast.system INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Configured Hazelcast Serialization version: 1 Jun 15, 2019 3:28:31 PM com.hazelcast.instance.Node INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] A non-empty group password is configured for the Hazelcast member. Starting with Hazelcast version 3.8.2, members with the same group name, but with different group passwords (that do not use authentication) form a cluster. The group password configuration will be removed completely in a future release. Jun 15, 2019 3:28:31 PM com.hazelcast.spi.impl.operationservice.impl.BackpressureRegulator INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Backpressure is disabled Jun 15, 2019 3:28:31 PM com.hazelcast.spi.impl.operationservice.impl.InboundResponseHandlerSupplier INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Running with 2 response threads Jun 15, 2019 3:28:32 PM com.hazelcast.instance.Node INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Creating TcpIpJoiner Jun 15, 2019 3:28:32 PM com.hazelcast.spi.impl.operationexecutor.impl.OperationExecutorImpl INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Starting 16 partition threads and 9 generic threads (1 dedicated for priority tasks) Jun 15, 2019 3:28:32 PM com.hazelcast.internal.diagnostics.Diagnostics INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Diagnostics disabled. To enable add -Dhazelcast.diagnostics.enabled=true to the JVM arguments. Jun 15, 2019 3:28:32 PM com.hazelcast.core.LifecycleService INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] [127.0.0.1]:5702 is STARTING WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by com.hazelcast.internal.networking.nio.SelectorOptimizer (file:/home/bswamina/Downloads/TTT/lib/hazelcast-3.10.5.jar) to field sun.nio.ch.SelectorImpl.selectedKeys WARNING: Please consider reporting this to the maintainers of com.hazelcast.internal.networking.nio.SelectorOptimizer WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release Jun 15, 2019 3:28:32 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Connecting to /127.0.0.1:5703, timeout: 0, bind-any: true Jun 15, 2019 3:28:32 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Could not connect to: /127.0.0.1:5703. Reason: SocketException[Connection refused to address /127.0.0.1:5703] Jun 15, 2019 3:28:32 PM com.hazelcast.cluster.impl.TcpIpJoiner INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] [127.0.0.1]:5703 is added to the blacklist. Jun 15, 2019 3:28:32 PM com.hazelcast.nio.tcp.TcpIpConnector INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Connecting to /127.0.0.1:5701, timeout: 0, bind-any: true Jun 15, 2019 3:28:32 PM com.hazelcast.nio.tcp.TcpIpConnectionManager INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Established socket connection between /127.0.0.1:40795 and /127.0.0.1:5701 Jun 15, 2019 3:28:33 PM com.hazelcast.system INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Cluster version set to 3.10 Jun 15, 2019 3:28:33 PM com.hazelcast.internal.cluster.ClusterService INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] Members {size:2, ver:2} [ Member [127.0.0.1]:5701 - 466d89c7-87af-4636-9fa5-5221329981be Member [127.0.0.1]:5702 - 2ae8bce5-cfa7-4046-b1ba-ac368e4bc36b this ] Jun 15, 2019 3:28:34 PM com.hazelcast.core.LifecycleService INFO: [127.0.0.1]:5702 [polarsparc] [3.10.5] [127.0.0.1]:5702 is STARTED Jun 15, 2019 3:28:34 PM io.vertx.config.impl.ConfigRetrieverImpl INFO: Config file path: conf/config.json, format:json Jun 15, 2019 3:28:34 PM com.polarsparc.Vertx.Sample11$ContactsServiceVerticle lambda$0 INFO: Configured server port: 8080 Jun 15, 2019 3:28:34 PM com.polarsparc.Vertx.Sample11$ContactsServiceVerticle lambda$6 INFO: Started contacts service on localhost:8080... Jun 15, 2019 3:28:34 PM com.polarsparc.Vertx.Sample11 lambda$2 INFO: Deployed contacts service instance ID: a2b36c8f-c147-4e10-8a3f-86db3dea7996
From DBeaver database tool, we see the database table CONTACTS is empty (has no data rows) as shown in the illustration below:
Open a new Terminal window and execute the following command to add a new contact:
curl -v -d "{\"fname\":\"Frank\",\"lname\":\"Polymer\",\"email\":\"frank_p@spacelab.io\",\"mobile\":\"777-888-9999\"}" -X POST http://localhost:8080/api/contacts/v1/addContact
The following would be the typical output:
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > POST /api/contacts/v1/addContact HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.64.1 > Accept: */* > Content-Length: 89 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 89 out of 89 bytes < HTTP/1.1 200 OK < content-type: application/json < content-length: 140 < * Connection #0 to host localhost left intact {"errcode":0,"payload":"{\"firstName\":\"Frank\",\"lastName\":\"Polymer\",\"emailId\":\"frank_p@spacelab.io\",\"mobile\":\"777-888-9999\"}"} * Closing connection 0
From DBeaver database tool, we see the database table CONTACTS now has one row (as expected) as shown in the illustration below:
Next, execute the following command to update the contact for the person with the last-name Polymer:
curl -v -d "{\"email\":\"fpolymer_2000@jupiter.io\",\"mobile\":\"888-666-5555\"}" -X PUT http://localhost:8080/api/contacts/v1/updateByLastName/Polymer
The following would be the typical output:
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > PUT /api/contacts/v1/updateByLastName/Polymer HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.64.1 > Accept: */* > Content-Length: 60 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 60 out of 60 bytes < HTTP/1.1 200 OK < content-type: application/json < content-length: 49 < * Connection #0 to host localhost left intact {"errcode":0,"payload":"{\"lname\":\"Polymer\"}"} * Closing connection 0
From DBeaver database tool, we see the database table CONTACTS row information has been updated as shown in the illustration below:
Next, execute the following command to fetch the contact for the person with the last-name Polymer:
curl -v http://localhost:8080/api/contacts/v1/getByLastName/Polymer
The following would be the typical output:
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /api/contacts/v1/getByLastName/Polymer HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < content-type: application/json < content-length: 148 < * Connection #0 to host localhost left intact {"errcode":0,"payload":"{\"FIRST_NAME\":\"Frank\",\"LAST_NAME\":\"Polymer\",\"EMAIL_ID\":\"fpolymer_2000@jupiter.io\",\"MOBILE\":\"888-666-5555\"}"} * Closing connection 0
Next, execute the following command to fetch the contact for the person with the last-name Martian:
curl -v http://localhost:8080/api/contacts/v1/getByLastName/Martian
The following would be the typical output:
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /api/contacts/v1/getByLastName/Martian HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < content-type: application/json < content-length: 13 < * Connection #0 to host localhost left intact {"errcode":1} * Closing connection 0
We have successfully demonstrated the database based persistence layer for the contacts management service and with this we conclude the series on the Introduction to Vert.x. We have barely scratched the surface of the capabilities and features in Vert.x.
References
[1] Introduction to Vert.x - Part-1
[2] Introduction to Vert.x - Part-2
[3] Introduction to Vert.x - Part-3
[4] Introduction to Vert.x - Part-4