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. Amiről szó lesz:
- Inversion of Control (IoC) koncepciók
- IoC Springben
- DI Springben
- Spring ApplicationContext konfigurálása
IoC és DI¶
Amint már láttuk az IoC, így 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 a 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, mely a régi motoros Java programozóknak elsőre jobban kézre állhat. A Dependency Injection ugyanakkor 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.
Dependency Pull¶
A dependency pull-t már láthattuk korábban, amikor az XML-es konfigurációt alkalmaztuk a bean-ek megadására.
1 2 3 4 5 |
|
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 ezen keresztül 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).
1 2 3 4 5 6 7 8 9 10 11 12 |
|
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.
Az előző fejezet záró akkordjaként ezt a fajta dependency injection-t használtuk, amikor a MessageRenderer
referenciát szeretett volna kapni a egy MessageProvider
-re.Ebben
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 reflection-nel működik belül, mely időben is erőforrásigényesebb, illetve megnehezítheti a tesztelhetőséget, í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 az inicializáló bean lekérdezésektől eltekintve, mint amit már láttunk is korábban, Dependency Injection alapú az IoC.
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ó).
DI Springben¶
A továbbiakban megvizsgáljuk kicsit közelebbről, hogy hogyan biztosítja a dependency injection-t a Spring keretrendszer.
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, azonban fontos, hogy lássunk legalább egy példát az XML alapú megadásra is.
Külső fájlleírókban található bean konfigurációk megadáskor a BeanDefinition
interfész játsza a fő szerepet, vagyis az összes ilyen megadás egy BeanDefinition
lesz.
A konfiguráció tartalmaz információt magáról a bean-ről, illetve annak függőségeiről.
Azok a konkrét BeanFactory
implementációk, melyek megvalósítják a BeanDefinitionReader
interfészt is, képesek a BeanDefinition
adatokat egy konfigurációs fájlból olvasni.
Ilyenek például:
PropertiesBeanDefinitionReader
: property fájlból olvassa aBeanDefinition
-tXmlBeanDefinitionReader
: XML fájlból olvassa aBeanDefinition
-t
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.
Vegyünk egy példát!
Legyen egy Oracle
interfészünk, amely képes megmondani az élet értelmét!
Ezen felül legyen egy BookwormOracle
osztályunk, mely megvalósítja ezt az interfészt!
Mielőtt ezeket megcsináljuk, győződjünk meg arról, hogy a pom.xml
-ben szerepel a következő dependency:
1 2 3 4 5 |
|
1 2 3 |
|
1 2 3 4 5 6 7 |
|
A BeanFactory
használata:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
A DefaultListableBeanFactory
egy konkrét BeanFactory
megvalósítás.
Az XmlBeanDefinitionReader
ennek a BeanFactory
-nak szolgáltatja a beolvasott bean definíciókat.
A factory
-tól az oracle
bean-t az id-ja alapján kérjük el.
Ahhoz, hogy értelmet nyerjen a fenti megvalósítás még hiányzik a konkrét fájl, amiben a bean defintion-t megadjuk:
1 2 3 4 5 6 7 |
|
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
.
Az eddig megismert property és XML alapú konfiguráció mellett az 5-ös JDK-tól kezdődően (Spring Framework 2.5-ös verziótól) támogatja az annotáció alapú konfigurációt is.
A property alapú megadás nagyobb projektek esetében hamar kivehetetlenné válik, így azok kizárólagos használata nem ajánlott.
A kérdés az, hogy akkor XML vagy annotáció alapon adjuk meg a konfigurációkat?
Az XML előnye, hogy kiszervezhető a konfiguráció egy külön fájlba, az annotációk esetében viszont a kódban tudom megadni és megnézni a DI beállításaimat.
A Spring támogatja a módszerek keverését is, tehát lehet egyszerre XML, property és annotáció alapú konfigurációm is.
Mi a következőt fogjuk használni:
- DI konfiguráció: annotációk segítségével
- infrastruktúra konfigurációk (datasource, transaction manager, stb): property fájlok
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ásuinkat ú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 a konténer majd).
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 komponens) 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 a @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, mivel a dependency-t ha nem akarjuk kifelé láthatóvá tenni, akkor ez megoldja ezt a problémát, 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 a tesztelhetjü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 |
|
Amennyiben a több megadott SampleBean
közül akarunk egy részhalmazt kiválasztani, akkor használhatjuk a @Qualifier
annotációt, melyben megadjuk a bean nevét:
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Megjegyzés
A fenti példában az Autowired
és a Qualifier
együttes használata kellett a bean név alapú feloldásához.
Ugyanezt a Resource
használatával egy lépésben megtehettük volna.
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 a 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 az 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.