This repo contains tools helping to develop a Vertx application in Java and Scala.
The key element is ServiceVerticle
, a io.vertx.core.Verticle
implementation that provides Future
abstraction for event-bus communication (instead of callbacks) and robust config injection mechanism.
Module | Description |
---|---|
vertx-bus | Verticles with Future event-bus abstraction and configuration injection |
vertx-bus-scala | Scala extensions for vertx-bus |
vertx-registry | Dependency injection and component management |
vertx-server | HTTP server framework |
vertx-client | Future-based wrapper of Vertx HTTP client with service-discovery, load-balancing and retries |
vertx-sd | Service-discovery |
vertx-sd-consul | Consul service-discovery provider |
vertx-config-classpath | Vertx config-stores reading from classpath |
vertx-config-consul-json | Vertx config-stores reading from Consul |
vertx-config-vault-keycerts | Vertx config-store reading keys and certificates from Vault |
vertx-config-ext | Wrappers for Vertx config-stores providing extra functionality |
vertx-test | Testing tools for vertx-bus |
vertx-test-scala | Testing tools for vertx-bus-scala |
vertx-server-test | Testing tools for vertx-server and modules |
- How to start
- Configuration
- Meta configuration
- Modules configuration
- Event bus communication
- Dependency injection
- HTTP server
- Examples
This section describes how to use vertx-server
module. If you are interested in vertx-bus
functionality then go to configuration or event bus communication.
Add following entry in your pom.xml
or its equivalent if you are not using Maven:
<dependency>
<groupId>com.cloudentity.tools.vertx</groupId>
<artifactId>vertx-server</artifactId>
<version>${vertx-tools.version}</version>
</dependency>
Note: vertx-tools are not available in public Maven repository. Build them first using mvn clean install
command with JDK 8.
meta-config.json
defines how to get the app's configuration. See more details.
The best place to put it is src/main/resources
. Let's assume we want to load configuration from a local file.
The meta-config.json
should contain following code:
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config.json"
}
}
]
}
In previous step we configured the app to read configuration from src/main/resources/config.json
file. The minimal configuration looks like this:
{
"apiServer": {
"http": {
"port": 8080
},
"routes": [
{
"id": "hello-world-route",
"method": "GET",
"urlPath": "/hello"
}
]
},
"registry:routes": {
"hello-world-route": { "main": "example.app.HelloWorldRoute" }
}
}
This configuration makes the HTTP server to start on port 8080 and expose one route GET /hello
that is handled by example.app.HelloWorldRoute
verticle.
Next step is to create a bootstrap class that looks like this:
package example.app;
import com.cloudentity.tools.vertx.launchers.OrchisCommandLauncher;
import com.cloudentity.tools.vertx.server.VertxBootstrap;
import io.vertx.core.Future;
public class App extends VertxBootstrap {
/**
* Your custom app initialization logic.
*/
@Override
protected Future beforeServerStart() {
return Future.succeededFuture();
}
}
And finally we need to implement route handler that returns 200 response with 'Hello world!' string body:
package example.app;
import com.cloudentity.tools.vertx.server.api.routes.RouteService;
import com.cloudentity.tools.vertx.server.api.routes.RouteVerticle;
import io.vertx.core.Future;
import io.vertx.ext.web.RoutingContext;
public class HelloWorldRoute extends RouteVerticle {
@Override
public void handle(RoutingContext ctx) {
ctx.response().end("Hello world!");
}
}
Make sure the following plugin is used to build the app
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>example.app.App</Main-Class>
</manifestEntries>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<artifactSet/>
</configuration>
</execution>
</executions>
</plugin>
Note that the Main-Class references the bootstrap class created in previous section.
When you build a fat jar then you should execute following command to run the app:
java -jar your-app.jar run example.app.App -conf src/main/resources/meta-config.json
If you are interested how the application config looks like you can run it in dry mode with following command:
java -jar your-app.jar print-config example.app.App -conf src/main/resources/meta-config.json
It prints available modules, configuration (with and without resolved modules and references) and environment variables referenced by root configuration and each module.
vertx-bus
project provides solution for configuration management. It implements com.cloudentity.tools.vertx.conf.ConfVerticle
singleton verticle that reads meta-config.json
file in and exposes configuration to other verticles.
The easiest way to have access to configuration from ConfVerticle
is to extend ComponentVerticle
(ServiceVerticle
extends ComponentVerticle
).
ComponentVerticle
implements getConfig()
method that returns JsonObject
with configuration associated with the instance of ComponentVerticle
.
ComponentVerticle
has configPath()
method that returns comma-separated path to the verticle's configuration JsonObject.
Let's assume the global configuration JsonObject is as follows:
{
"apiServer": {
"http": {
"port": 8080
},
"routes": []
},
"components": {
"my-component": {
"message": "Hello world!"
}
}
}
If configPath()
returns "components.my-component" then the verticle's configuration is resolved to { "message": "Hello world!" }
.
Let's have a look at the default implementation of configPath()
and verticle:
public String configPath() {
String configPath = config().getString("configPath");
return configPath != null ? configPath : verticleId();
}
public String verticleId() {
return config().getString("verticleId");
}
configPath()
reads information passed to the verticle at the deployment time in DeploymentOptions.config.configPath
and falls back to verticleId
attribute. You can pass it on your own or use RegistryVerticle
that does it for you.
Note: if you are using Registry to deploy verticles, you can put the configuration with deployment options. See Injecting configuration to verticle
It is possible to make a reference to configuration value. This way it is easy to share common configuration.
The reference is a JSON string with following format "$ref:{configuration-path}"
. The configuration-path in
the reference string is a path at which the value that should be injected is.
For example, the following configuration:
{
"verticle-a": {
"ldap-port": "$ref:ldap.port",
"ldap-host": ["$ref:ldap.host1", "$ref:ldap.host2"]
},
"ldap": {
"port": 1389,
"host1": "localhost",
"host2": "127.0.0.1"
}
}
after reference resolution looks like this:
{
"verticle-a": {
"ldap-port": 1389,
"ldap-host": ["localhost", "127.0.0.1"]
},
"ldap": {
"port": 1389,
"host1": "localhost",
"host2": "127.0.0.1"
}
}
When we call getConfig()
from ComponentVerticle
or ServiceVerticle
we get the configuration with references resolved.
When the path from reference is invalid the attribute is set to null. It is up to consumer to validate the configuration and fail the verticle start.
If we want to define default configuration reference we should provide it in the following format: "$ref:{reference-path}:{default-value-type}:{default-value}"
.
When the value cannot be resolved at {reference-path}
then default-value is cast to default-value-type
and used instead.
E.g. following config resolves server.port to 80:
{
"server": {
"port": "$ref:port:int:8080"
},
"port": 80
}
E.g. following config resolves server.port to 8080:
{
"server": {
"port": "$ref:port:int:8080"
}
}
If we want to cast the referenced value to "string", "int", "double" or "boolean" we should provide it in the following format: "$ref:{reference-path}:{cast-type}"
.
E.g. following config resolves server.port to 80:
{
"server": {
"port": "$ref:port:int"
},
"port": "80"
}
In similar manner we can make reference to system or environment property. The reference for system property has following format
"$sys:{property-name}:{property-type}:{default-value}"
and for environment property "$env:{property-name}:{property-type}:{default-value}"
.
System and environment properties are resolved using System.getProperty
and System.getenv
methods respectively.
property-type
defines what type the property value should be cast to. It is one of "string", "int", "double" or "boolean" (for array and object support see <>).
default-value
is optional. It is used when the property value is missing. When the reference is invalid it is resolved to null
value.
Example, following configuration:
{
"ldap-port": "$env:LDAP_PORT:int:1389"
}
is resolved to this one (provided LDAP_PORT environment property is set to 2636):
{
"ldap-port": 2636
}
or to this one (provided LDAP_PORT is not set):
{
"ldap-port": 1389
}
Default value is optional, so we can have following configuration:
{
"ldap-port": "$env:LDAP_PORT:int"
}
It is possible to use Spring-like property placeholder ${path.to.value:default-value}
.
E.g. following config resolves address
to localhost:8080
:
{
"server": {
"host": "localhost"
"port": 8080
},
"address": "${server.host}:${server.port}"
}
You can use default value if referenced value is missing:
{
"address": "${server.host:localhost}:${server.port:8080}"
}
In above example address
is resolved to localhost:8080
.
NOTE
Spring-like configuration reference value is always converted to string. Use$ref
to preserve or convert the type.
NOTE
Spring-like configuration reference are resolved before $ref, $env and $sys references.
If referenced value is not found in the configuration then it is searched in environment variables.
E.g.:
{
"address": "${HOST:localhost}:${PORT:8080}"
}
In above example HOST
and PORT
environment variables will be checked - if they are missing then localhost
and 8080
default values are used.
It might be the case, that spring-like references should not be resolved (e.g. some mapping uses spring-like references syntax).
To disable spring-like reference resolution configure _ignoreSpringRefPaths
attributes.
It contains a map from a string to array of paths at which spring-like references should not be resolved.
For example, given following configuration:
{ "_ignoreSpringRefPaths": { "ingore-a": ["x.a"], "ingore-b": ["x.b"] }, "y": 100, "x": { "a": { "flag": true "value": "${y}" }, "b": "${y}", "c": "${y}" } }
the resolution of all spring-like references at x.a
and x.b
paths are kept intact. The final configuration is following:
{
"y": 100,
"x": {
"a": {
"flag": true
"value": "${y}"
},
"b": "${y}",
"c": "100"
}
}
The reason why _ignoreSpringRefPaths
is a map is that different modules/config-stores can provide their own paths for
which spring-like references should be ignored.
When using configuration modules controlled by MODULES
environment variable it is impossible to deploy/undeploy a module at runtime.
Also, for unit testing it might be cumbersome to provide environment or system variables.
To overcome that, instead of setting those variables you can define a map in the root configuration at env
or sys
attribute and provide values for the variables.
Environment or system variable is overridden by the value read from corresponding attribute in root configuration.
For example let's have following reference to LDAP_PORT
environment variable:
{
"ldap-port": "$env:LDAP_PORT:int",
"env": {
"LDAP_PORT": 1389
}
}
After resolution, we end up with the following configuration:
{
"ldap-port": 1389,
"env": {
"LDAP_PORT": 1389
}
}
NOTE
Environment variables can be overridden also for spring-like references.
If an environment variable contains sensitive value (e.g. database password) we should read it from secure storage like Vault. Sensitive environment variable might be referenced in multiple places, so trying to overwrite it is cumbersome task. Instead, we can put configuration reference as a value of environment variable fallback.
NOTE
We still should use environment variable in this case. It creates separation between low-level configuration structure and deployment options.
For example let's have following reference to POSTGRES_PASSWORD
environment variable and secrets from secure storage:
{
"postgres-password": "$env:POSTGRES_PASSWORD:string",
"secrets": {
"postgres": {
"password": "#@!"
}
},
"env": {
"POSTGRES_PASSWORD": "$ref:secrets.postgres.password"
}
}
After resolution, we end up with the following configuration:
{
"postgres-password": "#@!",
...
}
Property value can be used as a part of value expression.
To do so, property name should be wrapped in curly braces {
and }
.
For example, given KEY=user
and following configuration:
{
"path": "$env:/apis/{KEY}:string"
}
the value of path
is /apis/user
.
We can also use default value:
{
"path": "$env:/apis/{KEY}:string:session"
}
if KEY
is not set then the value of path
is /apis/session
.
You can use env/sys references to set array or object configuration attribute.
The property_type
in the reference string is array
and object
respectively.
The env/sys value should be string representation of JSON array/object.
As an example, let's have KAFKA_TOPICS env variable set to ["value1", "value2"]
.
The following configuration:
{
"topics": "$env:KAFKA_TOPICS:array"
}
is resolved to:
{
"topics": ["value1", "value2"]
}
You can use default value as well:
{
"topics": "$env:KAFKA_TOPICS:array:[\"value1\", \"value1\"]"
}
If a reference could not be resolved and had no default value then warning is logged.
If configuration attribute is optional you can prepend ?
to its reference path to silence the warning.
{
"consul": {
"tags": "$env:?CONSUL_TAGS:array"
}
}
Reference path and default value can contain escaped colon '\:'.
E.g. Given $ref:path:string:localhost\\:8080
the default value is localhost:8080
.
Let's consider scenario where we need to configure object with one-of alternative via environment variables.
As an example let's configure HttpServerOptions.trustOptions
. To do so you choose one of: PemTrustOptions, JksTrustOptions or PfxTrustOptions.
If you choose PemTrustOptions, then pemTrustOptions
configuration attribute should be set and jksTrustOptions
and pfxTrustOptions
should be null
.
{
"pemTrustOptions": {
"certPaths": ["/etc/ssl/cert.pem"]
}
}
Let's configure trustOptions
using environment variables:
{
"pemTrustOptions": {
"certPaths": "$env:PEM_CERT_PATHS:array"
},
"jksTrustOptions": {
"value": "$env:JKS_VALUE:string"
},
"pfxTrustOptions": {
"value": "$env:PFX_VALUE:string"
}
}
The problem with above configuration is that even if we set only PEM_CERT_PATHS
both jksTrustOptions
and pfxTrustOptions
have value of empty JSON object. We need jksTrustOptions
and pfxTrustOptions
to be set to null
.
In order to do so we need to set _nullify
attribute to true
in all config objects:
{
"pemTrustOptions": {
"_nullify": true,
"certPaths": "$env:PEM_CERT_PATHS:array"
},
"jksTrustOptions": {
"_nullify": true,
"value": "$env:JKS_VALUE:string"
},
"pfxTrustOptions": {
"_nullify": true,
"value": "$env:PFX_VALUE:string"
}
}
If _nullify
attribute is set and all other attributes in JsonObject are null
then entire JsonObject is replaced with null
.
In our example, if PEM_CERT_PATHS
is only variable set then we end up with following final configuration:
{
"pemTrustOptions": {
"certPaths": ["/etc/ssl/cert.pem"]
}
}
See section on ServiceVerticle initialization.
When we start vertx-server
application we need to pass path to meta-config.json
in the command line argument -conf
.
Sample meta-config.json
looks like this:
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config.json"
}
}
],
"vertx": {
"options": {
"addressResolverOptions": {
"servers": ["127.0.0.11"]
}
}
}
The advantage of using meta-config.json
is ease of changing the way configuration is distributed.
Imagine all applications in your system read configuration from local files, but we wanted to make them read it from Consul.
What we need to do is just modify the meta-config.json
providing Consul access configuration and restart the servers.
Another benefit is possibility to split one giant configuration file into several smaller ones that are easier to maintain.
We would just need to list them in the stores section in meta-config
.
The underlying mechanism uses vertx-config project.
Attribute | Description |
---|---|
stores | holds an array of JSON objects that are parsed to io.vertx.config.ConfigStoreOptions |
scanPeriod | defines configuration refresh period in milliseconds |
Every time you call ComponentVerticle.getConfig()
you retrieve version of the configuration that is not older than scan period.
You can register ComponentVerticle
to receive information whenever global configuration changes. To do so you need to call
ComponentVerticle.registerConfChangeConsumer
method passing consumer of io.vertx.config.ConfigChange
object.
You can use enabled
flag to control whether config store should be used. It's set to true
by default:
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"enabled": false,
"config": {
"path": "src/main/resources/config.json"
}
}
]
}
If enabled
is false
then the entry is filtered out from the stores
array. If you reference environment variable
(see Configuration references) then you can control what config stores are used without changing content of meta config file.
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"enabled": "$env:CONF_FILE_ENABLED:boolean:true",
"config": {
"path": "src/main/resources/config.json"
}
}
]
}
Store configuration can be read from a classpath file in store-modules
folder.
Given following meta-config:
{
"scanPeriod": 5000,
"stores": [
{
"module": "config-store-module-a"
},
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-b.json"
}
}
]
}
store-modules/config-store-module-a.json
classpath file is read and its content replaces module JSON object in meta-config.
For example, if store-modules/config-store-module-a.json
has following content:
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-a.json"
}
}
then the resolved meta-config is:
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-a.json"
}
},
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-b.json"
}
}
]
}
Config-store module can contain a JSON array with configuration of multiple config-stores instead of JSON object with single config-store.
If the content of config-store-module-a.json
was:
[
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-a1.json"
}
},
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-a2.json"
}
}
]
then the resolved meta-config is:
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-a1.json"
}
},
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-a2.json"
}
},
{
"type": "file",
"format": "json",
"config": {
"path": "src/main/resources/config-b.json"
}
}
]
}
You can define io.vertx.core.VertxOptions
that will be used to initialize Vertx instance at the application startup.
You need to provide JSON object at "vertx.options" path that is decoded to VertxOptions.
Add Vault store in meta-config.json
:
{
"type": "vault",
"format": "json",
"config": {
"host": "$env:VAULT_HOST:string:localhost",
"port": "$env:VAULT_PORT:int:8200",
"auth-backend": "token",
"token": "$env:VAULT_TOKEN:string",
"path": "secret/{YOUR_APP_NAME}"
}
}
VAULT_HOST, VAULT_PORT and VAULT_TOKEN should be set as ENV variables. In path attribute, replace {YOUR_APP_NAME} with your app name.
Store passwords/secrets in Vault at secret/{YOUR_APP_NAME} You can store only string values in Vault. Reference values stored in Vault in your configuration. The values from Vault are set as top-level configuration attributes.
E.g. data stored in Vault with this command:
vault write secret/my_app x=a y=b
is retrieved by Vertx in a form of JSON object:
{ "x": "a", "y": "b" }
Let's assume we store two passwords in Vault:
vault write secret/my_app pass1=!@#$ pass2=*&^%
and we have following meta-config.json
:
{
"scanPeriod": 5000,
"stores": [
{
"type": "file",
"format": "json",
"config": {
"path": "config.json"
}
},
{
"type": "vault",
"format": "json",
"config": {
"host": "$env:VAULT_HOST:string:localhost",
"port": "$env:VAULT_PORT:int:8200",
"auth-backend": "token",
"token": "$env:VAULT_TOKEN:string",
"path": "secret/my_app"
}
}
]
}
Following config.json:
{
"my-service-verticle": {
"password1": "$ref:pass1",
"password1": "$ref:pass2",
}
}
The final configuration object looks like this:
{
"my-service-verticle": {
"password1": "$ref:pass1",
"password1": "$ref:pass2",
},
"pass1": "!@#$",
"pass2": "*&^%"
}
When configuration references have been resolved then "my-service-verticle" stores passwords from Vault:
{
"my-service-verticle": {
"password1": "!@#$",
"password1": "*&^%",
},
"pass1": "!@#$",
"pass2": "*&^%"
}
When we develop an application we usually want to split it into modules. Some modules implement the same functionality and we choose one of them to run (e.g. modules implementing storage). Other are optional (e.g. module registering app for service-discovery).
In order to make it easier to configure application there is a special configuration attribute modules
introduced.
ConfVerticle
takes that attribute and reads configuration objects from classpath and merges them with root configuration.
Module configuration should reference environment variable for those attributes we want to set for different environments
(urls, hosts, ports, header names, etc.)
Let’s say our root configuration looks like this:
{
"apiServer": {
"http": {
"port": 9090
}
},
"modules": [
"policy-storage/ldap",
"sd-registrar/consul"
]
}
ConfVerticle
tries to read policy-storage/ldap
and sd-registrar/consul
modules configuration from classpath.
The modules configuration should be stored on classpath in modules
folder. This means that ConfVerticle
searches for
modules/policy-storage/ldap.json
and modules/sd-registrar/consul.json
files.
Let modules/policy-storage/ldap.json
has following content:
{
"ldap": {
"host": "localhost",
"port": 9042
}
}
and modules/sd-registrar/consul.json
following:
{
"consul": {
"host": "localhost",
"port": 8500
}
}
ConfVerticle
merges module configuration objects sequentially and then the root configuration is merged last.
The global configuration looks as follows:
{
"apiServer": {
"http": {
"port": 9090
}
},
"ldap": {
"host": "localhost",
"port": 9042
},
"consul": {
"host": "localhost",
"port": 8500
},
"modules": [
"policy-storage/ldap",
"sd-registrar/consul"
]
}
When the modules are merged with root configuration then ConfVerticle
resolves configuration references. See Configuration references.
What actually makes configuration modules useful is the possibility to control what verticles are deployed. In Dependency Injection you learn how to deploy them. The application must start a verticle registry that the modules will use to deploy their verticles.
The simplest root configuration looks like this:
{
"registry:components": {
},
"modules": ["module-a", "module-b"]
}
registry:components
is a placeholder for modules' verticles. Make sure that this registry is programmatically deployed by the application.
The modules configuration can look like this:
{
"registry:components": {
"module-a-verticle": {
"main": "com.example.moduleA.Verticle",
"verticleConfig": {
"someAVerticleAttribute": true
}
}
}
}
and
{
"registry:components": {
"module-b-verticle": {
"main": "com.example.moduleB.Verticle"
}
}
}
When modules configuration and root configuration is merged we end up with following configuration:
{
"registry:components": {
"module-a-verticle": {
"main": "com.example.moduleA.Verticle",
"verticleConfig": {
"someAVerticleAttribute": true
}
},
"module-b-verticle": {
"main": "com.example.moduleB.Verticle"
}
},
"modules": ["module-a", "module-b"]
}
When the application starts the registry:components
is deployed that in turn deploys module-a-verticle
and module-b-verticle
.
If you build your application using modules with environment variable references (see Configuration references it's easy to run it in Docker container.
Your modules
configuration attribute should have following value:
{
"modules": "$env:MODULES:array"
}
This means that you can control what modules are used by setting MODULES
environment variable.
From the perspective of someone who wants to run a docker with your application the process looks as follows:
-
set meta-config environment variables (if any) that control where the configuration is coming from
-
decide what modules you want to run (e.g.
policy-storage/ldap
andsd-registrar/consul
) and setMODULES
environment variable (e.g.policy-storage/ldap,sd-registrar/consul
) -
set required application environment variables
-
run docker
As an extra feature, ConfVerticle
prints out what env variables are used and what values they have.
You can specify a set of default modules that will be used if modules
attribute is not set.
{
"modules": "$env:MODULES:array",
"defaultModules": ["module-a", "module-b"]
}
As a rule of thumb the module configuration file should be complete, i.e. it should not depend on other configuration attributes to be present in the root config. However it still can reference attributes from root configuration - e.g. secrets, but it should be limited to minimum and well documented. In particular, it should deploy all verticles the module requires.
The only exception for deploying verticles should be when the application itself uses a verticle the modules depends on. In this case the verticle is configured in the root configuration so we can be sure it is deployed.
If we have two modules that require the same verticle to be deployed (not used by application itself), then its configuration should be the same in both modules. After modules configuration merge, there is only one instance of it.
Example:
Let's have two modules that require open-api client verticle. The configuration of module A should like like this:
{
"registry:components": {
"sevice-x-client": {
"main": "x.y.z.ServiceAClient",
"verticleConfig": {
"serviceLocation": {
"host": "$env:SERVICE_A_HOST:string:localhost",
"port": "$env:SERVICE_A_PORT:int:8010",
"ssl": "$env:SERVICE_A_SSL:boolean:false"
}
}
},
# other verticles of module A
...
},
# other attributes of module A
}
similarly module B:
{
"registry:components": {
"sevice-x-client": {
"main": "x.y.z.ServiceAClient",
"verticleConfig": {
"serviceLocation": {
"host": "$env:SERVICE_A_HOST:string:localhost",
"port": "$env:SERVICE_A_PORT:int:8010",
"ssl": "$env:SERVICE_A_SSL:boolean:false"
}
}
},
# other verticles of module B
...
},
# other attributes of module B
}
finally we get the following config:
{
"registry:components": {
"sevice-x-client": {
"main": "x.y.z.ServiceAClient",
"verticleConfig": {
"serviceLocation": {
"host": "$env:SERVICE_A_HOST:string:localhost",
"port": "$env:SERVICE_A_PORT:int:8010",
"ssl": "$env:SERVICE_A_SSL:boolean:false"
}
}
},
# other verticles of module A
...
# other verticles of module B
...
},
# other verticles of module A
...
# other attributes of module B
}
If we want to load some modules regardless the deployment (e.g. to split classpath configuration for easier maintenance) we can define them in requiredModules
attribute.
{
"requiredModules": ["module-x"]
}
First requiredModules
are loaded and then all the other modules.
It might be the case that you want to deploy the same module multiple times, but with different configuration values.
Module instance supports id
that can be referenced in the module configuration.
{
"modules": [
{
"module": "module-x",
"id": "a"
},
{
"module": "module-x",
"id": "b"
}
]
}
Let's suppose that module-x
has following configuration:
{
"{MODULE_ID}x": "abc"
}
{MODULE_ID}
is replaced with id
value. If id
is missing then {MODULE_ID}
is removed. In the example above, the final configuration is:
{
"ax": "abc",
"bx": "abc"
}
The id placeholder {MODULE_ID}
can also be used in the attribute value.
Given the following module-x
:
{
"{MODULE_ID}x": "{MODULE_ID}abc"
}
The final configuration would be:
{
"ax": "aabc",
"bx": "babc"
}
Module id placeholder can contain separator that will make the resulting configuration more readable.
Let's use -
separator in module-x
:
{
"{MODULE_ID-}x": "abc"
}
The final configuration would be:
{
"a-x": "abc",
"b-x": "abc"
}
The separator can be any character but }
.
Normally, you would configure module using environment variable references. With module instances you can overwrite env variables per instance.
To do so configure module instances:
{
"modules": [
{
"module": "module-x",
"id": "a",
"env": {
"X": "def"
}
},
{
"module": "module-x",
"id": "b"
}
]
}
First instance of module-x
will use overwritten value of env variable X
. The second instance will use the original value of the variable.
Given the following module-x
and X=abc
:
{
"{MODULE_ID-}x": "$env:X:string"
}
The final configuration will be:
{
"a-x": "def",
"b-x": "abc"
}
A list of module instances can be collected recursively from JSON object tree.
Let's consider following global configuration object:
{
"apiGroups" : {
"x" : {
"xx" : {
"_modules" : [...],
"xxx" : {
"_modules" : [...]
}
}
},
"y" : {
"yy" : {
"_modules" : [...]
}
}
}
}
We want to collect all the module definitions at _modules
keys defined within apiGroups
object.
To do so we need to configure modules
in following way:
{
"modules": [
{
"collect": "tree",
"path": "apiGroups",
"key": "_modules"
}
]
}
Additionally, if we want to append the path of _modules
attribute to id
in the final module definition we need to set idWithPath
to true
.
Let's consider simpler case in full detail. Given following global configuration:
{
"apiGroups" : {
"x" : {
"xx" : {
"_modules" : [
{
"module": "module-x",
"id": "a",
"env": {
"X": "def"
}
}
]
}
}
}
}
and modules
:
{
"modules": [
{
"collect": "tree",
"path": "apiGroups",
"key": "_modules",
"idWithPath": true
}
]
}
then single module instance will be collected and deployed (note id
prepended with path of the module definition):
{
"module": "module-x",
"id": "apiGroups-x-xx-a",
"env": {
"X": "def"
}
}
In order to test modules you can use VertxModuleTest
from vertx-server-test
. VertxModuleTest
provides methods
that load the module configuration and deploy verticle registries.
Given configuration module stored in classpath at modules/path/some-module.json
:
{
"registry:some": {
"x-service": {
"main": "com.example.AVerticle",
}
},
"registry:other": {
"y-service": {
"main": "com.example.BVerticle"
}
},
"x-service": {
"url": "example.com/test"
}
}
you can test the module like this:
public class SomeModuleTest extends VertxModuleTest {
@Test
public void test(TestContext ctx) {
// deploys config verticle with 'path/some-module' module configuration
// then deploys 'some' and 'other' registries
deployModule("path/some-module", "some", "other")
.compose(x -> {
// implement test logic
}).onComplete(ctx.asyncAssertSuccess());
;
}
}
VertxModuleTest
defines additional methods that you can use to deploy module with extra test configuration,
either providing JsonObject or path to file with JSON object format.
deployModule("path/some-module", new JsonObject().put("x-service", ...), "some", "other")
deployModuleWithFileConfig("path/some-module", "path/to/test/configuration.json", "some", "other")
Using Vertx' event bus is quite cumbersome. You need to make sure you send messages on proper address and of proper type.
It's easy to make a mistake that is difficult to discover. To fix this you can use ServiceVerticle
that works like regular Java class,
but in fact you are passing messages via event bus. Note that ServiceVerticle
extends ComponentVerticle
, so you still have access to configuration.
Let's imagine we have a simple verticle that wants to send a string to UpperCaseVerticle
and receive that string in upper-case. We need to do following steps:
import io.vertx.core.Future;
import com.cloudentity.tools.vertx.bus.VertxEndpoint;
public interface UpperCaseService {
@VertxEndpoint(address = "to-uppercase") // address is optional - it is defaulted to full method name
Future<String> toUpperCase(String s);
}
import io.vertx.core.Future;
import com.cloudentity.tools.vertx.bus.ServiceVerticle;
public class UpperCaseVerticle extends ServiceVerticle implements UpperCaseService {
@Override
public Future<String> toUpperCase(String s) {
return Future.succeededFuture(s.toUpperCase());
}
}
When you deploy UpperCaseVerticle
it's gonna register event-bus consumer on the address configured in the VertxEndpoint
annotation of UpperCaseService.toUpperCase()
. If the address is not set it is defaulted to full name of the method in
the follwoing format: {class-name}.{method-name}({comma-separated-parameter-types})
.
When the message is received on that address the body of the message is unpacked and passed to the implementation of toUpperCase
method in UpperCaseVerticle
.
The value returned by toUpperCase is sent back to the message sender.
Note that all the methods in UpperCaseService
interface return a Future
even though the implementation in UpperCaseVerticle.toUpperCase
might
have been synchronous (i.e. s.toUpperCase()
). It is so due to the fact that the interface is used also by the client,
so there will be asynchronous operations to send and receive messages over event bus.
If there is no need to return any value then the method in the service interface should return void
.
Under the hood the message will not be sent but published.
import io.vertx.core.Future;
import com.cloudentity.tools.vertx.bus.VertxEndpoint;
public interface NotifierService {
@VertxEndpoint
void notify(String event);
}
import io.vertx.core.Future;
import com.cloudentity.tools.vertx.bus.ServiceVerticle;
public class NotifierVerticle extends ServiceVerticle implements NotifierService {
@Override
void notify(String event) {
// do something with `event`
}
}
import io.vertx.core.Future;
import com.cloudentity.tools.vertx.bus.VertxBus;
import com.cloudentity.tools.vertx.bus.VertxEndpointClient;
public class ClientVerticle extends AbstractVerticle {
public void start() {
VertxBus.registerPayloadCodec(vertx.eventBus()); // you don't need this line if you use VertxBootstrap in your project
UpperCaseService client = VertxEndpointClient.make(vertx, UpperCaseService.class); // when extending ComponentVerticle use: createClient(UpperCaseService.class);
Future<String> response = client.toUpperCase("hello world!");
response.setHandler(async -> {
if (async.succeeded()) {
System.out.println("hello world to upper-case is " + async.result());
}
});
}
}
VertxEndpointClient.make()
builds a proxy object using reflection. The proxy uses event-bus to send messages on addresses defined
in the VertxEndpoint
annotation on UpperCaseService
interface. If you are extending ServiceVerticle
or ComponentVerticle
then instead of using
VertxEndpointClient
use ComponentVerticle.createClient
method.
import io.vertx.core.Future;
import com.cloudentity.tools.vertx.bus.VertxBus;
import com.cloudentity.tools.vertx.bus.VertxEndpointClient;
public class ClientVerticle extends ComponentVerticle {
@Override
protected void initComponent() {
UpperCaseService client = createClient(UpperCaseService.class);
Future<String> response = client.toUpperCase("hello world!");
response.setHandler(async -> {
if (async.succeeded()) {
System.out.println("hello world to upper-case is " + async.result());
}
});
}
}
When you create a client using VertxEndpointClient
or createClient
then by default all the calls timeout after 30 seconds.
You can change that timeout by setting VERTX_SERVICE_CLIENT_TIMEOUT
system or environment variable (in milliseconds).
It applies to all clients unless they are created using VertxEndpointClient
and DeliveryOptions
as argument.
DeliveryOptions
should have sendTimeout
property set.
In vertx-server we are using hierarchy of verticles: ComponentVerticle - ServiceVerticle - RouteVerticle.
Using vanilla Vertx you would override AbstractVerticle.start()
and AbstractVerticle.start(Future)
methods to initialize your verticles.
We could do the same when extending base verticles from vertx-server, but it would be quite tricky due to the need to call
super.start(Future)
. Moreover, it's easy to forget about calling super. Instead of overriding start methods you should override
initService
, initServiceAsync
or initComponent
, initComponentAsync
.
Let's follow Initialization sequence of ServiceVerticle
. It covers initialization of ComponentVerticle
, since one extends the other.
- ServiceVerticle.start(Future)
- call ComponentVerticle.start(Future)
- load verticle's configuration
- call AbstractVerticle.start()
- call ComponentVerticle.initComponent
- call ComponentVerticle.initComponentAsync
- register event bus consumers based on VertxEndpoint annotations
- call ServiceVerticle.initService
- call ServiceVerticle.initServiceAsync
- call ComponentVerticle.start(Future)
If your ServiceVerticle
or ComponentVerticle
needs to do some cleanup when the verticle is stopped, e.g. close connection pool when closing application, then implement one of cleanup
or cleanupAsync
.
These methods are called when Vertx executes AbstractVerticle.stop
method.
public class ResourceVerticle extends ComponentVerticle {
SomeResource resource;
@Override
protected void initComponent() {
resource = createResource();
}
...
@Override
protected void cleanup() {
if (resource != null) {
resource.close();
}
}
}
public class ResourceVerticle extends ComponentVerticle {
SomeResource resource;
@Override
protected void initComponent() {
resource = createResource();
}
...
@Override
protected Future cleanupAsync() {
Handler<Future> action = fut -> {
if (resource != null) {
resource.close();
fut.complete();
}
};
Future promise = Future.future();
vertx.executeBlocking(action, promise);
return promise;
}
}
RegistryVerticle
and ServiceVerticle
provides Dependency Injection capabilities. ServiceVerticle
gives you the way to define interface
with VertxEndpoint
annotations. RegistryVerticle
allows to define what verticles should be deployed.
In order to use RegistryVerticle
you need to come up with its identifier. Let's our id be "components".
Following code snippet deploys "components" RegistryVerticle
at the application start:
import com.cloudentity.tools.vertx.registry.RegistryVerticle;
import com.cloudentity.tools.vertx.verticles.VertxDeploy;
public class App extends VertxBootstrap {
@Override
protected Future beforeServerStart() {
return VertxDeploy.deploy(vertx, new RegistryVerticle(new RegistryType("components")));
}
}
The "components" RegistryVerticle
gets its configuration at "registry:components" key and deploys defined verticles.
The minimal registry configuration has following structure:
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.VerticleA"
},
"verticle-b-id": {
"main": "com.example.VerticleB"
}
}
}
RegistryVerticle
reads verticle ids ("verticle-a-id", "verticle-b-id") and deploys corresponding verticles defined under "main" key.
The value of "main" is full name of verticle's class. The deployment order is undefined. When at least one verticle fails to start
it means that RegistryVerticle
deployment fails as well.
The verticle's id can be accessed from verticle's code with AbstractVerticle.config().getString("verticleId")
method call.
IMPORTANT: config
is reserved key in the registry configuration object, it can't be used as verticle id.
NOTE: VertxBootstrap
deploys system-init
registry before HTTP server start (before beforeServerStart
method is executed)
and system-ready
after HTTP server start (before afterServerStart
method).
Deployment strategy controls how many instances of a verticle is deployed.
By default simple
strategy is used, which uses options.instances
attribute from verticle descriptor.
The following configuration deploys 5 instances of `com.example.Verticle``:
{
"registry:components": {
"verticle-id": {
"main": "com.example.Verticle",
"options": {
"instances": 5
}
}
}
}
Default value of options.instances
is 1.
CPU deployment strategy deploys one verticle instance per available CPU.
To use it set deploymentStrategy
to cpu
in verticle descriptor:
{
"registry:components": {
"verticle-id": {
"main": "com.example.Verticle",
"deploymentStrategy": "cpu"
}
}
}
If you want to deploy 2 times number of CPUs then use cpux2
deployment strategy.
You can define default deployment strategy for all verticles in the registry. If deployment strategy is not defined in the verticle descriptor, then default one is used. To do so set config.defaultDeploymentStrategy
:
.default deployment strategy
{
"registry:components": {
"config": {
"defaultDeploymentStrategy": "cpu"
},
"verticle-id": {
"main": "com.example.Verticle"
}
}
}
When you are deploying ComponentVerticle
or ServiceVerticle
you can override its default configuration path TODO (see default implementation of configPath()
in Configuration and verticles.
To do so add "configPath" string at the level of "main" key:
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"configPath": "components.verticle-a"
}
},
"components": {
"verticle-a": {
...
}
}
}
In result, "verticle-a-id" verticle will get it's configuration from "components.verticle-a" object.
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"configPath": "components.verticle-a"
}
},
"components": {
"verticle-a": {
"someGlobalConfig": "$ref:globalConfig",
"anotherConfig" : "$ref:components.aConfig",
"anotherConfigKey" : "$ref:components.aConfig.anotherKey",
....
},
"aConfig" : {
"aKey" : "aValue",
"anotherKey": "anotherValue"
}
},
"globalConfig" : {
"a" : "value",
"b": "value1"
}
}
In result, "verticle-a-id" verticle will get it's configuration from "components.verticle-a" object and resolved json path references.
You can keep verticle's configuration next to its deployment options.
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"verticleConfig": {
"ttl": 1000
}
}
}
}
In result, "verticle-a-id" verticle is configured with { "ttl": 1000 }
.
ServiceVerticle.vertxServiceAddressPrefix()
method allows to deploy multiple verticles implementing the same @VertxEndpoint
interface.
Instead of setting the address value programmatically we can use verticle's id defined in Registry
configuration.
To do so set prefix
attribute to true:
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"prefix": true
}
}
}
In this case vertxServiceAddressPrefix()
returns verticle-a-id
.
Alternatively, we can set prefix
to custom address:
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"prefix": "address-prefix"
}
}
}
You can make the RegistryVerticle to deploy verticle using custom io.vertx.core.DeploymentOptions defined in configuration file. To do so add JSON object "options" key at the level of "main" key.
For example, let's deploy 4 instances ServiceVerticleA:
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"options": {
"instances": 4
}
}
}
}
You can skip deployment of a verticle defined in registry. To do so, set enabled
flag to false.
E.g.
{
"registry:components": {
"verticle-a-id": {
"main": "com.example.ServiceVerticleA",
"enabled": false
}
}
}
NOTE
For backward compatibility you can use 'disabled' flag. Set it to true to skip verticle deployment.
By default, registry deploys verticles in undefined order. You can enforce ordering of verticles deployment using 'dependsOn' attribute.
E.g.
{
"registry:components": {
"verticle-a": {
"main": "com.example.ServiceVerticleA",
"dependsOn": ["verticle-b", "verticle-c"]
},
"verticle-b": {
"main": "com.example.ServiceVerticleB"
},
"verticle-c": {
"main": "com.example.ServiceVerticleC"
}
}
}
Using above configuration, registry 'components' deploys verticles 'verticle-b' and 'verticle-c' first and when they are up then it deploys 'verticle-a'.
vertx-server
gives you easy way to define HTTP APIs. You've already seen in TODO <> section how to configure
and implement simple HTTP route. Once you created and deployed RouteVerticle
you need to implement handle(RoutingContext)
method.
The vertx-web docs will guide you how to do it (you may want to cut
to the chase).
Let's focus on routes configuration now.
{
"apiServer": {
...
"routes": [
{
"id": "route-id",
"handler": "route-handler", // optional, defaults to value in 'id' attribute
"method": "GET",
"urlPath": "/hello",
"skipBodyHandler": false // optional, default value 'false'
}
]
},
"registry:routes": {
"route-handler": { "main": ... }
},
...
}
Route configuration has following fields:
- id - route identifier
- handler - defines what RouteVerticle defined in "registry:routes" handles the route, optional, defaults to value in 'id' attribute
- method - HTTP method of the route
- urlPath - path of the route
- skipBodyHandler - defines whether
io.vertx.ext.web.handler.BodyHandler
should be registered on the route, optional, default valuefalse
Route configuration is used to register io.vertx.ext.web.Route
using io.vertx.ext.web.Router.route(HttpMethod, String)
.
If method attribute in configuration is missing then Router.route(String)
method is used instead (effectively the Route matches all requests with given urlPath regardless HTTP method).
NOTE
routes
can also be defined as a map from string to an array of route objects. All the arrays are joined in a single array and then the logic for default array routes configuration format applies. The arrays are joined in alphabetical order of keys.
You can set server's base path using 'basePath' attribute, e.g.:
{
"apiServer": {
...
"basePath": "/api"
"routes": [
{
"id": "route-id",
"method": "GET",
"urlPath": "/hello"
},
{
"id": "other-route-id",
"method": "GET",
"urlPath": "/hi"
}
]
},
...
}
The above configuration defines two routes that will be exposed at /api/hello
and /api/hi
paths.
If you are using classpath
vertx-store then you might need to modify the default routes configuration.
To avoid overriding entire routes
array you can use disabledRoutes
and appendRoutes
or prependRoutes
attributes.
The reason for having separate appendRoutes
and prependRoutes
is that Vertx routes are being matched sequentially,
so you may want to execute some routes before or after others. The final list of routes consists of prependRoutes
, routes
and appendRoutes
.
E.g. let's disable "route-id" route:
{
"apiServer": {
...
"routes": [
{
"id": "route-id",
...
}
]
},
"disabledRoutes": [ "route-id" ]
...
}
E.g. let's append extra route:
{
"apiServer": {
...
"routes": [
...
]
},
"appendRoutes": [
{
"id": "extra-route-id",
"method": "GET",
"urlPath": "/extra/hello"
}
]
...
}
If you want to apply HTTP filters to your route you need to add filter configuration in filters
attribute, e.g.:
{
"apiServer": {
...
"routes": [
{
"id": "route-id",
"method": "GET",
"urlPath": "/hello",
"filters": [ "my-filter" ]
}
]
}
...
}
Make sure that registry:filters
contains all the filters you need, e.g.:
{
"registry:filters": {
"my-filter": { "main": "com.example.MyFilter" }
}
...
}
If the filter you use has some configuration you can pass it in instead of filter name in filters
attribute following way:
{
"apiServer": {
...
"routes": [
{
"id": "route-id",
"method": "GET",
"urlPath": "/hello",
"filters": [
{
"name": "my-filter",
"conf": { "param": "value" }
}
]
}
]
}
...
}
HTTP filter is a ServiceVerticle that implements com.cloudentity.tools.vertx.server.api.filters.RouteFilter
interface.
RouteFilter has two methods:
public interface RouteFilter {
@VertxEndpoint
Future applyFilter(RoutingContext ctx, String rawJsonConf);
@VertxEndpoint
Future<RouteFilterConfigValidation> validateConfig(String rawJsonConf);
}
In applyFilter
implement you filtering logic. Remember to call RoutingContext.next()
to pass the context to next filter or RouteVerticle
for handling.
validateConfig
is invoked at app startup to validate configuration of all applications of the filter.
If some configurations are invalid then the app fails to start.
The configuration is sent as string-representation of JSON.
E.g. rawJsonConf
contains null
with following configuration:
...
"routes": [
{
...
"filters": ["my-filter"]
}
]
...
E.g. rawJsonConf
contains { "param": "value" }
with following configuration:
...
"routes": [
{
...
"filters": [
{
"name": "my-filter",
"conf": { "param": "value" }
}
]
}
]
...
E.g. rawJsonConf
contains "param"
with following configuration:
...
"routes": [
{
...
"filters": [
{
"name": "my-filter",
"conf": "param"
}
]
}
]
...
You can use ScalaRouteFilterVerticle
as a base for your filter.
Let's implement a filter that returns 401 if "role" header doesn't contain configured value.
Configuration looks like this:
...
"routes": [
{
...
"filters": [
{
"name": "role-security",
"conf": {
"role": "admin"
}
}
]
}
]
...
Now we can implement our RoleSecurityFilter
:
import io.circe.generic.semiauto._
case class RoleSecurityFilter(role: String)
class RoleSecurityFilter extends ScalaRouteFilterVerticle[RoleSecurityFilter] with RouteFilter {
override def confDecoder: Decoder[RoleSecurityFilter] = deriveDecoder[RoleSecurityFilter]
override def filter(ctx: RoutingContext, conf: RoleSecurityFilter): Unit =
if (conf.role == ctx.request().getHeader("role")) {
ctx.next()
} else {
ctx.response().setStatusCode(401).end()
}
override def checkConfigValid(conf: RoleSecurityFilter): RouteFilterConfigValidation =
if (conf == "admin" || conf == "user") RouteFilterConfigValidation.success()
else RouteFilterConfigValidation.failure(s"Invalid role '${conf.role}'")
}
ScalaRouteFilterVerticle
is generic with regard to type of configuration.
We need to define configuration decoder in confDecoder
and implement filter
and checkConfigValid
methods using decoded configuration.
If your filter does not accept configuration use Unit
as type of configuration and io.circe.Decoder.decodeUnit
as confDecoder
.
Note: ScalaRouteFilterVerticle
caches decoded configuration.
vertx-server uses io.vertx.core.http.HttpServer
as underlying implementation.
You can provide its io.vertx.core.http.HttpServerOptions
in "apiServer.http" configuration.
For example, let's define that the HTTP server starts on port 8081 and binds to localhost:
{
"apiServer": {
"http": {
"port": 8081,
"host": "localhost"
},
"routes": [
{
...
]
},
...
}
By default configuration of API server and its routes and filters are at apiServer
, registry:routes
and registry:filters
configuration paths.
If you want to deploy another API Server you can do that programmatically using com.cloudentity.tools.vertx.server.api.ApiServerDeployer.deploy(Vertx, String)
method.
The second argument is verticle id of the API server.
static void deployApiServer(Vertx vertx) {
ApiServerDeployer.deploy(vertx, "anotherApiServer");
}
the configuration should look like this:
{
"anotherApiServer": {
"routesRegistry": "another-routes",
"filtersRegistry": "another-filters",
"http": {
"port": 8082,
"host": "localhost"
},
"routes": [
{
...
]
},
...
"registry:another-routes": {
...
},
"registry:another-filters": {
...
}
}
anotherApiServer.routesRegistry
and anotherApiServer.filtersRegistry
attributes define names of the corresponding registries.
We gonna create RouteVerticle that serves static content read from configuration.
Add route definition at "apiServer.routes" in config.json:
{
"id": "accessing-configuration-route",
"method": "GET",
"urlPath": "/config"
}
- Set any JSON object as route configuration at "accessing-configuration-route" in config.json:
...
"accessing-configuration-route": {
"content": "Hello world!"
}
...
- Create AccessingConfigurationRoute verticle:
package examples.app.routes;
...
public class AccessingConfigurationRoute extends RouteVerticle {
@Override
public void handle(RoutingContext ctx) {
String content = getConfig().getString("content");
ctx.response().setStatusCode(200).end(content);
}
}
- Add AccessingConfigurationRoute to "registry:routes" in config.json:
...
"registry:routes": {
"accessing-configuration-route": { "main": "examples.app.routes.AccessingConfigurationRoute" }
}
...
- RouteVerticle injection
Final config.json:
{
"apiServer": {
"http": {
"port": 8081
},
"routes": [
{
"id": "accessing-configuration-route",
"method": "GET",
"urlPath": "/config"
}
]
},
"registry:routes": {
"accessing-configuration-route": { "main": "examples.app.routes.AccessingConfigurationRoute" }
},
"accessing-configuration-route": {
"content": "Hello world!"
}
}
We gonna create a RandomGenerator service that generates random integer smaller than value given as method argument. Next, we create a RouteVerticle that reads request path parameter and passes it to RandomGenerator to generate value.
- Add route definition at "apiServer.routes" in config.json:
{
"id": "calling-singleton-route",
"method": "GET",
"urlPath": "/random/:max"
}
- Add CallingSingletonServiceRoute to "registry:routes" in config.json:
...
"registry:routes": {
"calling-singleton-route": { "main": "examples.app.routes.CallingSingletonServiceRoute" }
}
...
- Create RandomGenerator service:
package examples.app.components;
...
public interface RandomGeneratorService {
@VertxEndpoint
Future<Integer> generate(int max);
}
...
public class RandomGenerator extends ServiceVerticle implements RandomGeneratorService {
@Override
public Future<Integer> generate(int max) {
return Future.succeededFuture(new Random().nextInt(max));
}
}
- You can deploy RandomGenerator manually or use RegistryVerticle - the latter is preferred. 4.1 Deploy RandomGenerator in bootstrap class:
public class App extends VertxBootstrap {
@Override
protected Future beforeServerStart() {
return VertxDeploy.deploy(vertx, new RandomGenerator());
}
}
4.2 Deploy RandomGenerator using RegistryVerticle:
4.2.1 Add "registry:components" entry in config.json and add ServiceVerticle:
...
"components:registry": {
"random-generator": { "main": "examples.app.components.RandomGenerator" }
}
...
4.2.2 Deploy "components:registry" in bootstrap class:
public class App extends VertxBootstrap {
@Override
protected Future beforeServerStart() {
return VertxDeploy.deploy(vertx, new RegistryVerticle(new RegistryType("components")));
}
}
- Create CallingSingletonServiceRoute verticle:
package examples.app.routes;
public class CallingSingletonServiceRoute extends RouteVerticle {
private static final Logger log = LoggerFactory.getLogger(CallingSingletonServiceRoute.class);
private RandomGeneratorService client;
@Override
protected void initService() {
client = createClient(RandomGeneratorService.class);
}
@Override
public void handle(RoutingContext ctx) {
int max = Integer.valueOf(ctx.request().getParam("max"));
client.generate(max).setHandler(async -> {
if (async.succeeded()) {
ctx.response().setStatusCode(200).end(async.result().toString());
} else {
log.error("Could not generate random value", async.cause());
ctx.response().setStatusCode(500).end();
}
});
}
}
Final config.json:
{
"apiServer": {
"http": {
"port": 8081
},
"routes": [
{
"id": "calling-singleton-route",
"method": "GET",
"urlPath": "/random/:max"
}
]
},
"registry:routes": {
"calling-singleton-route": { "main": "examples.app.routes.CallingSingletonServiceRoute" }
},
"registry:components": {
"random-generator": { "main": "examples.app.components.RandomGenerator" }
}
}
We gonna create a DateTimeGenerator service that returns string representation of current date-time. It reads the timezone from configuration. There will be two instances of DateTimeGenerator: GMT and Europe/Warsaw. The RouteVerticle will decide what time generator should be used based on the request parameter.
- Add route definition at "apiServer.routes" in config.json:
{
"id": "calling-non-singleton-route",
"method": "GET",
"urlPath": "/date-time/:timer"
}
- Add CallingNonSingletonServiceRoute to "registry:routes" in config.json:
...
"registry:routes": {
"calling-non-singleton-route": { "main": "examples.app.routes.CallingNonSingletonServiceRoute" }
}
...
- Create DateTimeGeneratorVerticle. To be able to differentiate between different instances of the same ServiceVerticle we need to return event-bus address prefix in ServiceVerticle.vertxServiceAddressPrefix():
package examples.app.components;
...
public interface DateTimeGeneratorService {
@VertxEndpoint
Future<String> generate();
}
...
public class DateTimeGeneratorVerticle extends ServiceVerticle implements DateTimeGeneratorService {
private ZoneId zone;
@Override
public Future<String> generate() {
ZonedDateTime now = ZonedDateTime.now(zone);
return Future.succeededFuture(now.toString());
}
@Override
protected void initService() {
String zoneIdString = getConfig().getString("zoneId");
zone = ZoneId.of(zoneIdString);
}
@Override
protected Optional<String> vertxServiceAddressPrefix() {
return Optional.ofNullable(verticleId());
}
}
- Add "registry:timers" entry in config.json:
...
"components:timers": {
"gmt-timer": { "main": "examples.app.components.DateTimeGeneratorVerticle" },
"cest-timer": { "main": "examples.app.components.DateTimeGeneratorVerticle" }
}
...
- Deploy "components:timers" in bootstrap class:
public class App extends VertxBootstrap {
@Override
protected Future beforeServerStart() {
return VertxDeploy.deploy(vertx, new RegistryVerticle(new RegistryType("timers")));
}
}
- Add configuration for "gmt-timer" and "cest-timer" verticles:
{
"gmt-timer": {
"zoneId": "GMT"
},
"cest-timer": {
"zoneId": "Europe/Warsaw"
}
}
- Create CallingNonSingletonServiceRoute verticle:
package examples.app.routes;
public class CallingNonSingletonServiceRoute extends RouteVerticle {
private static final Logger log = LoggerFactory.getLogger(CallingSingletonServiceRoute.class);
private ServiceClientsRepository<DateTimeGeneratorService> clientRepo;
@Override
protected Future initServiceAsync() {
return ServiceClientsFactory.build(vertx.eventBus(), "timers", DateTimeGeneratorService.class)
.map((repo) -> clientRepo = repo);
}
@Override
public void handle(RoutingContext ctx) {
String timer = ctx.request().getParam("timer");
DateTimeGeneratorService client = clientRepo.get(timer);
if (client != null) {
client.generate()
.setHandler(async -> {
if (async.succeeded()) {
ctx.response().setStatusCode(200).end(async.result());
} else {
log.error("Could not generate date-time value", async.cause());
ctx.response().setStatusCode(500).end();
}
});
} else {
ctx.response().setStatusCode(400).end("Time generator not found");
}
}
}
Final config.json:
{
"apiServer": {
"http": {
"port": 8081
},
"routes": [
{
"id": "calling-non-singleton-route",
"method": "GET",
"urlPath": "/date-time/:timer"
}
]
},
"registry:routes": {
"calling-non-singleton-route": { "main": "examples.app.routes.CallingNonSingletonServiceRoute" }
},
"registry:timers": {
"gmt-timer": { "main": "examples.app.components.DateTimeGeneratorVerticle" },
"cest-timer": { "main": "examples.app.components.DateTimeGeneratorVerticle" }
},
"gmt-timer": {
"zoneId": "GMT"
},
"cest-timer": {
"zoneId": "Europe/Warsaw"
}
}