Vert.x
Reactive application development toolkit on the JVM. Achieves high performance and high concurrency through event-driven, non-blocking architecture.
GitHub Overview
eclipse-vertx/vert.x
Vert.x is a tool-kit for building reactive applications on the JVM
Topics
Star History
Web Framework
Vert.x
Overview
Vert.x is a reactive application building toolkit known as "the Java framework for busy servers". It adopts an event-driven, single-threaded, non-blocking I/O model, enabling high performance handling of numerous concurrent connections with a small number of kernel threads. It's an asynchronous network application framework based on Netty.
Details
Vert.x is an open-source project developed and maintained by the Eclipse Foundation. As of 2024, active development continues, and it consistently ranks in the top 5 of TechEmpower Web Framework Benchmarks, demonstrating superior performance over renowned frameworks like Express.js, ASP.NET Core, and Spring Boot.
Key architectural features:
- Reactive Non-blocking: High concurrency processing through event loop model
- Verticle Architecture: Modular design enabling independent deployment and scaling
- Event Bus: Asynchronous messaging mechanism between Verticles
- Polyglot Support: Support for Java, JavaScript, Groovy, Ruby, Scala, Kotlin
- Toolkit Approach: Ability to select and combine only necessary functions
Vert.x is designed as a "toolkit" rather than a "framework", imposing no strong constraints on application structure and allowing developers to freely select and combine necessary modules and clients.
Advantages and Disadvantages
Advantages
- Exceptional Performance: Achieves significant speed improvements compared to Spring Boot in latest benchmarks
- High Concurrency Handling: Efficiently processes numerous concurrent requests with few threads
- Resource Efficiency: Lower CPU and memory consumption compared to traditional blocking I/O-based frameworks
- Polyglot: Multi-JVM language support enables easy integration with existing codebases
- Modular Design: Lightweight and flexible architecture allowing selection of only necessary features
- Comprehensive Ecosystem: Complete stack for Web APIs, databases, messaging, and cloud compatibility
- Low Learning Curve: Relatively accessible design for new developers
- Container Optimization: Excellent performance in constrained environments like VMs and containers
Disadvantages
- Code Complexity: Non-blocking code is more difficult to read, write, and debug
- Callback Hell: Risk of readability degradation due to numerous nested callbacks
- Blocking Operation Pitfalls: Risk of accidentally including blocking code
- Overkill for Simple Apps: Unsuitable for CRUD applications that don't require high concurrency or low latency
- Reactive Programming Learning Curve: Requires specialized knowledge for concept understanding and blocking operation avoidance
- Debugging Complexity: Debugging asynchronous processing can become complex
Reference Links
- Eclipse Vert.x Official Site
- Vert.x GitHub Repository
- Vert.x Official Documentation
- Vert.x Examples
- Red Hat Developer - Vert.x
- InfoQ - Reactive Applications with Vert.x
Code Examples
Hello World
// HelloWorldVerticle.java
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerResponse;
public class HelloWorldVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
HttpServerResponse response = request.response();
response
.putHeader("content-type", "text/plain; charset=utf-8")
.end("Hello World from Vert.x!");
});
server.listen(8080, result -> {
if (result.succeeded()) {
System.out.println("Server started on port 8080");
startPromise.complete();
} else {
System.out.println("Failed to start server: " + result.cause());
startPromise.fail(result.cause());
}
});
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new HelloWorldVerticle());
}
}
RESTful API and Routing
// UserApiVerticle.java
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class UserApiVerticle extends AbstractVerticle {
private Map<Long, JsonObject> users = new HashMap<>();
private AtomicLong idCounter = new AtomicLong(1);
@Override
public void start(Promise<Void> startPromise) throws Exception {
Router router = Router.router(vertx);
// Add body parser
router.route().handler(BodyHandler.create());
// Initialize sample data
initializeData();
// Route definitions
router.get("/").handler(this::handleRoot);
router.get("/api/users").handler(this::getAllUsers);
router.get("/api/users/:id").handler(this::getUser);
router.post("/api/users").handler(this::createUser);
router.put("/api/users/:id").handler(this::updateUser);
router.delete("/api/users/:id").handler(this::deleteUser);
vertx.createHttpServer()
.requestHandler(router)
.listen(8080, result -> {
if (result.succeeded()) {
System.out.println("User API Server started on port 8080");
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void initializeData() {
users.put(1L, new JsonObject()
.put("id", 1)
.put("name", "John Doe")
.put("email", "[email protected]")
.put("age", 30));
users.put(2L, new JsonObject()
.put("id", 2)
.put("name", "Jane Smith")
.put("email", "[email protected]")
.put("age", 25));
idCounter.set(3);
}
private void handleRoot(RoutingContext context) {
context.response()
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("message", "Welcome to User API")
.put("endpoints", new JsonArray()
.add("GET /api/users")
.add("GET /api/users/:id")
.add("POST /api/users")
.add("PUT /api/users/:id")
.add("DELETE /api/users/:id"))
.encode());
}
private void getAllUsers(RoutingContext context) {
JsonArray userArray = new JsonArray();
users.values().forEach(userArray::add);
context.response()
.putHeader("content-type", "application/json; charset=utf-8")
.end(userArray.encode());
}
private void getUser(RoutingContext context) {
String idParam = context.request().getParam("id");
try {
Long id = Long.parseLong(idParam);
JsonObject user = users.get(id);
if (user != null) {
context.response()
.putHeader("content-type", "application/json; charset=utf-8")
.end(user.encode());
} else {
context.response()
.setStatusCode(404)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "User not found")
.encode());
}
} catch (NumberFormatException e) {
context.response()
.setStatusCode(400)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "Invalid user ID")
.encode());
}
}
private void createUser(RoutingContext context) {
JsonObject body = context.getBodyAsJson();
if (body == null || !isValidUser(body)) {
context.response()
.setStatusCode(400)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "Invalid user data")
.encode());
return;
}
Long id = idCounter.getAndIncrement();
JsonObject user = new JsonObject()
.put("id", id)
.put("name", body.getString("name"))
.put("email", body.getString("email"))
.put("age", body.getInteger("age"));
users.put(id, user);
context.response()
.setStatusCode(201)
.putHeader("content-type", "application/json; charset=utf-8")
.end(user.encode());
}
private void updateUser(RoutingContext context) {
String idParam = context.request().getParam("id");
JsonObject body = context.getBodyAsJson();
if (body == null || !isValidUser(body)) {
context.response()
.setStatusCode(400)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "Invalid user data")
.encode());
return;
}
try {
Long id = Long.parseLong(idParam);
if (users.containsKey(id)) {
JsonObject user = new JsonObject()
.put("id", id)
.put("name", body.getString("name"))
.put("email", body.getString("email"))
.put("age", body.getInteger("age"));
users.put(id, user);
context.response()
.putHeader("content-type", "application/json; charset=utf-8")
.end(user.encode());
} else {
context.response()
.setStatusCode(404)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "User not found")
.encode());
}
} catch (NumberFormatException e) {
context.response()
.setStatusCode(400)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "Invalid user ID")
.encode());
}
}
private void deleteUser(RoutingContext context) {
String idParam = context.request().getParam("id");
try {
Long id = Long.parseLong(idParam);
if (users.remove(id) != null) {
context.response()
.setStatusCode(204)
.end();
} else {
context.response()
.setStatusCode(404)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "User not found")
.encode());
}
} catch (NumberFormatException e) {
context.response()
.setStatusCode(400)
.putHeader("content-type", "application/json; charset=utf-8")
.end(new JsonObject()
.put("error", "Invalid user ID")
.encode());
}
}
private boolean isValidUser(JsonObject user) {
return user.getString("name") != null && !user.getString("name").trim().isEmpty() &&
user.getString("email") != null && !user.getString("email").trim().isEmpty() &&
user.getInteger("age") != null && user.getInteger("age") > 0;
}
}
Event Bus and Inter-Verticle Communication
// EventBusExampleVerticle.java
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonObject;
public class EventBusExampleVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
EventBus eventBus = vertx.eventBus();
// Register message consumers
eventBus.consumer("user.service", this::handleUserService);
eventBus.consumer("notification.service", this::handleNotification);
// Start HTTP server
vertx.createHttpServer()
.requestHandler(request -> {
String path = request.path();
if (path.equals("/send-message")) {
// Send message via event bus
JsonObject message = new JsonObject()
.put("action", "get_user")
.put("userId", 123);
eventBus.request("user.service", message, reply -> {
if (reply.succeeded()) {
request.response()
.putHeader("content-type", "application/json")
.end(reply.result().body().toString());
} else {
request.response()
.setStatusCode(500)
.end("Error: " + reply.cause().getMessage());
}
});
} else if (path.equals("/publish")) {
// Publish/Subscribe model
JsonObject notification = new JsonObject()
.put("type", "info")
.put("message", "System notification");
eventBus.publish("notification.service", notification);
request.response()
.putHeader("content-type", "text/plain")
.end("Notification sent");
} else {
request.response()
.putHeader("content-type", "text/plain")
.end("Event Bus examples: /send-message, /publish");
}
})
.listen(8080, result -> {
if (result.succeeded()) {
System.out.println("EventBus server started on port 8080");
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void handleUserService(Message<JsonObject> message) {
JsonObject body = message.body();
String action = body.getString("action");
if ("get_user".equals(action)) {
Integer userId = body.getInteger("userId");
// Simulate user data retrieval
JsonObject user = new JsonObject()
.put("id", userId)
.put("name", "User" + userId)
.put("email", "user" + userId + "@example.com")
.put("timestamp", System.currentTimeMillis());
message.reply(user);
} else {
message.fail(400, "Unknown action: " + action);
}
}
private void handleNotification(Message<JsonObject> message) {
JsonObject notification = message.body();
String type = notification.getString("type");
String msg = notification.getString("message");
// Simulate notification processing
System.out.println("[" + type.toUpperCase() + "] " + msg);
// Forward to other services
if ("info".equals(type)) {
vertx.setTimer(1000, id -> {
System.out.println("Notification processing completed: " + msg);
});
}
}
}
WebSocket Support
// WebSocketServerVerticle.java
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.json.JsonObject;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class WebSocketServerVerticle extends AbstractVerticle {
private Set<ServerWebSocket> connectedSockets = ConcurrentHashMap.newKeySet();
@Override
public void start(Promise<Void> startPromise) throws Exception {
HttpServer server = vertx.createHttpServer();
// WebSocket handler
server.webSocketHandler(websocket -> {
System.out.println("WebSocket connected: " + websocket.textHandlerID());
connectedSockets.add(websocket);
// Connection notification
JsonObject welcomeMsg = new JsonObject()
.put("type", "welcome")
.put("message", "Connected to WebSocket server")
.put("clientId", websocket.textHandlerID());
websocket.writeTextMessage(welcomeMsg.encode());
// Message handler
websocket.textMessageHandler(message -> {
try {
JsonObject msgObj = new JsonObject(message);
handleWebSocketMessage(websocket, msgObj);
} catch (Exception e) {
JsonObject errorMsg = new JsonObject()
.put("type", "error")
.put("message", "Invalid JSON message");
websocket.writeTextMessage(errorMsg.encode());
}
});
// Close handler
websocket.closeHandler(v -> {
System.out.println("WebSocket disconnected: " + websocket.textHandlerID());
connectedSockets.remove(websocket);
// Notify other clients of disconnection
JsonObject disconnectMsg = new JsonObject()
.put("type", "user_disconnected")
.put("clientId", websocket.textHandlerID());
broadcastMessage(disconnectMsg, websocket);
});
// Exception handler
websocket.exceptionHandler(throwable -> {
System.err.println("WebSocket error: " + throwable.getMessage());
connectedSockets.remove(websocket);
});
});
// Periodic heartbeat
vertx.setPeriodic(30000, id -> {
JsonObject heartbeat = new JsonObject()
.put("type", "heartbeat")
.put("timestamp", System.currentTimeMillis())
.put("connectedClients", connectedSockets.size());
broadcastMessage(heartbeat, null);
});
server.listen(8080, result -> {
if (result.succeeded()) {
System.out.println("WebSocket server started on port 8080");
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void handleWebSocketMessage(ServerWebSocket sender, JsonObject message) {
String type = message.getString("type");
switch (type) {
case "chat":
// Broadcast chat message
JsonObject chatMsg = new JsonObject()
.put("type", "chat")
.put("from", sender.textHandlerID())
.put("message", message.getString("message"))
.put("timestamp", System.currentTimeMillis());
broadcastMessage(chatMsg, sender);
break;
case "ping":
// Respond to ping with pong
JsonObject pongMsg = new JsonObject()
.put("type", "pong")
.put("timestamp", System.currentTimeMillis());
sender.writeTextMessage(pongMsg.encode());
break;
case "get_users":
// Get list of connected users
JsonObject usersMsg = new JsonObject()
.put("type", "users_list")
.put("count", connectedSockets.size())
.put("users", connectedSockets.stream()
.map(ServerWebSocket::textHandlerID)
.toArray());
sender.writeTextMessage(usersMsg.encode());
break;
default:
JsonObject unknownMsg = new JsonObject()
.put("type", "error")
.put("message", "Unknown message type: " + type);
sender.writeTextMessage(unknownMsg.encode());
}
}
private void broadcastMessage(JsonObject message, ServerWebSocket excludeSocket) {
String messageStr = message.encode();
connectedSockets.forEach(socket -> {
if (socket != excludeSocket && !socket.isClosed()) {
socket.writeTextMessage(messageStr);
}
});
}
}
Asynchronous Database Operations
// DatabaseVerticle.java
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.SQLConnection;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
public class DatabaseVerticle extends AbstractVerticle {
private JDBCClient jdbcClient;
@Override
public void start(Promise<Void> startPromise) throws Exception {
// Database connection configuration
JsonObject config = new JsonObject()
.put("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
.put("driver_class", "org.h2.Driver");
jdbcClient = JDBCClient.createShared(vertx, config);
// Initialize database
initializeDatabase().onComplete(dbResult -> {
if (dbResult.succeeded()) {
setupRouter(startPromise);
} else {
startPromise.fail(dbResult.cause());
}
});
}
private Promise<Void> initializeDatabase() {
Promise<Void> promise = Promise.promise();
String createTable = """
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""";
jdbcClient.getConnection(connResult -> {
if (connResult.succeeded()) {
SQLConnection connection = connResult.result();
connection.execute(createTable, createResult -> {
if (createResult.succeeded()) {
// Insert sample data
String insertData = """
INSERT INTO users (name, email, age) VALUES
('John Doe', '[email protected]', 30),
('Jane Smith', '[email protected]', 25),
('Bob Johnson', '[email protected]', 35)
""";
connection.execute(insertData, insertResult -> {
connection.close();
if (insertResult.succeeded()) {
promise.complete();
} else {
promise.fail(insertResult.cause());
}
});
} else {
connection.close();
promise.fail(createResult.cause());
}
});
} else {
promise.fail(connResult.cause());
}
});
return promise;
}
private void setupRouter(Promise<Void> startPromise) {
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.get("/api/users").handler(this::getAllUsers);
router.get("/api/users/:id").handler(this::getUser);
router.post("/api/users").handler(this::createUser);
router.put("/api/users/:id").handler(this::updateUser);
router.delete("/api/users/:id").handler(this::deleteUser);
vertx.createHttpServer()
.requestHandler(router)
.listen(8080, result -> {
if (result.succeeded()) {
System.out.println("Database API server started on port 8080");
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void getAllUsers(RoutingContext context) {
jdbcClient.getConnection(connResult -> {
if (connResult.succeeded()) {
SQLConnection connection = connResult.result();
connection.query("SELECT * FROM users ORDER BY id", queryResult -> {
connection.close();
if (queryResult.succeeded()) {
JsonArray users = new JsonArray();
queryResult.result().getRows().forEach(row -> {
users.add(new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("email", row.getString("email"))
.put("age", row.getInteger("age"))
.put("created_at", row.getString("created_at")));
});
context.response()
.putHeader("content-type", "application/json")
.end(users.encode());
} else {
handleDatabaseError(context, queryResult.cause());
}
});
} else {
handleDatabaseError(context, connResult.cause());
}
});
}
private void getUser(RoutingContext context) {
// Implementation similar to getAllUsers pattern
}
private void createUser(RoutingContext context) {
// Implementation similar to getAllUsers pattern
}
private void updateUser(RoutingContext context) {
// Implementation similar to getAllUsers pattern
}
private void deleteUser(RoutingContext context) {
// Implementation similar to getAllUsers pattern
}
private boolean isValidUser(JsonObject user) {
return user.getString("name") != null && !user.getString("name").trim().isEmpty() &&
user.getString("email") != null && !user.getString("email").trim().isEmpty() &&
user.getInteger("age") != null && user.getInteger("age") > 0;
}
private void handleDatabaseError(RoutingContext context, Throwable error) {
System.err.println("Database error: " + error.getMessage());
context.response()
.setStatusCode(500)
.putHeader("content-type", "application/json")
.end(new JsonObject()
.put("error", "Internal server error")
.encode());
}
}
Vert.x Unit Testing
// VerticleTest.java
package com.example;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(VertxUnitRunner.class)
public class VerticleTest {
private Vertx vertx;
private HttpClient client;
@Before
public void setUp(TestContext context) {
vertx = Vertx.vertx();
// Deploy test Verticle
Async async = context.async();
vertx.deployVerticle(UserApiVerticle.class.getName(), context.asyncAssertSuccess(id -> {
async.complete();
}));
client = vertx.createHttpClient();
}
@After
public void tearDown(TestContext context) {
vertx.close(context.asyncAssertSuccess());
}
@Test
public void testGetAllUsers(TestContext context) {
Async async = context.async();
client.getNow(8080, "localhost", "/api/users", response -> {
context.assertEquals(200, response.statusCode());
context.assertEquals("application/json; charset=utf-8",
response.getHeader("content-type"));
response.bodyHandler(body -> {
context.assertTrue(body.toString().contains("John Doe"));
async.complete();
});
});
}
@Test
public void testCreateUser(TestContext context) {
Async async = context.async();
String userData = """
{
"name": "Test User",
"email": "[email protected]",
"age": 28
}
""";
client.post(8080, "localhost", "/api/users")
.putHeader("content-type", "application/json")
.handler(response -> {
context.assertEquals(201, response.statusCode());
response.bodyHandler(body -> {
context.assertTrue(body.toString().contains("Test User"));
async.complete();
});
})
.end(userData);
}
@Test
public void testGetUserNotFound(TestContext context) {
Async async = context.async();
client.getNow(8080, "localhost", "/api/users/999", response -> {
context.assertEquals(404, response.statusCode());
async.complete();
});
}
}