Javalin how-to
Javalin er et lille Java-framework, som gør det muligt at lave en webserver i Java.
I vores projekter bruger vi Javalin til at modtage HTTP-requests fra browseren og sende responses tilbage.
Et meget enkelt eksempel:
var app = Javalin.create(config -> {
config.routes.get("/", ctx -> ctx.result("Hej verden"));
}).start(7070);
Når serveren kører, kan vi åbne:
http://localhost:7070/
og få:
Hej verden
En route fortæller Javalin, hvad der skal ske, når en bestemt HTTP-request kommer ind.
En route består grundlæggende af:
- en HTTP-metode
- en path
- en handler
config.routes.get("/", ctx -> {
ctx.result("Hej verden");
});
eller kort:
config.routes.get("/", ctx -> ctx.result("Hej verden"));
En route til /users:
config.routes.get("/users", ctx -> {
ctx.result("Her er brugerne");
});
Hvis browseren laver:
GET /users
bliver handleren udført.
POST bruges typisk, når browseren sender data til serveren, for eksempel fra en formular.
config.routes.post("/login", ctx -> {
ctx.result("Du forsøger at logge ind");
});
GET og POST er forskellige HTTP-metoder, selv om de godt kan have samme path:
config.routes.get("/login", ctx -> {
ctx.render("login.html");
});
config.routes.post("/login", ctx -> {
// behandl login
});
Det er meget almindeligt at bruge GET til at vise en formular og POST til at behandle den.
ctx er Javalins context for den aktuelle request.
Det er gennem ctx, at vi blandt andet kan:
- læse data fra requesten
- hente parametre
- sende en response
- sætte statuskode
- gemme data til en template
- arbejde med sessionen
Et meget enkelt eksempel:
config.routes.get("/hello", ctx -> {
ctx.result("Hello!");
});
Her er ctx det objekt, vi bruger til at sende "Hello!" tilbage til browseren.
ctx.result("Hej verden");
Sender tekst tilbage til klienten.
HTML kan også sendes som response:
ctx.html("<h1>Hej verden</h1>");
I vores projekter vil vi normalt bruge Thymeleaf til HTML-sider i stedet.
ctx.status(404);
Sætter HTTP-statuskoden til 404.
Det kan kombineres med en besked:
ctx.status(404).result("Brugeren blev ikke fundet");
Når applikationen vokser, bliver det hurtigt upraktisk at have al kode i Main.
Vi placerer derfor routes og request-håndtering i controllers.
En simpel struktur kan være:
src/main/java
├── controllers
│ └── UserController.java
├── entities
│ └── User.java
├── factories
│ └── UserFactory.java
└── services
└── UserService.java
Controlleren tager sig af kommunikationen med webdelen.
Eksempel:
public class UserController {
public static void login(Context ctx, UserService userService) {
// hent data fra request
// kald service
// send response
}
}
Routes kan derefter pege på controllerens metoder:
config.routes.post("/login",
ctx -> UserController.login(ctx, userService));
Controlleren skal ikke indeholde al forretningslogikken.
Et typisk flow er:
Browser
↓
Controller
↓
Service
↓
Mapper / database
Controlleren arbejder med HTTP.
Servicen arbejder med programmets logik.
Mapperen arbejder med databasen.
Når en HTML-formular sender data til Javalin, kan vi hente værdierne med ctx.formParam().
HTML:
<form action="/login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">Log ind</button>
</form>
Controller:
String username = ctx.formParam("username");
String password = ctx.formParam("password");
Det vigtige er forbindelsen mellem HTML og Java:
name="username"
bliver til:
ctx.formParam("username")
Hvis navnene ikke passer sammen, får du ikke den forventede værdi.
Det er derfor en klassisk fejl at skrive:
<input name="userName">
og forsøge at hente:
ctx.formParam("username");
De to navne er forskellige.
Form-parametre kommer ind som tekst.
String ageText = ctx.formParam("age");
Hvis vi skal bruge et tal, skal vi selv konvertere:
int age = Integer.parseInt(ageText);
Input fra brugeren kan være ugyldigt, så konvertering bør håndteres, når det er relevant.
Query-parametre sendes som en del af URL’en.
Eksempel:
/users?group=1&page=2
Her er:
group = 1
page = 2
De kan hentes med:
String group = ctx.queryParam("group");
String page = ctx.queryParam("page");
Hvis værdien skal bruges som et tal:
int page = Integer.parseInt(ctx.queryParam("page"));
Query-parametre er især nyttige, når brugeren foretager en GET-request.
HTML:
<a href="/users?group=1&page=2">
Vis brugere
</a>
Controller:
config.routes.get("/users", ctx -> {
String group = ctx.queryParam("group");
String page = ctx.queryParam("page");
});
En værdi kan også være en del af selve URL’en.
/users/42
Route:
config.routes.get("/users/{id}", ctx -> {
String id = ctx.pathParam("id");
});
Her er 42 værdien af id.
Vi kan for eksempel konvertere den:
int id = Integer.parseInt(ctx.pathParam("id"));
Path-parametre er praktiske, når URL’en identificerer en bestemt ressource.
/users/42
/tasks/17
/products/5
En controller kan sende browseren videre til en anden route.
ctx.redirect("/users");
Det er ofte relevant efter en POST.
For eksempel:
config.routes.post("/login", ctx -> {
// log brugeren ind
ctx.redirect("/tasks");
});
Browseren bliver derefter sendt til /tasks.
Et typisk flow kan derfor være:
POST /login
↓
Login behandles
↓
redirect("/tasks")
↓
GET /tasks
↓
Vis tasks
Når vi bruger Thymeleaf, kan Javalin rendere en template:
ctx.render("login.html");
Templates ligger i:
src/main/resources/templates
En controller kan også sende data med:
ctx.attribute("user", user);
ctx.render("profile.html");
Thymeleaf kan derefter bruge user i HTML’en.
<h1 th:text="${user.username}"></h1>
Se Thymeleaf-opslagsværket for flere eksempler.
CSS, billeder og almindelige HTML-filer kan ligge i public.
src/main/resources
├── public
│ ├── css
│ └── images
└── templates
Javalin kan konfigureres til at servere filer fra public:
Javalin.create(config -> {
config.staticFiles.add("/public");
});
Javalin matcher først dine endpoints og forsøger derefter at finde en static file. Hvis ingen route eller static file matcher, får klienten en 404.
Her er et eksempel, hvor flere ting sættes sammen:
config.routes.post("/login", ctx -> {
String username = ctx.formParam("username");
String password = ctx.formParam("password");
User user = userService.login(username, password);
if (user != null) {
ctx.sessionAttribute("user", user);
ctx.redirect("/tasks");
} else {
ctx.status(401);
ctx.render("login.html");
}
});
Controlleren:
public static void login(Context ctx, UserService userService) {
String username = ctx.formParam("username");
String password = ctx.formParam("password");
User user = userService.login(username, password);
if (user != null) {
ctx.sessionAttribute("user", user);
ctx.redirect("/tasks");
} else {
ctx.status(401);
ctx.render("login.html");
}
}
Og routen:
config.routes.post("/login",
ctx -> UserController.login(ctx, userService));
Det er denne type opdeling, vi bruger, når applikationen vokser.
| Jeg vil… | Jeg bruger… |
|---|---|
| Vise en side | GET |
| Sende data fra en formular | POST |
| Hente en formværdi | ctx.formParam() |
Hente en værdi fra URL’en efter ? | ctx.queryParam() |
| Hente en værdi fra en path | ctx.pathParam() |
| Sende tekst tilbage | ctx.result() |
| Sende HTML tilbage | ctx.html() |
| Vise en Thymeleaf-template | ctx.render() |
| Sende data til Thymeleaf | ctx.attribute() |
| Gemme data i sessionen | ctx.sessionAttribute() |
| Sende browseren til en anden route | ctx.redirect() |
| Sætte HTTP-status | ctx.status() |
Javalin tager sig af webkommunikationen.
Controlleren tager sig af request og response.
Servicen tager sig af programmets logik.
Thymeleaf tager sig af præsentationen.
Databasen kommer senere:
Browser
↓
Javalin
↓
Controller
↓
Service
↓
Mapper
↓
PostgreSQL
Du behøver ikke kunne huske alle metoderne. Det vigtigste er at vide, hvad du leder efter, når du skal løse en konkret opgave.