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.
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.
### 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.
### 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!");
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(FuturestartFuture) { 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()); } }
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()); } });
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 FuturefetchUserData(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() + "\"}"); } }); } }
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.
Aspekt | Vert.x | Spring Boot |
---|---|---|
Performance | Bardzo wysoka, non-blocking I/O | Dobra, thread-per-request |
Memory usage | Niskie zużycie | Wyższe zużycie |
Learning curve | Stromy, wymaga zmiany myślenia | Łagodny, znane wzorce |
Ekosystem | Rozwijający się | Bardzo bogaty |
Debugging | Trudniejsze (async stack traces) | Łatwiejsze |
Team onboarding | Wymaga szkolenia | Szybkie 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ą
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(Messagemessage) { 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\"}"); } }); } }
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// 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()); }); io.vertx vertx-micrometer-metrics 3.8.4
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.
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.
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.
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.
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.
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.
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:
- Oficjalna dokumentacja Vert.x
- Vert.x Examples na GitHub
- Vert.x Blog z najnowymi trendami
- Reactive Manifesto
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!