Javalin - Lekki Framework Webowy dla Javy
Co to jest Javalin?
- Javalin to lekki framework do tworzenia aplikacji webowych i REST API w Javie
- Oparty na serwerze Jetty (serwer HTTP i servletów)
- Przeznaczony dla programistów, którzy chcą szybko tworzyć aplikacje bez dużej ilości boilerplate'u
- Idealne do:
- REST API
- Mikroserwisów
- Prototypów
- Małych i średnich aplikacji webowych
- „Czysty” kod Java, bez adnotacji i refleksji
- Następca Spark Java, ale z nowoczesnym designem i lepszą wydajnością
- Obecna wersja: 7.x (znaczące zmiany od wersji 6.x)
- W Javalin 7 trasy definiujemy przez
config.routespodczasJavalin.create(…)
Dodanie Javalin do projektu Maven
Plik pom.xml:
<!-- Javalin Core - minimalna zależność --> <dependency> <groupId>io.javalin</groupId> <artifactId>javalin</artifactId> <version>7.2.2</version> </dependency> <!-- Javalin Bundle zawiera wszystko co potrzebne --> <dependency> <groupId>io.javalin</groupId> <artifactId>javalin-bundle</artifactId> <version>7.2.2</version> </dependency>
Zawartość i zależności javalin-bundle
javalin-bundle to „all-in-one” pakiet zawierający:
javalin-core- główny frameworkjetty-server- serwer HTTPjetty-servlet- obsługa servletówjackson- serializacja JSONslf4j-api- API loggowania
Zaleta: Nie musisz ręcznie dodawać zależności, wszystko jest już skonfigurowane i kompatybilne
Inne moduły: javalin-rendering-*, javalin-ssl, javalin-micrometer
Uruchomienie serwera
import io.javalin.Javalin; public class HelloWorldApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.routes.get("/", ctx -> { ctx.result("Hello World!"); }); }); // Uruchom na porcie 8080 app.start(8080); } }
Uchwyty
- Podstawowe typy:
- akcje przed:
before,beforeMatched - punktów końcowych
endpoints:get,post,put,delete,patch - akcje po:
after,afterMatched
- Ścieżka:
/,/hello-world,/hello/{name},/user/*
- Handler:
ctx → { … }- lambda z obiektemContext- Pobieranie danych z żądania i wysyłanie odpowiedzi przez
Context
- Uchwyty do tych samych scieżek wykonywane w kolejności definicji
config.routes.get("/", ctx -> { ctx.result("Hello World!"); });
Routing: Metody HTTP
Javalin obsługuje wszystkie standardowe metody HTTP:
GET- pobranie danychPOST- utworzenie zasobuPUT- aktualizacja zasobuDELETE- usunięcie zasobuPATCH- częściowa aktualizacja
Składnia tras
Javalin app = Javalin.create(config -> { // GET - pobierz dane config.routes.get("/api/users", ctx -> { ctx.result("List of users"); }); // POST - utwórz zasób config.routes.post("/api/users", ctx -> { ctx.status(201); // Created ctx.result("User created"); }); // PUT - aktualizuj config.routes.put("/api/users/{id}", ctx -> { String id = ctx.pathParam("id"); ctx.result("Updated user " + id); }); // DELETE - usuń config.routes.delete("/api/users/{id}", ctx -> { String id = ctx.pathParam("id"); ctx.result("Deleted user " + id); }); }).start(8080);
Priorytety tras
- Trasy są przetwarzane w kolejności zdefiniowania
- Bardziej konkretne trasy powinny być zdefiniowane przed ogólnymi
- Wildcard
*dopasowuje dowolny tekst
Javalin app = Javalin.create(config -> { config.routes.get("/admin/users", ctx -> ctx.result("Admin users")); // Konkretna config.routes.get("/admin/*", ctx -> ctx.result("Admin section")); // Ogólna config.routes.get("/*", ctx -> ctx.result("Everything else")); // Najogólniejsza });
Obiekt Context?
Contextto centralny obiekt w Javalin reprezentujący:- Żądanie HTTP od klienta (request)
- Odpowiedź HTTP dla klienta (response)
- Każdy handler otrzymuje obiekt
Contextjako argument i poprzez niego:- Pobiera dane z żądania
- Wysyła dane w odpowiedzi
- Handler nic nie zwraca, wszystko idzie przez
Context
Javalin app = Javalin.create(config -> { config.routes.get("/example", ctx -> { String query = ctx.queryParam("q"); String path = ctx.path(); ctx.status(200); ctx.result("Response, query=" + query + ", path=" + path); }); });
Kluczowe metody Context
path()- ścieżka żądaniamethod()- metoda HTTPpathParam(key)- parametr z ścieżki (np./users/{id})queryParam(key)- parametr z query string (/search?q=java)body()- ciało żądaniaresult(String)- wysłanie odpowiedzijson(Object)- wysłanie JSONstatus(int)- kod statusu HTTP- Zobacz: Dokumentacja Context
Parametry w ścieżce
Javalin app = Javalin.create(config -> { // Definicja: /users/{id}/posts/{postId} config.routes.get("/users/{id}/posts/{postId}", ctx -> { String userId = ctx.pathParam("id"); // Z walidacją typu int postId = ctx.pathParamAsClass("postId", Integer.class).get(); ctx.result("User: " + userId + ", Post: " + postId); }); }).start(8080); // GET /users/123/posts/456 -> User: 123, Post: 456 // GET /users/123/posts/abc -> "Serwer error" (NumberFormatException)
Query Parameters
Parametry po znaku ? w URL:
Javalin app = Javalin.create(config -> { config.routes.get("/search", ctx -> { String query = ctx.queryParam("q"); int limitInt = ctx.queryParamAsClass("limit", Integer.class).getOrDefault(42); ctx.result("Szukam: " + query + " (limit: " + limitInt + ")"); }); }); // GET /search?q=java&limit=5 // Odpowiedź: Szukam: java (limit: 5)
Wiele wartości tego samego parametru:
// GET /filter?tag=java&tag=web&tag=api List<String> tags = ctx.queryParams("tag"); // tags = ["java", "web", "api"]
Body - Ciało żądania
Dane przesłane w ciele żądania (zazwyczaj POST/PUT):
Javalin app = Javalin.create(config -> { config.routes.post("/echo", ctx -> { String body = ctx.body(); ctx.result("Echo: " + body); }); }); // CMD: curl -X POST http://localhost:8080/echo -d "hello" // POST /echo // Body: hello // Odpowiedź: Echo: hello
Nagłówki żądania
Javalin app = Javalin.create(config -> { config.routes.get("/headers", ctx -> { String userAgent = ctx.header("User-Agent"); String contentType = ctx.header("Content-Type"); ctx.result("User-Agent: " + userAgent); Map<String,String> headers = ctx.headerMap(); }); });
Kody statusu HTTP
Javalin app = Javalin.create(config -> { config.routes.post("/create", ctx -> { // Utworzenie zasobu ctx.status(201); // Created ctx.result("Zasób utworzony"); }); config.routes.get("/notfound", ctx -> { ctx.status(404); // Not Found ctx.result("Nie znaleziono"); }); }); // Powszechne kody: // 200 - OK (GET, POST, PUT zwrócił dane) // 201 - Created (POST/PUT utworzył zasób) // 204 - No Content (DELETE, operacja OK bez zawartości) // 400 - Bad Request (błąd w żądaniu klienta) // 401 - Unauthorized (brak autoryzacji) // 404 - Not Found (nie znaleziono) // 500 - Internal Server Error (błąd serwera)
Wysłanie JSON
// Record - idealny do przesyłania JSON record User(int id, String name, String email) {} Javalin app = Javalin.create(config -> { config.routes.get("/user", ctx -> { User user = new User(1, "Alice", "alice@example.com"); ctx.json(user); // Automatyczna konwersja do JSON }); }); // Odpowiedź: {"id":1,"name":"Alice","email":"alice@example.com"}
Rekordy w Java 16+
Rekordy to specjalne klasy wprowadzone w Javie 16, idealne do przechowywania danych:
- Automatycznie generują konstruktor, gettery,
equals(),hashCode()itoString() - niemutowalne (immutable) - wartości nie mogą być zmieniane po utworzeniu
record Product(int id, String name, double price) {} Product p = new Product(1, "Laptop", 999.99); System.out.println(p.name());
Serialiacja i deserializacja JSON
Rekordy to idealne do przesyłania danych w JSON:
record User(int id, String name, String email) {} record Product(int id, String title, double price) {} Javalin app = Javalin.create(config -> { // Wysłanie config.routes.get("/user", ctx -> { User user = new User(1, "Alice", "alice@example.com"); ctx.json(user); // Konwersja Record -> JSON }); // Otrzymanie config.routes.post("/user", ctx -> { User user = ctx.bodyAsClass(User.class); // Konwersja JSON -> Record System.out.println("User: " + user.name()); ctx.json(user); }); });
Przechwytywanie wyjątków
config.routes.exception(ValidationException.class, (e, ctx) -> { ctx.status(400); ctx.result("Liczba musi byc liczba calkowita\n" + e.getMessage()); }); config.routes.exception(RuntimeException.class, (e, ctx) -> { ctx.status(500); ctx.result("Blad serwera: " + e.getMessage()); }); config.routes.get("/boom", ctx -> { throw new RuntimeException("DEMO: kontrolowany wyjątek"); });
Error Handlers
Obsługa błędów HTTP na poziomie aplikacji:
// Wszystkie 404 config.routes.error(404, ctx -> { ctx.result("Strona nie znaleziona"); }); // Wszystkie 500 config.routes.error(500, ctx -> { ctx.result("Błąd serwera"); });
Serwowanie HTML/CSS/JavaScript
Javalin app = Javalin.create(config -> { config.staticFiles.add("/public"); // Folder z plikami }); app.start(8080); // Pliki z src/main/resources/public/: // - /index.html → http://localhost:8080/index.html // - /css/style.css → http://localhost:8080/css/style.css // - /js/app.js → http://localhost:8080/js/app.js
Struktura projektu
src/main/resources/
└── public/
├── index.html
├── css/
│ └── style.css
└── js/
└── app.js
Przykład index.html
<!DOCTYPE html> <html> <head> <title>Moja aplikacja</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <h1>Witaj w Javalin</h1> <form id="userForm"> <input type="text" id="name" placeholder="Imię"> <button type="submit">Wyślij</button> </form> <div id="result"></div> <script src="/js/app.js"></script> </body> </html>
Ćwiczenie: Przykład formularza HTML z Javalin
Before Handler
Wykonywane przed obsługą żądania
// Globalny middleware Javalin app = Javalin.create(config -> { config.routes.before(ctx -> { System.out.println(">>> " + ctx.method() + " " + ctx.path()); // Logging, walidacja, autoryzacja }); // Przed dopasowaniem trasy config.routes.beforeMatched("/api/*", ctx -> { String token = ctx.header("Authorization"); if (token == null) { ctx.status(401).result("Brak tokenu"); } }); });
After Handler
Wykonywane po obsłudze żądania:
Javalin app = Javalin.create(config -> { config.routes.after(ctx -> { // run after all requests }); config.routes.after("/path/*", ctx -> { // runs after request to /path/* }); config.routes.afterMatched( // runs after all matched requests (including static files) ); });
Resource-Oriented Design
REST API powinno być zorientowane na zasoby (rzeczy), nie na akcje (czasowniki):
- Złe:
/api/getUser,/api/createProduct - Dobre:
/api/users,/api/products
Prawidłowe użycie metod HTTP
| Metoda | Akcja | Przykład |
|---|---|---|
| GET | Pobranie | GET /api/users (pobranie listy) |
| POST | Utworzenie | POST /api/users (utwórz nowego) |
| PUT | Aktualizacja | PUT /api/users/1 (aktualizuj) |
| DELETE | Usunięcie | DELETE /api/users/1 (usuń) |
apiBuilder
config.routes.apiBuilder(() -> { path("/users", () -> { get(UserController::getAllUsers); post(UserController::createUser); path("/{id}", () -> { get(UserController::getUser); }); ws("/events", UserController::webSocketEvents); }); });
Role
Interfejs RouteRole do definiowania ról użytkowników:
enum Role implements RouteRole { ANYONE, USER_READ, USER_WRITE }
Przypisanie ról do endpointów:
config.routes.get("/public", ctx -> ctx.result("Hello public"), Role.ANYONE); config.routes.get("/private", ctx -> ctx.result("Hello private"), Role.USER_READ);
Pobieranie ról z kontekstu:
config.routes.before("/private/*", ctx -> { Set<RouteRole> roles = ctx.roles(); if (!roles.contains(Role.USER_READ)) { ctx.status(403).result("Forbidden"); } });
Materiały
- Dokumentacja Javalin 7: https://javalin.io/documentation
- Tutorial: https://javalin.io/tutorials
- Przykłady na GitHub: https://github.com/tipsy/javalin-examples
