What's new in Vert.x 4.5
Vert.x 4.5 comes a couple of new exciting features.
Here is an overview of the most important features and changes supported in Vert.x 4.5.
Virtual threads
Java 21 has finally brought virtual threads to Java and you can use them right now in Vert.x to write code that looks like it is synchronous.
You still write the traditional Vert.x code processing events, but you have the opportunity to write synchronous code for complex workflows and use thread locals in such workflows.
A virtual thread verticle is capable of awaiting Vert.x futures and gets the result synchronously.
Verticle verticle = new AbstractVerticle() {
@Override
public void start() {
HttpClient client = vertx.createHttpClient();
HttpClientRequest req = Future.await(client.request(
HttpMethod.GET,
8080,
"localhost",
"/"));
HttpClientResponse resp = Future.await(req.send());
int status = resp.statusCode();
Buffer body = Future.await(resp.body());
}
};
// Run the verticle a on virtual thread
vertx.deployVerticle(verticle, new DeploymentOptions().setThreadingModel(ThreadingModel.VIRTUAL_THREAD));
Vert.x virtual threads can block on any Vert.x future using await
:
// create a test table
await(pool.query("create table test(id int primary key, name varchar(255))").execute());
// insert some test data
await(pool.query("insert into test values (1, 'Hello'), (2, 'World')").execute());
// query some data
RowSet<Row> rows = await(pool.query("select * from test").execute());
for (Row row : rows) {
System.out.println("row = " + row.toJson());
}
You can find more in our example repo.
Dynamic SQL connection creation
By default, a connection pool always connects to the same host, in other words the database config is static.
Sometimes database config needs to be dynamic, e.g. connecting to an array of databases or the database config can change.
With dynamic connection configuration you can easily implement this in Vert.x:
Pool pool = PgBuilder.pool()
.with(poolOptions)
.connectingTo(() -> {
Future<SqlConnectOptions> connectOptions = retrieveOptions();
return connectOptions;
})
.using(vertx)
.build();
Each time the pool needs to create a connection, the options supplier is called and the returned options is used to create the connection.
PG bouncer transaction pooling mode
Level 7 proxies can load balance queries on several connections to the actual database. When it happens, the client can be confused by the lack of session affinity and unwanted errors can happen like ERROR: unnamed prepared statement does not exist (26000).
Vert.x SQL client now supports Level 7 proxies like PgBouncer.
TCP SSL options update
You can now update TCP client/server SSL options at runtime which is very useful for certificate rotation.
Future<Boolean> fut = server.updateSSLOptions(
new SSLOptions()
.setKeyCertOptions(new JksOptions()
.setPath("/path/to/your/server-keystore.jks")
.setPassword("password-of-your-keystore")));
New connections will use the updated configuration.
WebSocket client
We have captured the WebSocket client API from Vert.x HTTP client in a new WebSocket client.
WebSocketClient wsClient = vertx.createWebSocketClient();
Future<WebSocket> f = wsClient.connect(connectOptions);
This purpose of this change is to let the HttpClient
interface focus on HTTP interactions
and clean up the interface.
Client builders
We start to introduce the builder pattern for advanced client creation in 4.5.
The builder pattern facilitates the configuration and creation of Vert.x clients when they need to be configured beyond options.
HTTP client builder
Most of the time you will create an HTTP client using the good old createHttpClient
method.
HttpClient client = vertx.createHttpClient(options);
Sometimes you want to configure the client with extra behavior like setting a connection handler or a redirect handler: the HTTP client builder provides these extra configuration capabilities.
// Since Vert.x 4.5 the following code
HttpClient client = vertx.createHttpClient(options)
.connectionHandler(connectionHandler)
.redirectHandler(redirectHandler);
// Should be replaced by
HttpClient client = vertx.httpClientBuilder()
.withOptions(options)
.withConnectionHandler(connectionHandler)
.withRedirectHandler(redirectHandler)
.build();
This enforces the configuration of handlers at creation time and produces an immutable client with its configuration and handlers.
The fact is Vert.x 5 will provide more configuration capabilities like a client side load balancer and address resolver.
SQL connection pool builder
Vert.x SQL client pool creation uses per database static pool creation methods, e.g.
PgPool#create
.
There are a couple of good reasons to get away from PgPool
like interfaces:
- those interfaces extend
Pool
but they never add new methods - configuring extra behaviour like
connectHandler
is not well suited
// Since Vert.x 4.5 the following code
PgPool client = PgPool.pool(vertx, connectOptions, poolOptions);
// Should be replaced by
Pool client = Pool.create(vertx, connectOptions, poolOptions);
// Or by
Pool client = PgBuilder.pool()
.with(poolOptions)
.connectingTo(connectOptions)
.using(vertx)
.build()
The latter is well suited when extra behavior is needed like setting a connectHandler
.
Pool client = PgBuilder.pool()
.with(poolOptions)
.withConnectHandler(connectHandler)
.connectingTo(connectOptions)
.using(vertx)
.build()
Redis command tracing
The Redis client can trace command execution when Vert.x has tracing enabled.
The client reports a client span with the following details:
- operation name: Command
- tags:
- db.user: the database username, if set
- db.instance: the database number, if known (typically 0)
- db.statement: the Redis command, without arguments (e.g. get or set)
- db.type: redis
Traffic shaping
TCP server (net/HTTP) can be configured with traffic shaping options to enable bandwidth limiting.
Vert.x API extensions for coroutines
The Vert.x EventBus
and MessageConsumer
objects are extended with support for coroutines inside a coroutineEventBus
scope function:
val bus = vertx.eventBus()
coroutineEventBus {
bus.coConsumer<String>("some-address") {
computeSomethingWithSuspendingFunction()
it.reply("done")
}
}
The scope function is not necessary if the surrounding type implements io.vertx.kotlin.coroutines.CoroutineEventBusSupport
.
For example, with a coroutine verticle:
class VerticleWithCoroutineEventBusSupport : CoroutineVerticle(), CoroutineEventBusSupport {
override suspend fun start() {
val bus = vertx.eventBus()
bus.coConsumer<String>("some-address") {
// call suspending functions and do something
}
}
}
Similarly, the Vert.x Web Router
and Route
objects are extended with support for coroutines inside a coroutineRouter
scope function:
val router = Router.router(vertx)
coroutineRouter {
// Route.coRespond is similar to Route.respond but using a suspending function
router.get("/my-resource").coRespond {
// similar to Route.respond but using a suspending function
val response = computeSomethingWithSuspendingFunction()
response // sent by Vert.x to the client
}
// Router.coErrorHandler is similar to Router.errorHandler but using a suspending function
router.coErrorHandler(404) { rc ->
val html = computeHtmlPageWithSuspendingFunction()
rc.response().setStatusCode(404).putHeader(CONTENT_TYPE, TEXT_HTML).end(html)
}
}
Again, the scope function is not necessary if the surrounding type implements io.vertx.kotlin.coroutines.CoroutineRouterSupport
.
For example, with a coroutine verticle:
class VerticleWithCoroutineRouterSupport : CoroutineVerticle(), CoroutineRouterSupport {
override suspend fun start() {
val router = Router.router(vertx)
router.get("/my-resource").coRespond {
// call suspending functions and build response
}
}
}