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, mely probléma erősen fenállhat a Spring esetében, 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? Általánosan ezt úgy válaszolhatjuk meg, hogy 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. 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 a JavaEE helyett használhatjuk. Támogatja a Groovy-t és a Kotlin-t 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 21 modullal rendelkezik (így 21 JAR-ral). A következő táblázat összefeoglalja ezeket a 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). |
beans-groovy | Ugyanaz, mint az előző, de Groovy osztályokkal. |
context | Spring Core kiterjesztéséhez használatás osztályok. ApplicationContext, EJB, JNDI, JMX, remoting, dinamikus szkriptnyelv integrációk (JRuby, Groovy, ...). |
context-indexer | Indexer implementáció |
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. |
expresison | SpEL támogatás |
instrument | Instrumentáláshoz segítség |
jdbc | JDBC osztályok, minden DB-vel dolgozó alkalmazáshoz kell. |
jms | JMS support |
messaging | Üzenet alapú rendszerek támogatás, STOMP support. |
orm | ORM eszközök: Hibernate, JDO, JPA, iBATIS. Függőség: pringf-jdbc |
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 |
web-reactive | Spring Reactive Web support |
web-mvc | Spring MVC support |
websocket | JSR-356 (Java API for WebSocket) support |
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-ek
Ez a kettő kiemelt fontossággal bír, így már most véssük őket az eszünkbe. Talán az a legegyszerűbb, ha bele is vágunk és nem szaporítjuk a szót. Menet közben kitérünk minden fontos újdonságra.
Hello World újragondolva¶
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ű, de van vele néhány probléma.
Először is, nem éppen rugalmas és bővíthető a kód.
- Mi van ha le szeretnénk cserélni a kiírandó szöveget?
- Mi van ha kiírandó szöveget másképpen szeretnénk kiírni? Mondjuk a standard error-ra akarjuk írni, vagy HTML tag-ek közé akarjuk 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 akkor 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, azaz 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.
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.
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 (statik 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 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.
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.
Továbbá 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.
A többi interfészt és megvalósítást hagyjuk meg, mert azokra szükségünk lesz. Húzzuk be a pom.xml-be a Spring Context függőséget!
1 2 3 4 5 6 7 |
|
Ezután a main
alakítsuk át a következőképpen:
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 ennek a context megadásához.
Ez az alábbi app-context.xml
1 2 3 4 5 6 7 8 9 10 |
|
A fenti vázat nem kell manuálisan létrehozni: Jobb klikk a spring
mappára -> New
-> XML Configuration File
-> Spring Config
.
Amit hozzá kell adnunk még az a xmlns:p="http://www.springframework.org/schema/p"
sor, mely a property xml namespace-t adja meg.
Ezzel tudjuk megmondani, hogy a renderer
bean messageProvider
property-je a már megadott másik bean legyen, aminek az id-ja a provider
.
A bean-ek rendelkeznek egy-egy id-val, illetve megadjuk a konkrét megvalósító osztályokat is itt az XML-ben.
Tehát 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 is.
Jelen esetben az XML állomány tölti be azt a szerepet, amelyet előzőleg a MessageSupportFactory
töltött be.
Kicsit vesézzük ki még az XML tartalmát!
Mivel a gyökér elemen prefix nélküli XML namespaceként a http://www.springframework.org/schema/beans
szerepel, ezért ez az alapértelmezett namespace.
Ez a namespace használható a Spring által menedzselni kívánt bean-ek megadására (beleértve a köztük lévő függőségeket is).
A Spring a megadott függőségeket feloldja és injektálja azt a kód megfelelő részeibe.
Miközben az ApplicationContext
inicializálása zajlik, a Spring regisztrálja a bean-t 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.
A NEM Spring-es megoldás esetében a main
-ben példányosítottuk mind a MessageProvider
-t, mind a MessageRenderer
-t.
A mostani megoldásban a köztük lévő függőség feloldását a Spring végzi automatikus módon, így most csak el kell kérnünk a MessageRenderer
bean-t és már mehet is a móka.
Spring konfigurálás annotációk használatával¶
A Spring 3.0-tól kezdődően annotációk és konfigurációs osztályok segítségével is megadhatjuk a konfigurációkat, nincs szükség XML-re.
A konfigurációs osztályokat a @Configuration
annotációval kell ellátni!
Ezek az osztályok tartalmazzák aztán a bean definíciókat (olyan metódusok, amelyek a @Bean
annotációval vannak ellátva).
Nézzük meg az előző példa megfelelőjét!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Mivel már nem XML alapú konfigurációt használunk, így egy másik ApplicationContext
megvalósítást kell használnunk a ClassPathXmlApplicationContext
helyett, ami azt tudja, hogy hogyan lehet annotációval megadott bean-eket kezelni.
Erre a célra szolgál az AnnotationConfigApplicationContext
, melynek paraméterben megmondhatjuk, hogy melyik konfigurációs osztályt vagy osztályokat szeretnénk használni a ApplicationContext
inicializálásához.
1 2 3 4 5 |
|
Látható, hogy hogyan egyszerűsödött a kód.
Továbbléphetünk még egy szintet, így még a komponensek közötti manuális megadást sem kell megtennünk (bean konfiguráció a HelloWorldConfiguration
).
Ezt a Spring keretrendszer megteszi helyettünk.
Először az interface-eket csupaszítsuk le!
1 2 3 |
|
1 2 3 |
|
Ezután nézzük a konkrét implementációkat!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 |
|
A fő különbség, hogy ezen megvalósításokat elláttuk a @Component
annotációval.
Ez alapján a Spring ApplicationContext
-jében, mint bean regisztrációra kerül (később ezt bővebben is megnézzük).
Ami még fontos, hogy a StandardOutMessageRenderer
tart egy adattagot a MessageProvider
-re és biztosítunk egy konstruktort is, mely ilyen típusú objektumot kap paraméterként (ez nagyon fontos).
Végül a fő osztályunkat ellátjuk a @ComponentScan
annotációval.
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.
Ebben az esetben nem kell külön konfigurációs osztályt sem használnunk.
A kód a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 |
|
A fenti kód már a Spring segítségével végzi el a komponensek közötti függőségek feloldását.
Ha megnézzük, akkor láthatjuk, hogy sehol sem használtunk a new
kulcsszót, nem kellett semmilyen XML-t írnunk, pusztán néhány annotációt kellett alkalmaznunk.
Hogy pontosan mi is történik a motorháztető alatt arra a következő fejezetben adunk választ, ahol részletesen foglalkozunk a Dependency Injection és az Inversion of Control fogalmaival.