Software Development

Vert.x: implementing a custom Service Type for the Service Discovery

Person writing code

Vert.x is a polyglot library that helps to develop reactive applications. A good starting point on how Vert.x can help you to write such applications is the free-ebook Building Reactive Microservices in Java from Clément Escoffier or these talks: here and here.

Besides Vert.x itself, Vert.x comes with lots of components that eases the development of reactive applications. One of them is the Service Discovery that helps to write distributed applications and especially, to get resources (services, data sources, etc.) from various distributed micro-services. By default (and as of writing), the Vert.x Service Discovery offers several predefined type of services:

HttpEndpoint to get a HttpClient configured with the host and port
– EventBusService to get a Service Proxy
– MessageSource to get a MessageConsumer
– JDBCDataSource to get a JDBCClient
– RedisDataSource to get a RedisClient
– MongoDataSource to get a MongoClient

That’s a lot. But what if you wish to implement your own Service Type? What do you have to do? The doc of the Vert.x Service Discovery gives the recipe… but as for cooking, it’s nice to have a recipe but it’s far nicer to have the result of the recipe (especially for cooking and if you are a bit gourmand 🙂 ). So, that’s the purpose of this blog post. Let’s create a custom Vert.x Service Type and use it!

 

So, what do we cook, chef?

So, what do we cook, chef?

Well, let’s cook our famous “Hello World” example!

What do we want to do? Let’s develop a Hello Service Type that enables us to register a new Type (the HelloType) in the Service Discovery and helps us to publish and consume a HelloService, whose purpose (if you can guess) is to display « Hello + name!!! » (woah! Amazing! ;)).

 

So, where do we start?

First of all, as every good recipes, let’s start with the ingredients: the HelloService and its implementation.


public interface HelloService {
    void sayHello(String aName);
}

public class HelloServiceImpl implements HelloService {

    private boolean isLowerCase;

    public HelloServiceImpl(final boolean aIsLowerCase) {
        isLowerCase = aIsLowerCase;
    }

    @Override
    public void sayHello(String aName) {
        String hello = "HELLO %s!!!\n";

        if (isLowerCase) {
            hello = hello.toLowerCase();
        }

        System.out.printf(hello, aName);
    }
}

We create an implementation that can write the service in upper cases or lower cases to illustrate how to configure a service through the Service Discovery (see below).

Now, the recipe steps (which are well described in the Vert.x doc):

1- create a custom interface extending ServiceType
2- create an implementation of this interface
3- create a class extending io.vertx.ext.discovery.types.AbstractServiceReference
4- declare our ServiceType in a META-INF file

Then, publish the service into the Service Discovery and cook a verticle that consumes this service through the Service Discovery (the touch of the chef 😉 ).

 

Create a custom interface HelloType

Ok. What’s simpler than that?


public interface HelloType extends ServiceType {
   String TYPE = "hello-type";
}

In addition, the Vert.x doc. “recommends” to provide some static methods to create the Record for your Service Type. You may not be fond of doing that in the interface but if you are, here is the piece of code:


public interface HelloType extends ServiceType {
    String TYPE = "hello-type";

    static Record createRecord(final String aName, final String aAddress, 
                               final JsonObject aMetadata) {
        Objects.requireNonNull(aName);
        Objects.requireNonNull(aAddress);

        JsonObject location = new JsonObject().put(ENDPOINT, aAddress);
        return createRecord(aName, location, aMetadata);
    }

    static Record createRecord(final String aName, final JsonObject aLocation, 
                               final JsonObject aMetadata) {
        Objects.requireNonNull(aName);
        Objects.requireNonNull(aLocation);

        Record record = new Record().setName(aName)
                             .setType(TYPE)
                             .setLocation(aLocation);

        if (aMetadata != null) {
            record.setMetadata(aMetadata);
        }

        return record;
    }
}

Create the implementation HelloTypeImpl

So, let’s create the implementation of our custom type:


public class HelloTypeImpl implements HelloType {
    @Override
    public String name() { // (1)
        return TYPE;
    }

    @Override
    public ServiceReference get(final Vertx aVertx, // (2)
                                final ServiceDiscovery aDiscovery,
                                final Record aRecord,
                                final JsonObject aConfiguration) {
        return new HelloReference(aVertx, aDiscovery, aRecord, aConfiguration);
    }
}

The doc says:

(1) the method name() must return the type declared in the Service Type
(2) the implementation needs to implement public ServiceReference get(.)

As we need to return a ServiceReference, it looks like we need one. Let’s cook one for the demo!

 

Create a HelloReference

Quite simple: we need to extend AbstractServiceReference<HelloService> and implement the abstract methods:


public class HelloReference extends AbstractServiceReference<HelloService> {

    private final JsonObject conf; // (1)

    public HelloReference(final Vertx aVertx, final ServiceDiscovery aDiscovery,
                          final Record aRecord, final JsonObject aConfiguration) {
        super(aVertx, aDiscovery, aRecord);
        Objects.requireNonNull(aConfiguration);
        conf = aConfiguration; // (1)
    }

    @Override
    protected HelloService retrieve() { // (2)
        Boolean isLowerCase = conf.getBoolean("lower-case", Boolean.TRUE);
        return new HelloServiceImpl(isLowerCase);
    }

    @Override
    public void close() { // (3)
        // add your code here, if ever your service object needs cleanup
        super.close();
    }
}

(1) we store the configuration of the service: we can pass a JsonObject to configure our service. Let’s say we can configure it by passing a boolean “lower-case”. If it’s true, “Hello” will be written in lower cases, otherwise in upper cases.
(2) we provide an implementation of our HelloService in the retrieve() method
(3) we override the close() method if ever the implementation has to cleanup resources (it’s not the case here. But it is better to highlight this point).

Right, that sounds good. Have we all to publish our service? Not yet, we need to declare our HelloTypeImpl as a service type.

 

Declare HelloTypeImpl as a ServiceType

To do that, create a file io.vertx.servicediscovery.spi.ServiceType under the META-INF/services folder with:


org.ws13.howtos.vertx.discoveries.hello.HelloTypeImpl

Now, we’re right: let’s publish our service!

 

Publish the HelloService in the Service Discovery


public class HelloPublisherVerticle extends AbstractVerticle {
    private ServiceDiscovery discovery;
    private Record helloServiceRecord;

    @Override
    public void start() throws Exception {
        super.start();
        discovery = ServiceDiscovery.create(vertx); // (1)

        Record helloRecord = // (2)
                   HelloType.createRecord("helloooo", 
                                          "the-hellooo-service-address", 
                                          new JsonObject());

        discovery.publish(helloRecord, ar -> { // (3)
            if (ar.succeeded()) {
                helloServiceRecord = ar.result(); // (4)
                System.out.println("Hello Service successfully published!");

            } else {
                System.err.println("Hello Service has not been published: something went wrong...");

            }
        });
    }

    @Override
    public void stop() throws Exception {
        super.stop();
        if (helloServiceRecord != null) {
            discovery.unpublish(helloServiceRecord.getRegistration(), // (5)
                                ar -> {
                                   if (ar.succeeded()) {
                                       System.out.println("Hello Service successfully unpublished!");

                                   } else {
                                      System.err.println("Hum, there was something wrong"
                                          + "with the unpublication of the Hello Service... ಠ_ಠ");
                                  }
                               });
        }
        discovery.close();
    }
}

(1) we create an instance of the Service Discovery
(2) we create a Record via a static method of HelloType but we could also have done it by creating manually a Record object.


Record record = 
     new Record()
         .setType(HelloType.TYPE)
         .setLocation(new JsonObject()
         .put("endpoint", "the-hellooo-service-address"))
         .setName("helloooo")
         .setMetadata(new JsonObject());

 

(3) we publish the service
(4) we keep track of the resulting record so that we can unpublish later on (5)

The record declares the type of service as well as the address / location of the service. The address of the service is in fact an address of the Vert.x Event Bus: under the hood, the Service Discovery uses the Vert.x Event Bus.

So far, what have we done? We created and declared a HelloTypeImpl that provides a HelloServiceReference. The HelloServiceReference gives access to a new instance of the HelloServiceImpl, which is one implementation of the HelloService. Actually, that’s the declaration of HelloTypeImpl as a ServiceType in the file io.vertx.servicediscovery.spi.ServiceType that will enable the Service Discovery to retrieve a HelloService.

Let’s have a look at how we retrieve a such service.

 

Consume the HelloService

To do so, we create another project in which we declare a verticle that will call the Service Discovery to retrieve an instance of the HelloService.


public class HelloConsumerVerticle extends AbstractVerticle {

    private ServiceDiscovery discovery;

    @Override
    public void start() throws Exception {
        super.start();

        discovery = ServiceDiscovery.create(vertx); // (1)
        discovery.getRecord(r -> HelloType.TYPE.equals(r.getType()), ar -> { // (2)
            if (ar.succeeded() && ar.result() != null) { // (3)
                ServiceReference reference;
                reference = discovery.getReference(ar.result()); // (4)

                HelloService helloService = reference.get(); // (5)
                helloService.sayHello("Scotty");

                // don't forget to release the reference
                reference.release(); // (6)
            } else {
                System.out.println("Failed to get a non-null Hello Service... (⊙_☉)");
            }
        });
    }

    @Override
    public void stop() throws Exception {
        discovery.close();
        super.stop();
    }
}

(1) we create an instance of the Service Discovery
(2) we get a Record asynchronously: we want a Record of a ServiceReference whose type is HelloType.TYPE (the lambda plays the role of a such filter). If the handler succeeds and the Service Discovery has returned a non null record (3), we get a reference of our HelloService (4) and then get the service object (5). We then call our HelloService. Once the service used, we don’t forget to release the reference (6).

And you should get a nice and amazing: “hello Scotty!!!” printed in your console. (So, awesome!)

Hey, wait! How do we call our service to print in upper cases?

Hey, wait! How do we call our service to print in upper cases?

In this case, the code is slightly different: we call the Service Discovery with a method that allows us to pass a configuration JsonObject (1):


public class HelloConsumerVerticle extends AbstractVerticle {

    private ServiceDiscovery discovery;

    @Override
    public void start() throws Exception {
        System.out.println("HelloConsumerVerticle.start");
        super.start();

        discovery.getRecord(r -> HelloType.TYPE.equals(r.getType()), ar -> {
            if (ar.succeeded() && ar.result() != null) {
                ServiceReference reference;
                reference = discovery
                        .getReferenceWithConfiguration(ar.result(), // (1)
                                                       new JsonObject().put("lower-case", false));

                HelloService helloService = reference.get();
                helloService.sayHello("Mr Spock");

                // don't forget to release the reference
                reference.release();
            } else {
                System.out.println("Failed to get a non-null configured Hello Service... (⊙_☉)");
            }
        });
    }
}

Remember the 4th parameter of our HelloReference? It’s this JsonObject configuration that the HelloReference will receive to configure the instance of our HelloService (i.e. to call the constructor of the HelloServiceImpl with the proper parameters).

You should get a nice: “HELLO Mr Spock!!!”

That’s it folks!

 

Conclusion:

Creating a custom Service Type is easy:

– declare an interface that extends ServiceType interface
– implement this interface
– create an implementation of io.vertx.ext.discovery.types.AbstractServiceReference
– declare the implementation of our custom ServiceType in the file META-INF/services/io.vertx.servicediscovery.spi.ServiceType

Pretty simple. You may not need to create your custom service (may be the EventBusService or MessageSource can fit your needs) but if you do, you know how to do it ;).

Have a look at the code. Oh, by the way, don’t forget to run it in a cluster mode (yes, I forgot it the first time…): what’s the point in using a Service Discovery if you’re not in cluster? 😉 The README of the project explains how to do that.

 

Takeaway: what if I want to declare another implementation of our service?

Let’s assume, we have 2nd HelloServiceImpl2 we wish to expose to the Service Discovery too. Just repeat the steps described in the conclusion.
If ever you wish to be able to distinguish between the two implementations, you may use the JsonObject metadata of the Record to perform a filtering that discriminates these two services. But in general, if ever you have several implementations of the same service, you don’t care which one is called provided the service is performed.

**Original source: streamdata.io blog