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, í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 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
public static void main(String[] args) {
    ApplicationContext ctx = SpringApplication.run(Application.class, args);
    MessageRenderer renderer = ctx.getBean(MessageRenderer.class);
}

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). Például a StandardOutMessageRenderer-nek ilyen módon biztosíthattuk a MessageProvider-t.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Component
public class StandardOutMessageRenderer implements MessageRenderer {
    private MessageProvider messageProvider;

    public StandardOutMessageRenderer(MessageProvider messageProvider) {
        this.messageProvider = messageProvider;
    }

    //...
}

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
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 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
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ó).

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
@Component
public class HalloWeltMessageProvider implements MessageProvider {
    @Override
    public String getMessage() {
        return "Hallo Welt";
    }
}

Amennyiben így futtatjuk az alkalmazásunkat akkor a következőt kapjuk:

1
2
3
4
5
6
7
Parameter 0 of constructor in hu.suaf.helloworld.StandardOutMessageRenderer required a single bean, but 2 were found:
    - halloWeltMessageProvider: defined in file [...\HalloWeltMessageProvider.class]
    - helloWorldMessageProvider: defined in file [...\HelloWorldMessageProvider.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

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
public StandardOutMessageRenderer(@Qualifier("helloWorldMessageProvider") MessageProvider messageProvider) {
    this.messageProvider = messageProvider;
}

Field és setter esetében az így nézne ki:

1
2
3
4
5
6
7
8
9
@Qualifier("helloWorldMessageProvider")
@Autowired
private MessageProvider messageProvider;

@Qualifier("helloWorldMessageProvider")
@Autowired
public void setMessageProvider(MessageProvider provider) {
    this.messageProvider = provider;
}

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
@Component
@Primary
public class HelloWorldMessageProvider implements MessageProvider {
    @Override
    public String getMessage() {
        return "Hello World!";
    }
}

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
@Profile("de")
@Component
public class HalloWeltMessageProvider implements MessageProvider {
    // ...
}

@Profile("en")
@Component
public class HelloWorldMessageProvider implements MessageProvider {
    // ...
}

Ezután adjuk meg az application.properties-ben az active profilt:

1
spring.profiles.active=en

Ezzel megadtuk azt, hogy melyik profilt fogjuk aktiválni. Indítsuk el az alkalmazást majd a konzolon figyeljük az üzeneteket:

1
... The following profiles are active: en

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 a 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
No active profile set, falling back to default profiles: default

Nyilván ebben az esetben elhasal az alkalmazásunk, mert a defult 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
@Profile({"en", "default"})
@Component
public class HelloWorldMessageProvider implements MessageProvider {
    // ...
}

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
public class Person{
    private static final String DEFAULT_NAME = "John Doe";

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @PostConstruct
    public void init() throws Exception {
        System.out.println("Initializing bean");
        if (name == null) {
            System.out.println("Using default name");
            name = DEFAULT_NAME;
        }
    }
}

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 így meghív a rendszer. A @Bean annotációról nemsokára lesz szó.
  • Interface alapú módszer: bean osztály implementálja a InitializingBean interface-t, melynek public void afterPropertiesSet(); metódusát kell kifejtenünk.

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
@Component
public class DestroyExample {
    private File file;

    private String filePath;

    @PostConstruct
    public void postInit() throws Exception {
        System.out.println("Initializing Bean");
        if (filePath == null) {
            throw new IllegalArgumentException("You must specify the filePath property of " + DestroyExample.class);
        }

        this.file = new File(filePath);
        this.file.createNewFile();
        System.out.println("File exists: " + file.exists());
    }

    @PreDestroy
    public void destroy() {
        System.out.println("Destroying Bean");

        if(!file.delete()) {
            System.err.println("ERROR: failed to delete file.");
        }

        System.out.println("File exists: " + file.exists());
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = (DestroyExample.class);
        DestroyExample bean = ctx.getBean("destroyExample", DestroyExample.class);
        System.out.println("Calling destroy()");
        ctx.close();
        System.out.println("Called destroy()");
    }
}

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 az kontextus megsemmisítését végzi el, így az 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 így 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
void setApplicationContext(ApplicationContext applicationContext) throws BeansException

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
public class ApplicationContextAwareImpl implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        User user = (User) applicationContext.getBean("user");
        System.out.println("User Id: " + user.getUserId() + " User Name :" + user.getName());
    }
}

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
public class ExampleBean implements BeanNameAware {

    @Override
    public void setBeanName(String beanName) {
        System.out.println(beanName);
    }
}

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!

Bean életciklusa
Bean Lifecycle

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
public class MessageDigestFactoryBean implements FactoryBean<MessageDigest>, InitializingBean {
    private String algorithmName = "MD5";
    private MessageDigest messageDigest = null;

    public MessageDigest getObject() throws Exception {
        return messageDigest;
    }
    public Class<MessageDigest> getObjectType() {
        return MessageDigest.class;
    }
    public boolean isSingleton() {
        return true;
    }
    public void afterPropertiesSet() throws Exception {
        messageDigest = MessageDigest.getInstance(algorithmName);
    }
    public void setAlgorithmName(String algorithmName) {
        this.algorithmName = algorithmName;
    }
}

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

  1. 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á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
    }
}

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: 2021-05-31 18:06:31