Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

First Steps With Vert.x and Infinispan - Part 1: REST API

DZone's Guide to

First Steps With Vert.x and Infinispan - Part 1: REST API

In this series on Eclipse Vert.x and Infinispan, this tutorial explains how to create a REST API using Eclipse Vert.x with Infinispan.

· Integration Zone ·
Free Resource

The State of API Integration 2018: Get Cloud Elements’ report for the most comprehensive breakdown of the API integration industry’s past, present, and future.

Welcome to the first in a multi-part series of blog posts about creating Eclipse Vert.x applications with Infinispan. The purpose of this first tutorial is to showcase how to create a REST API.

All the code of this tutorial is available in this GitHub repository. The backend is a Java project using Maven, so all the needed dependencies can be found in the pom.xml.

What Is Vert.x?

Vert.x is a tool-kit for building reactive applications on the JVM. It's an event-driven and non-blocking tool-kit. It is based on the Reactor Pattern, like Node.js, but unlike Node, it can easily use all the cores of your machine so you can create highly concurrent and performant applications. Code examples can be found in this repository.

What Is Infinispan?

Infinispan is an in-memory distributed cache/datagrid. It uses peer-to-peer communication between different nodes, hence there are no master/slave nodes. Infinispan has multiple use-cases:

  • Distributed cache to boost the performance of your application

  • Temporary, scalable in-memory  data store with failover capabilities

  • Data analysis via distributed Java Streams API or Spark/Hadoop integrations

  • Event-driven computation thanks to live-updating query functionality

REST API

Let's start creating a simple endpoint that will display a welcome message on '/'. In Vert.x this is done by creating a Verticle. A verticle is a unit of deployment and processes incoming events over an event-loop. Event-loops are used in asynchronous programming models. I won't spend more time here explaining these concepts as this is very well done in this Devoxx Vert.x talk or in the documentation available here.

We need to override the start method, create a 'router' so '/' requests can be handled, and finally create an HTTP server.

The most important thing to remember about vert.x, is that we can NEVER EVER call blocking code (we will see how to deal with blocking APIs just after). If we do so, we will block the event loop and we won't be able to serve incoming requests properly.

public class CuteNamesRestAPI extends AbstractVerticle {

    @Override
    public void start() throws Exception {
          Router router = Router.router(vertx);

          router.get("/").handler(rc -> {
             rc.response().putHeader("content-type", "text/html")
                   .end("Welcome to CuteNames API Service");
          });

          vertx.createHttpServer() // creates a HttpServer
            .requestHandler(router::accept) // router::accept will handle the requests
            .listen(config().getInteger("http.port", 8080)); // Get "http.port" from the config, default value 8080
    }

    public static void main(String[] args) {
          Vertx vertx = Vertx.vertx();
          DeploymentOptions options = new DeploymentOptions()
            .setConfig(new JsonObject()
                  .put("http.port", 8081)
                  .put("infinispan.host", "localhost")
            );
          vertx.deployVerticle(CuteNamesRestAPI.class.getName(), options);
    }
}


Run the main method, go to your browser to http://localhost:8081/  and we see the welcome message!

Connecting With Infinispan

Now we are going to create a REST API that uses Infinispan. The purpose here is to post and get names by id. We are going to use the default cache in Infinispan for this example, and we will connect to it remotely. To do that, we are going to use the Infinispan hotrod protocol, which is the recommended way to do it (but we could use REST or Memcached protocol too).

Start Infinispan Locally

The first thing we are going to do is to run an Infinispan Server locally. We download the Infinispan Server from here, unzip the downloaded file and run  ./bin/standalone.sh 

If you are using Docker on Linux, you can use the Infinispan Docker Image Available easily. If you are using Docker for Mac, at the time of this writing there is an issue with internal IP addresses and they can't be called externally. Different workarounds exist to solve the problem, but the easiest thing for this example is simply downloading and running the standalone server locally. 

The hotrod server is listening in  localhost:11222 .

Connect the Client to the Server

The code we need to connect with Infinispan from our Java application is the following:

Configuration configuration = new ConfigurationBuilder().addServer()
               .host(config().getString("infinispan.host", "localhost"))
               .port(config().getInteger("infinispan.port", 11222))
               .build();

RemoteCacheManager client = new RemoteCacheManager(configuration);

RemoteCache<String, String> defaultCache = client.getCache();


This code is blocking. As I said before, we can't block the event loop and this will happen if we directly call these APIs from a verticle. The code must be called using vertx.executeBlocking method, and passing a Handler. The code in the handler will be executed from a worker thread pool and will pass the result back asynchronously.

To stop the client, the API supplies a non-blocking method that can be called when the verticle is stopped, so we are safe on that.

We are going to create an abstract CacheAccessVerticle  where we will initialize the manager and get default cache. When everything is correct and the defautCache variable is initialized, we will log a message and execute the initSuccess abstract method. Instead of overriding start  method, we will override  start(Future startFuture) , this way we will be able to handle startup errors properly.

public abstract class CacheAccessVerticle extends AbstractVerticle {

   protected RemoteCacheManager client;
   protected RemoteCache<String, String> defaultCache;

   @Override
   public void start(Future<Void> startFuture) throws Exception {
     vertx.executeBlocking(fut -> {
         Configuration configuration = new ConfigurationBuilder().addServer()
               .host(config().getString("infinispan.host", "localhost"))
               .port(config().getInteger("infinispan.port", 11222))
               .build();
         client = new RemoteCacheManager(
               configuration);

         defaultCache = client.getCache();
         fut.complete();
      }, res -> {
         if (res.succeeded()) {
            getLogger().log(Level.INFO, "Cache connection successfully done");
            initSuccess(startFuture);
         } else {
            getLogger().log(Level.SEVERE, "Cache connection error", res.cause());
            startFuture.fail(res.cause());
         }
      });
   }

   @Override
   public void stop(Future<Void> stopFuture) throws Exception {
      if (client != null) {
         client.stopAsync().whenComplete((e, ex) -> stopFuture.complete());
      } else
         stopFuture.complete();
   }

   protected abstract void initSuccess(Future<Void> startFuture);

   protected abstract Logger getLogger();
}


REST API to Create Names

We are going to add 3 new endpoints.

  •  GET /api  displays the API name
  •  POST /api/cutenames  creates a new name
  •  GET /api/cutenames/id  displays a name by id

CuteNamesRestAPI verticle can now extend this class and override the initSuccess  method instead of the start  method.

public class CuteNamesRestAPI extends CacheAccessVerticle {

   private final Logger logger = Logger.getLogger(CuteNamesRestAPI.class.getName());

   @Override
   protected void initSuccess(Future<Void> startFuture) {
      ...
      router.get("/api").handler(rc -> {
         rc.response().putHeader("content-type", "application/json")
               .end(new JsonObject().put("name", "cutenames").put("version", 1).encode());
      });

      // put route is created here
      ...

      // get by id route is created here
      ...

      vertx.createHttpServer()
            .requestHandler(router::accept)
            .listen(port, ar -> {
               if (ar.succeeded()) {
                  startFuture.complete();
               } else {
                  startFuture.fail(ar.cause());
               }
            });
   }

   ...
}


POST

Our goal is to use a curl to create a name like this :

 curl -X POST -H "Content-Type: application/json"

-d '{"name":"Oihana"}' "http://localhost:8081/api/cutenames" 

For those that are not familiar with basques names, Oihana means 'rainforest' and is a super cute name. Those who know me will confirm that I'm absolutely not biased making this statement.

To read the body content, we need to add a body handler to the route, otherwise the body won't be parsed. This is done by calling  router.route().handler(BodyHandler.create()) .

The handler that will handle the post method in '/api/cutenames' is a RoutingContext handler. We want to create a new name in the default cache. For that, we will call putAsync method from the defaultCache .

// put route is created here
router.route().handler(BodyHandler.create());
router.post("/api/cutenames").handler(this::handleAddCuteName);
private void handleAddCuteName(RoutingContext rc) {
      logger.info("Add called");
      HttpServerResponse response = rc.response();
      JsonObject bodyAsJson = rc.getBodyAsJson();
      if (bodyAsJson != null && bodyAsJson.containsKey("name")) {
         String id = id();
         defaultCache.putAsync(bodyAsJson, bodyAsJson.getString("name"))
               .thenAccept(s -> {
                  logger.info(String.format("Cute name added [%s]", id));
                  response.setStatusCode(201)
                        .end("Cute name added");
               });
      } else {
         response.setStatusCode(400)
               .end(String.format("Body is %s. 'id' and 'name' should be provided", bodyAsJson));
      }
   }
}

private String id(JsonObject bodyAsJson) {
      bodyAsJson.containsKey("id") ? bodyAsJson.getString("id") : UUID.randomUUID().toString();
}


The server responds 201 when the name is correctly created, and 400 when the request is not correct.

GET by id

To create a get endpoint by id, we need to declare a route that will take a parameter :id. In the route handler, we are going to call getAsync method.

// get by id route is created here
router.get("/api/cutenames/:id").handler(this::handleGetById);
private void handleGetById(RoutingContext rc) {
      String id = rc.request().getParam("id");
      logger.info("Get by id called id=" + id);
      defaultCache.getAsync(rc.request().getParam("id"))
            .thenAccept(value -> {
               String cuteName;
               if (value == null) {
                  cuteName = String.format("Cute name %s not found", id);
                  rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code());
               } else {
                  cuteName = new JsonObject().put("name", value).encode();
               }
               rc.response().end(cuteName);
            });
   }

If we run the main, we can POST and GET names using curl!

 curl -X POST -H "Content-Type: application/json"

-d '{"id":"42", "name":"Oihana"}'

"http://localhost:8081/api/cutenames"


curl -X GET -H "Content-Type: application/json" 

"http://localhost:8081/api/cutenames/42"{"name":"Oihana"} 

Wrap-Up

We have learned how to create a REST API with Vert.x, powered by Infinispan. The repository has some unit tests using the web client. Feedback is more than welcome to improve the code and the provided examples. I hope you enjoyed this tutorial! Now learn how to create your first PUSH API!

Your API is not enough. Learn why (and how) leading SaaS providers are turning their products into platforms with API integration in the ebook, Build Platforms, Not Products from Cloud Elements.

Topics:
vert.x ,infinispan ,rest api ,java ,integration

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}