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 fenná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? 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, lehet microservice vagy egy monolit alkalmazás. 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 önmagában id, de JavaEE helyett is használhatjuk. Támogatja a Groovy és a Kotlin nyelveket 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 több, mint 20 modullal rendelkezik (így ennyi JAR-ral is). A következő táblázat összefoglalja a főbb 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).
context Spring Core kiterjesztéséhez használatos osztályok. ApplicationContext, EJB, JNDI, JMX, remoting, dinamikus szkriptnyelv integrációk (JRuby, Groovy, ...).
context-indexer Nagy projektek esetén fordítási időben index-et készíthetünk a komponensekről, így gyorsíthatjuk az alkalmazás indítását. Az indexelést ez a modul képes elvégezni.
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.
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
webflux Spring Reactive Web support
web-mvc Spring MVC support
websocket JSR-356 (Java API for WebSocket) support

Ne aggódjunk, ha a fentiek közül sokat nem ismerünk és kicsit idegennek tűnnek, a lista sok-sok elemét fel fogjuk fedezni.

Fejlesztői környezet

A használt JDK verzió a 11-es lesz, így mindenkinek ezt javaslom telepíteni.

A kurzus során az IntelliJ-t fogjuk használni, viszont a forráskódok abszolút IDE függetlenek, tehát használhatjuk a következő IDE-ket is:

Hello World reboot

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

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

}

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 rugalmas és nem 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
public class FirstSpringApplication {

    public static void main(String[] args) {
        if (args.length > 0) {
            System.out.println(args[0]);
        } else {
            System.out.println("Hello World!");
        }
    }

}

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
public interface MessageProvider {
    String getMessage();
}

Ugyanígy hozzunk létre egy MessageRenderer interface-t az üzenetek renderelésére.

1
2
3
4
5
public interface MessageRenderer {
    void render();
    void setMessageProvider(MessageProvider provider);
    MessageProvider getMessageProvider();
}

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, hiszen nélküle nem tud mit kiírni.

Miután megvannak az interface-ek, könnyen adhatunk ezekhez implementációt is.

1
2
3
4
5
6
public class HelloWorldMessageProvider implements MessageProvider {
    @Override
    public String getMessage() {
        return "Hello World!";
    }
}

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
public class StandardOutMessageRenderer implements MessageRenderer {
    private MessageProvider messageProvider;

    @Override
    public void render() {
        if(messageProvider == null){
            throw new RuntimeException("MessageProvider should be set first for class: " + this.getClass().getName());
        }
        System.out.println(messageProvider.getMessage());
    }

    @Override
    public void setMessageProvider(MessageProvider provider) {
        this.messageProvider = provider;
    }

    @Override
    public MessageProvider getMessageProvider() {
        return messageProvider;
    }
}

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
public static void main(String... args) {
    MessageRenderer mr = new StandardOutMessageRenderer();
    MessageProvider mp = new HelloWorldMessageProvider();
    mr.setMessageProvider(mp);
    mr.render();
}

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. A laza csatoltság szem előtt tartása kulcsfontosságú a jól karbantartható kód eléréséhez.

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

    private static MessageSupportFactory instance;

    private Properties props;
    private MessageProvider provider;
    private MessageRenderer renderer;

    static {
        instance = new MessageSupportFactory();
    }

    private MessageSupportFactory(){
        props = new Properties();

        try{
            props.load(this.getClass().getResourceAsStream("/msf.properties"));

            String rendererClass = props.getProperty("renderer.class");
            String providerClass = props.getProperty("provider.class");

            renderer = (MessageRenderer)Class.forName(rendererClass).getDeclaredConstructor().newInstance();
            provider = (MessageProvider)Class.forName(providerClass).getDeclaredConstructor().newInstance();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static MessageSupportFactory getInstance(){
        return instance;
    }

    public MessageProvider getMessageProvider() {
        return provider;
    }

    public MessageRenderer getMessageRenderer() {
        return renderer;
    }
}

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 (static init miatt). A msf.properties fájl tartalma a következő:

1
2
renderer.class=hu.suaf.firstspring.StandardOutMessageRenderer
provider.class=hu.suaf.firstspring.HelloWorldMessageProvider

Ennek tükrében a main így módosul:

1
2
3
4
5
6
public static void main(String... args) {
    MessageRenderer renderer = MessageSupportFactory.getInstance().getMessageRenderer();
    MessageProvider provider = MessageSupportFactory.getInstance().getMessageProvider();
    renderer.setMessageProvider(provider);
    renderer.render();
}

A fenti megvalósítás ilyen formában már elfogadható és könnyebben karbantartható.

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.

Új Spring projekt létrehozás

Egy Spring-es projektet fel lehet építeni egy üres projektből is, viszont ez kidobott idő, mivel elég favágós meló lenne. Kis projektek esetében ez okoz nagy problémát, de nagyobbak esetében sok fejfájást okozhat és ezen a ponton lép be a képbe a Spring Boot, melynek segítségével sokkal egyszerűbben készíthetünk Spring Framework alapú alkalmazásokat. Jelen pillanatban annyit jegyezzünk meg, hogy a Spring Boot a Spring Framework-re épül, azaz kiegészíti azt, hogy szebb legyen a világ.

Létezik egy eszköz, amit Spring Initializr hívnak, melynek néhány alaptulajdonságot megadnuk és ezek alapján legenerál nekünk egy Spring-es alkalmazást (egész pontosan Spring Boot alkalmazást). Maga a projekt egy open-source fejlesztés, melynek a repository-ja megtalálható a GitHub-on.

A webes interfész a következőképpen néz ki:

spring _initializr

Az eszköz elérhető az IntelliJ-n belül is (mivel REST API-ja is van) a New -> Project alatt bal oldalt ki tudjuk ezt választani (Spring Initializr). Mi az IntelliJ-n belüli verziót fogjuk használni, mivel így nem kell bajlódni az eredmény *.zip állomány kicsomagolásával sem.

Első alkalommal nem kell semmit sem beállítanunk (kivéve, hogy 11-es Java-t fogunk használni, ezt ennek megfelelően állítsuk át), egyszerűen nyomjuk végig a Next-et! Miután létrejött a projektünk, vegyük sorra, hogy mi keletkezett. Alapvetően egy maven-es projekt struktúrát látunk. Az alkalmazásunk Java kódjai az src/main/java alatt találhatóak, a teszt kódok az src/test/java alatt, a nem Java kódok pedig az src/main/resources alatt. Ezen felül van néhány dolog ami még belekerül a projektbe, így tegyük őket is a helyükre.

  • mvnw és mvnw.cmd: Ezek a Maven wrapper scriptjei, amik akkor jönnek jól, ha a build gépen nincs maven telepítve, akkor is végre lehet hajtani a build-et (mvnw -> Linux, mvnw.cmd -> Windows alatt).
  • pom.xml a szokásos maven build descriptor. Mindjárt részleteiben is tanulmányozzuk
  • DemoApplication.java: Az alkalmazásunk fő belépési pontja (Az összes Java forráskód a legenerált projekt src mappájában található)
  • application.properties: alapból üres, de itt adhatjuk meg a konfigurációs beállításokat, később látni fogjuk, hogy miket lehet ide elhelyezni
  • DemoApplicationTests.java: Egy egyszerű teszt, mely ellenőrzi, hogy sikerült-e betölteni az ApplicationContext-et (később látjuk, hogy mi is az pontosan).

A generált Maven build specifikáció

 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
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Az első és legfontosabb dolog, hogy meg van adva egy parent pom, mégpedig a spring-boot-starter-parent. Ez azért jó, mert a dependency-k alatt a függőségek verzióját ez alapján állítja be a rendszer, így nincs szükség ennek megadására. Például a spring-boot-starter dependency megadásánál biztosak lehetünk abban, hogy kompatibilis verziókkal dolgozunk, ezért hívják ezeket curated dependency-knek.

Megjegyzés

A verziókat magunk is megnézhetjük, ha Windows alatt lenyomva tartjuk a Ctrl billentyűt és közben rákattintunk a <artifactId>spring-boot-starter-parent</artifactId>-ra. Itt a következő rész található a pom.xml-ben:

1
2
3
4
5
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.4.0</version>
</parent>

Ahol egy ismételt Ctrl + kattintás után láthatjuk a megadott property-ket:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
<properties>
    <activemq.version>5.16.0</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.83</appengine-sdk.version>
    <artemis.version>2.15.0</artemis.version>
    <aspectj.version>1.9.6</aspectj.version>
    <assertj.version>3.18.1</assertj.version>
    ...
</properties>
...

A fenti lista eltérhet a saját projektünkben, attól függően, hogy milyen Spring Boot verziót használunk, illetve milyen dependency-ket adtunk meg az Initializr-nek.

Érdemes megfigyelni, hogy a dependencyk artifactID-jában szerepel a starter szó. Ezek a library-k speciálisak abban az értelemben, hogy nincs bennük tényleges kód, hanem tranzitív módon behúznak további library-kat. Ezen starter dependency-k előnyei:

  • Kisebb build file-t eredményez, így könnyebben karbantarthatóvá válik a build leíró
  • Elég a képességek alapján behúznunk a dependency-ket, mintsem konkrét library neveket kelljen összeszedni (például, ha webes alkalmazást szeretnénk fejleszteni, akkor behúzzúk a spring-boot-starter-web dependency-t, és ő minden olyan dependency-t behúz ami ebben a starter projektben szerepel, mint szükséges függőség)
  • A verziók megadásával sem kell bajlódnunk. Az egyetlen verzió, amit ki kell találnunk, hogy melyik Spring Boot-ot használjunk

A Spring Boot Starter-ek listáját megtekinthetjük a GitHub repoban.

A build leíró a spring-boot-maven-plugin megadásával végződik. Ez a plugin néhány igen fontos dolgot biztosít a számunkra:

  • Ad egy Maven goal-t, amivel futtathatjuk is az alkalmazásunkat (spring-boot:run)
  • Biztosítja, hogy az összes library, amit használunk, belekerül a futtatható JAR file-ba, és azok belekerülnek a classpath-ba.
  • Készít egy manifest fájlt is, amit szintén belerak a futtatható JAR-ba. Ebben megadja az alkalmazás fő belépési pontját.

A Spring Initializr használatát a következő videó mutatja be:

Spring Initializr

Feladat

Hozzunk létre egy új Spring Boot projektet! Használjuk a Spring Initializr-t! Próbáljuk ki a webes interfészt és az IntelliJ adta lehetőségeket is!

Az alkalmazásunk bootstrappelése

Bootstrapping: Az alkalmazás olyan formájúra alakítása, ami nem igényel külső inputot ahhoz, hogy elinduljon és ezt az indítást automatikusan meg tudja tenni.

Esetünkben fő osztályunk, mely a DemoApplication, elindul és ő tölti be az összes keretrendszer által szükséges dolgot, a modelleket, a konfigurációkat, a kontrollereket, majd átadja az irányítást ez utóbbiaknak. Tehát a lényeg, hogy egy nagyon egyszerű fájl hatalmas háttér processeket indít, de ez el van rejtve előlünk. Ezt a hatalmas melót rejti el igazából egy Spring Boot alkalmazás. Nézzük meg hogy, hogyan!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

A legfontosabb a @SpringBootApplication annotáció, mely elvégzi a fent említett hatalmas munkamennyiséget. Ez az annotáció igazából 3 másik annotáció kombinálása:

  • @SpringBootConfiguration: Az adott osztály egy konfigurációs osztály (ezekről lesz szó később részletesen). Ez az annotáció a @Configuration annotáció egy speciális változata. Ez jelenleg annyit mond, hogy írhatnánk konfigurációs beállításokat a fő fájlunkba is.
  • @EnableAutoConfiguration: A Spring Boot automatikusan konfigurálja azokat a komponenseket, amelyekről azt gondolja, hogy szükségesek lehetnek a számunkra (később erről is bővebben lesz szó).
  • @ComponentScan: Segítségével lehetővé válik egyes komponensek (osztályok) automatikus betöltése (példányosítása). Az automatikusan létrehozott komponenseket @Component, @Controller, @Service, stb. annotációkkal látjuk el. Ezeket a Spring automatikusan regisztrálja a Spring Application Context-jében, azon belül is a bean konténerben..

A fentiek közül az utóbbi fogunk nemsokára példát látni. Ne ijedjünk meg, ha most még nem értjük ezek pontos működését.

A következő fontos rész a main() metódus. Ez belül meghívja a statikus run metódusát a SpringApplication-nek, mely a tényleges bootstrappelést elindítja, illetve megkonstruálja az ApplicationContext-et. Itt megmondjuk a konfigurációs osztályunkat (ami saját magunk), illetve tovább tudjuk passzolni a parancssori argumentumokat is.

Nem maradt más mint futtatni a projektünket. Jelen esetben az alkalmazás nem fog semmit sem csinálni, de futtassuk le, hogy ha bármi félresikerült akkor az már most tapasztalhassuk!

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. Ezen felül 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.

Nézzük is a példát, amin kicsit egyszerűbben megérthetjük, hogy mi is történik!

TODO: videó

Először a konkrét megvalósításokból komponenseket képzünk, amiket így a Spring fel fog ismerni és a @ComponentScan miatt bele is fogja azokat tenni az úgynevezett bean konténerbe az ApplicationContext-en belül.

Például a HelloWorldMessageProvider-t a következőképpen alakítjuk át:

1
2
3
4
5
6
7
@Component
public class HelloWorldMessageProvider implements MessageProvider {
    @Override
    public String getMessage() {
        return "Hello World!";
    }
}

Az egyetlen különbség, hogy az osztályt elláttuk a @Component annotációval. A @Component annotáció az org.springframework.stereotype csomagban található a többi úgynevezett stereotype annotációval együtt. Ezen annotációval ellátott osztályokat a @ComponentScan automatikusan regisztrálni fogja. Ami azt jelenti, hogy regisztrál egy példányt a bean konténerben. A hangsúly azon van, hogy példányosít is számunkra egy objektumot és beleteszi egy konténerbe. Futás közben a használat helyén a Spring ebből a konténerből képes számunkra objektumokat adni és nem kell példányoítanunk azokat.

Nézzük a MessageRenderer megvalósítását:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Component
public class StandardOutMessageRenderer implements MessageRenderer {
    private MessageProvider messageProvider;

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

    @Override
    public void render() {
        if(messageProvider == null){
            throw new RuntimeException("MessageProvider should be set first for class: " + this.getClass().getName());
        }
        System.out.println(messageProvider.getMessage());
    }

    // getters and setters
}

Ezt az osztályt is ellátjuk a @Component annotációval, így az bekerül a bean konténerbe. Mivel a StandardOutMessageRenderer megvalósításnak szüksége van egy MessageProvider-re, így azt egy field-ben adjuk meg. Ugyanakkor nem példányosítunk a függőségből, mert azt a Spring fogja számunkra adni. Egy lehetséges megoldás az, hogy a StandardOutMessageRenderer konstruktorában, mint paraméter megadjuk a MessageProvider-t, így a Spring tudni fogja, hogy amikor egy StandardOutMessageRenderer objektumot akar példányosítani, akkor egy MessageProvider típusú objektumot kell átadni, azaz injektálnia. Ezt nevezzük Dependency Injection-nek. A fenti megoldás csak egy lehetséges megközelítés, mely mellett számos más megoldást is alkalmazhatunk, de ezekről majd később lesz szó.

Ezután már csak a main-t kell igazítanunk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@SpringBootApplication
public class Application {
    private static MessageRenderer renderer;

    public Application(MessageRenderer renderer) {
        Application.renderer = renderer;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);

        renderer.render();
    }
}

Jelen esetben, mivel a main egy statikus metódus, így egy statikus field-be injektáltatjuk be a MessageRenderer objektumot (itt sem kell példányosítanunk). Ezután egyszerűen használhatjuk az objektumot és meghíjuk, annak render metódusát. A futtatás után a következő kimenetet kapjuk:

1
Hello World!

Említettük, hogy a bean konténer az ApplicationContext részét képezi, mely osztály a spring-context modulban található meg, így a függőségeink között szerepelnie kell a következőnek:

1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
</dependencies>

Amennyiben megnézzük, a pom.xml-ben nincs ilyen függőség megadva, azonban a spring-boot-starter dependency, mint ahogy említettük, tranzitívan behúz további függőségeket, az alkalmazásunk megkapja ezt a függőséget és ezen felül még néhányat (pl.: aop, beans, stb.).

ApplicationContext

Most, hogy láttunk egy Spring Boot alkalmazást, nézzünk bele kicsit az ApplicationContext-be:

1
2
3
4
5
6
public static void main(String[] args) {
    ApplicationContext ctx = SpringApplication.run(Application.class, args);

    MessageRenderer renderer = ctx.getBean(MessageRenderer.class);
    renderer.render();
    }

A SpringApplication statikus run helper metódusa vissza is ad számunkra egy ApplicationContext-et, melyet eltárolhatunk, ha éppen erre van szükségünk. Az ApplicationContext rendelkezik néhány túlterhelt (overloaded) getBean metódussal, melyek közül most azt használtuk, mely egy típust vár paraméterként, vagyis azt, hogy milyen típusú bean-re van szükségünk. A Spring minden olyan osztályt bean-nek hív melyet a Spring menedzsel, azaz ami belekerül a bean konténerbe.

Ha körbenézünk, akkor van lehetőség bean név alapján is lekérni egy-egy objektumot a konténerből. Mivel az előző példákban nem adtunk meg sehol nevet, így jogosan feltehető a kérdés, hogy ez használható-e jelen esetben. A válasz igen, mivel a Spring a megadott @Component osztályokat alapértelmezetten az osztály nevével regisztrálja, annyi különbséggel, hogy kis kezdőbetűvel kezdi azok nevét. A fenti helyett, így használhatjuk a következőt is, azonban itt mivel nem ismerjük előre a típusát egy natúr Object eredményünk van, amit aztán kasztolnunk kell.

1
MessageRenderer renderer = (MessageRenderer) ctx.getBean("standardOutMessageRenderer");

A két előbbit kombinálhatjuk is, hogy elkerüljük a kasztolást.

1
MessageRenderer renderer = ctx.getBean("standardOutMessageRenderer", MessageRenderer.class);

A név alapú lekérdezéskor és a típus alapú lekérdezéskor is használható egy haladó lekérdezés, melynek során akár felül is írhatjuk a beanhez tartozó konstruktor paramétereket:

1
MessageRenderer renderer = ctx.getBean(MessageRenderer.class, new HelloWorldMessageProvider());

Jelen eseben ugyanúgy a HelloWorldMessageProvider-t használtuk, mely pusztán demonstrációs jellegű.

Arra is lehetőségünk van, hogy az ApplicationContext-től elkérjük a konténerben lévő bean-ek listáját (azok nevét), illetve azok számát:

1
System.out.println("There are " + ctx.getBeanDefinitionCount() + " beans in the container");

Amennyiben így futtatjuk az alkalmazásunkat, akkor egy kis meglepetés ér minket, ugyanis a kimenet a következő:

 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
42
There are 41 beans in the container
    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    application
    org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
    helloWorldMessageProvider
    standardOutMessageRenderer
    org.springframework.boot.autoconfigure.AutoConfigurationPackages
    org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
    propertySourcesPlaceholderConfigurer
    org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
    mbeanExporter
    objectNamingStrategy
    mbeanServer
    org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
    springApplicationAdminRegistrar
    org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$ClassProxyingConfiguration
    org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
    org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
    applicationAvailability
    org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
    org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
    org.springframework.boot.context.internalConfigurationPropertiesBinderFactory
    org.springframework.boot.context.internalConfigurationPropertiesBinder
    org.springframework.boot.context.properties.BoundConfigurationProperties
    org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter
    org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
    lifecycleProcessor
    spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties
    org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
    spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties
    org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
    taskExecutorBuilder
    applicationTaskExecutor
    spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties
    org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
    taskSchedulerBuilder
    spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
    org.springframework.aop.config.internalAutoProxyCreator

A 41 határozottan több, mint az a két komponens, amit mi létrehoztunk és a Spring gondjaira bíztunk. Ugyanakkor a listában határozottan jelen van a két bean-ünk. A többi 39 bean-t a Spring automatikusan hozza létre, melyek között megtalálhatóak azok, amik az automatikus konfigurációhoz, a property állományok beolvasásához és további tevékenységekhez szükségesek, de ezekről később ejtünk majd szót.

@Configuration

Spring Boot nélküli élet

Ahhoz, hogy jobban megértsük, hogy mi történik a háttérben megnézzük azt is, hogy Spring Boot nélkül milyen opcióink lennének. A Spring Boot alkalmazásunkban a run metódus egy ConfigurableApplicationContext objektumot adott vissza, ugyanakkor, ha Spring Boot nélküli kóddal találkozunk, akkor ott nem is tudjuk meghívni ezt a statikus run metódust, mivel a SpringApplication osztály a Spring Boot-ból jön. A megoldás a következőképpen néz ki:

1
2
3
4
5
6
7
8
9
@ComponentScan
public class Application {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(Application.class);
        MessageRenderer renderer = ctx.getBean(MessageRenderer.class);
        renderer.render();
    }
}

A fő osztályunkat ellátjuk a @ComponentScan annotációval. Itt elevenítsük fel, hogy a @SpringBootApplcation annotáció magában foglalja a @ComponentScan annotációt is. 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.

Az AnnotationConfigApplicationContext, ahogy a neve is mutatja, képes az annotációkkal ellátott komponensek betöltésére, melynek paraméterben megmondhatjuk, hogy melyik komponens osztályokat szeretnénk használni a ApplicationContext inicializálásához. Mivel itt megadjuk a fő osztályunkat, amin a @ComponentScan is megtalálható, így a MessageRenderer és a MessageProvider komponensek is betöltődnek, hiszen a paraméter nélküli @ComponentScan az adott osztály csomagjában (rekurzívan) keresi a komponenseket.

Látható, hogy Spring Boot nélkül se lenne olyan nehéz az élet, ugyanakkor ezen felül rengeteg egyéb dolgot is tesz értünk a Spring Boot, így használatát teljes mértékben javaslom.

XML alapú élet

Régebben az XML alapú megadás volt a bevett szokás a komponensek kezeléséhez. A bean-ek kezelése azonban kicsit nehézkesebb vele. A részletekbe nem belemenve, nézzünk meg egy példát:

1
2
3
4
5
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/app-context.xml");
    MessageRenderer renderer = ctx.getBean("renderer", MessageRenderer.class);
    renderer.render();
}

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. A bean-eket a következőképpen adjuk meg az XML-ben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="provider" class="hu.suaf.helloworld.HelloWorldMessageProvider"/>
    <bean id="renderer" class="hu.suaf.helloworld.StandardOutMessageRenderer" p:messageProvider-ref="provider"/>

</beans>

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.

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.

Összegzés és kitekintés

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

A következő fejezetben mélyebben is tanulmányozni fogjuk a ezen fogalmakat.


Utolsó frissítés: 2021-05-31 18:06:31