Vert.x - Services Proxy

About

A service is just a functionality that simplify Vertx code by generating boilerplate code that will handle all event bus operations.

When creating a service there’s a certain amount of code to listen on the event bus for incoming messages, route them to the appropriate method and return results on the event bus. With Vert.x service proxies, that boilerplate code is generated and you can concentrate on writing the service.

To create a Vert.x service, you

Features

  • the service may be contacted locally or somewhere else on the event bus entirely.
  • The client side proxy will work irrespective of where your service actually lives on the event bus (potentially on a different machine).

Code generation

Code generation happens through two annotation:

  • @ProxyGen that generates the service helper classes - the boilerplate code required to access your service over the event bus
  • @VertxGen that generates the service client classes - a client side proxy for your service (with all event bus handling).

Example:

@ProxyGen // Generate service proxies
@VertxGen // Generate the clients
public interface SomeDatabaseService {
 // ...
}

Service helper classes

Service annotated with @ProxyGen annotation trigger the generation of the two service helper classes:

  • The service proxy: a compile time generated proxy that uses the EventBus to interact with the service via messages
  • The service handler: a compile time generated EventBus handler that reacts to events sent by the proxy

The service proxy mechanism relies on code generation, so modifications to the service interface require to re-compile the sources to regenerate the code.

Example:

_

Service client classes

With the @VertxGen annotation, you generate service stubs in any of the languages supported by Vert.x

For instance, you can write your service once in Java and interact with it with another language.

For this don’t forget to add the dependency on your language in your build descriptor.

Processor

How to configure the build tool to configure the annotation processing.

The process code can be found at this addresse CodeGenProcessor.java

Maven

Gradle

  • The vertx version
ext {
  vertxVersion = '3.8.4' 
}
  • Dependencies for Gradle < 5
dependencies {
    compile "io.vertx:vertx-core:$vertxVersion"
    compile "io.vertx:vertx-service-proxy:$vertxVersion:processor"
    compile "io.vertx:vertx-codegen:$vertxVersion:processor"
}
  • Dependencies for Gradle > 5
dependencies {
  implementation "io.vertx:vertx-core:$vertxVersion"
  implementation "io.vertx:vertx-service-proxy:$vertxVersion"
  compileOnly  "io.vertx:vertx-codegen:$vertxVersion"
  annotationProcessor "io.vertx:vertx-codegen:$vertxVersion:processor"
  // <3.8.4
  // annotationProcessor "io.vertx:vertx-service-proxy:$vertxVersion:processor"
  // >=3.8.4
  annotationProcessor "io.vertx:vertx-service-proxy:$vertxVersion"
}
task annotationProcessing(type: JavaCompile, group: 'build') { // codegen
  description 'Generates the stubs'
  options.incremental = false
  source = sourceSets.main.java
  classpath = configurations.compileClasspath
  destinationDir = project.file('src/main/generated')
  options.annotationProcessorPath = configurations.annotationProcessor
  options.debugOptions.debugLevel = "source,lines,vars"
  options.compilerArgs = [
    "-proc:only",
    "-processor", "io.vertx.codegen.CodeGenProcessor",
    "-Acodegen.output=${project.projectDir}/src/main"
  ]
}
  • Adding the dependency on the compile step
compileJava {
  dependsOn (annotationProcessing)
}
  • Adding the generated map to the set of source directory
sourceSets {
  main {
    java {
      srcDirs += 'src/main/generated'
    }
  }
}

If it's not working, there is also a Plugin

Processor Options

The processor is configured by a few options:

  • codegen.output : where the non Java classes / non resources are stored (mandatory) - the processors requires this option to know where to place them
  • codegen.output.<generator-name> : relocate the output of to another directory
  • codegen.generators : a comma separated list of generators, each expression is a regex, allow to filter undesired generators

Compiler:

  • -proc: {none,only} : Controls whether annotation processing and/or compilation is done.
    • -proc:none means that compilation takes place without annotation processing.
    • -proc:only means that only annotation processing is done, without any subsequent compilation.

Management

Creation

  • You write your service as a Java interface and annotate it with the @ProxyGen annotation
@ProxyGen
public interface SomeDatabaseService {

 // A couple of static factory methods to create the instances (respectively implementation and proxy
 // Create is used when you deploy a vertical (ie you deploy the service)
 @GenIgnore
  static SomeDatabaseService create(arguments ..., Handler<AsyncResult<DatabaseServiceInterface>> readyHandler) {
    // The SomeDatabaseServiceImpl  class is an implementation class that you are creating
    return new SomeDatabaseServiceImpl(argument, readyHandler);
  }
 
 @GenIgnore
 // Proxy is used when you use/test the service as server client code
 static SomeDatabaseService createProxy(Vertx vertx, String address) {
   // The SomeDatabaseServiceVertxEBProxy class is generated by the annotation processor
   return new SomeDatabaseServiceVertxEBProxy(vertx, address);
 }

// Actual service operations here...
@Fluent
SomeDatabaseService doSomething(Arguments ..., Handler<AsyncResult<YourReturnType>> resultHandler);

}
  • You give code generation metadata in the package-info.java file where groupPackage is the package of the generated class.
@ModuleGen(name = "database", groupPackage = "net.bytle.api.db")
package net.bytle.api.db;

import io.vertx.codegen.annotations.ModuleGen;

Deploy

In the start method of a verticle, you will use the static factory create method of the interface to deploy your service (ie 1 service = 1 verticle)

SomeServiceInterface.create(argument, ready -> {
  if (ready.succeeded()) {
	// ready result is the implementation (ie SomeServiceInterfaceImpl instance)
	ServiceBinder binder = new ServiceBinder(vertx).setAddress(IP_QUEUE_NAME);
	binder.register(SomeServiceInterface.class, ready.result());
	promise.complete();
  } else {
	promise.fail(ready.cause());
  }
});

Call

The proxy is used in every server call to the service.

SomeDatabaseService service = SomeDatabaseService.createProxy(vertx,
    "database-service-address");

// Save some data in the database - this time using the proxy
service.doSomething(Arguments ..., res2 -> {
  if (res2.succeeded()) {
    // done
  }
});

Test

In a unit, you would:

  • deploy your verticle (service)
  • and instantiate your service proxy (that you can use in every test)
@Before
  public void prepare(TestContext context) throws InterruptedException {
    vertx = Vertx.vertx();

    JsonObject conf = new JsonObject() 
      .put(DatabaseVerticle.KEY_JDBC_URL, "jdbc:hsqldb:mem:testdb;shutdown=true")
      .put(DatabaseVerticle.KEY_JDBC_MAX_POOL_SIZE, 4);

    vertx.deployVerticle(
      new DatabaseVerticle(), // The service
      new DeploymentOptions().setConfig(conf),
      context.asyncAssertSuccess(
        id -> { 
           // the deployment was successful, you get the deployment id (no needed)
           // but you can instantiate your service proxy
           service = DatabaseServiceInterface.createProxy(vertx, DatabaseVerticle.IP_QUEUE_NAME)) // The service proxy
        }
    );

  }

Documentation


Powered by ComboStrap