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
public static void main(String... args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/app-context.xml");
    MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
    mr.render();
}

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
public class ConstructorInjection {
    private Dependency dependency;

    public ConstructorInjection(Dependency dependency) {
        this.dependency = dependency;
    }

    @Override
    public String toString() {
        return dependency.toString();
    }
}

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
public class SetterInjection {
    private Dependency dependency;

    public void setDependency(Dependency dependency) {
        this.dependency = dependency;
    }

    @Override
    public String toString() {
        return dependency.toString();
    }
}

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
public void performLookup(Container container) {
    this.dependency = (Dependency) container.getDependency("myDependency");
}

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 a BeanDefinition-t
  • XmlBeanDefinitionReader: XML fájlból olvassa a BeanDefinition-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
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>
1
2
3
public interface Oracle {
    String defineMeaningOfLife();
}
1
2
3
4
5
6
7
public class BookwormOracle implements Oracle {

    @Override
    public String defineMeaningOfLife() {
        return "Encyclopedias are a waste of money - go see the world instead";
    }
}

A BeanFactory használata:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.suaf;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;

public class XmlConfigWithBeanFactory {
    public static void main(String... args) {

        DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); // egy BeanFactrory megvalósítás
        XmlBeanDefinitionReader rdr = new XmlBeanDefinitionReader(factory);  // bean definíciók beolvasása fájlból
        rdr.loadBeanDefinitions(new ClassPathResource("bean-factory-config.xml")); // konkrét beolvasás
        Oracle oracle = (Oracle) factory.getBean("oracle"); // oracle elkérése a beanfactory-tól
        System.out.println(oracle.defineMeaningOfLife());
    }
}

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
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="oracle" name="wiseworm" class="hu.suaf.BookwormOracle"/>
</beans>

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
@Configuration
public class HelloWorldConfiguration {
    @Bean
    public MessageProvider provider() {
        return new HelloWorldMessageProvider();
    }

    @Bean
    public MessageRenderer renderer(){
        MessageRenderer renderer = new StandardOutMessageRenderer();
        renderer.setMessageProvider(provider());
        return renderer;
    }
}

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
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class HelloWorldSpringAnnotated {
    public static void main(String... args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);
        MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
        mr.render();
    }
}

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
@ComponentScan(basePackages = {"com.suaf"})
@Configuration
public class HelloWorldConfiguration {
}

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
@ImportResource(locations = {"classpath:app-context.xml"})
@Configuration
public class HelloWorldConfiguration {
}

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
package hu.suaf;

import org.springframework.beans.factory.annotation.Autowired;

@Service("renderer")
public class StandardOutMessageRenderer implements MessageRenderer {
    ...
    @Autowired
    public void setMessageProvider(MessageProvider provider) {
        this.messageProvider = provider;
    }
}

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
@Service("provider")
public class ConfigurableMessageProvider implements MessageProvider {
    private String message;

    @Autowired
    public ConfigurableMessageProvider(@Value("Configurable Message") String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

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
provider.message=This is my message

Ezután a ConfigurableMessageProvider konstruktorát a következőképpen módosítsuk:

1
2
3
4
@Autowired
public ConfigurableMessageProvider(@Value("${provider.message}") String message) {
    this.message = message;
}

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
@Service("renderer")
public class StandardOutMessageRenderer implements MessageRenderer {
    @Autowired
    private MessageProvider provider;

    public void render(){
        ...
    }
}

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
...
@Value("This is the massage")
private String message;
...

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
@Component("injectSimpleValues")
public class InjectSimpleValuesConfig {

    @Value("John Doe")
    private String name;

    @Value("42")
    private int age;

    @Value("1.87")
    private float height;

    @Value("true")
    private boolean male;

    @Value("234234234521")
    private Long numberOfHairs;

    ...
}

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
@Component("injectSpEL")
public class InjectSpEL {

    @Value("#{injectSimpleValues.name}")
    private String name;

    @Value("#{injectSimpleValues.age + 1}")
    private int age;

    @Value("#{injectSimpleValues.height}")
    private float height;

    @Value("#{injectSimpleValues.male}")
    private boolean male;

    @Value("#{injectSimpleValues.numberOfHairs}")
    private Long numberOfHairs;

    ...
}

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
public class CollectionsBean {

    private List<String> nameList;

    @Autowired
    public void setNameList(List<String> nameList){
        this.nameList = nameList;
    }

    public void printNameList() {
        System.out.println(nameList);
    }
}

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
@Configuration
public class CollectionConfig {

    @Bean
    public CollectionsBean getCollectionsBean() {
        return new CollectionsBean();
    }

    @Bean
    public List<String> nameList() {
        return Arrays.asList("Arnold", "Bela", "Cecilia");
    }
}

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
ApplicationContext context = new AnnotationConfigApplicationContext(CollectionConfig.class);
CollectionsBean collectionsBean = context.getBean(CollectionsBean.class);
collectionsBean.printNameList();

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
public class SampleBean {

    private String name;

    // constructor
}

A kollekciót felhasználó bean ebben az esetben a következőképpen alakul:

1
2
3
4
5
6
7
8
9
public class CollectionsBean {

    @Autowired(required = false)
    private List<SampleBean> beanList;

    public void printBeanList() {
        System.out.println(beanList);
    }
}

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
@Configuration
public class CollectionConfig {

    @Bean
    public SampleBean getElement() {
        return new SampleBean("Arnold");
    }

    @Bean
    public SampleBean getAnotherElement() {
        return new SampleBean("Bela");
    }

    @Bean
    public SampleBean getOneMoreElement() {
        return new SampleBean("Cecilia");
    }

    // other factory methods
}

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
@Autowired(required = false)
private List<SampleBean> beanList = new ArrayList<>();

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
    @Configuration
public class CollectionConfig {

    @Bean
    @Order(2)
    public SampleBean getElement() {
        return new SampleBean("Arnold"); // second
    }

    @Bean
    @Order(3)
    public SampleBean getAnotherElement() {
        return new SampleBean("Bela");  // third
    }

    @Bean
    @Order(1)
    public SampleBean getOneMoreElement() {
        return new SampleBean("Cecilia"); // first
    }
}

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
@Autowired
@Qualifier("CollectionsBean")
private List<SampleBean> beanList;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class CollectionConfig {

    @Bean
    @Qualifier("CollectionsBean")
    public SampleBean getElement() {
        return new SampleBean("Arnold");
    }

    @Bean
    public SampleBean getAnotherElement() {
        return new SampleBean("Bela");
    }

    @Bean
    public SampleBean getOneMoreElement() {
        return new SampleBean("Cecilia");
    }

   ...
}

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
@Service
public class ConfigurableMessageProvider implements MessageProvider {
    private String message;

    @Autowired
    public ConfigurableMessageProvider(@Value("Configurable Message") String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

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
@Configuration
public class HelloWorldConfiguration {
    @Bean
    public MessageProvider provider() {
        return new HelloWorldMessageProvider();
    }

    ...
}

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(name = {"provider", "providerAlias1", "providerAlias2" })
public MessageProvider provider() {
    return new HelloWorldMessageProvider();
}

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
@Service("provider")
@Scope("prototype")
public class ConfigurableMessageProvider implements MessageProvider {
    private String message;

    @Autowired
    public ConfigurableMessageProvider(@Value("Configurable Message") String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

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
@Component
public class Bar {

    public void doSomething() {
        System.out.println("Yeah");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Component
@DependsOn("bar")
public class Foo implements ApplicationContextAware{
    private Bar bar;

    private ApplicationContext ctx;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }

    public Foo(){}

    public void useBar() {
        bar = applicationContext.getBean("bar", Bar.class);
        bar.doSomething();
    }
}

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.


Utolsó frissítés: 2020-12-02 12:56:22