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:

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  
  <groupId>com.polarsparc</groupId>
  <artifactId>Vertx</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <name>Vertx</name>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.3</version>
          <configuration>
            <fork>true</fork>
            <meminitial>128m</meminitial>
            <maxmem>512m</maxmem>
            <source>1.8</source>
            <target>1.8</target>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

  <dependencies>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-core</artifactId>
        <version>3.7.0</version>
    </dependency>  
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-config</artifactId>
        <version>3.7.0</version>
    </dependency>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-hazelcast</artifactId>
        <version>3.7.0</version>
    </dependency>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-web</artifactId>
        <version>3.7.0</version>
    </dependency>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-jdbc-client</artifactId>
        <version>3.7.0</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.2</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.199</version>
    </dependency>    
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

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:

DB Setup
Figure.1

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:

Contacts DB
Figure.2

Next, click on Test Connection to verify we are able to connect to the contacts database as shown in the illustration below:

Test Connection
Figure.3

Finally, create a database table CONTACTS with the table columns ( FIRST_NAME, LAST_NAME, EMAIL_ID, and MOBILE) as shown in the illustration below:

Contacts Table
Figure.4

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:

conf/config.json
{
  "http.port" : 8080,
  "commands.endpoint": "contact.commands",
  "driver_class": "org.h2.Driver",
  "url": "jdbc:h2:tcp://localhost:9092/contacts",
  "user": "admin",
  "password": "s3cr3t"
}

The following is the listing for the contacts management JDBC compliant database based persistence layer Sample12.java:

Sample12.java
/*
 * Topic:  Introduction to Vert.x
 * 
 * Name:   Sample 12
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.Vertx;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import io.vertx.config.ConfigRetriever;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.spi.cluster.ClusterManager;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.SQLConnection;
import io.vertx.spi.cluster.hazelcast.HazelcastClusterManager;

public class Sample12 {
    private static Logger LOGGER = Logger.getLogger(Sample12.class.getName());
    
    private static String ADDRESS = null;
    
    private static JDBCClient JDBC = null;
    
    private static String SQL_ADD_CONTACT     = "INSERT INTO CONTACTS (FIRST_NAME, LAST_NAME, EMAIL_ID, MOBILE) VALUES (?, ?, ?, ?)";
    private static String SQL_DEL_CONTACT     = "DELETE FROM CONTACTS WHERE LAST_NAME = ?";
    private static String SQL_QRY_ALL_CONTACT = "SELECT FIRST_NAME, LAST_NAME, EMAIL_ID, MOBILE FROM CONTACTS";
    private static String SQL_QRY_ONE_CONTACT = "SELECT FIRST_NAME, LAST_NAME, EMAIL_ID, MOBILE FROM CONTACTS WHERE LAST_NAME = ?";
    private static String SQL_UPD_CONTACT     = "UPDATE CONTACTS SET EMAIL_ID = ?, MOBILE = ? WHERE LAST_NAME = ?";
    
    // Consumer verticle that responds to "commands"
    private static class DbCommandConsumerVerticle extends AbstractVerticle {
        @Override
        public void start(Future<Void> fut) {
            ConfigRetriever retriever = ConfigRetriever.create(vertx);
            
            ConfigRetriever.getConfigAsFuture(retriever).setHandler(confres -> {
                 if (confres.succeeded()) {
                     JsonObject config = confres.result();
                     
                     ADDRESS = config.getString("commands.endpoint");
                     
                     JDBC = JDBCClient.createShared(vertx, config, "ContactsMgmt");
                     
                     LOGGER.log(Level.INFO, "Eventbus commands address: " + ADDRESS);
                    
                     vertx.eventBus().consumer(ADDRESS, message -> {
                        String payload = message.body().toString();
                                
                        LOGGER.log(Level.INFO, "Received payload - " + payload);
                         
                        JsonObject req = new JsonObject(payload);
                        
                        String cmd = req.getString(Commands.FLD_COMMAND);
                        
                        switch (cmd) {
                            case Commands.ADD_NEW: {
                                JsonObject data = new JsonObject(req.getString(Commands.FLD_PAYLOAD));
                                
                                String fname = data.getString(Commands.FLD_FNAME);
                                String lname = data.getString(Commands.FLD_LNAME);
                                String email = data.getString(Commands.FLD_EMAIL);
                                String mobile = data.getString(Commands.FLD_MOBILE);
                                
                                LOGGER.log(Level.INFO, Commands.ADD_NEW + ":: Contact first name - " + fname
                                    + ", last name: " + lname + ", email: " + email + ", mobile: " + mobile);
                                
                                addContact(fname, lname, email, mobile).setHandler(res -> {
                                    if (res.succeeded()) {
                                        message.reply(res.result());
                                    }
                                    else {
                                        message.reply(errorResponse());
                                    }
                                });
                                
                                break;
                            }
                            case Commands.DEL_BY_LASTNAME: {
                                JsonObject data = new JsonObject(req.getString(Commands.FLD_PAYLOAD));
                                
                                String name = data.getString(Commands.FLD_LNAME);
                                
                                LOGGER.log(Level.INFO, Commands.DEL_BY_LASTNAME + ":: Contact last name - " + name);
                                
                                deleteContactByLastName(name).setHandler(res -> {
                                    if (res.succeeded()) {
                                        message.reply(res.result());
                                    }
                                    else {
                                        message.reply(errorResponse());
                                    }
                                });
                                
                                break;
                            }
                            case Commands.GET_ALL: {
                                getAllContacts().setHandler(res -> {
                                    if (res.succeeded()) {
                                        message.reply(res.result());
                                    }
                                    else {
                                        message.reply(errorResponse());
                                    }
                                });
                                
                                break;
                            }
                            case Commands.GET_BY_LASTNAME: {
                                JsonObject data = new JsonObject(req.getString(Commands.FLD_PAYLOAD));
                                
                                String name = data.getString(Commands.FLD_LNAME);
                                
                                LOGGER.log(Level.INFO, Commands.GET_BY_LASTNAME + ":: Contact last name - " + name);
                                
                                getContactByLastName(name).setHandler(res -> {
                                    if (res.succeeded()) {
                                        message.reply(res.result());
                                    }
                                    else {
                                        message.reply(errorResponse());
                                    }
                                });
                                
                                break;
                            }
                            case Commands.UPD_BY_LASTNAME: {
                                JsonObject data = new JsonObject(req.getString(Commands.FLD_PAYLOAD));
                                
                                String email = data.getString(Commands.FLD_EMAIL);
                                String mobile = data.getString(Commands.FLD_MOBILE);
                                String name = data.getString(Commands.FLD_LNAME);
                                
                                LOGGER.log(Level.INFO, Commands.UPD_BY_LASTNAME + ":: Contact last name - " + name + ", email: "
                                    + email + ", mobile: " + mobile);
                                
                                updateContactByLastName(name, email, mobile).setHandler(res -> {
                                    if (res.succeeded()) {
                                        message.reply(res.result());
                                    }
                                    else {
                                        message.reply(errorResponse());
                                    }
                                });
                                
                                break;
                            }
                        }
                     });
                    
                     fut.complete();
                 } else {
                     fut.fail(confres.cause());
                 }
            });
        }
    }
    
    // ----- Database Operation(s) -----
    
    // Get an SQL connection
    private static Future<SQLConnection> connect() {
        Future<SQLConnection> future = Future.future();
        
        JDBC.getConnection(res -> {
            if (res.succeeded()) {
                future.complete(res.result());
            }
            else {
                future.fail(res.cause());
            }
        });
        
        return future;
    }
    
    // Database insert
    private static Future<Contact> insert(SQLConnection connection, Contact obj) {
        Future<Contact> future = Future.future();
        
        LOGGER.log(Level.INFO, "insert() - fname: " + obj.getFirstName() + ", lname: " + obj.getLastName()
            + ", email: " + obj.getEmailId() + ", mobile: " + obj.getMobile());
        
        JsonArray params = new JsonArray();
        params.add(obj.getFirstName()).add(obj.getLastName()).add(obj.getEmailId()).add(obj.getMobile());
        
        connection.updateWithParams(SQL_ADD_CONTACT, params, res -> {
            connection.close();
            if (res.succeeded()) {
                future.complete(obj);
            }
            else {
                future.fail(res.cause());
            }
        });
        
        return future;
    }
    
    // Database delete
    private static Future<Void> delete(SQLConnection connection, String name) {
        Future<Void> future = Future.future();
        
        LOGGER.log(Level.INFO, "delete() - name: " + name);
        
        JsonArray params = new JsonArray();
        params.add(name);
        
        connection.updateWithParams(SQL_DEL_CONTACT, params, res -> {
            connection.close();
            if (res.succeeded()) {
                if (res.result().getUpdated() == 0) {
                    future.fail(new NoSuchElementException("delete() - No contact with LAST_NAME = " + name));
                }
                else {
                    future.complete();
                }
            }
            else {
                future.fail(res.cause());
            }
        });
        
        return future;
    }
    
    // Database select all
    private static Future<List<JsonObject>> select(SQLConnection connection) {
        Future<List<JsonObject>> future = Future.future();
        
        connection.query(SQL_QRY_ALL_CONTACT, res -> {
            connection.close();
            if (res.succeeded()) {
                future.complete(res.result().getRows().stream().collect(Collectors.toList()));
            }
            else {
                future.fail(res.cause());
            }
        });
        
        return future;
    }
    
    // Database select one
    private static Future<JsonObject> selectOne(SQLConnection connection, String name) {
        Future<JsonObject> future = Future.future();
        
        LOGGER.log(Level.INFO, "selectOne() - name: " + name);
        
        JsonArray params = new JsonArray();
        params.add(name);
        
        connection.queryWithParams(SQL_QRY_ONE_CONTACT, params, res -> {
            connection.close();
            if (res.succeeded()) {
                if (res.result().getRows().size() == 0) {
                    future.fail(new NoSuchElementException("selectOne() - No contact with LAST_NAME = " + name));
                }
                else {
                    future.complete(res.result().getRows().get(0));
                }
            }
            else {
                future.fail(res.cause());
            }
        });
        
        return future;
    }
    
    // Database update
    private static Future<Void> update(SQLConnection connection, String name, String email, String mobile) {
        Future<Void> future = Future.future();
        
        LOGGER.log(Level.INFO, "update() - name: " + name + ", email: " + email + ", mobile:" + mobile);
        
        JsonArray params = new JsonArray();
        params.add(email);
        params.add(mobile);
        params.add(name);
        
        connection.updateWithParams(SQL_UPD_CONTACT, params, res -> {
            connection.close();
            if (res.succeeded()) {
                if (res.result().getUpdated() == 0) {
                    future.fail(new NoSuchElementException("update() - No contact with LAST_NAME = " + name));
                }
                else {
                    future.complete();
                }
            }
            else {
                future.fail(res.cause());
            }
        });
        
        return future;
    }
    
    // ----- Command Handler(s) -----
    
    // Error JSON response
    private static String errorResponse() {
         JsonObject json = new JsonObject();
         json.put(Commands.FLD_ERRCODE, 1);
         
         return json.encode();
    }
    
    // Add a new contact
    private static Future<String> addContact(String fname, String lname, String email, String mobile) {
        Future<String> future = Future.future();
        
        // Valid contact:
        // 1. First name and last name are required
        // 2. Email and/or mobile required (either or both)
        if ((fname != null && fname.trim().length() > 0) &&
            (lname != null && lname.trim().length() > 0)) {
            Contact obj = new Contact(fname, lname, "", "");
            if (email != null && email.trim().length() > 0) {
                obj.setEmailId(email);
            }
            if (mobile != null && mobile.trim().length() > 0) {
                obj.setMobile(mobile);
            }
            
            // Database insert
            connect().setHandler(sqlres -> {
                if (sqlres.succeeded()) {
                    insert(sqlres.result(), obj).setHandler(conres -> {
                         if (conres.succeeded()) {
                             JsonObject json = new JsonObject();
                             json.put(Commands.FLD_ERRCODE, 0);
                             json.put(Commands.FLD_PAYLOAD, JsonObject.mapFrom(conres.result()).encode());
                             
                             String response = json.encode();
                            
                             LOGGER.log(Level.INFO, "addContact() - " + response);
                            
                             future.complete(response);
                         }
                         else {
                             future.fail(conres.cause());
                         }
                    });
                }
                else {
                    future.fail(sqlres.cause());
                }
            });
        }
        else {
            future.fail(new IllegalArgumentException("addContact - Invalid name for contact"));
        }
        
        return future;
    }
    
    // Delete a contact by last name
    private static Future<String> deleteContactByLastName(String name) {
        Future<String> future = Future.future();

        if (name != null && name.trim().length() > 0) {
            // Database delete
            connect().setHandler(sqlres -> {
                if (sqlres.succeeded()) {
                    delete(sqlres.result(), name).setHandler(conres -> {
                         if (conres.succeeded()) {
                             JsonObject json = new JsonObject();
                             json.put(Commands.FLD_ERRCODE, 0);
                             json.put(Commands.FLD_PAYLOAD, new JsonObject().put(Commands.FLD_LNAME, name).encode());
                             
                             String response = json.encode();
                            
                             LOGGER.log(Level.INFO, "deleteContactByLastName() - " + response);
                             
                             future.complete(response);
                         }
                         else {
                             future.fail(conres.cause());
                         }
                    });
                }
                else {
                    future.fail(sqlres.cause());
                }
            });
        }
        else {
            future.fail(new IllegalArgumentException("deleteContactByLastName - Invalid last name for contact"));
        }
        
        return future;
    }
    
    // Fetch all contacts
    private static Future<String> getAllContacts() {
        Future<String> future = Future.future();
        
        // Database select all
        connect().setHandler(sqlres -> {
            if (sqlres.succeeded()) {
                select(sqlres.result()).setHandler(conres -> {
                     if (conres.succeeded()) {
                         JsonArray array = new JsonArray(conres.result());
                         JsonObject json = new JsonObject();
                         json.put(Commands.FLD_ERRCODE, 0);
                         json.put(Commands.FLD_PAYLOAD, array.encode());
                         
                         String response = json.encode();
                        
                         LOGGER.log(Level.INFO, "getAllContacts() - " + response);
                         
                         future.complete(response);
                     }
                     else {
                         future.fail(conres.cause());
                     }
                });
            }
            else {
                future.fail(sqlres.cause());
            }
        });
        
        return future;
    }
    
    // Fetch a contact by last name
    private static Future<String> getContactByLastName(String name) {
        Future<String> future = Future.future();
        
        if (name != null && name.trim().length() > 0) {
            // Database delete
            connect().setHandler(sqlres -> {
                if (sqlres.succeeded()) {
                    selectOne(sqlres.result(), name).setHandler(conres -> {
                         if (conres.succeeded()) {
                             JsonObject json = new JsonObject();
                             json.put(Commands.FLD_ERRCODE, 0);
                             json.put(Commands.FLD_PAYLOAD, conres.result().encode());
                             
                             String response = json.encode();
                            
                             LOGGER.log(Level.INFO, "getContactByLastName() - " + response);
                             
                             future.complete(response);
                         }
                         else {
                             future.fail(conres.cause());
                         }
                    });
                }
                else {
                    future.fail(sqlres.cause());
                }
            });
        }
        else {
            future.fail(new IllegalArgumentException("getContactByLastName - Invalid last name for contact"));
        }
        
        return future;
    }
    
    // Update a contact by last name
    private static Future<String> updateContactByLastName(String name, String email, String mobile) {
        Future<String> future = Future.future();

        if ((name != null && name.trim().length() > 0) &&
            (email != null && email.trim().length() > 0) &&
            (mobile != null && mobile.trim().length() > 0)) {
            // Database update
            connect().setHandler(sqlres -> {
                if (sqlres.succeeded()) {
                    update(sqlres.result(), name, email, mobile).setHandler(conres -> {
                         if (conres.succeeded()) {
                             JsonObject json = new JsonObject();
                             json.put(Commands.FLD_ERRCODE, 0);
                             json.put(Commands.FLD_PAYLOAD, new JsonObject().put(Commands.FLD_LNAME, name).encode());
                             
                             String response = json.encode();
                            
                             LOGGER.log(Level.INFO, "updateContactByLastName() - " + response);
                             
                             future.complete(response);
                         }
                         else {
                             future.fail(conres.cause());
                         }
                    });
                }
                else {
                    future.fail(sqlres.cause());
                }
            });
        }
        else {
            future.fail(new IllegalArgumentException("updateContactByLastName - Invalid last name, email, mobile for contact"));
        }
        
        return future;
    }
    
    // ----- Main -----
    
    public static void main(String[] args) {
        ClusterManager manager = new HazelcastClusterManager();
        
        VertxOptions options = new VertxOptions().setClusterManager(manager);
        
        Vertx.clusteredVertx(options, cluster -> {
            if (cluster.succeeded()) {
                cluster.result().deployVerticle(new DbCommandConsumerVerticle(), res -> {
                    if (res.succeeded()) {
                        LOGGER.log(Level.INFO, "Deployed command consumer instance ID: " + res.result());
                    } else {
                        res.cause().printStackTrace();
                    }
               });
            } else {
                cluster.cause().printStackTrace();
            }
        });
    }
}

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:

Output.1

:./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:

Output.2

:./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:

Contacts Empty
Figure.5

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:

Output.3

*   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:

Contacts One Row
Figure.6

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:

Output.4

*   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:

Contacts Updated Row
Figure.7

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:

Output.5

*   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:

Output.6

*   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

[5] Introduction to Vert.x - Part-5

[6] Vert.x Core Manual (Java)

[7] Vert.x JDBC Client Manual (Java)



© PolarSPARC