Vert.x

JVM上のリアクティブアプリケーション開発ツールキット。イベント駆動・非ブロッキングアーキテクチャによる高性能・高並行性を実現。

JavaフレームワークReactiveEvent-drivenMicroservicesNon-blocking非同期

GitHub概要

eclipse-vertx/vert.x

Vert.x is a tool-kit for building reactive applications on the JVM

スター14,521
ウォッチ521
フォーク2,091
作成日:2011年6月17日
言語:Java
ライセンス:Other

トピックス

concurrencyevent-loophigh-performancehttp2javajvmnettynionon-blockingreactivevertx

スター履歴

eclipse-vertx/vert.x Star History
データ取得日時: 2025/7/17 10:32

フレームワーク

Eclipse Vert.x

概要

Eclipse Vert.xは、JVM上でリアクティブアプリケーションを構築するためのイベント駆動型、ノンブロッキングツールキットです。

詳細

Vert.xはEclipse Foundationによって開発・保守されているオープンソースプロジェクトです。2024年現在も活発に開発が続けられており、TechEmpower Web Framework Benchmarksでは常にトップ5にランクインし、Express.js、ASP.NET Core、Spring Bootなどの著名フレームワークを上回るパフォーマンスを実証しています。

主要なアーキテクチャの特徴:

  • リアクティブ・ノンブロッキング: イベントループモデルによる高並行性処理
  • Verticleアーキテクチャ: 独立してデプロイ・スケール可能なモジュール設計
  • イベントバス: Verticle間の非同期メッセージング機構
  • ポリグロット対応: Java、JavaScript、Groovy、Ruby、Scala、Kotlin対応
  • ツールキット方式: 必要な機能のみを選択して組み合わせ可能

Vert.xは「フレームワーク」ではなく「ツールキット」として設計されており、アプリケーション構造に関して強い制約を課さず、開発者が必要なモジュールとクライアントを選択して自由に組み合わせることができます。

メリット・デメリット

メリット

  • 卓越したパフォーマンス: 最新ベンチマークでSpring Boot比較で大幅な高速化を実現
  • 高並行性処理: 少数スレッドで多数の並行リクエストを効率的に処理
  • リソース効率: 従来のブロッキングI/Oベースフレームワークより少ないCPU・メモリ消費
  • ポリグロット: 複数のJVM言語サポートで既存コードベースとの統合が容易
  • モジュラー設計: 必要機能のみを選択でき、軽量で柔軟なアーキテクチャ
  • 包括的エコシステム: Web API、データベース、メッセージング、クラウド対応の完全なスタック
  • 学習コストの低さ: 比較的習得しやすい設計で新規開発者にもアクセシブル
  • コンテナ最適化: 仮想マシンやコンテナなどの制約環境での優れた動作

デメリット

  • コード複雑性: ノンブロッキングコードの読み書き・デバッグが困難
  • コールバック地獄: 多数のネストしたコールバックによる可読性低下の危険性
  • ブロッキング操作の注意: 誤ってブロッキングコードを含めてしまうリスク
  • 簡単なアプリには過剰: 高並行性や低レイテンシが不要なCRUDアプリには不向き
  • リアクティブプログラミング学習コスト: 概念理解とブロッキング操作回避のための専門知識が必要
  • デバッグの困難さ: 非同期処理のデバッグが複雑になる場合がある

参考ページ

書き方の例

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("サーバーがポート8080で起動しました");
                startPromise.complete();
            } else {
                System.out.println("サーバー起動に失敗しました: " + result.cause());
                startPromise.fail(result.cause());
            }
        });
    }

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new HelloWorldVerticle());
    }
}

RESTful APIとルーティング

// 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);
        
        // ボディパーサーを追加
        router.route().handler(BodyHandler.create());
        
        // サンプルデータの初期化
        initializeData();
        
        // ルート定義
        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 サーバーがポート8080で起動しました");
                    startPromise.complete();
                } else {
                    startPromise.fail(result.cause());
                }
            });
    }

    private void initializeData() {
        users.put(1L, new JsonObject()
            .put("id", 1)
            .put("name", "田中太郎")
            .put("email", "[email protected]")
            .put("age", 30));
        users.put(2L, new JsonObject()
            .put("id", 2)
            .put("name", "佐藤花子")
            .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", "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", "ユーザーが見つかりません")
                        .encode());
            }
        } catch (NumberFormatException e) {
            context.response()
                .setStatusCode(400)
                .putHeader("content-type", "application/json; charset=utf-8")
                .end(new JsonObject()
                    .put("error", "無効なユーザー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", "無効なユーザーデータ")
                    .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", "無効なユーザーデータ")
                    .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", "ユーザーが見つかりません")
                        .encode());
            }
        } catch (NumberFormatException e) {
            context.response()
                .setStatusCode(400)
                .putHeader("content-type", "application/json; charset=utf-8")
                .end(new JsonObject()
                    .put("error", "無効なユーザー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", "ユーザーが見つかりません")
                        .encode());
            }
        } catch (NumberFormatException e) {
            context.response()
                .setStatusCode(400)
                .putHeader("content-type", "application/json; charset=utf-8")
                .end(new JsonObject()
                    .put("error", "無効なユーザー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;
    }
}

イベントバスとVerticle間通信

// 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();
        
        // メッセージコンシューマーの登録
        eventBus.consumer("user.service", this::handleUserService);
        eventBus.consumer("notification.service", this::handleNotification);
        
        // HTTPサーバーの起動
        vertx.createHttpServer()
            .requestHandler(request -> {
                String path = request.path();
                
                if (path.equals("/send-message")) {
                    // イベントバス経由でメッセージ送信
                    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("エラー: " + reply.cause().getMessage());
                        }
                    });
                } else if (path.equals("/publish")) {
                    // パブリッシュ/サブスクライブモデル
                    JsonObject notification = new JsonObject()
                        .put("type", "info")
                        .put("message", "システム通知です");
                    
                    eventBus.publish("notification.service", notification);
                    
                    request.response()
                        .putHeader("content-type", "text/plain")
                        .end("通知を送信しました");
                } else {
                    request.response()
                        .putHeader("content-type", "text/plain")
                        .end("イベントバス例: /send-message, /publish");
                }
            })
            .listen(8080, result -> {
                if (result.succeeded()) {
                    System.out.println("EventBusサーバーがポート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");
            
            // ユーザーデータ取得のシミュレーション
            JsonObject user = new JsonObject()
                .put("id", userId)
                .put("name", "ユーザー" + userId)
                .put("email", "user" + userId + "@example.com")
                .put("timestamp", System.currentTimeMillis());
            
            message.reply(user);
        } else {
            message.fail(400, "不明なアクション: " + action);
        }
    }
    
    private void handleNotification(Message<JsonObject> message) {
        JsonObject notification = message.body();
        String type = notification.getString("type");
        String msg = notification.getString("message");
        
        // 通知処理のシミュレーション
        System.out.println("[" + type.toUpperCase() + "] " + msg);
        
        // 他のサービスにも転送
        if ("info".equals(type)) {
            vertx.setTimer(1000, id -> {
                System.out.println("通知処理が完了しました: " + msg);
            });
        }
    }
}

WebSocketサポート

// 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ハンドラー
        server.webSocketHandler(websocket -> {
            System.out.println("WebSocket接続: " + websocket.textHandlerID());
            connectedSockets.add(websocket);
            
            // 接続通知
            JsonObject welcomeMsg = new JsonObject()
                .put("type", "welcome")
                .put("message", "WebSocketサーバーに接続しました")
                .put("clientId", websocket.textHandlerID());
            websocket.writeTextMessage(welcomeMsg.encode());
            
            // メッセージ受信ハンドラー
            websocket.textMessageHandler(message -> {
                try {
                    JsonObject msgObj = new JsonObject(message);
                    handleWebSocketMessage(websocket, msgObj);
                } catch (Exception e) {
                    JsonObject errorMsg = new JsonObject()
                        .put("type", "error")
                        .put("message", "無効なJSONメッセージ");
                    websocket.writeTextMessage(errorMsg.encode());
                }
            });
            
            // 接続終了ハンドラー
            websocket.closeHandler(v -> {
                System.out.println("WebSocket切断: " + websocket.textHandlerID());
                connectedSockets.remove(websocket);
                
                // 他のクライアントに切断を通知
                JsonObject disconnectMsg = new JsonObject()
                    .put("type", "user_disconnected")
                    .put("clientId", websocket.textHandlerID());
                broadcastMessage(disconnectMsg, websocket);
            });
            
            // 例外ハンドラー
            websocket.exceptionHandler(throwable -> {
                System.err.println("WebSocketエラー: " + throwable.getMessage());
                connectedSockets.remove(websocket);
            });
        });
        
        // 定期的なハートビート
        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サーバーがポート8080で起動しました");
                startPromise.complete();
            } else {
                startPromise.fail(result.cause());
            }
        });
    }
    
    private void handleWebSocketMessage(ServerWebSocket sender, JsonObject message) {
        String type = message.getString("type");
        
        switch (type) {
            case "chat":
                // チャットメッセージの配信
                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":
                // Pingに対するPong応答
                JsonObject pongMsg = new JsonObject()
                    .put("type", "pong")
                    .put("timestamp", System.currentTimeMillis());
                sender.writeTextMessage(pongMsg.encode());
                break;
                
            case "get_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", "不明なメッセージタイプ: " + 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);
            }
        });
    }
}

非同期データベース操作

// 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 {
        // データベース接続設定
        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);
        
        // テーブル初期化
        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()) {
                        // サンプルデータ挿入
                        String insertData = """
                            INSERT INTO users (name, email, age) VALUES
                            ('田中太郎', '[email protected]', 30),
                            ('佐藤花子', '[email protected]', 25),
                            ('鈴木一郎', '[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("データベースAPIサーバーがポート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) {
        String idParam = context.request().getParam("id");
        
        try {
            int id = Integer.parseInt(idParam);
            
            jdbcClient.getConnection(connResult -> {
                if (connResult.succeeded()) {
                    SQLConnection connection = connResult.result();
                    
                    connection.queryWithParams(
                        "SELECT * FROM users WHERE id = ?",
                        new JsonArray().add(id),
                        queryResult -> {
                            connection.close();
                            
                            if (queryResult.succeeded()) {
                                if (queryResult.result().getNumRows() > 0) {
                                    JsonObject row = queryResult.result().getRows().get(0);
                                    JsonObject user = 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(user.encode());
                                } else {
                                    context.response()
                                        .setStatusCode(404)
                                        .putHeader("content-type", "application/json")
                                        .end(new JsonObject()
                                            .put("error", "ユーザーが見つかりません")
                                            .encode());
                                }
                            } else {
                                handleDatabaseError(context, queryResult.cause());
                            }
                        });
                } else {
                    handleDatabaseError(context, connResult.cause());
                }
            });
        } catch (NumberFormatException e) {
            context.response()
                .setStatusCode(400)
                .putHeader("content-type", "application/json")
                .end(new JsonObject()
                    .put("error", "無効なユーザー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")
                .end(new JsonObject()
                    .put("error", "無効なユーザーデータ")
                    .encode());
            return;
        }
        
        jdbcClient.getConnection(connResult -> {
            if (connResult.succeeded()) {
                SQLConnection connection = connResult.result();
                
                connection.updateWithParams(
                    "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
                    new JsonArray()
                        .add(body.getString("name"))
                        .add(body.getString("email"))
                        .add(body.getInteger("age")),
                    updateResult -> {
                        connection.close();
                        
                        if (updateResult.succeeded()) {
                            JsonObject user = new JsonObject()
                                .put("id", updateResult.result().getKeys().getInteger(0))
                                .put("name", body.getString("name"))
                                .put("email", body.getString("email"))
                                .put("age", body.getInteger("age"));
                            
                            context.response()
                                .setStatusCode(201)
                                .putHeader("content-type", "application/json")
                                .end(user.encode());
                        } else {
                            handleDatabaseError(context, updateResult.cause());
                        }
                    });
            } else {
                handleDatabaseError(context, connResult.cause());
            }
        });
    }
    
    private void updateUser(RoutingContext context) {
        // 実装省略(createUserと同様のパターン)
    }
    
    private void deleteUser(RoutingContext context) {
        // 実装省略(getUserと同様のパターン)
    }
    
    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("データベースエラー: " + error.getMessage());
        context.response()
            .setStatusCode(500)
            .putHeader("content-type", "application/json")
            .end(new JsonObject()
                .put("error", "内部サーバーエラー")
                .encode());
    }
}

Vert.x Unit テスト

// 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();
        
        // テスト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("田中太郎"));
                async.complete();
            });
        });
    }
    
    @Test
    public void testCreateUser(TestContext context) {
        Async async = context.async();
        
        String userData = """
            {
                "name": "テストユーザー",
                "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("テストユーザー"));
                    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();
        });
    }
}