Kaparjuk meg a felszínt¶
Általában a legnehezebb egy új fejlesztői környezet elsajátításában azt meghatározni, hogy hol is kezdjünk neki. Ez a probléma erősen fennállhat a Spring esetében is, hiszen hihetetlenül változatos és sokrétű megoldásokat biztosít. Remélhetőleg jelen jegyzet segít ezen probléma legyűrésében.
Spring Framework dokumentáció¶
Bevezetés¶
Az első és legfontosabb kérdés az, hogy mi is a Spring? A Spring egy lightweight keretrendszer Java alkalmazások fejlesztéséhez. A Spring keretrendszer nem korlátozza azt, hogy milyen jellegű alkalmazások fejleszthetőek a segítségével, lehet az webes vagy asztali, lehet microservice vagy egy monolit alkalmazás. A lightweight szó jelentése viszont itt nem arra utal, hogy a Spring kevés osztályt biztosít a számunkra, mert ez egyáltalán nincs így. Ez arra utal, hogy egy meglévő alkalmazásunkhoz a Spring nyújtotta előnyöket könnyen hozzáadhatjuk úgy, hogy csupán néhány helyen kell változtatnunk a meglévő kódbázison. A Spring Framework-öt önmagában, vagy akár JavaEE helyett is használhatjuk. Támogatja a Groovy és a Kotlin nyelveket is.
Az első verziója 2002-ben jelent meg Rod Johnson - Expert One-on-One J2EE Design and Development könyve alapján. Jelenleg az 5.0 fő verziónál tartunk, mely első kiadása 2017-ben történt meg.
Maga a Spring keretrendszer nyílt forráskódú.
A Spring modulokra van osztva, melyek egy-egy JAR fájlba kerülnek bele. Az 5.0.0.RELEASE-től a Spring több, mint 20 modullal rendelkezik (így ennyi JAR-ral is). A következő táblázat összefoglalja a főbb modulokat:
Modul neve | Leírás |
---|---|
aop | Aspektus orientált fejlesztéshez szükséges osztályok. AspectJ alap integrációt támogató osztályok is itt találhatóak. |
aspects | Haladó Aspect integrációt támogató osztályok |
beans | A bean-ek manipulálásához szükséges osztályok. Bean factory implementációk is itt találhatóak (pl. XML vagy annotáció alapú megadáshoz). |
context | Spring Core kiterjesztéséhez használatos osztályok. ApplicationContext, EJB, JNDI, JMX, remoting, dinamikus szkriptnyelv integrációk (JRuby, Groovy, ...). |
context-indexer | Nagy projektek esetén fordítási időben index-et készíthetünk a komponensekről, így gyorsíthatjuk az alkalmazás indítását. Az indexelést ez a modul képes elvégezni. |
context-support | spring-core module kiterjesztése: mail support, template engine integráció, task execution, scheduling (CommonJ, Quartz) |
core | A fő modul, mely minden Spring-es alkalmazásban kelleni fog. Ezt az összes további modul is használja. |
expression | Spring Expression Language (SpEL) támogatás |
instrument | Instrumentáláshoz segítség |
jdbc | JDBC osztályok, minden DB-vel dolgozó alkalmazáshoz kell. |
jms | Java Message Service (JMS) támogatás |
messaging | Üzenet alapú rendszerek támogatás, STOMP support. |
orm | ORM eszközök: Hibernate, JDO, JPA, iBATIS. |
oxm | Object XML Mapping eszközök: Castor, JAXB, XMLBeans, XStream |
test | Mock osztályok teszteléshez. Szoros JUnit integráció |
tx | Tranzakciós infrastruktúra (JTA) |
web | Webes fejlesztéshez szükséges core osztályok |
webflux | Spring Reactive Web support |
web-mvc | Spring MVC support |
websocket | JSR-356 (Java API for WebSocket) support |
Ne aggódjunk, ha a fentiek közül sokat nem ismerünk és kicsit idegennek tűnnek, a lista sok-sok elemét fel fogjuk fedezni.
Fejlesztői környezet¶
A használt JDK verzió a 11-es lesz, így mindenkinek ezt javaslom telepíteni.
A kurzus során az IntelliJ-t fogjuk használni, viszont a forráskódok abszolút IDE függetlenek, tehát használhatjuk a következő IDE-ket is:
Hello World reboot¶
IntelliJ-ben készítsünk egy új Maven-es projektet (semmi extra függőség nem kell egyelőre)! Tekintsük a klasszikus Hello World programot, amiről nagy valószínűséggel már mindenki hallott, aki eddig nem a holdon élt!
1 2 3 4 5 6 7 |
|
A program egyszerűen kiírja a Hello World
szöveget a konzolra.
Nagyszerű. Azonban, ha igazán górcső alá vesszük, akkor kiderül, hogy van vele néhány probléma.
Először is, nem rugalmas és nem bővíthető a kód.
- Mi van, ha le szeretnénk cserélni a kiírandó szöveget?
- Mi van, ha a kiírandó szöveget másképpen szeretnénk kiírni? Mondjuk a standard error-ra akarjuk írni, vagy HTML tag-ek közé szeretnénk zárni.
Oké, ezek alapján tervezzük át az alkalmazást, hogy a szöveget könnyen módosíthassuk, illetve az is könnyen megadható legyen, hogy a "renderelés" hogyan történjen. Ez a kettő megoldható lenne a fenti alkalmazásban is olyan módon, hogy átírjuk a kódot, de az egy nagy alkalmazás esetén teljes újrafordítást igényel, illetve a teszteket is újra kell futtatni, stb. Egy jobb megoldás lehet, hogy futás közben töltjük be a kiírandó szöveget, például a parancssori argumentumokat használjuk erre a célra.
1 2 3 4 5 6 7 8 9 10 11 |
|
A fenti program az első paramétert veszi, amennyiben az létezik és ezt írja ki a konzolra.
Amennyiben nem létezik ilyen, akkor egyszerűen a Hello World
szöveget írja ki.
Ez a kód már tudja azt, amit szerettünk volna. Nem kell újrafordítanunk a programot ahhoz, hogy a szöveget megváltoztassuk. Azonban a másik probléma továbbra is fent áll: az a komponens, ami felelős az üzenet kiírásáért azért is felelős, hogy beolvassa a kiírandó szöveget. Például, ha szeretnénk átdolgozni azt hogy hogyan teszünk szert egy üzenetre, akkor magába a renderer-be is bele kell nyúlnunk, hiszen ezek egy helyen vannak.
Ennek tükrében valósítsuk meg az alkalmazást úgy, hogy a fenti kettő két külön komponens-ben legyen!
Továbbá, ha igazán flexibilis alkalmazást szeretnénk, akkor ezen komponenseket interface-ek mögé kell rejtenünk.
Az üzenet elérésére hozzunk létre egy MessageProvider
interfészt, melynek van egy getMessage()
metódusa!
1 2 3 |
|
Ugyanígy hozzunk létre egy MessageRenderer
interface-t az üzenetek renderelésére.
1 2 3 4 5 |
|
A MessageRenderer
azon felül, hogy képes egy szöveget renderelni, ismernie kell egy MessageProvider
-t, aki ellátja majd azokkal az üzenetekkel, amiket ki akarunk renderelni.
A MessageRenderer
Java Bean-es getter/setter párossal éri el/állítja be a használni kívánt MessageProvider
-t.
A fentiek alapján kijelenthetjük, hogy a MessageRenderer
függ a MessageProvider
-től, hiszen nélküle nem tud mit kiírni.
Miután megvannak az interface-ek, könnyen adhatunk ezekhez implementációt is.
1 2 3 4 5 6 |
|
A MessageProvider
megvalósításunk, minden esetben a Hello World
szöveget adja vissza.
Hasonlóképpen a MessageRenderer
-hez is adhatunk egy egyszerű megvalósítást!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
A megvalósításban a getter/setter
a megszokott módon működik (létrehoztunk egy MessageProvider
field-et).
A render
pedig elkéri a messageProvider
-től a kiírandó üzenetet, majd kiírja azt a konzolra.
Ezek után már csak a main
-t kell újraírni.
1 2 3 4 5 6 |
|
A fenti megvalósítás elég egyszerű, de mégis megszűntette a szoros kapcsolatot az üzenet beszerzése és az üzenet kiírása között. A laza csatoltság szem előtt tartása kulcsfontosságú a jól karbantartható kód eléréséhez.
Mi van akkor, ha meg akarom változtatni az implementációját valamelyik interfésznek (lecserélni az implementációt egy másik osztályra)?
Ilyen esetben megint a kódban kell matatnom, ami megint csak azt jelenti, hogy újra kell fordítanom az egész kódot, stb.
Ennek megoldására készítsünk egy Factory
osztályt, ami egy properties fájlból beolvassa futás közben a használni kívánt megvalósításokat és ennek tükrében példányosítja le a MessageRenderer
és MessageProvider
megvalósításokat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Vizsgáljuk meg a MessageSupportFactory
osztályt!
Először is egy Singleton
-al van dolgunk, mivel ebből nem szeretnénk több példányt egyszerre a memóriában.
Ezt valósítja meg a private konstruktor, a static init, és a statikus instance
field.
A static init
blokkban példányosítunk egyet a factory-ból, ahol betöltjük a properties
fájlt, ami megmondja, hogy mely osztályokat szeretnénk használni a program végrehajtása közben.
A kapott osztálynevek alapján futás közben reflection-nel példányosítunk egy MessageProvider
-t és egy MessageRenderer
-t.
Amikor kívülről meghívjuk a getMessageProvider
és getMessageRenderer
metódusokat, akkor már ez a betöltés megtörtént (static init miatt).
A msf.properties
fájl tartalma a következő:
1 2 |
|
Ennek tükrében a main
így módosul:
1 2 3 4 5 6 |
|
A fenti megvalósítás ilyen formában már elfogadható és könnyebben karbantartható.
A fentiek eddig semmilyen módon nem használták fel a Spring nyújtotta előnyöket. A következőkben megnézzük, hogyan lehetne megoldani ugyanezt a problémát a Spring keretrendszerrel.
Új Spring projekt létrehozás¶
Egy Spring-es projektet fel lehet építeni egy üres projektből is, viszont ez kidobott idő, mivel elég favágó meló lenne. Kis projektek esetében ez nem okoz nagy problémát, de nagyobbak esetében sok fejfájást okozhat. Ezen a ponton lép be a képbe a Spring Boot, melynek segítségével sokkal egyszerűbben készíthetünk Spring Framework alapú alkalmazásokat. Jelen pillanatban annyit jegyezzünk meg, hogy a Spring Boot a Spring Framework-re épül, azaz kiegészíti azt, hogy szebb legyen a világ.
Létezik egy eszköz, amit Spring Initializr hívnak, melynek néhány alaptulajdonságot megadnuk és ezek alapján legenerál nekünk egy Spring-es alkalmazást (egész pontosan Spring Boot alkalmazást). Maga a projekt egy open-source fejlesztés, melynek a repository-ja megtalálható a GitHub-on.
A webes interfész a következőképpen néz ki:
Az eszköz elérhető az IntelliJ-n belül is (mivel REST API-ja is van) a New -> Project
alatt bal oldalt ki tudjuk ezt választani (Spring Initializr
).
Mi az IntelliJ-n belüli verziót fogjuk használni, mivel így nem kell bajlódni az eredmény *.zip
állomány kicsomagolásával sem.
Első alkalommal nem kell semmit sem beállítanunk (kivéve, hogy 11-es Java-t fogunk használni, ezt ennek megfelelően állítsuk be, ha szükséges), egyszerűen nyomjuk végig a Next-et!
Miután létrejött a projektünk, vegyük sorra, hogy mi keletkezett.
Alapvetően egy maven-es projekt struktúrát látunk.
Az alkalmazásunk Java kódjai az src/main/java
alatt találhatóak, a teszt kódok az src/test/java
alatt, a nem Java kódok pedig az src/main/resources
alatt.
Ezen felül van néhány dolog ami még belekerül a projektbe, így tegyük őket is a helyükre.
mvnw és mvnw.cmd
: Ezek a Maven wrapper scriptjei, amik akkor jönnek jól, ha a build gépen nincs maven telepítve, akkor is végre lehet hajtani a build-et (mvnw
-> Linux,mvnw.cmd
-> Windows alatt).pom.xml
a szokásos maven build descriptor. Mindjárt részleteiben is tanulmányozzuk.DemoApplication.java
: Az alkalmazásunk fő belépési pontja. (Az összes Java forráskód a legenerált projektsrc
mappájában található.)application.properties
: Alapból üres, de itt adhatjuk meg a konfigurációs beállításokat. Később látni fogjuk, hogy miket lehet ide elhelyezni.DemoApplicationTests.java
: Egy egyszerű teszt, mely ellenőrzi, hogy sikerült-e betölteni azApplicationContext
-et (később látjuk, hogy mi is az pontosan).
A generált Maven build specifikáció¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Az első és legfontosabb dolog, hogy meg van adva egy parent POM, mégpedig a spring-boot-starter-parent
.
Ez azért jó, mert a dependency-k alatt a függőségek verzióját ez alapján állítja be a rendszer, így nincs szükség ennek megadására.
Például a spring-boot-starter
dependency megadásánál biztosak lehetünk abban, hogy kompatibilis verziókkal dolgozunk, ezért hívják ezeket curated dependency-knek.
Megjegyzés
A verziókat magunk is megnézhetjük, ha Windows alatt lenyomva tartjuk a Ctrl billentyűt és közben rákattintunk a <artifactId>spring-boot-starter-parent</artifactId>
-ra.
Itt a következő rész található a pom.xml
-ben:
1 2 3 4 5 |
|
Ahol egy ismételt Ctrl + kattintás után láthatjuk a megadott property-ket:
1 2 3 4 5 6 7 8 9 10 11 |
|
A fenti lista eltérhet a saját projektünkben, attól függően, hogy milyen Spring Boot verziót használunk, illetve milyen dependency-ket adtunk meg az Initializr-nek.
Érdemes megfigyelni, hogy a dependency-k artifactId-jában szerepel a starter
szó.
Ezek a library-k speciálisak abban az értelemben, hogy nincs bennük tényleges kód, hanem tranzitív módon behúznak további library-kat.
Ezen starter dependency-k előnyei:
- Kisebb build file-t eredményez, így könnyebben karbantarthatóvá válik a build leíró.
- Elég a képességek alapján behúznunk a dependency-ket, mintsem konkrét library neveket kelljen összeszedni (például, ha webes alkalmazást szeretnénk fejleszteni, akkor behúzzúk a
spring-boot-starter-web
dependency-t, és ő minden olyan dependency-t behúz ami ebben a starter projektben szerepel, mint szükséges függőség). - A verziók megadásával sem kell bajlódnunk. Az egyetlen verzió, amit ki kell találnunk, hogy melyik Spring Boot-ot használjunk.
A Spring Boot Starter-ek listáját megtekinthetjük a GitHub repoban.
A build leíró a spring-boot-maven-plugin
megadásával végződik.
Ez a plugin néhány igen fontos dolgot biztosít a számunkra:
- Ad egy Maven goal-t (
spring-boot:run
), amivel futtathatjuk is az alkalmazásunkat. - Biztosítja, hogy az összes library, amit használunk, belekerül a futtatható JAR file-ba, és azok belekerülnek a classpath-ba.
- Készít egy manifest fájlt is, amit szintén belerak a futtatható JAR-ba. Ebben megadja az alkalmazás fő belépési pontját.
A Spring Initializr használatát a következő videó mutatja be:
Feladat
Hozzunk létre egy új Spring Boot projektet! Használjuk a Spring Initializr-t! Próbáljuk ki a webes interfészt és az IntelliJ adta lehetőségeket is!
Az alkalmazásunk bootstrappelése¶
Bootstrapping: Az alkalmazás olyan formájúra alakítása, ami nem igényel külső inputot ahhoz, hogy elinduljon és ezt az indítást automatikusan meg tudja tenni.
Esetünkben fő osztályunk, mely a DemoApplication
, elindul és ő tölti be az összes keretrendszer által szükséges dolgot, a modelleket, a konfigurációkat, a kontrollereket, majd átadja az irányítást ez utóbbiaknak.
Tehát a lényeg, hogy egy nagyon egyszerű fájl hatalmas háttér processeket indít, de ez el van rejtve előlünk.
Ezt a hatalmas melót rejti el igazából egy Spring Boot alkalmazás.
Nézzük meg hogy, hogyan!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
A legfontosabb a @SpringBootApplication
annotáció, mely elvégzi a fent említett hatalmas munkamennyiséget.
Ez az annotáció igazából 3 másik annotáció kombinálása:
@SpringBootConfiguration
: Az adott osztály egy konfigurációs osztály (ezekről lesz szó később részletesen). Ez az annotáció a@Configuration
annotáció egy speciális változata. Ez jelenleg annyit mond, hogy írhatnánk konfigurációs beállításokat a fő fájlunkba is.@EnableAutoConfiguration
: A Spring Boot automatikusan konfigurálja azokat a komponenseket, amelyekről azt gondolja, hogy szükségesek lehetnek a számunkra (később erről is bővebben lesz szó).@ComponentScan
: Segítségével lehetővé válik egyes komponensek (osztályok) automatikus betöltése (példányosítása). Az automatikusan létrehozott komponenseket@Component, @Controller, @Service
, stb. annotációkkal látjuk el. Ezeket a Spring automatikusan regisztrálja a Spring Application Context-jében, azon belül is a bean konténerben.
A fentiek közül az utóbbira fogunk nemsokára látni egy példát. Ne ijedjünk meg, ha most még nem értjük ezek pontos működését.
A következő fontos rész a main()
metódus.
Ez belül meghívja a statikus run
metódusát a SpringApplication
-nek, mely a tényleges bootstrappelést elindítja, illetve megkonstruálja az ApplicationContext
-et.
Itt megmondjuk a konfigurációs osztályunkat (ami saját magunk), illetve tovább tudjuk passzolni a parancssori argumentumokat is.
Nem maradt más mint futtatni a projektünket. Jelen esetben az alkalmazás nem fog semmit sem csinálni, de futtassuk le, hogy ha bármi félresikerült akkor az már most tapasztalhassuk!
Hello World Spring használatával¶
Az előző alkalmazásunkkal az a probléma, hogy ahhoz hogy az alkalmazásunk ténylegesen összeálljon (laza csatolás mellett) elég sok "glue code"-ot kellett írnunk.
Ezen felül a MessageRenderer
-t továbbra is nekünk kellett manuálisan ellátni egy MessageProvider
példánnyal, hogy az működni tudjon (ha megvan, hogy pontosan milyen provider
kell, akkor ezt automatikusan is megoldhatná a rendszer).
Ezeket a problémákat a Spring segítségével mind meg tudjuk oldani.
A Spring-es megoldásban teljesen megszabadulhatunk a MessageSupportFactory
-tól és helyette használhatjuk a Spring által biztosított ApplicationContext
interface-t.
Ez az interface szolgáltatja a környezeti információkat a programról a Spring számára.
Továbbá ez az interface egy másik interface leszármazottja, a ListableBeanFactory
-é, ami bármilyen Spring által menedzselt bean szolgáltatójaként funkcionálhat.
Nézzük is a példát, amin kicsit egyszerűbben megérthetjük, hogy mi is történik!
TODO: videó
Először a konkrét megvalósításokból komponenseket képzünk, amiket így a Spring fel fog ismerni és a @ComponentScan
miatt bele is fogja azokat tenni az úgynevezett bean konténerbe az ApplicationContext
-en belül.
Például a HelloWorldMessageProvider
-t a következőképpen alakítjuk át:
1 2 3 4 5 6 7 |
|
Az egyetlen különbség, hogy az osztályt elláttuk a @Component
annotációval.
A @Component
annotáció az org.springframework.stereotype
csomagban található a többi úgynevezett stereotype annotációval együtt.
Ezen annotációval ellátott osztályokat a @ComponentScan
automatikusan regisztrálni fogja.
Ami azt jelenti, hogy regisztrál egy példányt a bean konténerben.
A hangsúly azon van, hogy példányosít is számunkra egy objektumot és beleteszi egy konténerbe.
Futás közben a használat helyén a Spring ebből a konténerből képes számunkra objektumokat adni és nem kell példányosítanunk azokat.
Nézzük a MessageRenderer
megvalósítását:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Ezt az osztályt is ellátjuk a @Component
annotációval, így az bekerül a bean konténerbe.
Mivel a StandardOutMessageRenderer
megvalósításnak szüksége van egy MessageProvider
-re, így azt egy field-ben adjuk meg.
Ugyanakkor nem példányosítunk a függőségből, mert azt a Spring fogja számunkra adni.
Egy lehetséges megoldás az, hogy a StandardOutMessageRenderer
konstruktorában, mint paraméter megadjuk a MessageProvider
-t, így a Spring tudni fogja, hogy amikor egy StandardOutMessageRenderer
objektumot akar példányosítani, akkor egy MessageProvider
típusú objektumot kell átadnia, azaz injektálnia.
Ezt nevezzük Dependency Injection-nek.
A fenti megoldás csak egy lehetséges megközelítés, mely mellett számos más megoldást is alkalmazhatunk, de ezekről majd később lesz szó.
Ezután már csak a main
-t kell igazítanunk:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Jelen esetben, mivel a main
egy statikus metódus, így egy statikus field-be injektáltatjuk be a MessageRenderer
objektumot (itt sem kell példányosítanunk).
Ezután egyszerűen használhatjuk az objektumot, azaz meghívhatjuk a render
metódusát.
A futtatás után a következő kimenetet kapjuk:
1 |
|
Említettük, hogy a bean konténer az ApplicationContext
részét képezi, mely osztály a spring-context
modulban található meg, így a függőségeink között szerepelnie kell a következőnek:
1 2 3 4 5 6 7 |
|
Amennyiben megnézzük, a pom.xml
-ben nincs ilyen függőség megadva, azonban a spring-boot-starter
dependency, mint ahogy említettük, tranzitívan behúz további függőségeket, az alkalmazásunk megkapja ezt a függőséget és ezen felül még néhányat (pl.: aop
, beans
, stb.).
ApplicationContext¶
Most, hogy láttunk egy Spring Boot alkalmazást, nézzünk bele kicsit az ApplicationContext-be:
1 2 3 4 5 6 |
|
A SpringApplication
statikus run
helper metódusa vissza is ad számunkra egy ApplicationContext-et
, melyet eltárolhatunk, ha éppen erre van szükségünk.
Az ApplicationContext
rendelkezik néhány túlterhelt (overloaded) getBean
metódussal, melyek közül most azt használtuk, mely egy típust vár paraméterként, vagyis azt, hogy milyen típusú bean-re van szükségünk.
A Spring minden olyan osztályt bean-nek hív melyet a Spring menedzsel, azaz ami belekerül a bean konténerbe.
Ha körbenézünk, akkor van lehetőség bean név alapján is lekérni egy-egy objektumot a konténerből.
Mivel az előző példákban nem adtunk meg sehol nevet, így jogosan feltehető a kérdés, hogy ez használható-e jelen esetben.
A válasz igen, mivel a Spring a megadott @Component
osztályokat alapértelmezetten az osztály nevével regisztrálja, annyi különbséggel, hogy kis kezdőbetűvel kezdi azok nevét.
A fenti helyett, így használhatjuk a következőt is, azonban itt mivel nem ismerjük előre a típusát egy "natúr" Object
típusú eredményünk van, amit aztán kasztolnunk kell.
1 |
|
A két előbbit kombinálhatjuk is, hogy elkerüljük a kasztolást.
1 |
|
A név alapú lekérdezéskor és a típus alapú lekérdezéskor is használható egy haladó lekérdezés, melynek során akár felül is írhatjuk a beanhez tartozó konstruktor paramétereket:
1 |
|
Jelen eseben ugyanúgy a HelloWorldMessageProvider
-t használtuk, mely pusztán demonstrációs jellegű.
Arra is lehetőségünk van, hogy az ApplicationContext
-től elkérjük a konténerben lévő bean-ek listáját (azok nevét), illetve azok számát:
1 2 3 4 |
|
Amennyiben így futtatjuk az alkalmazásunkat, akkor egy kis meglepetés ér minket, ugyanis a kimenet a következő:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
A 41 határozottan több, mint az a két komponens, amit mi létrehoztunk és a Spring gondjaira bíztunk. Ugyanakkor a listában határozottan jelen van a két bean-ünk. A többi 39 bean-t a Spring automatikusan hozza létre, melyek között megtalálhatóak azok, amik az automatikus konfigurációhoz, a property állományok beolvasásához és további tevékenységekhez szükségesek, de ezekről később ejtünk majd szót.
@Configuration¶
Spring Boot nélküli élet¶
Ahhoz, hogy jobban megértsük, hogy mi történik a háttérben megnézzük azt is, hogy Spring Boot nélkül milyen opcióink lennének.
A Spring Boot alkalmazásunkban a run
metódus egy ConfigurableApplicationContext
objektumot adott vissza, ugyanakkor, ha Spring Boot nélküli kóddal találkozunk, akkor ott nem is tudjuk meghívni ezt a statikus run
metódust, mivel a SpringApplication
osztály a Spring Boot-ból jön.
A megoldás a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 |
|
A fő osztályunkat ellátjuk a @ComponentScan
annotációval.
Itt elevenítsük fel, hogy a @SpringBootApplcation
annotáció magában foglalja a @ComponentScan
annotációt is.
A @ComponentScan
a főosztály csomagjában rekurzívan keresi a bean-eket (pl.: amit @Component
-el láttunk el) és ezeket regisztrálja az ApplicationContext
-ben.
Az AnnotationConfigApplicationContext
, ahogy a neve is mutatja, képes az annotációkkal ellátott komponensek betöltésére, melynek paraméterben megmondhatjuk, hogy melyik komponens osztályokat szeretnénk használni a ApplicationContext
inicializálásához.
Mivel itt megadjuk a fő osztályunkat, amin a @ComponentScan
is megtalálható, így a MessageRenderer
és a MessageProvider
komponensek is betöltődnek, hiszen a paraméter nélküli @ComponentScan
az adott osztály csomagjában (rekurzívan) keresi a komponenseket.
Látható, hogy Spring Boot nélkül se lenne olyan nehéz az élet, ugyanakkor ezen felül rengeteg egyéb dolgot is tesz értünk a Spring Boot, így használatát teljes mértékben javaslom.
XML alapú élet¶
Régebben az XML alapú megadás volt a bevett szokás a komponensek kezeléséhez. A bean-ek kezelése azonban kicsit nehézkesebb vele. A részletekbe nem belemenve, nézzünk meg egy példát:
1 2 3 4 5 |
|
Mint látható egy ClassPathXmlApplicationContext
osztályt használunk az ApplicationContext
elérésére.
Ez az osztály a konstruktorában megkapja, hogy melyik XML állományt szeretnénk használni az adott context konfigurálásához.
A bean-eket a következőképpen adjuk meg az XML-ben:
1 2 3 4 5 6 7 8 9 10 |
|
Miután betöltöttük a szükséges context információkat, a main
-ben a getBean
visszaad nekünk egy inicializált MessageRenderer
-t, melyet egyből használhatunk.
A Spring a megadott függőségeket feloldja és injektálja azokat a kód megfelelő részeibe.
Miközben az ApplicationContext
inicializálása zajlik, a Spring regisztrálja a bean-t a provider
id-val és példányosít is nekünk egy objektumot belőle.
A renderer
esetében hasonló történik, de értesítjük is a rendszert arról, hogy ez a bean bizony függ egy másiktól, aminek meg is adjuk az id-ját, így tudja, hogy az előbb példányosított bean-t kell felhasználnia a renderer-nél is, azaz jóformán meghívja a setter metódusát a provider
bean paraméterrel.
Összegzés és kitekintés¶
A Spring lelke az úgynevezett Inversion of Control (IOC), amely kiszervezi (automatizálja) a komponensek (osztályok) példányosítását, illetve az azok közötti függőségek kezelését is vezérli.
Tekintsünk egy olyan esetet, amikor a Foo
osztály használni akarja a Bar
nevű osztály egy példányát.
Tipikusan a Foo
példányosít magának egy Bar
objektumot a konstruktor meghívásával (vagy esetleg egy Factory
használatával), majd felhasználja a kapott objektumot.
IoC használatával a Bar
objektumot futás közben biztosítja a keretrendszer, nincs szükség példányosításra.
Ezt a viselkedést nevezzük dependency injection-nek, amit gyakran azonosítani is szoktak az IoC-vel.
A Dependecy Injection két dologra alapszik:
- Java Bean-ek használatára
- Interface-ekre
A következő fejezetben mélyebben is tanulmányozni fogjuk ezen fogalmakat.