Vert.x Service Proxy
When you compose a Vert.x application, you may want to isolate a functionality somewhere and make it available to the rest of your application. That’s the main purpose of service proxies. It lets you expose a service on the event bus, so, any other Vert.x component can consume it, as soon as they know the address on which the service is published.
A service is described with a Java interface containing methods following the async pattern. Under the hood, messages are sent on the event bus to invoke the service and get the response back. But for ease of use, it generates a proxy that you can invoke directly (using the API from the service interface).
Using Vert.x service proxies
To use Vert.x Service Proxies, add the following dependency to the dependencies section of your build descriptor:
-
Maven (in your
pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>4.4.9</version>
</dependency>
-
Gradle (in your
build.gradle
file):
compile 'io.vertx:vertx-service-proxy:4.4.9'
To implement service proxies, also add:
-
Maven (in your
pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>4.4.9</version>
<classifier>processor</classifier>
<scope>provided</scope>
</dependency>
-
Gradle < 5 (in your
build.gradle
file):
compileOnly 'io.vertx:vertx-codegen:4.4.9'
-
Gradle >= 5 (in your
build.gradle
file):
annotationProcessor 'io.vertx:vertx-codegen:4.4.9:processor'
annotationProcessor 'io.vertx:vertx-service-proxy:4.4.9'
Be aware that as the service proxy mechanism relies on code generation, so modifications to the service interface require to re-compile the sources to regenerate the code.
To generate the proxies in different languages, you will need to add the language dependency such as
vertx-lang-groovy
for Groovy.
Introduction to service proxies
Let’s have a look at service proxies and why they can be useful. Let’s imagine you have a database service exposed on the event bus, you should do something like this:
JsonObject message = new JsonObject();
message
.put("collection", "mycollection")
.put("document", new JsonObject().put("name", "tim"));
DeliveryOptions options = new DeliveryOptions().addHeader("action", "save");
vertx.eventBus()
.request("database-service-address", message, options)
.onSuccess(msg -> {
// done
}).onFailure(err -> {
// failure
});
When creating a service there’s a certain amount of boilerplate 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, you can avoid writing all that boilerplate code and concentrate on writing your service.
You write your service as a Java interface and annotate it with the @ProxyGen
annotation, for example:
@ProxyGen
public interface SomeDatabaseService {
// A couple of factory methods to create an instance and a proxy
static SomeDatabaseService create(Vertx vertx) {
return new SomeDatabaseServiceImpl(vertx);
}
static SomeDatabaseService createProxy(Vertx vertx,
String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// Actual service operations here...
void save(String collection, JsonObject document,
Handler<AsyncResult<Void>> resultHandler);
}
You will also need a package-info.java
file somewhere in (or above) the package your interface is defined in.
That package needs to be annotated with @ModuleGen
so that Vert.x CodeGen can recognize your interfaces and generate
the appropriate EventBus proxy code.
@io.vertx.codegen.annotations.ModuleGen(groupPackage = "io.vertx.example", name = "services", useFutures = true)
package io.vertx.example;
Given the interface, Vert.x will generate all the boilerplate code required to access your service over the event bus, and it will also generate a client side proxy for your service, so your clients can use a rich idiomatic API for your service instead of having to manually craft event bus messages to send. The client side proxy will work irrespective of where your service actually lives on the event bus (potentially on a different machine).
That means you can interact with your service like this:
SomeDatabaseService service = SomeDatabaseService
.createProxy(vertx, "database-service-address");
// Save some data in the database - this time using the proxy
service.save(
"mycollection",
new JsonObject().put("name", "tim"),
res2 -> {
if (res2.succeeded()) {
// done
}
});
You can also combine @ProxyGen
with language API code generation (@VertxGen
) in order to create service stubs
in any of the languages supported by Vert.x - this means you can write your service once in Java and interact with it
through an idiomatic other language API irrespective of whether the service lives locally or is somewhere else on
the event bus entirely. For this don’t forget to add the dependency on your language in your build descriptor:
@ProxyGen // Generate service proxies
@VertxGen // Generate the clients
public interface SomeDatabaseService {
// ...
}
Async interface
To be used by the service-proxy generation, the service interface must comply to a couple of rules. First it
should follow the async pattern. To return a result, the method should declare a Future<ResultType>
return type.
ResultType
can be another proxy (and so a proxies can be factories for other proxies).
Let’s see an example:
@ProxyGen
public interface SomeDatabaseService {
// A couple of factory methods to create an instance and a proxy
static SomeDatabaseService create(Vertx vertx) {
return new SomeDatabaseServiceImpl(vertx);
}
static SomeDatabaseService createProxy(Vertx vertx, String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// A method notifying the completion without a result (void)
Future<Void> save(String collection, JsonObject document);
// A method providing a result (a json object)
Future<JsonObject> findOne(String collection, JsonObject query);
// Create a connection
Future<MyDatabaseConnection> createConnection(String shoeSize);
}
with:
@ProxyGen
@VertxGen
public interface MyDatabaseConnection {
void insert(JsonObject someData);
Future<Void> commit();
@ProxyClose
void close();
}
You can also declare that a particular method unregisters the proxy by annotating it with the @ProxyClose
annotation. The proxy instance is disposed when this method is called.
More constraints on the service interfaces are described below.
Async interface with callbacks
Until Vert.x 4.1, service async interfaces were defined by callbacks.
You can still create service async interface using callbacks, with this module declaration:
@io.vertx.codegen.annotations.ModuleGen(groupPackage = "io.vertx.example", name = "services", useFutures = false)
package io.vertx.example;
Note
|
for backward compatibility, the default value of useFutures is false so you can also omit the declaration
|
The service async interface with callbacks looks like:
@ProxyGen
public interface SomeDatabaseService {
// A method notifying the completion without a result (void)
void save(String collection, JsonObject document,
Handler<AsyncResult<Void>> result);
// A method providing a result (a json object)
void findOne(String collection, JsonObject query,
Handler<AsyncResult<JsonObject>> result);
// Create a connection
void createConnection(String shoeSize,
Handler<AsyncResult<MyDatabaseConnection>> resultHandler);
}
Return types must be one of:
-
void
-
@Fluent
and return reference to the service (this
):
@Fluent
SomeDatabaseService doSomething();
This is because methods must not block and it’s not possible to return a result immediately without blocking if the service is remote.
Security
Service proxies can perform basic security using a simple interceptor. A authentication provider must be provided,
optionally Authorization`s can be added and in this case an `AuthorizationProvider
must also be present. Note that
the authentication works based on tokens, that are extracted from the auth-token
header.
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// Register the handler
new ServiceBinder(vertx)
.setAddress("database-service-address")
// Secure the messages in transit
.addInterceptor(
"action",
// Tokens will be validated using JWT authentication
AuthenticationInterceptor.create(
JWTAuth.create(vertx, new JWTAuthOptions())))
.addInterceptor(
AuthorizationInterceptor.create(JWTAuthorization.create("permissions"))
// optionally we can secure permissions too:
// an admin
.addAuthorization(RoleBasedAuthorization.create("admin"))
// that can print
.addAuthorization(PermissionBasedAuthorization.create("print")))
.register(SomeDatabaseService.class, service);
Code generation
Service annotated with @ProxyGen
annotation trigger the generation of the 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
Generated proxies and handlers are named after the service class, for example if the service is named MyService
the handler is called MyServiceProxyHandler
and the proxy is called MyServiceEBProxy
.
In addition Vert.x Core provides a generator creating data object converters to ease data object usage in
service proxies. Such converter provides a basis for the JsonObject
constructor and the toJson()
method
that are necessary for using data objects in service proxies.
The codegen annotation processor generates these classes at compilation time. It is a feature of the Java compiler so no extra step is required, it is just a matter of configuring correctly your build:
Just add the io.vertx:vertx-codegen:processor
and io.vertx:vertx-service-proxy
dependencies to your build.
Here a configuration example for Maven:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>4.4.9</version>
<classifier>processor</classifier>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>4.4.9</version>
</dependency>
This feature can also be used in Gradle:
compile "io.vertx:vertx-codegen:4.4.9:processor"
compile "io.vertx:vertx-service-proxy:4.4.9"
IDE provides usually support for annotation processors.
The codegen processor
classifier adds to the jar the automatic configuration of the service proxy annotation processor
via the META-INF/services
plugin mechanism.
If you want you can use it too with the regular jar but you need then to declare the annotation processor explicitly, for instance in Maven:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
Exposing your service
Once you have your service interface, compile the source to generate the stub and proxies. Then, you need some code to "register" your service on the event bus:
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// Register the handler
new ServiceBinder(vertx)
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
This can be done in a verticle, or anywhere in your code.
Once registered, the service becomes accessible. If you are running your application on a cluster, the service is available from any host.
To withdraw your service, use the unregister
method:
ServiceBinder binder = new ServiceBinder(vertx);
// Create an instance of your service implementation
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// Register the handler
MessageConsumer<JsonObject> consumer = binder
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
// ....
// Unregister your service.
binder.unregister(consumer);
Proxy creation
Now that the service is exposed, you probably want to consume it. For this, you need to create a proxy. The proxy
can be created using the ServiceProxyBuilder
class:
ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx)
.setAddress("database-service-address");
SomeDatabaseService service = builder.build(SomeDatabaseService.class);
// or with delivery options:
SomeDatabaseService service2 = builder.setOptions(options)
.build(SomeDatabaseService.class);
The second method takes an instance of DeliveryOptions
where you can configure the
message delivery (such as the timeout).
Alternatively, you can use the generated proxy class. The proxy class name is the service interface class name
followed by VertxEBProxy
. For instance, if your service interface is named SomeDatabaseService
, the proxy
class is named SomeDatabaseServiceVertxEBProxy
.
Generally, service interface contains a createProxy
static method to create the proxy. But this is not required:
@ProxyGen
public interface SomeDatabaseService {
// Method to create the proxy.
static SomeDatabaseService createProxy(Vertx vertx, String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// ...
}
Error Handling
Service methods may return errors to the client by passing a failed Future
containing a ServiceException
instance to the method’s Handler
. A ServiceException
contains an int
failure code, a message, and an optional
JsonObject
containing any extra information deemed important to return to the caller. For convenience, the
ServiceException.fail
factory method can be used to create an instance of
ServiceException
already wrapped in a failed Future
. For example:
public class SomeDatabaseServiceImpl implements SomeDatabaseService {
private static final BAD_SHOE_SIZE = 42;
private static final CONNECTION_FAILED = 43;
// Create a connection
public Future<MyDatabaseConnection> createConnection(String shoeSize) {
if (!shoeSize.equals("9")) {
return Future.failedFuture(ServiceException.fail(BAD_SHOE_SIZE, "The shoe size must be 9!",
new JsonObject().put("shoeSize", shoeSize)));
} else {
return doDbConnection().recover(err -> Future.failedFuture(ServiceException.fail(CONNECTION_FAILED, result.cause().getMessage())));
}
}
}
The client side can then check if the Throwable
it receives from a failed Future
is a ServiceException
,
and if so, check the specific error code inside. It can use this information to differentiate business logic
errors from system errors (like the service not being registered with the Event Bus), and to determine exactly
which business logic error occurred.
public Future<JsonObject> foo(String shoeSize) {
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
server.createConnection("8")
.compose(connection -> {
// Do success stuff.
return doSuccessStuff(connection);
})
.recover(err -> {
if (err instanceof ServiceException) {
ServiceException exc = (ServiceException) err;
if (exc.failureCode() == SomeDatabaseServiceImpl.BAD_SHOE_SIZE) {
return Future.failedFuture(
new InvalidInputError("You provided a bad shoe size: " +
exc.getDebugInfo().getString("shoeSize")));
} else if (exc.failureCode() == SomeDatabaseServiceImpl.CONNECTION) {
return Future.failedFuture(new ConnectionError("Failed to connect to the DB"));
}
} else {
// Must be a system error (e.g. No service registered for the proxy)
return Future.failedFuture(new SystemError("An unexpected error occurred: + " result.cause().getMessage()));
}
});
}
If desired, service implementations may also return a sub-class of ServiceException
, as long as a
default MessageCodec
is registered for it . For example, given the following ServiceException
sub-class:
class ShoeSizeException extends ServiceException {
public static final BAD_SHOE_SIZE_ERROR = 42;
private final String shoeSize;
public ShoeSizeException(String shoeSize) {
super(BAD_SHOE_SIZE_ERROR, "In invalid shoe size was received: " + shoeSize);
this.shoeSize = shoeSize;
}
public String getShoeSize() {
return extra;
}
public static <T> Future<T> fail(int failureCode, String message, String shoeSize) {
return Future.failedFuture(new MyServiceException(failureCode, message, shoeSize));
}
}
As long as a default MessageCodec
is registered, the Service implementation can return the custom
exception directly to the caller:
public class SomeDatabaseServiceImpl implements SomeDatabaseService {
public SomeDataBaseServiceImpl(Vertx vertx) {
// Register on the service side. If using a local event bus, this is all
// that's required, since the proxy side will share the same Vertx instance.
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
vertx.eventBus().registerDefaultCodec(ShoeSizeException.class,
new ShoeSizeExceptionMessageCodec());
}
// Create a connection
Future<MyDatabaseConnection> createConnection(String shoeSize) {
if (!shoeSize.equals("9")) {
return ShoeSizeException.fail(shoeSize);
} else {
// Create the connection here
return Future.succeededFuture(myDbConnection);
}
}
}
Finally, the client can now check for the custom exception:
public Future<JsonObject> foo(String shoeSize) {
// If this code is running on a different node in the cluster, the
// ShoeSizeExceptionMessageCodec will need to be registered with the
// Vertx instance on this node, too.
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
service.createConnection("8")
.compose(connection -> {
// Do success stuff.
return doSuccessStuff(connection);
})
.recover(err -> {
if (result.cause() instanceof ShoeSizeException) {
ShoeSizeException exc = (ShoeSizeException) result.cause();
return Future.failedFuture(
new InvalidInputError("You provided a bad shoe size: " + exc.getShoeSize()));
} else {
// Must be a system error (e.g. No service registered for the proxy)
return Future.failedFuture(
new SystemError("An unexpected error occurred: + " result.cause().getMessage())
);
}
});
}
Note that if you’re clustering Vertx
instances, you’ll need to register the custom Exception’s MessageCodec
with each Vertx
instance in the cluster.
Restrictions for service interface
There are restrictions on the types and return values that can be used in a service method so that these are easy to marshall over event bus messages and so they can be used asynchronously. They are:
Data types
Let JSON
= JsonObject | JsonArray
Let PRIMITIVE
= Any primitive type or boxed primitive type
Parameters can be any of:
-
JSON
-
PRIMITIVE
-
List<JSON>
-
List<PRIMITIVE>
-
Set<JSON>
-
Set<PRIMITIVE>
-
Map<String, JSON>
-
Map<String, PRIMITIVE>
-
Any Enum type
-
Any class annotated with
@DataObject
Asynchronous results are modelled as
-
Future<R>
-
Handler<AsyncResult<R>>
for callback style
R
can be any of:
-
JSON
-
PRIMITIVE
-
List<JSON>
-
List<PRIMITIVE>
-
Set<JSON>
-
Set<PRIMITIVE>
-
Any Enum type
-
Any class annotated with
@DataObject
-
Another proxy
Convention for invoking services over the event bus (without proxies)
Service Proxies assume that event bus messages follow a certain format so they can be used to invoke services.
Of course, you don’t have to use client proxies to access remote service if you don’t want to. It’s perfectly acceptable to interact with them by just sending messages over the event bus.
In order for services to be interacted with a consistent way the following message formats must be used for any Vert.x services.
The format is very simple:
-
There should be a header called
action
which gives the name of the action to perform. -
The body of the message should be a
JsonObject
, there should be one field in the object for each argument needed by the action.
For example to invoke an action called save
which expects a String collection and a JsonObject document:
Headers: "action": "save" Body: { "collection", "mycollection", "document", { "name": "tim" } }
The above convention should be used whether or not service proxies are used to create services, as it allows services to be interacted with consistently.
In the case where service proxies are used the "action" value should map to the name of an action method in the
service interface and each [key, value]
in the body should map to a [arg_name, arg_value]
in the action method.
For return values the service should use the message.reply(…)
method to send back a return value - this can be of
any type supported by the event bus. To signal a failure the method message.fail(…)
should be used.
If you are using service proxies the generated code will handle this for you automatically.