BlikjentmedSpring-Kapittel2:VårførsteSpringwebapp
12.11.21
Kapittel 2 - Vår første Spring webapp
I dette kapittelet skal vi bygge en enkel webapplikasjon ved hjelp av Spring Boot. Prosessen vil demonstrere hvor raskt og enkelt det er å få bygget en fungerende applikasjon som kan brukes både til prototyping og senere i et produksjonsmiljø.
Koden til kapittelet ligger på GitLab her.
Dette trenger du før du starter
Dette er en veiledning i bruk av rammeverket Spring og i den forbindelse er det en fordel å inneha noe kjennskap til Java/Kotlin/Groovy eller et annet objekt-orientert programmeringsspråk. Det vil primært være Spring-relaterte aspekter som vil bli forklart i detalj.
Det er kun én nødvendig, og mange valgfrie, programmer du trenger å installere på forhånd for å kunne kjøre sluttproduktet av denne veiledningen. Den nødvendige programvaren er Java JDK (Java Development Kit). Java er open-source og det finnes flere distributører som tilbyr en JDK. Her lar jeg det være opp til leseren å velge ønsket distributør og metode å installere, men jeg har valgt å installere AdoptOpenJDK versjon 17 på min maskin. Ansvaret for utviklingen av AdoptOpenJDK har nylig blitt overtatt av Eclipse Foundation og har fått nytt navn; Temurin. Denne organisasjonen ble så vidt introdusert i kapittel 1 av denne serien. Om dere ønsker å bruke en annen versjon av JDK så er det helt i orden, men jeg anbefaler å bruke versjon 11 eller nyere.
Oracle meldte nylig at de reverserer deres upopulære beslutning om å ta seg betalt for å bruke deres JDK i produksjon. Dette er nok et resultat av at kundene hoppet over til alternative versjoner av JDK som AdoptOpenJDK og at denne potensielle inntekten for Oracle uteble.
Jeg kommer til å bruke Visual Studio Code som teksteditor i denne artikkelen. Bakgrunnen for dette er at den er er gratis, enkel i bruk og modulær. Visual Studio Code kan brukes til alt av utvikling og tekstredigering og det er stor sjanse for at den kan bli gjenbrukt til andre prosjekter. En av de store fordelene med Visual Studio Code er at den baserer seg på extensions (tilvalg). Rett ut av boksen er programmet i bunn og grunn et veldig fancy tekstredigeringsprogram. Ønsker du å bruke Visual Studio Code til å programmere i for eksempel Java så kan du installere en extension som gir deg kodenavigering, auto-fullføring, syntax-highlightfng, refaktorering, debugging og mye, mye mer. Dersom du ikke allerede har installert extension for Java så anbefales Extension Pack for Java av Microsoft. Den skal dekke alle behovene. Som ekstra tilvalg kan du installere Spring Boot Tools da den gir ekstra validering av Spring-relaterte filer som er praktisk i senere kapitler.
Dersom du heller foretrekker IntelliJ IDEA, Eclipse eller noe annet så er du velkommen til å bruke det du ønsker, men veiledningen fremover vil bruke illustrasjoner fra Visual Studio Code.
Spring Initializr
En film som alltid etterlater meg motivert og engasjert er Marvel sin Iron Man. Jeg har flere ganger søkt opp montasjer på YouTube av hvordan Tony Stark utvikler drakten sin og hvordan han for hver iterasjon utbedrer en mangel som forhindret han fra å bekjempe en en motstander. Hver iterasjon har også en mer avansert (og dødskul) aktiveringsmetode (suit-up). Jeg tror ikke jeg er alene om å tenke «hva skal til for å bygge en slik drakt i virkeligheten?». «Hvis man integrerer alle disse sensorene, vil det fungere?». Helt siden jeg var barn har jeg ventet tålmodig på at menneskeheten skal oppdage en energikilde som er god nok til å kunne drifte et lasersverd. Nå inkluderer ventingen også Iron Man drakt. Jeg heier på dere, forskere!
Spring Boot har også en ganske kul aktivering. Eller Spring Initializr som det heter. I bunn og grunn er det et API som returnerer en ferdig oppsatt Spring-applikasjon som kjører. Ønsker du deg noen bestemte biblioteker i tillegg? Ikke noe problem. Spring Initializr ber deg velge alt du ønsker deg og svarer med et ready-to-go oppsettet.
Det første steget er å åpne nettleseren og naviger til start.spring.io. Her blir du møtt med et skjema i to deler. På venstre side av skjermen får du muligheten til å velge grunnleggende oppsett som byggverktøy og språk. På høyre siden velger du hvilke avhengigheter du ønsker. La oss ta for oss venstre side først.
Det første valget du får er om det skal være et maven -eller gradleprosjekt. Dette er to populære byggverktøy som bistår med å bygge applikasjonen din. Jeg har valgt maven da det er det jeg kjenner best. Jeg oppfordrer deg til å velge det du selv ønsker. Maven er et byggverktøy som har eksistert lenge, mens Gradle er nyere og litt mer kompakt. I kodeeksemplene vil du se eksempler på både maven og gradle. Merk at dersom du velger Gradle bør du velge java 11 for å unngå å måtte oppgradere versjonen Spring Initializr gir deg.
Videre blir du bedt om å velge språk. Her kan du velge mellom Java, Kotlin eller Groovy. Nok en gang oppfordres du til å velge det du er mest komfortabel med, men dersom du skal følge kodeeksemplene videre så vil disse være i Java.
Neste valg er hvilken Spring Boot-versjon man ønsker. Spring Initializr har forhåndsvalgt den siste stabile versjonen for deg. Jeg anbefaler å velge den. Versjonsnummeret kan variere fra skjermbildet over avhengig av når du leser denne artikkelen. Nye versjoner av Spring Boot slippes fortløpende.
Videre blir du bedt om å fylle inn informasjon om prosjektet ditt. Denne informasjonen blir brukt til å navngi mappene i filstrukturen og tilhørende pakkenavn. Her kan du også velge fritt, men det er kutyme å ha reversert domenenavn. I mitt tilfelle er domenenavnet mitt progit.no så gruppenavnet mitt blir no.progit. Artifaktnavnet er selve navnet på applikasjonen, som i eksempelet over er kapittel2. Det er naturligvis mulig å gjøre endringer på dette i etterkant om du ombestemmer deg.
Det siste steget på venstre side av skjemaet er hvordan du ønsker at applikasjonen skal pakkes og hvilken versjon av Java du ønsker å bruke. Her kan du velge «Jar» og velge den versjonen av Java du har installert på maskinen din.
På høyre side av skjema får du muligheten til å velge avhengigheter applikasjonen din trenger. Her er det veldig mange valgmuligheter. Spring Boot har god basiskonfigurasjon for veldig mange biblioteker. Første gangen man skal bygge en Spring Boot applikasjon kan disse valgene føles litt overveldende. I applikasjonen vi skal bygge begrenser vi oss til tre avhengigheter, nemlig Spring Boot DevTools, Thymeleaf og Spring Web.
Spring Boot DevTools er en kjempekul pakke som tilrettelegger for at endringer du gjør i Java-koden automatisk blir bygget på nytt og webserveren blir oppfrisket automatisk. Dette skjer da Spring Boot vet hva som er din kode og hva som er eksterne avhengigheter. Med denne informasjonen vil det være mulig å identifisere og kun kompilere din kode på nytt, noe som er mye raskere enn å måtte bygge alt sammen på nytt.
Thymeleaf er en Java template-engine for å lage HTML-dokumenter.
Spring Web inneholder mange verktøy som er nødvendig for å starte opp en webserver og implementerer REST-protokollen. Denne pakken er essensiell når vi bygger en webapplikasjon.
Det eneste som gjenstår nå er å trykke på «Generate». En nedlastning som inneholder en komprimert versjon av mappestrukturen vil starte. Pakk ut denne og du har en fungerende Spring Boot applikasjon.
Spring Initializr er ofte integrert i de fleste moderne IDE’er. Du kan for eksempel oppnå det samme i Visual Studio Code ved hjelp av en Spring Initializr-extension. Du kan også få identisk nedlastning ved å kun kjøre URL’en som start.spring.io benytter bak kulissene. Dette er noe mer tungvint å gjøre manuelt, men det er fint å vite hvordan det fungerer.
Mappestrukturen
I skjermbildet over kan vi se mappestrukturen du mottar fra Spring Initializr. Dersom du har arbeidet med et maven prosjekt tidligere så er dette kjent terreng. For dere som ikke har arbeidet med Maven tidligere så følger en kort forklaring på mappe -og filstrukturen.
- pom.xml er nøkkelfilen for mavenprosjekter. Det er en XML fil som beskriver applikasjonen, hvilke biblioteker applikasjonen behøver og konfigurasjonsdetaljer som Maven bruker til å bygge applikasjonen. Pom er en forkortelse for Project Object Model. Dersom du valgte Gradle som byggverktøy vil tilsvarende fil hete build.gradle.
- Mvnw.cmd er en kommando som kjører en lokal integrert versjon av Maven for deg. Mvnw er en forkortelse for maven wrapper. Spring initializr pakker med en versjon av maven inn i filsystemet slik at du slipper å ha Maven installert på din maskin. Dersom du valgte Gradle så vil tilsvarnde fil hete gradlew.
- Src-mappen er hvor kildekoden er plassert. Det er i denne mappen Maven/Gradle leter etter Java-klasser for kompilering når applikasjonen bygges.
- Target er mappen der alle de kompilerte klassene samt eventuelle genererte klasser blir plassert.
Dersom du utvider src-mappen så vil du se følgende mappestruktur.
Src-mappen består av to mapper, nemlig main og test. Disse mappene speiler hverandre. Main vil inneholde produksjonskode og test vil inneholde testklasser. Innunder hver av disse mappene så vil det ligge en eller flere mapper med navn java, kotlin eller groovy avhengig av hva du har valgt som programmeringsspråk. (Merk at det er mulig å ha kildekode fra flere språk i samme applikasjon.) I tillegg til dette vil det ligge en mappe som heter resources som inneholder ressurser brukt av kildekoden din. Eksempler på dette er templates, bilder og konfigurasjon.
Til slutt vil det i kildekode-mappen din ligge en fil som heter Kapittel2Application.java under mappestrukturen no.progit. Denne klassen inneholder Java sin main-metode som brukes som startpunkt når programmet kjøres. Main metoden har kun én oppgave og den består av å kjøre SpringApplication.run() metoden. Denne metoden vil opprette en Application Context og tilrettelegge alt som trengs for å starte en Spring-applikasjon.
package no.progit.kapittel2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Kapittel2Application {
public static void main(String[] args) {
SpringApplication.run(Kapittel2Application.class, args);
}
}
Kjør applikasjonen
Dersom du installerte Java-extension til Visual Studio Code som beskrevet tidligere kan du starte Spring-applikasjonen ved å trykke run (eller debug) rett over main-klassen i Kapittel2Application.java. Alternativt kan du skrive følgende kommando i terminalen:
# Maven
./mvnw spring-boot:run
# Gradle
./gradlew bootRun
Uavhengig av hvordan du starter applikasjonen så vil du forhåpentligvis se i terminalen at Spring-applikasjonen startet opp korrekt og kjører på port 8080.
2021-10-23 18:43:36.255 INFO 67603 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-10-23 18:43:36.255 INFO 67603 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 672 ms
2021-10-23 18:43:36.506 INFO 67603 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2021-10-23 18:43:36.527 INFO 67603 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-10-23 18:43:36.535 INFO 67603 --- [ restartedMain] n.p.j.Kapittel2Application : Started Kapittl2Application in 1.465 seconds (JVM running for 1.757)
Spring skriver til loggen at en TomCat-instanse har startet og benytter port 8080. TomCat blir pakket med som en del av Spring Boot og du trenger derfor ikke å deploye applikasjonen din til en applikasjonsserver. Det skjer helt automatisk! Dette er enda en nyttig fordel ved å bruke Spring Boot. Dersom du åpner en nettleser og ser hva som befinner seg på localhost:8080 så vil du kun se en whitelabel page. La oss utbedre dette slik at vi kan vise innhold på denne adressen.
La oss lage en controller
package no.progit.kapittel2.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
Opprett klassen over i en ny mappe som du gir navnet controller innunder no.progit-mappestrukturen. Denne klassen har kun en metode, nemlig home(). Metoden returnerer en String med verdien "home". Klassen har også et par annotasjoner @Controller og @GetMapping. Hva betyr alt dette?
En av oppgavene som utføres når du starter SpringApplication.run() metoden er at Spring scanner etter Java-klasser som befinner seg på samme nivå eller under der main-klassen befinner seg. Spring bruker Java sitt Reflection-API til å kartlegge om klassen skal håndteres av Spring. Dersom en klasse er annotert med annotasjonen @Component så vil Spring plassere denne klassen i sin ApplicationContext. Det finnes i tillegg en håndfull annotasjoner som arver fra @Component og tilbyr ekstra konfigurasjon. Eksempler på dette er @Controller, @Service, @Repository og @Configuration.
I koden vår er klassen annotert med @Controller. Det betyr at Spring forteller DispatcherServlet (som håndterer alle inkommende kall til tjenesten) om klassen vår og sørger for at den delegerer gyldige kall til riktig metode. Men uten mer informasjon blir ingen kall henvist til metoden vår. For å få til dette må vi fortelle DispatcherServlet hvilket kall metoden ønsker å være knyttet til. I koden vår gjøres dette med annotasjonen @GetMapping sammen med en String som beskriver hvilken path den er koblet til. I vårt eksempel er dette rot (/), men du kan naturligvis eksperimentere med dette. Dersom du skriver @GetMapping("test") vil du få samme resultat om du skriver localhost:8080/test i nettleseren din.
Metoden vår returnerer kun teksten "home" akkurat nå. Prøv gjerne å starte opp applikasjonen og ta en titt. Ikke bli overrasket dersom du får en feilmelding. HomeController er nemlig annotert med @Controller som også betyr at Spring forventer at den skal lete etter et view (HTML og CSS) som den skal sende til klienten. Hadde du byttet ut @Controller med @RestController så ville kun teksten "home" blitt sendt til klienten.
La oss opprette et view. Opprett filen home.html I mappen /resources/templates og fyll filen med følgende innhold:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
lang="en">
<head>
<meta charset="UTF-8">
<title>Bli kjent med Spring - kapittel 1</title>
</head>
<body>
<h1>Velkommen til Progit</h1>
<img th:src="@{/images/progit-logo.png}" alt="logo til progit" />
</body>
</html>
Dette er en veldig enkel HTML-fil som skriver ut teksten "Velkommen til Progit" samt viser et bilde. I eksempelet over ligger bilde i mappen /resources/static/ og bildet heter progit-logo.png. Her er du velkommen til å velge et bilde selv dersom du ønsker det eller hente det samme bildet fra kodeeksemplene.
Forsøk å start opp applikasjonen på nytt så vil du se følgende:
Enig, dette er ikke den beste websiden, men det er en start. Og det er ikke mer komplisert enn dette. Med veldig få kodelinjer har vi fått opp en fungerende applikasjon. For dere observante som la merke til at vi også tok inn spring-boot-devtools som en avhengighet så kan jeg melde om at det ikke var nødvendig å stoppe og starte applikasjonen på nytt for å se endringene som ble gjort. Det er utrolig kult og jeg kan love at dere vil spare en masse tid om dere benytter dere av denne reload-mekanismen når applikasjonen blir stor nok.
En annen måte å få verifisert at dette fungerer er å skrive enhetstester. Dette er tester som raskt kan verifisere at en komponent fungerer som den skal. Dersom man lager mange og gode enhetstester underveis i utviklingen så oppdager man raskt dersom en endring har hatt en uønsket innvirkning på en annen komponent. La oss skrive en enhetstest for HomeController.
Enhetstesting av controller
Applikasjonen har allerede alt den trenger av testverktøy. Selv om du ikke eksplisitt valgte det så la spring-initializr til avhengigheten spring-boot-starter-test for deg slik at du har alle verktøyene du trenger for å teste koden din.
Som nevnt i beskrivelsen av mappestruktur så befinner kildekoden seg i main og enhetstestene i test. Det er vanlig å ha samme oppbygning av mappestruktur og filnavn i test-mappen. Vi skal teste HomeController som befinner seg i mappen controller så vi oppretter likegreit en tilsvarende mappe controller i test og oppretter filen HomeControllerTest. Legg merke til at vi har lagt på ordet Test bak klassen vi skal teste.
Fyll HomeControllerTest med:
package no.progit.kapittel2.controller;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@WebMvcTest(HomeController.class)
public class HomeControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testHomePage() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("home"))
.andExpect(content().string(
containsString("Velkommen til Progit")
));
}
}
Når vi skal teste en controller i Spring så vil vi gjerne at Spring skal starte opp og opprette en kobling til samme GET-request som den ville gjort når den kjører normalt. Derfor må vi i kontekst av enhetstesting også starte opp Spring sin ApplicationContext og Spring må finne og håndtere kontrolleren vår. Heldigvis har Spring gjort dette til en lek.
Først og fremst må vi annotere klassen med @ExtendWith(SpringExtension.class). Selve annotasjonen kommer fra jUnit som er et populært testrammeverk. Dette testrammeverket gir muligheten til at man kan kjøre integrasjoner mot selve enhetstestene på forskjellige steg i prosessen. For eksempel før en test er kjørt, etter at den er kjørt og etter at alle testene er kjørt. Spring har laget en extension som sørger for at den kan opprette og opprettholde en ApplicationContext i dette testmiljøet. Det er praktisk.
Den neste annotasjonen @WebMvcTest er også veldig praktisk. I hovedsak så sørger den for å begrense hvor mye auto-konfigurasjon Spring gjør når den starter opp. For det andre så konfigurerer den og gir oss muligheten til å bruke Spring Security (ikke relevant i denne sammenhengen) og MockMvc (relevant). Sistnevnte kan sees som en privat Property for klassen vår med annotasjonen @Autowired. En Autowired-annotasjon over et felt er en teknikk man kan benytte for å fortelle Spring at den kan ta ansvar og håndtere implementasjonen av denne klassen for oss.
Deretter kommer testmetoden vår som naturligvis er annotert med jUnit sin @Test annotasjon. Det betyr at jUnit finner denne metoden og kjører denne i forbindelse med testing. Kun metoder annotert med @Test blir kjørt som enhetstest.
Her bruker vi vår MockMvc-klasse som Spring så generøst har gitt oss til å utføre et kall mot endepunktet /. Deretter vil vi sjekke om responsen fra denne requesten er slik vi forventer. I testen så sjekker vi om responskoden er 200 (OK), at viewet har navnet (home) og at HTML-payloaden inneholder "Velkommen til Progit".
Når du kjører denne testen så kan du legge merke til at Spring starter opp på samme måte som om den hadde blitt startet ordinært, men noe kortere oppstartstid. Her starter Spring opp en en kjørende applikasjon med akkurat nok informasjon slik at den kan utføre testen.
Forhåpentligvis kjører testen OK og du har en fungerende Spring-applikasjon. Dersom du ønsker å se hvordan nøyaktig samme applikasjon ser ut dersom du bruker en kombinasjon av java/kotlin/groovy med enten maven eller Gradle så har jeg opprettet kode som demonstrerer alle mulighetene. Se kodeeksempler.