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
- defines an interface that a verticle exposes.
- and generates a proxy service class that will handle the event bus messaging
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
See https://vertx.io/docs/guide-for-java-devs/#_maven_configuration_changes
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"
}
- Annotation Processing task. See processor_options
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
}
);
}