Kaparjuk meg a felszínt

Általában a legnehezebb egy új fejlesztői környezet elsajátításában azt meghatározni, hogy hol is kezdjünk neki, mely probléma erősen fenállhat a Spring esetében, hiszen hihetetlenül változatos és sokrétű megoldásokat biztosít. Remélhetőleg jelen jegyzet segít ezen probléma legyűrésében.

Spring Framework dokumentáció

Bevezetés

Az első és legfontosabb kérdés az, hogy mi is a Spring? Általánosan ezt úgy válaszolhatjuk meg, hogy a Spring egy lightweight keretrendszer Java alkalmazások fejlesztéséhez. A Spring keretrendszer nem korlátozza azt, hogy milyen jellegű alkalmazások fejleszthetőek a segítségével, lehet az webes vagy asztali. A lightweight szó jelentése viszont itt nem arra utal, hogy a Spring kevés osztályt biztosít a számunkra, mert ez egyáltalán nincs így. Ez arra utal, hogy egy meglévő alkalmazásunkhoz a Spring nyújtotta előnyöket könnyen hozzáadhatjuk úgy, hogy csupán néhány helyen kell változtatnunk a meglévő kódbázison. A Spring Framework-öt a JavaEE helyett használhatjuk. Támogatja a Groovy-t és a Kotlin-t is.

Az első verziója 2002-ben jelent meg Rod Johnson - "Expert One-on-One J2EE Design and Development" könyve alapján. Jelenleg az 5.0 főverziónál tartunk, mely első kiadása 2017-ben történt meg.

Maga a Spring keretrendszer nyílt forráskódú.

A Spring modulokra van osztva, melyek egy-egy JAR fájlba kerülnek bele. Az 5.0.0.RELEASE-től a Spring 21 modullal rendelkezik (így 21 JAR-ral). A következő táblázat összefeoglalja ezeket a modulokat:

Modul neve Leírás
aop Aspektus orientált fejlesztéshez szükséges osztályok. AspectJ alap integrációt támogató osztályok is itt találhatóak.
aspects Haladó Aspect integrációt támogató osztályok
beans A bean-ek manipulálásához szükséges osztályok. Bean factory implementációk is itt találhatóak (pl XML vagy annotáció alapú megadáshoz).
beans-groovy Ugyanaz, mint az előző, de Groovy osztályokkal.
context Spring Core kiterjesztéséhez használatás osztályok. ApplicationContext, EJB, JNDI, JMX, remoting, dinamikus szkriptnyelv integrációk (JRuby, Groovy, ...).
context-indexer Indexer implementáció
context-support spring-core module kiterjesztése: mail support, template engine integráció, task execution, scheduling (CommonJ, Quartz)
core A fő modul, mely minden Spring-es alkalmazásban kelleni fog. Ezt az összes további modul is használja.
expresison SpEL támogatás
instrument Instrumentáláshoz segítség
jdbc JDBC osztályok, minden DB-vel dolgozó alkalmazáshoz kell.
jms JMS support
messaging Üzenet alapú rendszerek támogatás, STOMP support.
orm ORM eszközök: Hibernate, JDO, JPA, iBATIS. Függőség: pringf-jdbc
oxm Object XML Mapping eszközök: Castor, JAXB, XMLBeans, XStream
test Mock osztályok teszteléshez. Szoros JUnit integráció
tx Tranzakciós infrastruktúra (JTA)
web Webes fejlesztéshez szükséges core osztályok
web-reactive Spring Reactive Web support
web-mvc Spring MVC support
websocket JSR-356 (Java API for WebSocket) support

A Spring lelke az úgynevezett Inversion of Control (IOC), amely kiszervezi (automatizálja) a komponensek (osztályok) példányosítását, illetve az azok közötti függőségek kezelését is vezérli. Tekintsünk egy olyan esetet, amikor a Foo osztály használni akarja a Bar nevű osztály egy példányát. Tipikusan a Foo példányosít magának egy Bar objektumot a konstruktor meghívásával (vagy esetleg egy Factory használatával), majd felhasználja a kapott objektumot. IoC használatával a Bar objektumot futás közben biztosítja a keretrendszer, nincs szükség példányosításra. Ezt a viselkedést nevezzük dependency injection-nek, amit gyakran azonosítani is szoktak az IoC-vel.

A Dependecy Injection két dologra alapszik:

  • Java Bean-ek használatára
  • Interface-ek

Ez a kettő kiemelt fontossággal bír, így már most véssük őket az eszünkbe. Talán az a legegyszerűbb, ha bele is vágunk és nem szaporítjuk a szót. Menet közben kitérünk minden fontos újdonságra.

Hello World újragondolva

IntelliJ-ben készítsünk egy új Maven-es projektet (semmi extra függőség nem kell egyelőre)! Tekintsük a klasszikus Hello World programot, amiről nagy valószínűséggel már mindenki hallott, aki eddig nem a holdon élt!

1
2
3
4
5
6
7
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 éppen rugalmas és bővíthető a kód.

  • Mi van ha le szeretnénk cserélni a kiírandó szöveget?
  • Mi van ha kiírandó szöveget másképpen szeretnénk kiírni? Mondjuk a standard error-ra akarjuk írni, vagy HTML tag-ek közé akarjuk zárni.

Oké, ezek alapján tervezzük át az alkalmazást, hogy a szöveget könnyen módosíthassuk, illetve az is könnyen megadható legyen, hogy a renderelés hogyan történjen. Ez a kettő megoldható lenne a fenti alkalmazásban is olyan módon, hogy átírjuk a kódot, de az egy nagy alkalmazás esetén teljes újrafordítást igényel, illetve a teszteket is újra kell futtatni, stb. Egy jobb megoldás lehet, hogy akkor futás közben töltjük be a kiírandó szöveget, például a parancssori argumentumokat használjuk erre a célra.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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.

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. 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 (statik 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 fentiek eddig semmilyen módon nem használták fel a Spring nyújtotta előnyöket. A következőkben megnézzük, hogyan lehetne megoldani ugyanezt a problémát a Spring keretrendszerrel.

Hello World Spring használatával

Az előző alkalmazásunkkal az a probléma, hogy ahhoz hogy az alkalmazásunk ténylegesen összeálljon (laza csatolás mellett) elég sok "glue code"-ot kellett írnunk. Továbbá a MessageRenderer-t továbbra is nekünk kellett manuálisan ellátni egy MessageProvider példánnyal, hogy az működni tudjon (ha megvan, hogy pontosan milyen provider kell, akkor ezt automatikusan is megoldhatná a rendszer). Ezeket a problémákat a Spring segítségével mind meg tudjuk oldani.

A Spring-es megoldásban teljesen megszabadulhatunk a MessageSupportFactory-tól és helyette használhatjuk a Spring által biztosított ApplicationContext interface-t. Ez az interface szolgáltatja a környezeti információkat a programról a Spring számára. Továbbá ez az interface egy másik interface leszármazottja, a ListableBeanFactory-é, ami bármilyen Spring által menedzselt bean szolgáltatójaként funkcionálhat.

A többi interfészt és megvalósítást hagyjuk meg, mert azokra szükségünk lesz. Húzzuk be a pom.xml-be a Spring Context függőséget!

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

Ezután a main alakítsuk át a következőképpen:

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. Ez az alábbi app-context.xml

 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.helloworldspring.HelloWorldMessageProvider"/>
    <bean id="renderer" class="hu.suaf.helloworldspring.StandardOutMessageRenderer" p:messageProvider-ref="provider"/>

</beans>

A fenti vázat nem kell manuálisan létrehozni: Jobb klikk a spring mappára -> New -> XML Configuration File -> Spring Config. Amit hozzá kell adnunk még az a xmlns:p="http://www.springframework.org/schema/p" sor, mely a property xml namespace-t adja meg. Ezzel tudjuk megmondani, hogy a renderer bean messageProvider property-je a már megadott másik bean legyen, aminek az id-ja a provider. A bean-ek rendelkeznek egy-egy id-val, illetve megadjuk a konkrét megvalósító osztályokat is itt az XML-ben.

Tehát miután betöltöttük a szükséges context információkat, a main-ben a getBean visszaad nekünk egy inicializált MessageRenderer-t, melyet egyből használhatunk is. Jelen esetben az XML állomány tölti be azt a szerepet, amelyet előzőleg a MessageSupportFactory töltött be.

Kicsit vesézzük ki még az XML tartalmát! Mivel a gyökér elemen prefix nélküli XML namespaceként a http://www.springframework.org/schema/beans szerepel, ezért ez az alapértelmezett namespace. Ez a namespace használható a Spring által menedzselni kívánt bean-ek megadására (beleértve a köztük lévő függőségeket is). A Spring a megadott függőségeket feloldja és injektálja azt a kód megfelelő részeibe. Miközben az ApplicationContext inicializálása zajlik, a Spring regisztrálja a bean-t provider id-val és példányosít is nekünk egy objektumot belőle. A renderer esetében hasonló történik, de értesítjük is a rendszert arról, hogy ez a bean bizony függ egy másiktól, aminek meg is adjuk az id-ját, így tudja, hogy az előbb példányosított bean-t kell felhasználnia a renderer-nél is, azaz jóformán meghívja a setter metódusát a provider bean paraméterrel.

A NEM Spring-es megoldás esetében a main-ben példányosítottuk mind a MessageProvider-t, mind a MessageRenderer-t. A mostani megoldásban a köztük lévő függőség feloldását a Spring végzi automatikus módon, így most csak el kell kérnünk a MessageRenderer bean-t és már mehet is a móka.

Spring konfigurálás annotációk használatával

A Spring 3.0-tól kezdődően annotációk és konfigurációs osztályok segítségével is megadhatjuk a konfigurációkat, nincs szükség XML-re. A konfigurációs osztályokat a @Configuration annotációval kell ellátni! Ezek az osztályok tartalmazzák aztán a bean definíciókat (olyan metódusok, amelyek a @Bean annotációval vannak ellátva).

Nézzük meg az előző példa megfelelőjét!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
public class HelloWorldConfiguration {

    // <bean id="provider" class="hu.suaf.helloworldspring.HelloWorldMessageProvider"/>
    @Bean
    public MessageProvider provider(){
        return new HelloWorldMessageProvider();
    }

    // <bean id="renderer" class="hu.suaf.helloworldspring.StandardOutMessageRenderer" p:messageProvider-ref="provider"/>
    @Bean
    public MessageRenderer renderer(){
        MessageRenderer renderer = new StandardOutMessageRenderer();
        renderer.setMessageProvider(provider());
        return renderer;
    }

}

Mivel már nem XML alapú konfigurációt használunk, így egy másik ApplicationContext megvalósítást kell használnunk a ClassPathXmlApplicationContext helyett, ami azt tudja, hogy hogyan lehet annotációval megadott bean-eket kezelni. Erre a célra szolgál az AnnotationConfigApplicationContext, melynek paraméterben megmondhatjuk, hogy melyik konfigurációs osztályt vagy osztályokat szeretnénk használni a ApplicationContext inicializálásához.

1
2
3
4
5
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);
    MessageRenderer renderer = ctx.getBean("renderer", MessageRenderer.class);
    renderer.render();
}

Látható, hogy hogyan egyszerűsödött a kód. Továbbléphetünk még egy szintet, így még a komponensek közötti manuális megadást sem kell megtennünk (bean konfiguráció a HelloWorldConfiguration). Ezt a Spring keretrendszer megteszi helyettünk. Először az interface-eket csupaszítsuk le!

1
2
3
public interface MessageProvider {
    String getMessage();
}
1
2
3
public interface MessageRenderer {
    void render();
}

Ezután nézzük a konkrét implementációkat!

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

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

    @Override
    public void render() {
        System.out.println(messageProvider.getMessage());
    }
}
1
2
3
4
5
6
@Component
public class HelloWorldMessageProvider implements MessageProvider{
    public String getMessage() {
        return "Hello World";
    }
}

A fő különbség, hogy ezen megvalósításokat elláttuk a @Component annotációval. Ez alapján a Spring ApplicationContext-jében, mint bean regisztrációra kerül (később ezt bővebben is megnézzük). Ami még fontos, hogy a StandardOutMessageRenderer tart egy adattagot a MessageProvider-re és biztosítunk egy konstruktort is, mely ilyen típusú objektumot kap paraméterként (ez nagyon fontos).

Végül a fő osztályunkat ellátjuk a @ComponentScan annotációval. A @ComponentScan a főosztály csomagjában rekurzívan keresi a bean-eket (pl.: amit @Component-el láttunk el) és ezeket regisztrálja az ApplicationContext-ben. Ebben az esetben nem kell külön konfigurációs osztályt sem használnunk. A kód a következőképpen néz ki:

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

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

A fenti kód már a Spring segítségével végzi el a komponensek közötti függőségek feloldását. Ha megnézzük, akkor láthatjuk, hogy sehol sem használtunk a new kulcsszót, nem kellett semmilyen XML-t írnunk, pusztán néhány annotációt kellett alkalmaznunk.

Hogy pontosan mi is történik a motorháztető alatt arra a következő fejezetben adunk választ, ahol részletesen foglalkozunk a Dependency Injection és az Inversion of Control fogalmaival.


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