Vert.x – reactive toolkit dla aplikacji wysokiej wydajności

TL;DR: Vert.x to toolkit do budowania reactive aplikacji na JVM z event-driven architecture. Obsługuje tysiące połączeń jednocześnie dzięki non-blocking I/O i event loop. Idealny do microservices, API gateway i real-time aplikacji.

Dlaczego Vert.x to przyszłość wysokowydajnych aplikacji?

Tradycyjne aplikacje Java oparte na thread-per-request model mają fundamentalne ograniczenie – każde połączenie wymaga osobnego wątku. Oznacza to że z domyślnym pool 200 wątków możesz obsłużyć maksymalnie 200 jednoczesnych użytkowników. Vert.x zmienia tę zasadę całkowicie.

Vert.x może obsłużyć tysiące połączeń jednocześnie używając zaledwie kilku wątków dzięki event-driven, non-blocking architecture.

Co to jest Vert.x?

Vert.x to reactive toolkit dla JVM który umożliwia budowanie responsive, resilient i elastic aplikacji. Został stworzony przez Tim Fox i obecnie jest rozwijany pod skrzydłami Eclipse Foundation.

Reactive aplikacja – aplikacja która reaguje na zmiany (events), jest odporna na błędy (resilient), elastyczna w skalowaniu (elastic) i responsive dla użytkowników.

### Kluczowe cechy Vert.x:

  • Event-driven: wszystko oparte na eventach i callbackach
  • Non-blocking I/O: operacje nie blokują wątków
  • Polyglot: wsparcie dla Java, Kotlin, Scala, JavaScript, Ruby
  • Distributed: wbudowana obsługa klastrów i komunikacji
  • Lightweight: małe zużycie pamięci i CPU

Co się nauczysz w tym artykule:

  • Jak działa event loop i non-blocking I/O w Vert.x
  • Tworzenie pierwszej aplikacji Vert.x z HTTP serverem
  • Obsługa asynchronicznych operacji i callbacków
  • Event Bus – komunikacja między komponentami
  • Kiedy wybrać Vert.x zamiast Spring Boot
  • Wzorce projektowe specyficzne dla Vert.x

Wymagania wstępne:

Poziom: Średniozaawansowany (2-5 lat doświadczenia)

  • Znajomość Java 8+ (lambdy, futures)
  • Podstawy HTTP i REST API
  • Zrozumienie koncepcji wątków i asynchroniczności
  • Doświadczenie z Maven lub Gradle

Event Loop – serce Vert.x

Najważniejszą koncepcją w Vert.x jest Event Loop. To pojedynczy wątek który obsługuje wszystkie eventy w nieskończonej pętli. Brzmi to ograniczająco, ale w praktyce jest niesamowicie wydajne.

Event Loop to jak barista w kawiarni – przyjmuje zamówienia (eventy), przekazuje je do wykonania i zajmuje się kolejnymi klientami nie czekając aż kawa będzie gotowa. Kiedy kawa jest gotowa, dostaje sygnał i wydaje ją klientowi.

### Jak to działa w praktyce:

// Event Loop nigdy nie blokuje!
vertx.createHttpServer()
  .requestHandler(request -> {
    // Ta operacja jest non-blocking
    request.response()
           .putHeader("content-type", "text/plain")
           .end("Hello from Vert.x!");
  })
  .listen(8080, result -> {
    if (result.succeeded()) {
      System.out.println("Server started on port 8080");
    }
  });

// Event Loop kontynuuje przetwarzanie innych eventów
System.out.println("This executes immediately!");
Uwaga: Nigdy nie blokuj Event Loop! Operacje jak Thread.sleep(), blocking I/O czy długie kalkulacje zatrzymają obsługę wszystkich pozostałych eventów.

Pierwsza aplikacja Vert.x

Stwórzmy prostą aplikację HTTP która demonstruje możliwości Vert.x.

### Setup projektu (Maven):


  
    io.vertx
    vertx-core
    3.8.4
  
  
    io.vertx
    vertx-web
    3.8.4
  

### Główna klasa aplikacji:

package com.example.vertx;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

public class MainVerticle extends AbstractVerticle {

  @Override
  public void start(Future startFuture) {
    HttpServer server = vertx.createHttpServer();
    Router router = Router.router(vertx);
    
    // Endpoint z natychmiastową odpowiedzią
    router.get("/hello").handler(this::handleHello);
    
    // Endpoint z asynchroniczną operacją
    router.get("/async").handler(this::handleAsync);
    
    // Endpoint z bazą danych (symulacja)
    router.get("/users/:id").handler(this::handleUser);
    
    server.requestHandler(router)
          .listen(8080, result -> {
            if (result.succeeded()) {
              System.out.println("HTTP server started on port 8080");
              startFuture.complete();
            } else {
              startFuture.fail(result.cause());
            }
          });
  }
  
  private void handleHello(RoutingContext context) {
    context.response()
           .putHeader("content-type", "application/json")
           .end("{\"message\": \"Hello from Vert.x!\", \"timestamp\": " + System.currentTimeMillis() + "}");
  }
  
  private void handleAsync(RoutingContext context) {
    // Symulacja asynchronicznej operacji
    vertx.setTimer(1000, id -> {
      context.response()
             .putHeader("content-type", "application/json")
             .end("{\"message\": \"Async operation completed\", \"delay\": 1000}");
    });
  }
  
  private void handleUser(RoutingContext context) {
    String userId = context.request().getParam("id");
    
    // Symulacja zapytania do bazy danych (non-blocking)
    fetchUserFromDatabase(userId)
      .setHandler(result -> {
        if (result.succeeded()) {
          context.response()
                 .putHeader("content-type", "application/json")
                 .end(result.result());
        } else {
          context.response()
                 .setStatusCode(500)
                 .end("{\"error\": \"User not found\"}");
        }
      });
  }
  
  private Future fetchUserFromDatabase(String userId) {
    Future future = Future.future();
    
    // Symulacja non-blocking database call
    vertx.setTimer(500, id -> {
      String userData = "{\"id\": " + userId + ", \"name\": \"John Doe\", \"email\": \"john@example.com\"}";
      future.complete(userData);
    });
    
    return future;
  }
  
  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle(new MainVerticle());
  }
}
Pro tip: Verticles to podstawowe jednostki deploymentu w Vert.x. Jedna aplikacja może mieć wiele Verticles które komunikują się przez Event Bus.

Event Bus – komunikacja w aplikacji

Event Bus to system wiadomości który pozwala różnym częściom aplikacji komunikować się bez bezpośrednich referencji. To jak nervous system w organizmie.

### Przykład użycia Event Bus:

// Producer Verticle - wysyła wiadomości
public class ProducerVerticle extends AbstractVerticle {
  
  @Override
  public void start() {
    vertx.setPeriodic(5000, id -> {
      String message = "Current time: " + System.currentTimeMillis();
      vertx.eventBus().publish("time.updates", message);
      System.out.println("Published: " + message);
    });
  }
}

// Consumer Verticle - odbiera wiadomości
public class ConsumerVerticle extends AbstractVerticle {
  
  @Override
  public void start() {
    vertx.eventBus().consumer("time.updates", message -> {
      System.out.println("Received: " + message.body());
      
      // Możemy odpowiedzieć na wiadomość
      message.reply("Message processed by ConsumerVerticle");
    });
  }
}

// Request-Response pattern
public class ServiceVerticle extends AbstractVerticle {
  
  @Override
  public void start() {
    vertx.eventBus().consumer("user.service", message -> {
      String userId = (String) message.body();
      
      // Symulacja przetwarzania
      String response = "{\"userId\": \"+ userId +\", \"status\": \"active\"}";
      message.reply(response);
    });
  }
}

// Klient używający serwisu
vertx.eventBus().request("user.service", "12345", reply -> {
  if (reply.succeeded()) {
    System.out.println("User data: " + reply.result().body());
  } else {
    System.err.println("Service error: " + reply.cause());
  }
});
Event Bus działa nie tylko w obrębie jednej JVM – może łączyć Verticles działające na różnych maszynach w klastrze!

Obsługa błędów i Future Pattern

W świecie asynchronicznym obsługa błędów wymaga innego podejścia. Vert.x używa wzorca Future do eleganciego zarządzania async operacjami.

public class AsyncErrorHandling extends AbstractVerticle {
  
  // Chainowanie asynchronicznych operacji
  private Future fetchUserData(String userId) {
    Future future = Future.future();
    
    // Pierwsza operacja - sprawdź czy user istnieje
    checkUserExists(userId)
      .compose(exists -> {
        if (exists) {
          return fetchUserFromDatabase(userId);
        } else {
          return Future.failedFuture("User not found");
        }
      })
      .compose(userData -> {
        // Druga operacja - wzbogać dane
        return enrichUserData(userData);
      })
      .setHandler(result -> {
        if (result.succeeded()) {
          future.complete(result.result());
        } else {
          future.fail(result.cause());
        }
      });
    
    return future;
  }
  
  private Future checkUserExists(String userId) {
    Future future = Future.future();
    
    vertx.setTimer(100, id -> {
      // Symulacja sprawdzenia
      boolean exists = !userId.equals("999");
      future.complete(exists);
    });
    
    return future;
  }
  
  private Future enrichUserData(String userData) {
    Future future = Future.future();
    
    vertx.setTimer(200, id -> {
      String enriched = userData.replace("}", ", \"lastLogin\": \"2019-12-15\"}");
      future.complete(enriched);
    });
    
    return future;
  }
  
  // Endpoint używający chainowanych operacji
  private void handleUserProfile(RoutingContext context) {
    String userId = context.request().getParam("id");
    
    fetchUserData(userId)
      .setHandler(result -> {
        if (result.succeeded()) {
          context.response()
                 .putHeader("content-type", "application/json")
                 .end(result.result());
        } else {
          context.response()
                 .setStatusCode(404)
                 .end("{\"error\": \"" + result.cause().getMessage() + "\"}");
        }
      });
  }
}
Pułapka: Callback hell! Gdy masz dużo zagnieżdżonych callbacków, kod staje się nieczytelny. Używaj Future.compose() lub rozważ RxJava integration.

Vert.x vs Spring Boot – kiedy co wybrać?

Często pojawia się pytanie czy wybrać Vert.x czy Spring Boot. Oba mają swoje miejsce w ekosystemie Java.

AspektVert.xSpring Boot
PerformanceBardzo wysoka, non-blocking I/ODobra, thread-per-request
Memory usageNiskie zużycieWyższe zużycie
Learning curveStromy, wymaga zmiany myśleniaŁagodny, znane wzorce
EkosystemRozwijający sięBardzo bogaty
DebuggingTrudniejsze (async stack traces)Łatwiejsze
Team onboardingWymaga szkoleniaSzybkie wdrożenie

### Kiedy wybrać Vert.x:
– Wysokie wymagania wydajnościowe
– Real-time aplikacje (WebSocket, SSE)
– Microservices z intensywną komunikacją I/O
– API Gateway czy proxy
– IoT applications z tysiącami połączeń

### Kiedy wybrać Spring Boot:
– Standardowe CRUD aplikacje
– Zespół bez doświadczenia z reactive programming
– Potrzebujesz bogatego ekosystemu (Spring Data, Security, etc.)
– Szybkie prototypowanie
– Enterprise aplikacje z kompleksową logiką biznesową

Uwaga: Nie mieszaj blocking i non-blocking kodu! Jeśli używasz tradycyjnych JDBC czy blocking HTTP clients w Vert.x, tracisz wszystkie korzyści z performance.

Wzorce projektowe w Vert.x

Reactive programming wymaga innych wzorców projektowych niż tradycyjne aplikacje.

### 1. Verticle per Service Pattern:

// Każdy serwis jako osobny Verticle
public class UserServiceVerticle extends AbstractVerticle {
  
  @Override
  public void start() {
    vertx.eventBus().consumer("user.create", this::createUser);
    vertx.eventBus().consumer("user.find", this::findUser);
    vertx.eventBus().consumer("user.update", this::updateUser);
  }
  
  private void createUser(Message message) {
    JsonObject userData = message.body();
    
    // Async database operation
    saveToDatabase(userData)
      .setHandler(result -> {
        if (result.succeeded()) {
          message.reply(result.result());
        } else {
          message.fail(500, result.cause().getMessage());
        }
      });
  }
}

// Deployment wielu instancji
vertx.deployVerticle("com.example.UserServiceVerticle", 
  new DeploymentOptions().setInstances(4));

### 2. Circuit Breaker Pattern:

import io.vertx.circuitbreaker.CircuitBreaker;
import io.vertx.circuitbreaker.CircuitBreakerOptions;

public class ResilientServiceVerticle extends AbstractVerticle {
  
  private CircuitBreaker circuitBreaker;
  
  @Override
  public void start() {
    CircuitBreakerOptions options = new CircuitBreakerOptions()
      .setMaxFailures(5)
      .setTimeout(2000)
      .setFallbackOnFailure(true)
      .setResetTimeout(10000);
    
    circuitBreaker = CircuitBreaker.create("external-service", vertx, options);
    
    // Setup route
    Router router = Router.router(vertx);
    router.get("/external-data").handler(this::handleExternalCall);
    
    vertx.createHttpServer()
         .requestHandler(router)
         .listen(8080);
  }
  
  private void handleExternalCall(RoutingContext context) {
    circuitBreaker.execute(future -> {
      // Call to external service
      callExternalService()
        .setHandler(result -> {
          if (result.succeeded()) {
            future.complete(result.result());
          } else {
            future.fail(result.cause());
          }
        });
    }).setHandler(result -> {
      if (result.succeeded()) {
        context.response().end(result.result().toString());
      } else {
        // Fallback response
        context.response()
               .end("{\"message\": \"Service temporarily unavailable\"}");
      }
    });
  }
}
Pro tip: Circuit Breaker zapobiega cascade failures w microservices. Gdy zewnętrzny serwis nie odpowiada, circuit breaker „otwiera się” i zwraca fallback response zamiast próbować nieskończenie.

Monitoring i debugging aplikacji Vert.x

Aplikacje reactive wymagają innych narzędzi do monitorowania niż tradycyjne aplikacje.

### Metryki i health checks:

// Dodaj do pom.xml

  io.vertx
  vertx-micrometer-metrics
  3.8.4


// Konfiguracja metryk
MicrometerMetricsOptions metricsOptions = new MicrometerMetricsOptions()
  .setPrometheusOptions(new PrometheusMeterRegistryOptions().setEnabled(true))
  .setEnabled(true);

VertxOptions vertxOptions = new VertxOptions().setMetricsOptions(metricsOptions);
Vertx vertx = Vertx.vertx(vertxOptions);

// Health check endpoint
router.get("/health").handler(context -> {
  JsonObject health = new JsonObject()
    .put("status", "UP")
    .put("timestamp", System.currentTimeMillis())
    .put("eventLoopUtilization", getEventLoopUtilization());
  
  context.response()
         .putHeader("content-type", "application/json")
         .end(health.encode());
});
Typowy błąd: Używanie System.out.println() do debugowania w production. W Vert.x używaj structured logging z SLF4J i async appenders żeby nie blokować Event Loop.
Czy Vert.x nadaje się do wszystkich typów aplikacji?

Nie. Vert.x świetnie sprawdza się w aplikacjach I/O intensive (API, proxy, real-time apps), ale dla CPU intensive operations tradycyjne podejście może być lepsze. Również dla standardowych CRUD aplikacji Spring Boot często będzie prostszym wyborem.

Jak radzić sobie z callback hell w Vert.x?

Użyj Future.compose() do chainowania operacji, podziel kod na mniejsze metody, rozważ RxJava integration (vertx-rx-java) lub czekaj na CompletableFuture support w nowszych wersjach Vert.x.

Czy mogę używać Spring komponenty w Vert.x?

Tak, ale ostrożnie. Spring beans mogą być używane w Verticles, ale musisz uważać żeby nie wprowadzać blocking operations. Istnieje również vertx-spring-ext extension dla lepszej integracji.

Jak testować aplikacje Vert.x?

Vert.x dostarcza vertx-unit do testowania async kodu. Możesz również używać TestContext do synchronizacji testów z asynchronicznymi operacjami. JUnit 5 ma lepsze wsparcie dla async testing.

Jakie są ograniczenia Vert.x w porównaniu do Spring Boot?

Mniejszy ekosystem, stromy learning curve, trudniejsze debugging, brak wielu gotowych integracji (security, data access), wymaga zmiany sposobu myślenia o aplikacjach z synchronicznego na asynchroniczny.

Czy Vert.x nadaje się do microservices?

Tak, bardzo dobrze! Małe zużycie zasobów, szybki startup, wbudowana obsługa klastrów i service discovery. Idealny do containerów i cloud deployments. Event Bus ułatwia komunikację między serwisami.

Jak monitorować performance aplikacji Vert.x?

Kluczowe metryki to event loop utilization, queue sizes, response times. Używaj Micrometer + Prometheus dla metryk, structured logging dla troubleshooting. Ważne: nigdy nie blokuj Event Loop!

🚀 Zadanie dla Ciebie

Stwórz prostą aplikację Vert.x która:

  • Udostępnia REST API do zarządzania zadaniami (TODO list)
  • Używa Event Bus do komunikacji między komponentami
  • Implementuje Circuit Breaker dla zewnętrznych wywołań
  • Ma endpoint /health z metrykami
  • Loguje wszystkie operacje w structured format

Bonus: Dodaj WebSocket endpoint dla real-time updates i przetestuj z wieloma jednoczesnych klientów.

Przydatne zasoby:

Masz doświadczenie z Vert.x w produkcji? Jakie były Twoje największe wyzwania z przejściem na reactive programming? Podziel się w komentarzach!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry