SOLID principles¶
A SOLID egy mozaikszó 5 különböző tervezési alapelv együttesére, melyek segítenek az alkalmazásunkat rugalmasabbá, érthetőbbé és karbantarthatóvá tenni. Amikor rossz kóddal találkozunk, akkor lehet, hogy elsőre nem is tudjuk megfogalmazni, hogy miért rossz. Ezek mögött általában a kód túlzott komplexitása és a függőségek elburjánzása áll. Amikor egy modulban végzett módosítás több másik modul módosítását is magával vonja, akkor már gyanakodhatunk. A SOLID tervezési elvek szem előtt tartása ezen problémákat igyekszik kiküszöbölni.
Az 5 alapelv a következő:
- Single-responsibility principle: Egy osztály csak egy jól definiált feladattal rendelkezzen, azaz ne legyen egynél több okunk arra, hogy módosítsuk azt. Kerüljük a god class antipatternt! A nagy osztályokat daraboljuk szét több, kisebb osztályra a funkcionalitás alapján.
- Open–closed principle: A komponenseink nyitottak legyenek a bővítésekre, de zártak a módosításokra. Azaz úgy tudjuk bővíteni az osztály funkcionalitását, hogy a meglévő funkcionalitásokon nem kell módosítanunk. Használhatóak absztrakt ősosztályok például.
- Liskov substitution principle: Az objektumoknak olyanoknak kell lenniük, hogy azokat bármikor lecserélhessük azok valamilyen altípusú objektumaira úgy, hogy a program helyes működéséhez nem módosítunk semmi egyebet. (Tegyük fel a kérdést, hogy az altípusú objektum az egy őstípusú objektum-e is egyben, pl.: a négyzet az egy téglalap-e. Amikor nincs szem előtt tartva ez az alapelv, akkor sokszor elbukik ez a kérdésfeltevés.)
- Interface segregation principle: Több kliens specifikus interface jobb, mint egy nagy általános interface.
- Dependency inversion principle: A függőségeket absztrakciókhoz adjuk meg és ne fordítva!
Inversion of Control (IoC) és Dependency Injection (DI)¶
Az előző fejezetben ízelítőt kaptunk a Spring keretrendszer előnyeiről.
Alapvetően a DI egy specializált formája az IoC-nek, de sokszor azonos értelemben kezeljük a kettőt. A következőkben megnézzük a kettő kapcsolatát, illetve azt is felfedezzük, hogy a Spring milyen lehetőségeket kínál a számunkra.
IoC és DI¶
Inversion of Control: Egy alapelv (principle), mely alapján a szoftver komponensek a vezérlést egy általános keretrendszertől kapják meg. Segítségével a függőségeket futás időben kaphatjuk meg a keretrendszertől. A használata elősegíti a modularitást, illetve a bővíthetőséget.
Az IoC és a DI a komponensek közötti függőségek kezelésében segít (azok életciklusait is kezeli).
A szoftver komponenst, mely más komponensektől függ dependant object
-nek vagy target
-nek hívjuk.
Az IoC-n belül két kategóriát különböztetünk meg, melyek aztán tovább csoportosíthatóak a konkrét megvalósítások mentén:
- Dependency Lookup
- Dependency Pull
- Contextualized Dependency Lookup
- Dependency Injection
- Constructor DI
- Setter DI
- Field-based DI
A Dependency Lookup képvisel egy tradicionálisabb nézetet, a Dependency Injection sokkal nagyobb flexibilitást nyújt. Az előző esetében a függő komponensnek kell referenciát szereznie arra, akitől függ, míg a DI esetében az úgynevezett IoC konténer injektálja be a függőséget a függő komponensbe, vagyis a keretrendszer elintézi a komponensek közötti függőségeket.
Dependency Pull¶
A dependency pull-t már láthattuk korábban, amikor az ApplicationContext
-től mi magunk kértük le a megadott nevű/típusú bean-t.
1 2 3 4 |
|
Ebben a helyzetben a függő komponens egy registry-n keresztül kéri el a konténertől a referenciát a függőségre.
Contextualized Dependency Lookup¶
Hasonlít a Dependency Lookup-hoz, de itt nincs egy központi registry (például JNDI), hanem közvetlenül a konténertől kérjük el a dependency-t. Általában azzal a teherrel jár, hogy a komponensnek implementálnia kell valamilyen interface-t, amelyen keresztül a konténer felé jelzi, hogy szeretné megkapni a függőséget.
Constructor Dependency Injection¶
Erről az esetről akkor beszélünk, amikor a komponens a függőségét a konstruktorban kapja meg paraméterként.
Amikor példányosítjuk a komponenst, akkor a konténer átadja a függőséget paraméterként (injektálja).
Például a StandardOutMessageRenderer
-nek ilyen módon biztosíthattuk a MessageProvider
-t.
1 2 3 4 5 6 7 8 9 10 |
|
Ennek a konstrukciónak a használata azt eredményezi, hogy a komponens-t nem lehet a függőségei nélkül példányosítani.
Setter Dependency Injection¶
Ebben az esetben nem a konstruktoron keresztül biztosítjuk a függőség injektálhatóságát, hanem a Java Bean-eknél ismert setter
metóduson keresztül történik a függőség injektálása.
A komponens setter metódusainak halmaza meghatározza a komponens függőségeit is egyben.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A konstruktor DI-vel szemben itt megengedett az, hogy a komponenst a függőségei nélkül hozzuk létre.
Magukat a függőségeket később a setterek révén tudja garantálni a rendszer.
Fontos a névképzésre figyelnünk!
A setDependency
metódus alapján a konténer dependency
néven regisztrálja a függőséget.
A konstruktor alapú DI mellett a setter alapú DI a leginkább alkalmazott.
Field-based DI¶
A Spring támogat még egy DI mechanizmust, melyet majd később fogunk látni. Ez az a DI, amikor közvetlenül a field-et jelöljük meg, mint függőséget, így azt a keretrendszer automatikusan biztosítja majd a számunkra. A field-alapú DI a reflection mechanizmuson alapszik, mely erőforrásigényesebb és megnehezítheti a tesztelhetőséget is, így elsősorban a konstruktor és a setter alapú DI az ajánlott.
Injection vs. Lookup¶
Ez sokszor nem is kérdés, mivel a használt környezet (konténer) diktálja a szabályokat. Ha például EJB (Enterprise Java Beans) 2.1 előtti verziót használunk, akkor biztosan Lookup-ot kell használnunk, mely valószínűleg JDNI-t jelent, mely által a JEE konténertől kérhetjük el az EJB-t. Spring-ben alapvetően Dependency Injection alapú az IoC, így ezt ajánlott alkalmazni.
Ha eltekintünk attól, hogy milyen környezetben vagyunk és azt mondjuk, hogy választhatunk a IoC típusok közül, akkor melyiket válasszuk? Az egyértelmű válasz, hogy Dependency Injection-t, mivel annak nincs semmilyen kézzel fogható hatása a forráskódra (nem kell extra interface-t implementálnunk vagy egy registry-t használnunk). DI-nál csak annyi dolgunk van, hogy a konstruktor és/vagy setterek segítségével engedélyezzük a függőségek injektálását. Ez azt is jelenti, hogy olyan lazán csatolt kódot kaphatunk eredményül, mely nem függ a konténertől. Egy további előnye a Lookup-al szemben a tesztelhetőség.
DI esetében kevesebb kódot is kell írnunk, mely tiszta haszon. A kevesebb kód ugyanis kevesebb hibalehetőséget tartalmaz. Vegyük például egy CDL megvalósítás részletet:
1 2 3 |
|
Hibalehetőségek:
- megváltozhat a dependency kulcsa (
myDependency
) - a konténer lehet
null
- a visszaadott dependency típusa megváltozhat, úgy hogy az inkompatibilis
Setter injection vs. Constructor injection¶
Most, hogy eldöntöttük, hogy dependency injection-t fogunk használni, azok közül mégis melyiket használjuk? Ahogy korábban is írtuk, a konstruktor alapú DI-nál, a példányosításkor muszáj átadni a függőséget az objektumnak, tehát biztosak lehetünk benne, hogy a szükséges függőség rendelkezésre áll a példányosításkor. A Spring ettől függetlenül ugyanezt megteszi a setter alapú injection esetében is, de az előbbi használata konténerfüggetlenül biztosítja ezt. A konstruktor alapú injection, akkor is jól jöhet, ha immutable objektumokat szeretnénk előállítani. A setter injection segítségével a dependency-ket akár menet közben is cserélni tudjuk, továbbá a bean-ünk használhat valamilyen alapértelmezett megvalósítást, amennyiben a dependency nem áll rendelkezésre (alapviselkedés megvalósításánál lehet jó).
Alapesetben használjuk a konstruktor alapú DI-t, jelenleg ez a leginkább preferált mód. Továbbá próbáljunk meg interface-ek mentén injektálni, hiszen így a rendszer futás közben döntheti el, hogy konkrétat melyik megvalósítást adja.
Best practice
A konstruktor által injektált bean-eket jelöljük meg final
-ként.
@Autowired¶
Az @Autowired
annotációt field-re és setter-re helyezhetjük el, mellyel jelezzük a Spring számára, hogy az adott elemet a Spring-től várjuk, neki kell azt injektálni a bean konténerből.
Az annotációt elhelyezhetjük a konstruktorra is, ugyanakkor ott már nem kötelező Spring 4.3 óta.
Ez is a konstruktor alapú DI-nak kedvez.
TODO: Kód/Videó amiben az összes DI típus szerepel
@Qualifier¶
Amennyiben több ugyanolyan típusú komponensünk van, akkor használhatjuk a @Qualifier
annotációt, melyben megadjuk a bean nevét és ezzel jelezzük, hogy pontosan melyik bean-re van szükségünk.
Ehhez először lássuk, hogy mi történik akkor, amikor két ugyanolyan típusú bean áll rendelkezésre a bean konténerben.
1 2 3 4 5 6 7 |
|
Amennyiben így futtatjuk az alkalmazásunkat akkor a következőt kapjuk:
1 2 3 4 5 6 7 |
|
A Spring megoldási lehetőséget is elénk tár a @Primary
és a @Qualifier
által.
A Qualifier
segítségével az injektálás helyén megmondhatjuk, hogy melyik bean-re van szükségünk.
1 2 3 |
|
Field és setter esetében az így nézne ki:
1 2 3 4 5 6 7 8 9 |
|
Emlékeztető
A bean-ek neve, ha másképpen nem rendelkezünk, akkor az osztály neve kisbetűvel kezdve.
TODO: videó
@Primary¶
A @Primary
annotációt a komponensen (osztályon) helyezhetjük el, mely megadás után a komponenst előnyben fogja részesíteni a Spring minden alkalommal, amikor az adott típusú függőséget kérjük (nyilván DI-al).
1 2 3 4 5 6 7 8 |
|
Ebben az esetben nem kell a @Qualifier
annotációt használnunk az injektálás helyén, hacsak máshogy nem kívánunk rendelkezni.
TODO: videó
Profile alapok¶
A Qualifier
és a Primary
annotációkon kívül van lehetőségünk arra is, hogy bizonyos bean-eket csak adott esetben regisztráljunk a bean konténerben.
Az alkalmazásunkban létrehozhatunk profilokat és megadhatjuk, hogy egy bean milyen aktív profil esetén kerüljön regisztrációra.
Alakítsuk át az alap alkalmazásunkat úgy, hogy a MessageProvider
-ek profilokat használjanak.
1 2 3 4 5 6 7 8 9 10 11 |
|
Ezután adjuk meg az application.properties
-ben az active profilt:
1 |
|
Ezzel megadtuk azt, hogy melyik profilt fogjuk aktiválni. Indítsuk el az alkalmazást majd a konzolon figyeljük az üzeneteket:
1 |
|
Azaz a Spring sikeresen felvette az application.properties állományból a beállítást.
Ilyenkor nem lesz a bean-ek között ütközés, mivel csak az en
profil az aktív, így csak a HelloWorldMessageProvider
kerül bele a context-be.
Default profile¶
Van egy alapértelmezetten létrehozott profil, melynek neve default
.
Ha kikommentezzük az application.properties
-ben az aktív profil megadását, és elindítjuk az alkalmazást, akkor a konzolon a következő üzenet jelenik meg:
1 |
|
Nyilván ebben az esetben elhasal az alkalmazásunk, mert a default profilhoz nem rendeltünk hozzá egyetlen MessageProvider
-t sem.
Módosítsuk az alkalmazást úgy, hogy a HelloWorldMessageProvider
-t hozzárendeljük a default profilhoz is.
1 2 3 4 5 |
|
A @Profile
használatakor több profilt is megadhatunk, ha listaként adjuk meg a profilokat, mint ahogy azt a példa is mutatja.
Tipp
Amennyiben csak egy profilra, nem akarunk valamit definiálni, akkor pedig használhatjuk a tagadást is: @Profile("!default")
, mely azt jelenti, hogy mindegyik profilhoz szeretnénk regisztrálni a bean-t kivéve a default
-ot.
Bean életciklus menedzsment¶
Az IoC konténer által adott előnyök egyike, hogy a bean-ek életciklusának bizonyos pontjain lehetőségünk van tetszőleges műveletek elvégzésére. Két kiemelt fontosságú életciklus esemény:
- Bean inicializálás utáni események (post-construct): miután az összes property-t beállította a keretrendszer és befejezte a függőségek ellenőrzését.
- Bean megsemmisítés előtti események (pre-destroy): mielőtt a Spring megsemmisítené a bean-t.
Ezekre az életciklus eseményekre 3 módon iratkozhatunk fel:
- interface-alapú: a bean-nek implementálnia kell a megadott életciklushoz tartozó interface-t, így értesülhet az eseményről (callback metódusban lehet megadni a viselkedést).
- method-alapú: Az
ApplicationContext
konfigurálásakor megadható, hogy milyen metódust hívjon meg a konténer az egyes életciklusok alkalmával - annotáció-alapú (JSR-250): a meghívandó metódusokat fel tudjuk annotálni a bean definíciójában
Bean létrehozások¶
Vegyük a következő példát! A bean-ünk több függőséggel rendelkezik, melyeket setter-eken keresztül kaphat meg. Ezek közül a függőségek közül van, amelyik nem kötelező, ugyanakkor ebben az esetben egy alapértelmezett megvalósítást szeretnénk szolgáltatni. Ilyen esetben hasznos lehet, ha a létrehozás után (Post Constuct) le tudjuk ellenőrizni, hogy a dependency rendelkezésre áll-e. Amennyiben nem, akkor létrehozzuk az alapértelmezettet.
Ilyenkor a bean konstruktorában nem tudjuk elvégezni ezeket az ellenőrzéseket, hiszen a konstruktorhívás után végzi a Spring a függőségek injektálását (legalábbis a setter alapon megadottakra ez igaz) és mindezt elrejtve előlünk. A fent említett módszerek közvetlenül a függőségek ellenőrzése/injektálása után hívódnak, amikor már valid ellenőrzéseket tehetünk.
A fent felsorolt mechanizmusok közül csak az annotáció alapút mutatjuk meg, a többiről csak alapinformációkat közlünk (a 3 módszer végeredményben ugyanazt adja).
Tekintsük a következő példát:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Nyilván a példa mesterkélt, viszont láthatjuk, hogy csupán annyi a dolgunk, hogy elhelyezzük az annotációt a megfelelő metóduson.
Megjegyzés
- Metódus alapú módszer: init-method megadása a bean definíció helyén (
@Bean
-nél). Az érték bármilyen metódus nevét felveheti, melyet majd meghív a rendszer. A@Bean
annotációról nemsokára lesz szó. - Interface alapú módszer: bean osztály implementálja az InitializingBean interface-t, melynek
public void afterPropertiesSet();
metódusát kell implementálnunk.
Mivel egyszerre mindhárom módszerrel rácsimpaszkodhatunk az inicializáció utáni eseményre, így fontos lehet ezek feloldási sorrendje. A konstruktorhívástól kezdődően a feloldások sorrendje:
- Konstruktor hívás (bean létrehozás)
- függőségek injektálása
@PostConstruct
InitializingBean -> afterPropertiesSet()
init-method
Bean megsemmisítés¶
Tipikusan akkor lehet rá szükség, ha az alkalmazás leáll és ilyenkor még fel kell szabadítanunk a lefoglalt erőforrásokat, továbbá ilyenkor még perzisztálhatjuk a memóriában lévő adatokat.
A megsemmisítéskor is az előző 3 módszer alternatíváját használhatjuk.
Itt is az annotáció alapú módszert mutatjuk be, melyhez a @PreDestroy
-t kell használnunk.
Tekintsük az alábbi kódrészletet!
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 |
|
A DestroyExample
példányosítása után létrehozunk egy fájlt a temp könyvtár alá, melynek neve test.txt
.
A @PreDestroy
-al felannotált destroy
metódusban pedig megpróbáljuk törölni ezt a temporális fájlt.
A main-en belül manuálisan hívjuk meg az AnnotationConfigApplicationContext
close
metódusát, mely a kontextus megsemmisítését végzi el, így triggerelni fogja a bean destroy
metódusát is.
Megjegyzés
- Metódus alapú módszer: destroy-method megadása a bean definíció helyén (
@Bean
-nél). Az érték bármilyen metódus nevét felveheti, melyet majd meghív a rendszer. - Interface alapú módszer: bean osztály implementálja a DisposableBean interface-t, melynek
public void destroy();
metódusát kell kifejtenünk.
Spring Awareness¶
A Dependency Injection legnagyobb előnye, hogy a beaneknek nem kell ismernie a konténer megvalósítását.
Előfordulhatnak olyan esetek, amikor pontosan a Spring által nyújtott objektumok közül van szükségünk valamelyikre (mint például az ApplicationContext
, BeanFactory
vagy a ResourceLoader
) az egyik beanben.
A Spring biztosít egy halom ...Aware
interface-t, melyeket megvalósíthatunk és így egy callback metóduson keresztül megkapjuk a megadott objektumot.
Például, ha az ApplicationContext
objektumhoz akarunk hozzáférni, akkor az ApplicationContextAware
interface-t kell megvalósítanunk.
Ezt például használhatjuk, akkor ha egy másik bean-t szeretnénk manuálisan lekérni (alapvetően használjuk a dependency injection-t), ha szükségünk van egy resource állományra, melyet a Spring menedzsel, vagy éppen alkalmazásszintű eseményeket akarunk kezelni.
Az interface implementálásakor a következő metódust kell definiálnunk:
1 |
|
Paraméterben meg is kapjuk az ApplicationContext
objektumot (ha több van, akkor azt amelyikben az aktuális bean definiálva van).
Példa egy user
nevű bean lekérésére:
1 2 3 4 5 6 7 8 |
|
Egy további hasznos interface lehet a BeanNameAware
interface, mely által a bean lekérdezheti, hogy a konténerben milyen néven lett létrehozva.
1 2 3 4 5 6 7 |
|
Most, hogy láttuk, hogy miként avatkozhatunk bele a bean életciklusaiba vizsgáljuk meg a következő ábrát, mely szemlélteti a fent bemutatott módszerek sorrendiségét!
FactoryBean¶
Probléma: Hogyan hozzon létre a Spring egy olyan bean-t, melyet nem lehet a new
használatával példányosítani?
Válasz: FactoryBean
interface használata.
A FactoryBean
-t implementáló bean-ek esetében a konténer nem a new
hívással próbálja példányosítani a bean-t, hanem a FactoryBean.getObject()
metódus meghívásával.
Példaként használjuk a MessageDigest
osztályt (mely üzenetek kriptográfiai feldolgozására szolgál), melyben egy konkrét algoritmus implementációt megvalósító objektumot a MessageDigest.getInstance()
hívással kérhetünk el.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
A Spring meghívja a getObject
metódust, hogy megkapjon egy MessageDigest
típusú bean-t.
Ezt a bean-t fogja felhasználni, amikor egy másik bean függőségként egy MessageDigest
típusú bean-t kér tőle.
Gyakori hibák¶
No bean named 'XYZ' available
: Általában az injektálni kívánt komponens nincs megjelölve a megfelelő stereotype annotációval.
Bean-ek és a BeanFactory¶
A Spring DI konténerében vezető szerepet játszik a BeanFactory
interfész.
Ő felelős a komponensek (bean-ek) menedzseléséért, függőségeik és életciklusuk vezényléséért.
Amikor konténer által menedzselt komponens-ről beszélünk, akkor ezen komponenseket (objektumokat) bean-nek nevezzük.
Ha az alkamazásnak nincs másra szüksége csak DI-ra, akkor a BeanFactory
-n keresztül léphetünk kapcsolatba a DI konténerrel (bár mi ennél többet szeretnénk a Spring-től, így majd az ApplicationContext
-et fogjuk használni a gyakorlatban, ami maga is egy BeanFactory
).
A bean-ek megadására számos lehetőségünk van. Használhatunk XML alapú megadást, property fájl alapú megadást, annotáció alapú megadást. Mivel a fejlesztők nem igazán szeretnek XML fájlokat írogatni, ezért manapság az annotáció alapú megadások a legjellemzőbbek.
A BeanFactory
-n belül minden bean-nek lehet egy egyedi azonosítója vagy egy neve, vagy egyszerre mindkettő.
Az is előfordulhat, hogy egy bean nem kap sem id-t, sem nevet, ilyenkor anonymous bean-ről beszélünk.
Továbbá az is fontos, hogy egy bean rendelkezhet több névvel is egyszerre, ahol az első utáni további neveket alias-oknak nevezzük.
A bean azonosítója és neve használható arra, hogy a bean-t elkérjük a BeanFactory
-tól, illetve a függőségek feloldása is ezek alapján történik.
Megjegyzés
A BeanFactory
nem támogatja az annotáció alapú konfigurációt (melyet mi preferálnánk), viszont a későbbiekben megismerjük részletesen az ApplicationContext
-et, ami a BeanFactory
superset-je és támogatja ezt a fajta lehetőséget is.
ApplicationContext¶
Az ApplicationContext
tekinthető a BeanFactory
kiterjesztésének is (konkrétan implementálja is azt).
A DI mellett az ApplicationContext
támogatja többek között a következőket is:
- Tranzakciókezelés
- AOP (Aspektus orientált paradigma)
- i18n (többnyelvűsítés)
- application event handling
Megjegyzés
BeanFactory
helyett erősen ajánlott mindig az ApplicationContext
-et használni!
Az ApplicationContext
messze több konfigurációs lehetőséget biztosít, mint a BeanFactory
, például az annotáció alapú bean definíciókat.
Nézzünk egy példát a bean definícióra annotációk használatával! Ekkor a bean-t el kell látnunk a megfelelő stereotype annotációval, mely lehet:
@Component
@Service
@Repository
@Controller
@Configuration
Ezeket azért hívják stereotype annotációknak, mivel a org.springframework.stereotype
package alatt találhatóak.
Itt található meg az összes olyan annotáció, melynek segítségével bean-eket definiálhatunk.
Egy bean annotációját a szerepe alapján válasszuk meg!
Az annotáció alapú konfigurációra láthattunk példát a bevezetésben is, amikor a HelloWorld
alkalmazásunkat újragondoltuk.
A bean definíciókat egy konfigurációs osztályban adtuk meg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Itt a @Configuration
-el ellátott osztályunk az XML/property fájlt váltja ki.
A Spring a konfigurációs osztály @Bean
-el ellátott metódusai alapján regisztrálja a bean-eket a konténerben (ezen metódusokat közvetlenül meghívja majd a konténer).
Ilyen esetben a bean neve megegyezik a metódus nevével.
A konfigurációs osztály alapján a konténer inicializációja a következőképpen végezhető:
1 2 3 4 5 6 7 8 9 10 |
|
A kulcs az AnnotationConfigApplicationContext
használata, mely meghatározza, hogy a bean definíciókat a megadott konfigurációs osztályból vegye a konténer.
A fenti esetben vegyük észre a redundanciát!
Amennyiben a MessageRenderer
osztályunkat ellátjuk a megfelelő stereotype annotációval a konfigurációs osztályban nem is kell megadnunk @Bean
-nel ellátott metódusokat, hiszen a bean definícióját (hogy hogyan kell a konténernek kezelnie azt a komponenst) maga az osztály adja meg.
Ahhoz, hogy a bean definíciókat az osztályokban is képes legyen detektálni a rendszer, meg kell adnunk a @ComponentScan
annotációt a konfigurációs osztályon.
Ennek tükrében a HelloWorldConfiguration
osztály a következőképpen egyszerűsíthető:
1 2 3 4 |
|
Ennek hatására a Spring a megtalált bean-eket (stereotype annotációval megjelölt) automatikusan regisztrálja.
Megjegyzés
Ha legacy kódot kell továbbfejlesztenünk, akkor a konfigurációs osztálynak megadhatjuk, hogy XML fájlból is olvasson bean definíciókat.
Ehhez az @ImportResource
annotációt használhatjuk.
Példa:
1 2 3 4 |
|
Előkészítettük a bean-ek beolvasását, így most lássuk a tényleges bean-eket hogyan kellett megváltoztatni.
Elsőként tekintsük meg a MessageRenderer
-t!
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Az osztályt el kell látnunk egy stereotype annotációval, mely jelen esetben @Service
(használhattunk volna sima @Component
-et is)!
A másik kulcs lépés, hogy a függőséget jelen esetben setter injection-nel adjuk meg és erre a setter-re alkalmazunk egy @Autowired
annotációt.
Mivel a konfigurációs osztályon szerepel a @ComponentScan
annotáció, így az ApplicationContext
inicializációja közben a Spring megtalálja az @Autowired
annotációval ellátott metódust és a függőséget (jelen esetben egy MessageProvider
objektum) injektálja a bean-be.
Megjegyzés
Az @Autowired
annotációt a Spring biztosítja, azonban létezik a @Resource
(melynek nevet is megadhatunk), mely a JSR-250 standardban definiált, így JSE-ben és JEE-ben is támogatott.
JEE-re később standardizálták a JSR-299-ben az @Inject
annotációt, mely egyenértékű az @Autowired
-el.
Ezután nézzük meg, hogy a HelloWorldMessageProvider
osztályunkat hogyan alakítanánk át?
Mivel rugalmasabbá akarjuk tenni, így át is nevezhetjük ConfigurableMessageProvider
-re.
A cél, hogy az üzenetet a konstruktorban rendelkezésre bocsájtjuk.
Azért a konstruktorban, mert így biztosítjuk a kötelező függőséget (konstruktor injection).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Konstruktor injection esetében a konstruktort látjuk el az @Autowired
annotációval, annak érdekében, hogy a ComponentScan
közben a függőségeket automatikusan el tudja végezni a rendszer.
A másik újdonság a @Value
annotáció használata, melynek a paraméterében egyszerűen egy String
-et adunk meg jelen helyzetben.
Ezt az értéket fogja injektálni a konténer, amikor példányosítja a ConfigurableMessageProvider
-t.
Ez így egyelőre nem tűnik túlságosan hasznosnak, mert úgyanúgy hard kódolva van az üzenet szövege.
A megoldás, hogy külső állományba helyezzük el az ilyen konfigurációs értékeket!
Ehhez használhatjuk a @PropertySource
annotációt, mellyel egyszerűen adhatunk meg a rendszernek property fájlokat, melyeket szeretnénk használni.
A resources
mappa alá hozzunk létre egy application.properties állományt, ha még nem létezik.
A fájlban adjuk meg a következőt:
1 |
|
Ezután a ConfigurableMessageProvider
konstruktorát a következőképpen módosítsuk:
1 2 3 4 |
|
A @Value
paraméterében ${...}
formában adhatunk meg kifejezéseket (property placeholdereket), melyeket ki is értékel a rendszer, így a property fájlban megadott üzenetet ("This is my message") adja át paraméterül a konstruktornak a konténer.
Field injection¶
A 3. típusú dependency injection a field injection, melynek során közvetlenül a field-et látjuk el az @Autowired
annotációval, így se setter-re se konstruktorra nincs szükség.
Praktikusnak tűnhet, arra az esetre, ha a dependency-t nem akarjuk kifelé láthatóvá tenni, de valójában pont ez okozza a problémát, mivel senki nem látja, hogy milyen elemtől függ a komponens.
Az előző példában a renderer
így nézne ki:
1 2 3 4 5 6 7 8 9 |
|
Bár a fenti példában a provider
private
láthatóságú, ez a Spring-et nem igazán érdekli, hiszen a field alapú injekció reflection-nel valósul meg futás közben.
Van azonban néhány hátulütő, ami miatt lehet, hogy jobb kerülni ezt a konstrukciót:
- Single Responsibility Principle megsértése sokkal könnyebben bekövetkezhet
- Spring iránti függés (
@Autowired
a Spring-ben van definiálva) final
adattagokra nem tudjuk használni (arra csak a konstruktoros működik)- Tesztek írásakor a dependency-t manuálisan kell átadnunk
Paraméterek injektálása¶
Már láttunk bean-ek másik beanekbe történő injektálást, illetve egyszerű érték (String
: message) injektálását a @Value
használatával.
A Spring rendkívül sokrétű ebben a tekintetben is, mivel akár kollekciókat, vagy másik factory-ban (pl.: másik ApplicationFactory
) definált bean-eket is injektálhatunk.
Az alábbiakban sorra vesszük, hogy milyen lehetőségeink vannak.
Egyszerű értékek injektálása¶
Korábban láttuk a @Value
alkalmazását, amikor az üzenetet szerettük volna konfigurálni.
A @Value
-t a setter-en alkalmaztuk, de lehet alkalmazni magán a field-en is:
1 2 3 4 |
|
A fenti példában String
-re adtunk egy egyszerű érték injektálást (melyet nyilván ki is szervezhetünk külső állományba a ${...}
használatával), de az összes alap típushoz megadható ilyen módon az érték injektálása.
Tekintsük meg a következő példát:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
A fenti érték injekciók működnek az összes alap típusra, illetve azokhoz tartozó wrapper osztályokra is (pl.: Integer
).
Ilyen esetben a megadott String
értékeket a rendszer automatikusan parsolja és átalakítja a kívánt típusra (helytelen megadáskor nyilván futás közbeni hibát kapunk).
Értékek injektálása SpEL-el¶
A SpEL (Spring Expression Language) a Spring 3-ban debütált, melynek segítségével dinamikusan számolhatjuk ki a megadott kifejezések értékét, melyet aztán az ApplicationContext
-ben felhasználhatunk.
Egy kézenfekvő használata az injekciók során kerül velünk szembe.
Szintaxis:
1 |
|
Fontos, hogy ne keverjük össze a property placeholdereket a SpEL kifejezésekkel. A property placeholderek szintaxisa:
1 |
|
Mi a különbség? A property placeholderekkel a property fájlokban megadott property-k értékét nyerhetjük ki és azok értékei futás közben behelyettesítődnek. A SpEL ennél sokkal többet tud. Például másik bean tulajdonságait is lekérhetjük általa, de használhatunk benne property placeholder-t is.
A fenti InjectSimpleValuesConfig
-ot Component
-ként adtuk meg, így azt a Spring menedzseli, így annak értékeit felhasználhatjuk egy másik bean-ben.
Példa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Nyilván a fenti csak egy gyógypélda, de a lényeg látható belőle.
Az age
adattagnál használtunk egy +1
-et, hogy ezzel megmutassuk, hogy ide kifejezéseket is megadhatunk.
A SpEL képességeiről később még lesz szó, egyelőre viszont elég ennyit tudnunk erről.
ApplicationContext hierarchia¶
Eddig olyan programokat láttunk, ahol egyetlen ApplicationContext
állt rendelkezésünkre.
A Spring azonban képes egyszerre több ApplicationContext
kezelésére is, pontosabban ezek az ApplicationContext
-ek hierarchiába szervezhetőek.
Ezáltal az alkalmazásunk konfigurációját több fájlba darabolhatjuk szét, ami nagyobb projektek esetében mennyei mannaként jön számunkra.
A hierachiában részt vevő ApplicationContext
-ek kapcsolatában így megkülönböztetünk szülő és gyerek szerepet.
A gyerek AppliacationContext
-ből hozzáférhetünk a szülőben definiált bean-ekhez, továbbá a szülőben megadott bean-eket felül is definiálhatjuk.
Ahhoz hogy a hierarchiát kialakítsuk nincs más dolgunk, mint a gyerek context-en meghívni a setParent()
metódust, melynek paraméterében megadjuk magát a szülőt.
Kollekciók injektálása¶
Egyszerű értékek injektálása mellett lehetőség van kollekciók injektálására is. Tekintsük a következő példát:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Most, hogy megvan a bean, melybe kollekciót kell injektálnunk, hozzunk létre egy konfigurációs osztályt, melyben gondoskodunk is erről:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
A CollectionsBean
regisztrációja mellett egy listát is injektálunk (nameList
).
Ezután teszteljük az eredményt a következőkkel:
1 2 3 |
|
A fenti kódrészletben annyi az újdonság, hogy a getBean
-nek most nem a bean nevét adjuk meg hanem típusát, azaz a bean-neket típus alapján is elkérhetjük.
A kód eredménye a következő lesz:
[Arnold, Bela, Cecilia]
Kollekciók injektálásakor használhatunk field-alapú és konstruktor alapú injektálást is teljes értékűen.
A List
mellett továbbá használhatunk Set
-et, illetve Map
-et is.
Azon felül, hogy primitív típusú kollekciókat injektálunk, lehetőség van arra is, hogy bean-ek kollekcióját is injektáljuk.
Ennek szemléltetésére hozzunk létre egy egyszerű bean-t, ami csak szimplán becsomagol egy egyszerű sztringet!
1 2 3 4 5 6 |
|
A kollekciót felhasználó bean ebben az esetben a következőképpen alakul:
1 2 3 4 5 6 7 8 9 |
|
A konfigurációban több SampleBean
-nel visszatérő definíciót adunk meg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Ilyen esetben a Spring a fenti SampleBean
típusú beaneket a listához adja hozzá (injektálja).
Amennyiben egy SampleBean
definíciónk sincs, akkor a CollectionsBean
-ben alapvetően kapunk egy kivételt, viszont mivel az @Autowired
annotációt elláttuk a required = false
paraméterrel, így ez nem következik be (a beanList
nem kerül inicializálásra, értéke null
lesz).
Amennyiben azt szeretnénk, hogy null
helyett üres listát kapjunk, amikor nincs megadva egyetlen egy SampleBean
sem, akkor a következőt kell megadnunk:
1 2 |
|
Amennyiben több SampleBean
is létezik és azoknak számít az injektálási sorrendje, akkor használhatjuk az @Order
annotációt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Bean elnevezések¶
Mint ahogy azt már láthattuk, a bean-ek igen változatos nevezékkel rendelkezhetnek.
Minden bean-nek rendelkeznie kell egy névvel, mely az őt tartalmazó ApplicationContext
-en belül egyedi.
Vegyünk a MessageProvider
példát:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
A korábbi ConfigurableMessageProvider
-hez képest annyi a módosítás, hogy a Service
stereotype annotációnál nem adtunk meg semmilyen nevet.
Ilyenkor a bean neve megegyezik az osztály nevével, de a kezdőbetűt kisbetűsíti a rendszer (configurableMessageProvider
lesz a neve).
Az eredeti példában felüldefiniáltuk ezt az alapértelmezett névgenerálást: @Service("provider")
, melynek eredményeképpen szimplán provider
lett a bean neve.
Minden bean rendelkezhet alias-okkal, azaz további nevekkel, melyekkel hivatkozhatunk rá.
Ezt a Component
és a többi stereotype annotáció nem támogatja, így ahhoz konfigurációs osztályt kell használnunk.
Vegyük alapul a régi HelloWorldConfiguration
osztályt
1 2 3 4 5 6 7 8 9 |
|
Ha a Bean
annotáció nem kap paraméter-t, akkor a bean id-ja a metódus neve lesz.
Amennyiben a Bean
annotációnak megadunk egy nevet úgy az lesz a bean id-ja.
Ezen felül használhatunk string tömböt is, mely eredményeképpen az első az id-ja lesz a többi pedig egy-egy alias.
1 2 3 4 |
|
Bean példányosítási módok¶
A Springben alapvetően minden bean singleton, noha nem a tradicionális tervezési mintára kell gondolnunk, ami fizikailag gátolja több példány létrehozását.
Ezt a korlátozást a Spring akkor is megteszi, amikor nem készítjük fel a bean-t erre (statikus adattag, statikus getInstance
metódus és private
konstruktor).
Spring-ben nincs szükség arra, hogy explicit singleton mintát alkalmazzunk.
Ez több szempontból is csak rosszat tenne, mivel növeli a csatolást (hiszen, aki használja a singleton-t, annak tisztában kell lennie a konkrét osztállyal és így nem tudjuk interface mögé rejteni a megvalósításunkat).
A Spring alapból a singleton példányosítást használja, így leveszi a terhet a vállunkról.
Az alapértelmezett példányosítási módot egyszerűen lecserélhetjük, ha menet közben rájövünk, hogy mégis több példányra van szükségünk (prototype
példányosítási mód).
A példányosítási mód cserélését mutatja be a következő kódrészlet ConfigurableMessageProvider
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A fenti kód hatására akárhányszor lekérés történik a provider
bean-re, új példányt hoz létre a keretrendszer.
Mikor milyen példányosítási módot használjak?
Singleton-t, ha:
- Állapot nélküli shared object: Olyan objektumoknál, ahol nincs belső állapot (pl.:
Service
-ek), viszont több függősége is van. Az állapotmentesség miatt nincs szükség szinkronizációra, így használható egy darab példány minden kérés kiszolgálására. - Csak olvasható állapotú shared object: Hasonló az előzőhöz, de lehet csak olvasható állapot.
- Állapottal rendelkező shared object: Ha az állapotot megosztottan kell használni, akkor is használhatunk singleton-t, viszont ilyenkor minimalizáljuk az
synchronized
kód mennyiségét!
Nem-singleton-t, ha:
- Írható állapotú objektum: Ahol sok állapotot leíró változó van, és ezek mind írhatóak, akkor jobb új példányokat létrehozni, mint szinkron blokkokat alkalmazni, hiszen ez eléggé rá fogja nyomni a bélyegét a teljesítményre.
- privát állapotú objketumok:
A singleton
és a prototype
példányosítás mellett a következő bean scope-ok állnak rendelkezésre, de ezekkel egyelőre még nem foglalkozunk részletesen:
- request
- session
- application
- websocket
Függőségek feloldása @DependsOn
annotációval¶
Normál körülmények között a Spring képes feloldani az összes függőséget, melyeket a bean-ek között megadtunk.
A függőségek feloldási sorrendjét a Spring dönti el, így ezzel alapvetően nem is kell foglalkoznunk.
Ahhoz, hogy a Spring képes legyen feloldani a bean-ek közötti függőségeket, ezeknek a függőségeknek szerepelnie kell valamilyen konfigurációs megadásban!
Vegyük például azt, amikor egy bean valamelyik metódusában használni szeretne egy másikat úgy, hogy meghívja a ctx.getBean()
metódust (az injektálásról viszont nem tájékoztattuk a Spring-et).
Ebben az esetben előfordulhat, hogy a Spring hamarabb példányosítja a függő objektumot, mint magát a függőséget, így pedig hibát kapunk.
Ilyen esetben használhatjuk a @DependsOn
annotációt.
Van viszont még egy fontos dolog!
A függő bean-nek szüksége van az ApplicationContext
-re, amihez arra van szükségünk, hogy a bean-ünk implementálja az ApplicationContextAware
interfészt, így tudatva a Spring-gel, hogy szüksége lesz az ApplicationContextre
.
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Az nagyon fontos, hogy az setApplicationContext
metódust a Spring a Foo
konstruktor után hívja csak meg, így a konstruktorban nem használható még a ctx
(NPE
-t kapunk).
Megjegyzés
Lehetőleg kerüljük el azokat az eseteket, amikor nekünk kell megadnunk a függést a DependsOn
használatával.
Ehelyett használjuk a konstruktor és setter alapú dependency injection adta lehetőségeket, így a Spring automatikusan elvégzi a piszkos munkát.
Ettől függetlenül jó tudni a DependsOn
létezéséről, mivel legacy kód esetében találkozhatunk olyan szituációval, amikor ez menti meg a bőrünket.