Spring

Kategória: Java keretrendszerek.

Áttekintés

A Spring egy keretrendszer nagyvállalati alkalmazások fejlesztéséhez. Igen sok komponenst integrál magába. A legfontosabbak az alábbiak:

  • a kontroll megfordítása (Inversion of Control, IoC) technika megvalósítása,
  • aspektusorientált programozás,
  • adatbázis elérés: egyszerűsített JDBC, JPA, ill. Data JPA,
  • üzenetkezelés,
  • biztonság (security),
  • MVC webes alkalmazások,
  • stb. stb. stb.

A technikai részletek megismerése előtt érdemes áttekinteni a Spring filozófiáját, ill. a főbb tulajdonságait.

  • A Spring webalkalmazás nem tartalmaz mindent egyszerre; az egyes modulokat szükség esetén egyesével kell hozzáadni. Ugyanakkor vannak előre gyártott kezdőszettek.
  • A Spring programok futtatásához nem szükséges külső keretrendszer, beépítve találunk hozzá modulokat, amelyek önmagában futtathatóvá (self-contained) teszik. De az is megoldható, hogy szabványos webalkalmazást (.war fájlt) készítsünk, amit valamilyen webszerver vagy alkalmazás szerves segítségével futtatunk.
  • Támogatja az invazív és a non-invazív megoldásokat is. A non-invazív azt jelenti, hogy magába a Java kódba nem kell írni semmit, és úgy is vezéreltté tudja tenni a komponenseket, XML konfiguráció segítségével. Ugyanakkor használhatunk annotációkat (invazív módon), így lehetőséget biztosít arra, hogy egyáltalán ne kelljen XML kódot írnunk.
  • A példányosítást megoldhatjuk megfelelő annotációkkal, de a Java-ban szokásos new-val is.
  • Nem szükséges Spring keretrendszerbeli interfészekből, osztályokból származtatnunk, és konfigurálnunk sem kell, sok esetben elég csak bizonyos kódolási konvenciókat kell betartanunk. Ennek a megoldásnak az angol neve convention over configuration, ami annyit jelent, hogy a konvenciót részesíti előnyben a konfigurációval szemben. Ugyanakkor a konvenciókat sem kötelező betartani, ez esetben persze megfelelően kell konfigurálnunk.
  • A sallang (boilerplate) kódot igyekszik teljes egészében elkerülni. (Ez az olyan kódot jelenti, amelyt mindenképpen meg kell írni, mindig ugyanabban a formában, de nem hordoz plusz információt. Ilyen pl. az adatbázis kapcsolatok felépítése és lezárása, a HTML fejléc-lábléc szokásos sorai, vagy mondjuk a Java programokban a kötelezően odaírt csomagnév, osztálynév, valamint a fő metódus neve.) Nekünk csak az üzleti logikát kell megírnunk, a "sallangot" megoldja ő.
  • Szinte mindenre nyújt saját megoldást, ugyanakkor maximálisan támogatja a szabványos Java megoldásokat is.

Láthatjuk, hogy ugyanarra a problémára számos megközelítést, megoldást kínál. Kicsit az az érzésem, hogy minden szempontot igyekszik kielégíteni, mindenkinek igyekszik megfelelni és a kedvébe járni egyszerre. Ennek persze pont az a nagy hátránya, hogy nagyon nehéz megfogni a lényeget: mi is a Spring? Nem invazív, nem kell XML hozzá, nem kell konvenciókat betartani, és örökölni sem kell? De valahogy csak meg kell adni, hogy mely osztályok a vezéreltek! Nem lehet egyszerre minden szemponthoz igazodni! Emiatt is volt egyébként nehéz elkezdenem ezt a leírást. A többi rendszer esetében ugyanis az a megközelítés, hogy ezt így meg így kell csinálni; akinek tetszik, az használja, akinek meg nem tetszik, az használjon mást! De ott legalább egyértelműek a dolgok, például a szokásos "Helló, világ" program elkészítése. De itt vajon mi a "Helló, világ"? Ha annotációt használok, akkor megsértem azt a hangoztatott tulajdonságot, hogy a Spring nem invazív. Ugyanis ha az egyik legfontosabb tulajdonsága az, hogy nem invazív, akkor legalább az alap példaprogram legyen az! De akkor használjak XML-t? Azt senki sem szereti, és a Spring is hangoztatja, hogy nem kell XML-t használni, az felesleges sallang. Igazából nehéz eligazodni azokon a dolgokon, amelyek mindent és mindennek az ellenkezőjét is tartalmazzák egyszerre. Az interneten sok információ van, talán túl sok is… Az oldalak vagy csak egy specifikus részt magyaráznak el, vagy túl sok az információ, ha pedig tömör, akkor az egyúttal felületes is. Igyekeztem a legjobb tudásom szerint körbe járni a témát, amely - nem panaszkodásképpen - elég sok időmet elvette; remélem, másoknak is segít a megértésben, és akkor már megérte!

Helló, Spring világ!

A Spring hangsúlyozza az egyszerűséget, de azért - amint látni fogjuk - a "helló világ" programmal is rendesen meg kell szenvednünk. A példák felépítése a következő. Létrehozunk egy MyMath nevű osztályt egy int add(int a, int b) metódussal, amely kiszámolja a két szám összegét. A MyMath osztályt nem példányosítjuk közvetlenül, hanem azt rábízzuk a Spring keretrendszerre. Ez persze - ahogy látni fogjuk - jól elbonyolítja a példát, viszont azáltal, hogy a Spring példányosítja, vezérlet lesz, melynek jelentősége lesz a későbbiekben. A vezérlet objektumot hívjuk bean-nek (ejtsd: bín; magyarul egyébként babot jelent, de nem szoktuk lefordítani). A példában meghívjuk a függvényt.

A bevezetőben leírtak alapján talán nem meglepő, hogy ezt is számos módon végre tudjuk hajtani.

XML konfiguráció

Az első példában a bean példányt XML konfigurációval állítjuk be. Készítsünk egy Java Maven projektet, pl. springxml néven. Mivel lesz Java 8-as megoldás a programunkban, Eclipse-ben kattintsunk jobb egérgombbal a projekten, a context menüben válasszuk ki a Properties menüpontot, majd a Java Compiler részben kapcsoljuk ki az Enable project specific settings-et. A Java 8-at a pom.xml-ben is be kell állítani.

A pom.xml tartalmazza tehát szokásos fejlécet, a Java verziót, és a Spring függőséget. Arról már volt szó, hogy a Spring nem egy nagy, monolitikus valami, hanem sok kis modulból áll. A példában ezek közül most egyet használunk: a spring-context-et:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>springxml</artifactId>
    <version>1.0</version>
 
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>
    </dependencies>
</project>

Hozzunk létre egy csomagot, pl. hu.faragocsaba.springxml, és oda tegyünk bele egy HelloWorld osztályt:

package hu.faragocsaba.springxml;
 
public class MyMath {
    public int add(int a, int b) {
        return a + b;
    }
}

Ez lesz a vezérelt bean-ünk. Most hozzuk létre a bean konfigurációt. A fájl helye és neve: src/main/resources/beans.xml, a tartama pedig a következő:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="myMath" class="hu.faragocsaba.springxml.MyMath"/>
</beans>

Végül készítsük el a főprogramot!

package hu.faragocsaba.springxml;
 
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml")) {
            MyMath myMath = context.getBean(MyMath.class);
            System.out.println(myMath.add(2, 3));
        }
    }
}

A főprogramban tehát betöltjük a beans.xml-t, megkeressük a bean-t, és meghívjuk a megfelelő függvényt. Futtassuk le a főprogramot. Ha mindent jól csináltunk, a képernyőre kiírja a program a megfelelő üzenetet. Mindezt úgy, hogy nem példányosítottuk közvetlenül a HelloWorld osztályt, a hívás egy vezérelt objektumon keresztül történt, ráadásul úgy, hogy nem kellett semmi extrát beleírni a bean-t tartalmazó osztályba. Ezt úgy hívjuk szakkifejezéssel, hogy nem invazív módon történt az bean létrehozása.

Annotáció

A fenti példa tartalmaz egy nagyon csúnya részt: az XML-t. Szabaduljunk meg tőle, használjunk inkább annotációt! Az új példának a neve nálam ez: springannotation; ennek megfelelően hozzuk létre a pom.xml-et, ill. célszerűen módosítsuk a csomagnevet. A HelloWorld.java nem változik a példában, csak a csomagnév az elején.

A bean-eket ún. konfigurációs osztályban példányosítjuk:

package hu.faragocsaba.springannotation;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class AppConfig {
 
    @Bean
    public MyMath myMath() {
        return new MyMath();
    }
 
}

Itt tehát megtörténik az explicit példányosítás, viszont azzal, hogy az osztály @Configuration annotációval, a getter pedig @Bean annotációval van ellátva, vezérlet lesz. A főprogramban ebben az esetben is kontextuson keresztül kérjük le a bean-t, és nem a közvetlenül példányosítottat használjuk:

package hu.faragocsaba.springannotation;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            MyMath myMath = context.getBean(MyMath.class);
            System.out.println(myMath.add(2, 3));
        }
    }
}

Figyeljük meg azt, hogy a kontextus típusa itt már nem ClassPathXmlApplicationContext, hanem AnnotationConfigApplicationContext, minden más viszont megegyezik a korábbival. A beans.xml fájlra nincs szükség, és a továbbiakban nem is fogjuk azt használni. Ha a programot lefuttatjuk, akkor ismét a megfelelő szöveget kell látnunk.

Komponens

Vezérelt példány nemcsak bean, hanem komponens is lehet. Ott nem kell törődnünk a példányosítással sem. Lássunk erre is egy példát!

A példa neve nálam springcomponent, ennek megfelelő csomagnévvel, egyébként minden egyébben megegyezik a fentiekkel. A HelloWorld osztályt lássuk el @Component annotációval (így ez a megoldás már invazív):

package hu.faragocsaba.springcomponent;
 
import org.springframework.stereotype.Component;
 
@Component
public class MyMath {
 
    public int add(int a, int b) {
        return a + b;
    }
 
}

Ebben a példában a főprogram lesz két sorral hosszabb:

package hu.faragocsaba.springcomponent;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
            context.scan("hu.faragocsaba.springcomponent");
            context.refresh();
            MyMath myMath = context.getBean(MyMath.class);
            System.out.println(myMath.add(2, 3));
        }
    }
}

Tehát fel kell derítenünk a komponenseket a scan(), majd a refresh() függvényekkel.

Komponens automatikus megkeresése

Neme személy szerint nem tetszik az, hogy a scan() sort explicit meg kellett adnom. Úgy tűnik, ízlésemmel nem vagyok egyedül, a Spring megalkotói erre is alkottak alternatív megoldást: az annotációt. Az új példa neve legyen springcomponentscan, ennek megfelelő csomagnévvel. A HelloWorld osztály megfelel az előző példában (csak a csomagnév változik), a főprogram viszont az alábbi lesz:

package hu.faragocsaba.springcomponentscan;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
 
@ComponentScan
public class Main {
 
    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class)) {
            HelloWorld helloWorldBean = context.getBean(HelloWorld.class);
            helloWorldBean.sayHello();
        }
    }
 
}

Az osztályt tehát elláttuk egy @ComponentScan annotációval, amivel egyúttal vezéreltté is tettük. Alapértelmezésben az aktuális csomagnévben található komponenseket deríti fel; ezt megfelelő paraméterrel felül tudjuk írni, pl. @ComponentScan({"package.path.one", "package.path.two"}). Ebben a megoldásban a második példához képest van egy kis csalás: itt összevontuk a konfigurációt és a főprogramot.

BOM

A fenti példában egyetlen Spring függőség van, a spring-context. A bevezető példát nem szerettem volna túlzottan elbonyolítani, viszont abból, hogy a Spring nem egy nagy, monolitikus valami, hanem moduláris, az is következik, hogy a valós méretű Spring alapú programok nagyon sok Spring függőséget tartalmaznak. Ehhez még kapcsolódnak a nem Spring függőségek is. Egy esetleges verzióváltás rémálommá válhat, ha egymással inkompatibilis könyvtárak kerülnek bele.

Ennek kezelésére alkották meg a Spring BOM-ot. A BOM a Bill of Materials rövidítése. Ez is egyfajta POM, tehát végső soron egy Maven szintű fogalom. Arról van szó, hogy speciális módon függőségként megadjuk a BOM-ot, amely beállítja a szükséges verziókat, és utána a tényleges függőségeknél elég csak felsorolnunk a könyvtárakat, verzió nélkül. Hasonlóan működik tehát a technológia, mintha szülő POM-ot használnánk, azzal a lényeges különbséggel, hogy szülő POM-ból csak egy lehet, és nem "lőjük el" a golyót a Spring miatt.

Mindegyik fenti példát át tudunk írni ily módon, mindenféle egyéb változtatás nélkül, a következőképpen:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>springbom</artifactId>
    <version>1.0</version>
 
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>5.2.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
    </dependencies>
</project>

A BOM függőséget, azaz a spring-framework-bom-ot a dependencyManagement alá helyezzük függőségként, majd a tényeges függőségnél már nem kell a verziót megadnunk.

Spring Boot

Áttekintés

Ahogy arról már többször volt szó, a Spring nagyon sok modult tartalmaz. A valóságban viszont kialakultak ezeknek gyakori részhalmazai: pl. webalkalmazások esetén nagyjából ugyanazokat a komponenseket szokás használni, melytől eltérhet mondjuk egy tipikus üzenetkezelő alkalmazás által használt komponenshalmaz. Vannak részem, melyeket szinte mindegyik Spring alkalmazás használ, ilyen pl. a naplózás. Valamint természetesen valahogy el is szeretnénk indítani a Srping alkalmazásunkat.

A Spring Boot a Spring egyfajta továbbgondolása. A Spring Boot kezdőkészletekből, ún. starterekől áll. Egy-egy starter tartalmazza a tipikus olyan jellegű alkalmazás komponenseit. A starterek nevei a következőképpen kezdődnek: spring-boot-starter. Pl. a spring-boot-starter-web a webalkalmazások függőségeit tartalmazza, valamint egy beépített webszervert is, ami beállítás nélkül automatikusan elindul. Egy program tetszőleges számú Spring Boot startert használhat.

Spring alkalmazás átírása Spring Boot-tá

A legalapabb kezdőkészlet önmagában a spring-boot-starter. Lássunk erre egy példát! A példaprogram neve nálam springbootexample, ennek megfelelő csomagnévvel. A pom.xml a következőképpen néz ki:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>springbootexample</artifactId>
    <version>1.0</version>
 
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>hu.faragocsaba.springbootexample.Main</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>                
            </plugin>
        </plugins>
    </build>
</project>

Függőségként tehát a spring-boot-starter komponenst adtuk meg. Figyeljük meg, hogy a verziózása is eltér a Springtől: az írás pillanatában a Spring 5-ös és a Spring Boot 2-es verziók voltak aktuálisak. Az eltérő verziózással is azt fejezik ki, hogy a Spring Boot nem a Spring egy újabb verziója vagy közvetlen kiterjesztése, hanem arra épülő, de attól függetlenül kezelt termék.

A Spring Boot nyújt egy build beépülőt is. Megfelelően felkonfigurálva egy olyan futtatható jar-t kapunk, ami tartalmaz minden függőséget, adott esetben pl. egy webszervert is. Ügyeljünk a megfelelő mainClass osztályra. Az executions rész nem kötelező; ez esetben a maven hívás a következőképpen néznek ki: mvn clean install spring-boot:repackage.

A példában a springcomponentscan osztályait másoltam át, és módosítás nélkül egyből működött. Futási időben viszont lényeges eltérés az előzőhöz képest az, hogy nemcsak egyszerűen megjelent az eredmény, hanem mindenféle naplóbejegyzések jelentek meg a konzolon.

A @SpringBootApplication annotáció

Láthattuk, hogy a Spring Boot felülről kompatibilis a Spring-gel, de természetesen ki szeretnénk használni a Spring Boot előnyeit is! Ehhez a Spring Boot nyújt néhány annotációs és osztályt. Lássunk egy példát! A pom.xml változatlan (ill. ha új példát készítünk, ahogy én springbootapp néven, akkor a megfelelő részeket át kell írni). A MyMath.java változatlan marad (a @Component annotációval ellátottat használjuk), a főprogram viszont a következő lesz:

package hu.faragocsaba.springbootapp;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
public class Main {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext app = SpringApplication.run(Main.class, args);
        MyMath myMath = app.getBean(MyMath.class);
        System.out.println(myMath.add(2, 3));
    }
 
}

A @SpringBootApplication annotáció teszi főprogrammá a főprogramot. Ez valójában a háttérben néhány alap Spring annotációra módosul. A programot a SpringApplication.run(…) hívással indítjuk, amely egyúttal visszaadja a kontextust is. A többivel már találkoztunk.

Ha elindítjuk így a programot, akkor a végén ugyanúgy kiírja a megfelelő szöveget, előtte viszont látunk néhány naplóbejegyzést, ami arra utal, hogy először elindult a Spring Boot keretrendszer. A későbbiekben majd látunk olyan programot, amikor a Spring Boot egy teljes webalkalmazás lesz.

Spring Boot parancssor

A Spring egyik fontos lényege a modularitás, és a komponensek laza kapcsolata. AMint azt a későbbiekben látni fogjuk, az @Autowired annotációt használjuk majd erre a célra. Alakítsuk tovább a már megkezdett példát! Én új alkalmazást hoztam létre springbootcmdline névvel, hogy a többi is megmaradjon, de folytathatjuk a korábbi átírásával is. Csaka főprogramot kell módosítani:

package hu.faragocsaba.springbootcmdline;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MyMath myMath;
 
    @Override
    public void run(String... args) throws Exception {
        System.out.println(myMath.add(2, 3));
    }
 
}

A háttérben példányosítja a Main osztályt, befecskendezi a MyMath példányt és meghívja a run() metódust a példányon.

Spring Boot parent POM

Idővel majd az is előfordul, hogy több Spring Boot függőséget kell beletennünk a pom.xml-be. Itt már kicsit ízlés kérdése, hogy "beáldozzuk" a parent-et, vagy mindenhova kiírjuk a verziót. Ha a programot a fenti példára építjük, akkor kell írnunk, a parent megoldással viszont a következőt kapjuk (az új projekt neve springbootparent):

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>springbootparent</artifactId>
    <version>1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>
 
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.1.1</version>
                </plugin>
            </plugins>
        </pluginManagement>
 
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>hu.faragocsaba.springbootparent.Main</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>                
            </plugin>
        </plugins>
    </build>
</project>

A <parent> és <depencency> mellett belekerült egy <pluginManagement> rész is. Ezt egy szoftverhiba miatt kell odatenni. Enélkül is működik, de az Eclipse hibát jelez a pom.xml első sorában. Ez se tökéletes, na, sajnos megjelenik a boilerplate kód!

A különbség akkor lesz nyilvánvaló, ha több függőségünk is lesz. Majd erre is látunk példát.

A későbbiekben a példákat erre a mintára építjük fel.

Általános problémák Spring specifikus megoldásai

Ebben a szakaszban olyan ismeretekre teszünk szert, amelyek minden rendszerben előfordulnak, és a Spring sajátos módon kezeli őket.

Spring integrációs tesztelés

A bevezető példákból már sejthető, hogy a Spring-ben a komponensek alapvetően lazán kapcsolódnak egymáshoz, az annotációkat követve maga a keretrendszer gondoskodik a megfelelő huzalozásról. De hogyan lehet ezt egységtesztelni? Szerencsére a Spring gondoskodik erről is.

Folytassuk a megkezdett programot! Ahhoz, hogy mindegyik lépés megmaradjon, én létrehoztam egy újat springbootunittest néven. A pom.xml-ben az alábbi függőséget állítsuk be:

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

Valójában az alap spring-boot-starter-re nincs is szükség, szóval a példához elég ez az egy függőség, bár a gyakorlatban többnyire a programok elég komplexek ahhoz, hogy használnunk kell külön a projekt specifikus startert és a tesztet is egyszerre.

A program lényegi része megmarad annak, ami az előző volt. Az egységtesztet az src/test/java/ könyvtár alá tegyük, a csomagnak megfelelő alkönyvtárba:

package hu.faragocsaba.springbootunittest;
 
import static org.junit.Assert.assertEquals;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
 
import hu.faragocsaba.springbootunittest.MyMath;
 
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MyMath.class)
public class MyMathTest {
 
    @Autowired
    private MyMath myMath;
 
    @Test
    public void testSaveAndList() {
        assertEquals(5, myMath.add(2, 3));
    }
 
}

Megjegyzések:

  • A SpringRunner a SpringJUnit4ClassRunner-ből származik, gyakorlatilag annak aliasaként tekinthető. Ennek az oka az, hogy ez utóbbi hosszú.
  • A @ContextConfiguration tartalmazza a szükséges komponenseket. Ha többet kell megadni, akkor a formátum a következő: classes = {Class1.class, Class2.class, Class3.class}.
  • Ha bean-t használunk komponens helyett, akkor paraméterként a konfigurációs fájlt kell megadnunk, pl. @ContextConfiguration(classes = AppConfig.class)
  • Létezik egy @SpringBootTest annotációs is. A @ContextConfiguration annotációt lecserélhetjük erre, és végül is működik, viszont a tapasztalatom szerint ekkor a teljes program lefut. TODO: utána járni, ennek mi az oka.

TODO: több komponens példa

Konfiguráció

Előfordulhat, hogy egy bizonyos értéket időnként vagy környezetenként meg kell változtatni. Például a használt adatbázis más lesz lokálisan, más egy teszt környezetben, és megint még élesben. Ez változhat is az idők folyamán. Ha ezzel nem törődünk, és beleírjuk a forrásba az aktuális értékeket (szakkifejezéssel hard-code-oljuk), akkor minden egyes változáskor bele kell nyúlnunk a kódba. Sőt, a különböző környezetekhez külön verziókat kell karbantartanunk, és természetesen gondoskodnunk arról is, hogy az egyiken végrehajtott változás megjelenjen a másikon is. Könnyen belátható, hogy nem egy jó ötlet.

Valahogy meg kell valósítanunk azt, hogy futási (vagy legkésőbb indulási) időben információt adjunk át a programnak. Ez sokféleképpen történhet: parancssori paraméter átadással, környezeti változókkal, fájl formátumban kulcs-érték párokkal, adatbázisból; a lehetőségek tárháza igen széles. A Spring-nek erre is van egy megoldása, ami - a Springtől nem meglepő módon - számos megoldás ötvözete.

A példát én springbootproperty-nek neveztem el, ennek megfelelő egyéb beállításokkal. A konfigurációt a @Configuration annotációval ellátott forráshoz adjuk hozzá:

package hu.faragocsaba.springbootproperty;
 
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class AppConfig {
 
    @Value("${my.fruit:banana}")
    private String myFruit;
 
    @Value("${my.color:red}")
    private String myColor;
 
    @Value("${my.number:10}")
    private int myNumber;
 
    public void displayValues() {
        System.out.println("Fruit: " + myFruit);
        System.out.println("Color: " + myColor);
        System.out.println("Number: " + myNumber);
    }
 
}

A főprogram:

package hu.faragocsaba.springbootproperty;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private AppConfig appConfig;
 
    @Override
    public void run(String... args) throws Exception {
        appConfig.displayValues();
    }
 
}

A property fájl alapértelmezett neve application.properties. Ezt hozzuk létre az src/main/resources/ könyvtárban a következő tartalommal:

src/main/resources/application.properties

my.fruit=apple
my.number=5

A kódból látható, hogy mindegyik paraméternek adhatunk alapértelmezett értéket, ezt két esetben meg is tettük. Ha mindent jól csináltunk, futáskor ez jelenik meg:

Fruit: apple
Color: red
Number: 5

A probléma a fenti megoldással az, hogy a property fájl benne van a jar-ban, így a változás újrafordítást igényel. Viszont ha a futáskor, abban a könyvtárban, ahol vagyunk, létrehozunk egy másik application.property fájlt egy másik tartalommal, akkor azzal felülírjuk a jar-ban levő értékeket. Pl. hozzuk létre a következő tartalommal:

my.number=6

Nem fordítsuk újra, hanem a fent elkészített jar fájlt futtassuk újra. Ha mindent jól csináltunk, a futáskor a Number: 6 fog megjelenni.

Ezzel a megoldással a környezeti változókból is beolvashatunk értékeket. Itt megfelelő konvenciót kell alkalmaznunk (convention over configuration): láthattuk, hogy a property fájlokban a kulcsok neveit csupa kisbetűvel írtuk, a szavakat pedig ponttal választottuk el. Ez nem csak példa volt, hanem konvenció, használjuk mindig így! Egy property kulcsnak megfelelő környezeti változó neve csupa nagybetűs, és a pontot alul vonásra kell cserélnünk. Pl. állítsuk be a következő környezeti változóz:

set MY_FRUIT=banana

Ill. Linuxon: export MY_FRUIT=banana. Ha most lefuttatjuk, akkor a Fruit: banana szöveg íródik ki.

Egyre inkább a környezeti változós megoldás terjed el. Különösen a virtualizációval egybekötve, ahol nem is kell operációs szinten megadni a környezeti változókat, hanem a virtualizációt (ami többnyire Docker) leíró fájlban kell felsorolni, ami, amikor létrehozza a megfelelő operációs rendszerrel ellátott konténert, beállítja ott a környezeti változókat is, magyarán már induláskor ott lesznek.

A property kezelés még számos lehetőséget tartogat. Mivel a leírásnak nem célja a teljesség igénye, csak felsorolás szinten említek meg néhányat:

  • A @PropertySource annotációval adhatjuk meg azt, ha eltér a fájlnév a fenti application.property-től.
  • A property fájlok profile specifikusan is lehetnek. Ez esetben az alapértelmezett a application-default.property, és más profile esetén más property fájlt kell létrehozni, a megadott fájlnév konvencióval.
  • A property fájlokban hivatkozhatunk más kulcsok értékeire, ${other.var} formában.
  • Random értéket is adhatunk nekik pl. a ${random.int} (vagy más egyéb) megadásával.
  • Támogatja a YML nyelvet is.
  • Lehetőségeket biztosít a típusbiztosságra (type-safety) több módon is, pl. a @ConfigurationProperties annotációval.
  • Validációs paramétereket is megadhatunk, pl. egész számok esetén a @Min(1) és @Max(5) értékekkel.
  • A Spring Boot property kezelése (az írás pillanatában) nem támogatta a titkosítást.

Ezekről részletesen a https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config oldalon olvashatunk.

Naplózás

A naplózás alapvető fontosságú minden rendszerben, így a Spring-ben is mélyen integráltan megtalálható. Ezzel kapcsolatban igen alapos munka a https://www.baeldung.com/spring-boot-logging.

Példaként hozzunk létre egy új Maven projektet, pl. springbootlogger néven, másoljuk bele a springbootparent tartalmát, és módosítsuk a BeanTwo osztályt a következőképpen:

package hu.faragocsaba.springbootlogger;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
 
@Component
public class MyMath {
 
    private Logger logger = LoggerFactory.getLogger(MyMath.class);
 
    public int add(int a, int b) {
        logger.info("MyMath.add() called");
        logger.debug("Parameters: a = " + a + ", b = " + b);
        return a + b;
    }
 
}

Ha lefuttatjuk a főprogramok, akkor annak az eredménye a következő:

2020-03-28 15:28:23.759  INFO 14616 --- [           main] hu.faragocsaba.springbootlogger.MyMath   : MyMath.add() called
5

Ahhoz, hogy a debug üzenet is megjelenjen a naplóban, azt be kell konfigurálni, amit többféleképpen is meg tudunk tenni. Parancssorból pl.:

java -Dlogging.level.hu.faragocsaba=DEBUG -jar target\springbootlogger-1.0.jar

Vagy hozzunk létre a classpath-on valahol (pl. ahonnan indítjuk) egy fájlt application.properties néven, az alábbi tartalommal:

logging.level.hu.faragocsaba=DEBUG

Ha így lefuttatjuk, akkor a paramétereket is kiírja.

A Spring-ben a naplózást is kiterjeszthetővé tették. Ha az alapértelmezett naplózás nem tetszik (pl. amiatt, mert az application.properties-be kell írni a naplózási szintet, és ezt a fájlt nem szeretnénk erre használni), akkor használhatjuk pl. a log4j2-t. A pom.xml fájlban a következőre kell módosítani a függőség részt:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
    </dependencies>

Ha csak ennyit módosítunk, semmi mást, akkor már a log4j2 naplózót fogja használni a naplózáskor, és az eredmény kicsit másképp néz majd ki. Ekkor a konfiguráció már nem az application.properties fájlban történik, hanem a log4j saját XML leírójában (src/main/resources/log4j2-spring.xml), pl.:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%style{%d{ISO8601}}{black} %highlight{%-5level }[%style{%t}{bright,blue}] %style{%C{1.}}{bright,yellow}: %msg%n%throwable"/>
        </Console>
        <RollingFile name="RollingFile"
            fileName="./logs/spring-boot-logger-log4j2.log"
            filePattern="./logs/$${date:yyyy-MM}/spring-boot-logger-log4j2-%d{-dd-MMMM-yyyy}-%i.log.gz">
            <PatternLayout>
                <pattern>%d %p %C{1.} [%t] %m%n</pattern>
            </PatternLayout>
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
        </RollingFile>
    </Appenders>
 
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console" />
            <AppenderRef ref="RollingFile" />
        </Root>
        <Logger name="hu.faragocsaba" level="debug"/>
    </Loggers> 
</Configuration>

Más egyéb módokon is tudunk naplózni Spring-ben, melyek részletes bemutatása túlhaladná ennek az oldalnak az erre szánt kereteit. A fenti oldalon olvashatunk ezekről a lehetőségekről is.

Eseménykezelés

Az események kezelése sokszor nem elég hangsúlyos, mégis nagyon hasznos. A Spring-ben is van - nem túl meglepő módon - eseménykezelés. Az események kezelésénél 3 fő komponens van: a küldő, a fogadó és az esemény maga. Lássunk egy példát (springbootevent)!

Az üzenetnek magának a ApplicationEvent osztályból kell származnia:

package hu.faragocsaba.springbootevent;
 
import org.springframework.context.ApplicationEvent;
 
public class MyEvent extends ApplicationEvent {
 
    private static final long serialVersionUID = 1L;
 
    private String eventContent;
 
    public MyEvent(Object source, String eventContent) {
        super(source);
        this.eventContent = eventContent;
    }
 
    public String getEventContent() {
        return eventContent;
    }
 
}

A küldőnek az ApplicationEventPublisher osztály publishEvent() metódusát kell meghívnia:

package hu.faragocsaba.springbootevent;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventPublisher {
 
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
 
    public void sendEvent() {
        System.out.println("Publishing event.");
        applicationEventPublisher.publishEvent(new MyEvent(this, "Hello, event!"));
        System.out.println("Event published.");
    }
 
}

A fogadót többféleképpen is meg tudjuk valósítani; talán a legegyszerűbb az @EventListener annotációval:

package hu.faragocsaba.springbootevent;
 
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventListener {
 
    @EventListener
    public void handleEvent(MyEvent myEvent) {
        System.out.println("Event received");
        System.out.println(myEvent.getEventContent());
        System.out.println("Event processed");
    }
 
}

Végül a főprogram nem okozhat túl nagy meglepetést:

package hu.faragocsaba.springbootevent;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MyEventPublisher publisher;
 
    @Override
    public void run(String... args) throws Exception {
        publisher.sendEvent();
    }
 
}

Ha lefuttatjuk, akkor eredményül a következőt kapjuk:

Publishing event.
Event received.
Hello, event!
Event processed
Event published.

Láthatjuk, hogy alapértelmezésben az üzenetkezelés szinkron módon történik: a küldő blokkolt állapotban van, amíg a fogadó feldolgozza az üzenetet. Aszinkronná a legegyszerűbben az @Async annotációval tudjuk tenni, amit a küldőhöz kell írni:

package hu.faragocsaba.springbootevent;
 
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventListener {
 
    @Async
    @EventListener
    public void handleEvent(MyEvent myEvent) {
        System.out.println("Event received.");
        System.out.println(myEvent.getEventContent());
        System.out.println("Event processed");
    }
 
}

Ahhoz, hogy működjön, a konfigurációs osztály el kell látni @EnableAsync annotációval. Jelen esetben elég, ha a főosztály elé tesszük ezt az annotációt:

package hu.faragocsaba.springbootevent;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
 
@SpringBootApplication
@EnableAsync
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MyEventPublisher publisher;
 
    @Override
    public void run(String... args) throws Exception {
        publisher.sendEvent();
    }
 
}

Az eredmény:

Publishing event.
Event published.
Event received.
Hello, event!
Event processed

Az üzenetek sorrendje megváltozott. Érdemes még eljátszani a példával, pl. késleltetést tenni a fogadóba.

Az üzenetek kezelésének egy másik módja az, hogy megvalósítjuk az ApplicationListener interfészt. Léteznek standard String események; most megnézünk egy ilyen eseményt, az interfész megvalósításos módszerrel:

package hu.faragocsaba.springbootevent;
 
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
 
@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
 
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("Context refreshed event occurred: " + event);
    }
 
}

Eredményül a következőt kapjuk:

Context refreshed event occurred: org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@589b3632, started on Sat Mar 21 10:26:53 CET 2020]

Standard események:

  • ContextRefreshedEvent: ApplicationContext indulásakor vagy frissítésekor
  • ContextStartedEvent: ApplicationContext indulásakor
  • ContextStoppedEvent: ApplicationContext leállásakor
  • ContextClosedEvent: ApplicationContext lezárásakor
  • RequestHandledEvent: HTTP kérésenként

Vezérelt osztályok

Problémafelvetés

A komponensek kezelésének problémáját az alábbi példán illusztrálom:

class A {
    int f(...) {...}
}
 
class B {
    int g(...) {
        ...
        A a = new A();
        ...
        a.f(...);
        ...
    }
}

A probléma ezzel az, hogy túl erős a függés a két osztály között. Ez számos problémát okoz, pl. megnehezíti az egységtesztelést. Ha a példában az f() függvény pl. adatbázisból olvas, akkor a g() egységteszteléséhez azt mockolni kell, ebben a formában viszont az már nem is olyan egyszerű. A másik probléma ezzel az, hogy tegyük fel, az A osztály példányát több helyen is használni szeretnénk. Mindezekre persze vannak megoldások, azok viszont felesleges, ún. boilerplate kódot eredményez. Érdemes ezt a folyamatot a keretrendszerre bízni.

IoC és DI

Mielőtt megismernénk a részletekkel, lássunk két fontos fogalmat erről a területről:

  • Inversion of Control, IoC (a kontroll megfordítása): ez egy programozási technika, melynek lényege az, hogy a hívás nem a megszokott módon történik, azaz nem az üzleti logikából történik a rendszer felé, hanem fordítva. Ilyen pl. az amikor egy keretrendszerben egy objektummal történik valami, egyik állapotból átmegy egy másikba, akkor meghívódik az üzleti logikát megvalósító kódban egy függvény. Ezt egyébként Hollywood elvnek is szokás hívni (Hollywood principle): "Ne hívj minket, majd mi hívunk téged." (Don't call us, we'll call you.) Ennek egy - a mi szempontunkból lényeges - speciális esete az, amikor nem az az osztály készíti el a példányokat, amelyik használja, hanem azt kívülről kapja.
  • Dependency Injection, DI (a függőség befecskendezése): ez egy tervezési minta (nem GoF), és lényegében az IoC egy speciális esete. A lényege az, hogy az objektum kívülről kapja meg azt a másik objektum példányt, amit használni szeretne. Technikailag ez tipikusan úgy történik, hogy attribútumként deklarálja azt, hogy milyen típusú objektumra van szüksége, de azt nem állítja be (alapból tehát null értéke van) és egy külső keretrendszer biztosítja azt, hogy mire az első hívás bekövetkezik, az rendelkezésre álljon. A példányosítás rejtve marad a használó elől.

Egy bean példa

Lássunk egy példát egy olyan esetre, ami a fenti problémát modellezi. Ebben létrehozunk egy bean-t MyFormat néven, amely a MyMath bean függvényét hívja meg, és megformázza a műveletet, amely tartalmazza a paramétereket, a műveleti jelent és az eredményt is. A DI technikát alkalmazzuk a beszúráshoz.

Hozzunk létre egy új alkalmazást springbootbean néven, és mindent (pom.xml, csomagnév) állítsuk be ennek megfelelően. Másoljuk be a MyMath osztály annotáció nélküli változatát. A MyFormatter tartalma a következő

package hu.faragocsaba.springbootbean;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public class MyFormatter {
 
    @Autowired
    private MyMath myMath;
 
    public String formatAddResult(int a, int b) {
        int result = myMath.add(a, b);
        return "" + a + " + " + b + " = " + result;
    }
 
}

Az @Autowired annotáció gondoskodik a beszúrásról. A példányosításhoz a következő konfiguráció osztályra van szükség:

package hu.faragocsaba.springbootbean;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class AppConfig {
 
    @Bean
    public MyMath myMath() {
        return new MyMath();
    }
 
    @Bean
    public MyFormatter myFormatter() {
        return new MyFormatter();
    }
 
}

A főprogram a formázót hívja meg:

package hu.faragocsaba.springbootbean;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MyFormatter myFormatter;
 
    @Override
    public void run(String... args) throws Exception {
        System.out.println(myFormatter.formatAddResult(2, 3));
    }
 
}

Ha mindent jól csináltunk, ez jelenik meg a képernyőn: 2 + 3 = 5.

Egységtesztet is készíthetünk, a következő módon (ez esetben ne feledkezzünk meg a pom.xml-ben a spring-boot-starter-test függőségről):

package hu.faragocsaba.springbootbean;
 
import static org.junit.Assert.assertEquals;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
 
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MyFormatterTest {
 
    @Autowired
    private MyFormatter myFormatter;
 
    @Test
    public void testSaveAndList() {
        assertEquals("2 + 3 = 5", myFormatter.formatAddResult(2, 3));
    }
 
}

XML konfiguráció

Kis kitérő: AppConfig nélkül, XML konfigurációval a következőképpen tudjuk beállítani (a példában a csomagnév springbootxml):

src/main/resources/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="myFormatter" class="hu.faragocsaba.springbootxml.MyFormatter">
        <property name="myMath" ref="myMath" />
    </bean>
 
    <bean id="myMath" class="hu.faragocsaba.springbootxml.MyMath"/>
</beans>

A MyMath osztály megmaradhat olyannak, amilyen volt, a MyFormatter-ben viszont kell egy setter (ejnye-bejnye boilerplate kód…):

package hu.faragocsaba.springbootxml;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public class MyFormatter {
 
    @Autowired
    private MyMath myMath;
 
    public void setMyMath(MyMath myMath) {
        this.myMath = myMath;
    }
 
    public String formatAddResult(int a, int b) {
        int result = myMath.add(a, b);
        return "" + a + " + " + b + " = " + result;
    }
 
}

A főprogramban a következőképpen adjuk meg a konfigurációs fájlt:

package hu.faragocsaba.springbootxml;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
 
@SpringBootApplication
@ImportResource({"classpath*:beans.xml"})
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MyFormatter myFormatter;
 
    @Override
    public void run(String... args) throws Exception {
        System.out.println(myFormatter.formatAddResult(2, 3));
    }
 
}

Ebben a formában tehát AppConfig (és eplicit példányosítás) nélkül oldottuk meg a problémát.

Befecskendezés

Amint láttuk, a @Autowired annotációval adtuk meg a beszúrást. Ez a Spring annotációja. Ezt még kétféleképpen megadhatjuk:

  • A @Resource annotációval (javax.annotation.Resource): ez Java szintű szabvány.
  • Az @Inject annotációval (javax.inject.Inject): ehhez az alábbi függőséget kell hozzáadni a pom.xml-hez.
        <dependency>
            <groupId>javax.inject</groupId>
            <artifactId>javax.inject</artifactId>
            <version>1</version>
        </dependency>

Scope

A bean-eknek van hatókörük (scope), ami meghatározza, hogy a keretrendszer mi módon példányosítsa. Ezt a @Bean annotáció után a @Scope annotációval tudjuk megadni. Lehetséges értékek:

  • Singleton (egyke): egyetlen példányt készít belőle a keretrendszer. Ez az alapértelmezett. Ha explicit meg szeretnénk adni, akkor a következő lehetőségeink vannak: @Scope("singleton"), @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON). (Meglepő módon nem hoztak létre @Singleton annotációt.)
  • Prototype (prototípus): a nevéből nehéz kitalálni; ebben az esetben minden esetben új példányt hoz létre. Az elnevezés valószínűleg a Prototype tervezési mintára utal: létrehoz egy prototípust, majd amikor szükség van egy példányra, akkor azt lemásolja. A prototípus tehát a megvalósítási technika, és nem a működés. Megadási módok: @Scope("prototype"), @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE). (Nincs @Prototype annotáció.)
  • Request (kérés): webalkalmazásoknál HTTP kérésenként jön létre egy példány. Használatához a spring-web függőségre van szükség. Megadása: @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS), ill. ebben az esetben van egy rövidítése: @RequestScope.
  • Session (munkamenet): munkamenetenként hoz létre egy példányt. Megadása: @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) ill. @SessionScope.
  • Application (alkalmazás): alkalmazásonként, egészen pontosan: ServletContext-enként egy példány jön létre. Ez többé-kevésbé megegyezik az singleton scope-pal. Eltérés a kettő között akkor adódhat, ha több szerver használja ugyanazt a ServletContext-et: sigleton scope esetén szerverenként egy jönne létre, míg application scope esetén összesen egy. Megadása: @Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS) ill. @ApplicationScope.

Qualifier

Előfordulhat az, hogy ugyanolyan típusú bean-ből több példány van. Emlékezzünk a példákra: a típus az egyetlen információ, ami alapján a keretrendszer beállítja a példányokat. Ez történhet úgy is, hogy a bean típusa egy interfész, melynek több megvalósítása van, de úgy is, hogy a @Configuration osztályban több példányt hozunk létre ugyanabból a típusból. Ez egyébként nem feltétlenül jelent szoftverhibát: tegyük fel pl. azt, hogy van egy interfész, mely adatbázis műveleteket definiál, és van pár megvalósítás: különböző adatbázis rendszerekhez történő csatlakozás, vagy egységtesztelésnél szimulált megvalósítással. A minősítő (qualifier) technikát alkalmazva tudjuk megadni, hogy melyiket szeretnénk használni.

Lássunk egy példát! Ehhez létrehoztam egy Spring Boot alkalmazást springqualifier néven. Ebben a fenti két bean lesz: MyMath, ami változatlan (annotáció nélküli) és a MyFormatter, amit kicsit átírunk:

package hu.faragocsaba.springbootqualifier;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public class MyFormatter {
 
    private boolean isWide;
 
    @Autowired
    private MyMath myMath;
 
    public MyFormatter(boolean isWide) {
        this.isWide = isWide;
    }
 
    public String formatAddResult(int a, int b) {
        int result = myMath.add(a, b);
        if (isWide) {
            return "" + a + " + " + b + " = " + result;
        } else {
            return "" + a + "+" + b + "=" + result;
        }
    }
 
}

Annyi változtatás történt, hogy a konstruktornak átadhatunk egy paramétert, amivel azt tudjuk beállítani, hogy a kiíráskor használjon-e szóközöket a műveleti jelek között (pl. 2 + 3 = 5) vagy ne (2+3=5). Az AppConfig a következőre változik:

package hu.faragocsaba.springbootqualifier;
 
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
 
@Configuration
public class AppConfig {
 
    @Bean
    public MyMath myMath() {
        return new MyMath();
    }
 
    @Bean
    @Qualifier("wide")
    @Primary
    public MyFormatter myFormatterWide() {
        return new MyFormatter(true);
    }
 
    @Bean
    @Qualifier("narrow")
    public MyFormatter myFormatterNarrow() {
        return new MyFormatter(false);
    }
 
}

Létrehoztunk tehát két MyFormatter példányt, és mindkettőt elláttuk megfelelően felparaméterezett @Qualifier annotációval. A főprogram maradjon ugyanaz mint fent. Ha lefuttatjuk, akkor nem változik semmit az eredmény. Ha a tömörebb kiírást szeretnénk használni, akkor a @Qualifier annotációt a @Autowired alá kell tennünk:

package hu.faragocsaba.springbootqualifier;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    @Qualifier("narrow")
    private MyFormatter myFormatter;
 
    @Override
    public void run(String... args) throws Exception {
        System.out.println(myFormatter.formatAddResult(2, 3));
    }
 
}

Néhány további megjegyzés:

  • Nem kötelező a @Primary, de ha a rendszer nem kap semmilyen támpontot, akkor hibát kapunk.
  • Név alapján, @Qualifier nélkül is be tudja szúrni (ez egy szép példája a convention over configuration elvnek), pl. ha a következő nevet adjuk neki a főprogramban: private MyFormatter myFormatterNarrow;. Ehhez ki kell törölni a @Primary annotációt, mert az felülírja ezt. Ebben az esetben tehát pusztán a név alapján kitalálja a rendszer, hogy melyik példányt kell beilleszteni.
  • Vegyük észre azt, hogy az AppConfig osztályban a @Bean függvényeknek nincs get előtagjuk. A függvénynév ott bármi lehet; a get nélküli konvenciót pont a név alapján történő példányosítás miatt alkalmazzuk.

Profile

A profile kicsit hasonlít az imént megismert qualifier-re: több ugyanolyan típusú bean-t hozunk létre, mindegyiknél megadjuk megfelelő annotációval a profile-ját, majd valahogy jelezzük, hogy most melyiket szeretnénk használni. A profile általánosabb mint a qualifier: itt ugyanis nem arról van szó, hogy egy adott bean esetén megadjuk a megfelelő változatot, hanem azt globálisan adjuk meg, ami több helyen is aktiválhat változást.

Lássunk egy példát! A program neve nálam springbootprofile; a fent bemutatott Spring Boot-ként hoztam létre. Ebben a példában egyúttal megnézzük azt, hogy hogyan tudunk úgy bean-t létrehozni, hogy annak típusa absztrakt osztály (lehetne interfész is). Először lássuk az absztrakt ősosztályt!

package hu.faragocsaba.springbootprofile;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public abstract class MyFormatter {
 
    @Autowired
    protected MyMath myMath;
 
    public abstract String formatAddResult(int a, int b);
 
}

Ízlés kérdése, hogy így hagyjuk, vagy a MyMath beillesztést a leszármazottakban helyezzük át, és ez esetben lehetne interfész; a kódismétlés elkerülése érdekében talán ez jobb.

Ennek legyen két megvalósítása:

package hu.faragocsaba.springbootprofile;
 
public class MyWideFormatter extends MyFormatter{
 
    @Override
    public String formatAddResult(int a, int b) {
        int result = myMath.add(a, b);
        return "" + a + " + " + b + " = " + result;
    }
 
}

ill.

package hu.faragocsaba.springbootprofile;
 
public class MyNarrowFormatter extends MyFormatter {
 
    @Override
    public String formatAddResult(int a, int b) {
        int result = myMath.add(a, b);
        return "" + a + "+" + b + "=" + result;
    }
 
}

A bean létrehozása során, az AppConfig-ban adjuk meg a profile-okat, a @Profile annotációval, a következőképpen:

package hu.faragocsaba.springbootprofile;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
 
@Configuration
public class AppConfig {
 
    @Bean
    public MyMath myMath() {
        return new MyMath();
    }
 
    @Bean
    @Profile("default")
    public MyFormatter myFormatterWide() {
        return new MyWideFormatter();
    }
 
    @Bean
    @Profile("narrow")
    public MyFormatter myFormatterNarrow() {
        return new MyNarrowFormatter();
    }
 
}

A fő osztály változatlan!

package hu.faragocsaba.springbootprofile;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MyFormatter myFormatter;
 
    @Override
    public void run(String... args) throws Exception {
        System.out.println(myFormatter.formatAddResult(2, 3));
    }
 
}

Ha ezt a programot hagyományosan lefuttatjuk, akkor a 2 + 3 = 5 feliratot kapjuk eredményül. Az alapértelmezett profile ugyanis a default. Ha a narrow profile-t szeretnénk megadni, azt parancssorból a következőképpen tudjuk megtenni:

java -Dspring.profiles.active="narrow" -jar target\springbootprofile-1.0.jar

Ha így indítjuk a programot, akkor eredményül ezt kapjuk: 2+3=5.

A profile-t számos más módon meg tudjuk adni, ill. egyszerre több profile-t is meg lehet adni, vesszővel elválasztva. Ennek a leírásnak nem célja minden részlet bemutatása; ezzel kapcsolatos ajánlott oldal a hivatalos API dokumentáció (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html) valamint a https://www.baeldung.com/spring-profiles.

Komponensek

A bevezetőben már láttunk példát a komponensre, azaz a @Component annotációra. Alakítsuk át a két bean példát komponensekre! A példa neve nálam springbootcomponent.

package hu.faragocsaba.springbootcomponent;
 
import org.springframework.stereotype.Component;
 
@Component
public class MyMath {
 
    public int add(int a, int b) {
        return a + b;
    }
 
}

Ill.:

package hu.faragocsaba.springbootcomponent;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
@Component
public class MyFormatter {
 
    @Autowired
    private MyMath myMath;
 
    public String formatAddResult(int a, int b) {
        int result = myMath.add(a, b);
        return "" + a + " + " + b + " = " + result;
    }
 
}

Az AppConfig osztályra nincs szükség. A főprogram megmarad eredeti formájában. Sok hasonlóság van a bean-ek és a komponensek között, de jelentősek az eltérések is. Néhány példa az eltérésekre:

  • A @Bean metódusra érvényes, míg a @Component osztályra.
  • Annotációval osztályt csak úgy tudunk komponenssé tenni, ha megváltoztatjuk azt (azaz invazív módon), míg a bean-t nem kell megváltoztatni. Ennek akkor van jelentősége, ha nem áll rendelkezésünkre a forráskód, vagy nem lehet csak úgy beleírni.
  • A komponenseket a Spring automatikusan detektálja, míg a bean-eket explicit megadjuk.
  • A bean-eket általában explicit példányosítjuk, míg a komponenseket a keretrendszer példányosítja.

A fenti példában a @Component annotációt használtuk. A program a következő annotációk bármelyikével működne. Ugyanakkor az alábbiak nem korlátlanul felcserélhetőek:

  • @Component: általános komponens annotáció.
  • @Repository: azt fejezi ki, hogy az adott komponens egy repository, azaz adatbázis elérés valósít meg. Ez - azon túl, hogy információt ad a kód olvasójának - az adatbázis specifikus kivételeket Spring specifikusra alakítja. MVC fogalmakkal élve ez a modell.
  • @Service: a szolgáltatás réteget jelenti. A @RequestMapping, amely az REST hívásokért felelős, csak ezzel a komponens fajtával működik. Így az MVC fogalmakkal ez áll a legközelebb a nézethez (view-hoz).
  • @Controller: a Spring MVC-ben a kontrollert jelenti.

Aspektusorientált programozás

Az aspektusorientált programozás lényege az, hogy bizonyos, sokszor ismétlődő műveletet megvalósító kódrészletet külön megvalósítunk, majd azt alkalmazzuk a megfelelő helyeken anélkül, hogy a futás tényleges helyéhez hozzá kellene nyúlnunk. Tipikus példája ennek pl. az, hogy mindegyik függvényhívás előtt naplózzuk a függvényhívás tényét, esetleg számoljuk is a hívások számát. Egész összetett feladatokat is meg lehet valósítani ennek segítségével: finomhangolhatjuk a hívás helyét, vagy drasztikusabb beavatkozásra is alkalmas: a paraméterek vagy a visszatérési értéket meg tudjuk változtatni, sőt, akár magát a hívást is meg tudjuk vele akadályozni.

A Spring AOP megértésében kivételesen most nem a Baeldung segített, hanem sokkal inkább ez az oldal: https://www.journaldev.com/2583/spring-aop-example-tutorial-aspect-advice-pointcut-joinpoint-annotations.

A példa előtt néhány fogalmat tisztázunk:

  • Advice: maga a funkcionalitás, amit el kell végezni, pl. a függvény nevének kiírása.
  • Join point: az alkalmazás azon pontjai, ahol az advice-t be lehet illeszteni. A Spring-ben ez a függvényhívások elejére és végre korlátozódik, de maga a fogalom ennél általánosabb; bele lehet érteni pl. azt is, ahol egy változó értéket kap.
  • Pointcut: a join pointok részhalmaza, ahol az advice-t le kell futtatni.
  • Aspect: advice + pointcut.

A Spring az AspectJ könyvtárat használja az aspektusorientált programozás megvalósításához. A Spring Boot-ban még ezt sem kell megadnunk, a spring-boot-starter helyett viszont a spring-boot-starter-aop függőséget kell használni.

A példában a springbootbean-t vegük alapul. Akár azt is folytathatjuk; én újat hoztam létre, springbootaspect néven, és oda belemásoltam mindent. A pom.xml-ben a szokásos változtatások mellett a következőt változtassuk meg:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

Az eltérés az eredetihez képest az -aop postfix: ezzel elérjük azt, hogy használhatjuk az aspektusorientáltságot. Valósítsuk meg az első aspektusunkat!

package hu.faragocsaba.springbootaspect;
 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
 
@Aspect
@Component
public class MyAspect {
 
    @Before("execution(* hu.faragocsaba.springbootaspect.*.*(..))")
    public void logBeforeInvocations(JoinPoint joinPoint) {
        System.out.println("Called: " + joinPoint.getSignature());
    }
 
}

Az execution részben ügyeljünk a megfelelő csomagnévre. Ha mindent jól csináltunk, akkor a következőt kell kaptunk eredményül:

Called: MyFormatter hu.faragocsaba.springbootaspect.AppConfig.myFormatter()
Called: MyMath hu.faragocsaba.springbootaspect.AppConfig.myMath()
Called: void hu.faragocsaba.springbootaspect.Main.run(String[])
Called: String hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(int,int)
Called: int hu.faragocsaba.springbootaspect.MyMath.add(int,int)
2 + 3 = 5

Láthatjuk tehát, hogy minden függvényhívás előtt kiírta annak szignatúráját. Egészen pontosan: ez csak a vezérelt hívásokra vonatkozik, tehát ahol az egyik bean hívta a másikat; ha bean-en belül hívunk egy másik függvényt, azt nem naplózná ki.

A pointcut megadásánál specifikusabbak is lehetünk, pl.:

    @Before("execution(* hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(..))")
    public void logBeforeInvocations(JoinPoint joinPoint) {
        System.out.println("Called: " + joinPoint.getSignature());
    }

A @Before mintájára az @After annotációval tudjuk kinaplózni a visszatérést:

    @After("execution(* hu.faragocsaba.springbootaspect.*.*(..))")
    public void logAfterInvocations(JoinPoint joinPoint) {
        System.out.println("Finished: " + joinPoint.getSignature());
    }

Ha mindkettő aktív, akkor a következőt kapjuk eredményül:

Called: MyFormatter hu.faragocsaba.springbootaspect.AppConfig.myFormatter()
Finished: MyFormatter hu.faragocsaba.springbootaspect.AppConfig.myFormatter()
Called: MyMath hu.faragocsaba.springbootaspect.AppConfig.myMath()
Finished: MyMath hu.faragocsaba.springbootaspect.AppConfig.myMath()
Called: void hu.faragocsaba.springbootaspect.Main.run(String[])
Called: String hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(int,int)
Called: int hu.faragocsaba.springbootaspect.MyMath.add(int,int)
Finished: int hu.faragocsaba.springbootaspect.MyMath.add(int,int)
Finished: String hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(int,int)
2 + 3 = 5
Finished: void hu.faragocsaba.springbootaspect.Main.run(String[])

Ebben a megoldásban vagy egy csúnya dolog: a kódismétlés. Az AspectJ erre a @Pointcut annotációt nyújtja: létre kell hoznunk egy üres függvényt, azt felparaméterezni az említett annotációval, melynek értékéül a megfelelő mintát kell megadnunk, és az imént létrehozott függvényt kell magának a @Before és @After annotációnak megadni, a következőképpen:

    @Pointcut("execution(* hu.faragocsaba.springbootaspect.*.*(..))")
    public void everyCall() {}
 
    @Before("everyCall()")
    public void logBeforeInvocations(JoinPoint joinPoint) {
        System.out.println("Called: " + joinPoint.getSignature());
    }
 
    @After("everyCall()")
    public void logAfterInvocations(JoinPoint joinPoint) {
        System.out.println("Finished: " + joinPoint.getSignature());
    }

Ugyanazt kapjuk eredményül.

Sokkal drasztikusabb beavatkozást tesz lehetővé az @Around annotáció; a lehetőségeket egy példán keresztül nézzük meg:

    @Around("execution(* hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(..))")
    public Object aroundMyFormtterFormatAddResult(ProceedingJoinPoint proceedingJoinPoint) {
        System.out.println("Before calling " + proceedingJoinPoint.getSignature());
        Object result = null;
        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println("Argument 1: " + args[0]);
        System.out.println("Argument 2: " + args[1]);
        args[0] = (Integer)args[0] + 1;
        args[1] = (Integer)args[1] + 1;
        try {
            result = proceedingJoinPoint.proceed(args);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("After finished calling " + proceedingJoinPoint.getSignature());
        return "[" + result + "]";
    }

Ez megváltoztatja a paramétert, majd a visszatérési értéket is. Ha lenne kivétel, akkor azt is kezelné. Végeredményül ez kerül kiírásra:

Before calling String hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(int,int)
Argument 1: 2
Argument 2: 3
After finished calling String hu.faragocsaba.springbootaspect.MyFormatter.formatAddResult(int,int)
[3 + 4 = 7]

A joint point lehet kivétel kiváltása is, melyet a @AfterThrowing annotációval ellátott függvénnyel tudjuk lekezelni. Erre példát láthatunk a https://howtodoinjava.com/spring-aop/aspectj-afterthrowing-annotation-example/ oldalon.

Integráció

JDBC

Az adatbázis kapcsolat alapvető fontosságú a vállalati alkalmazásoknál, jelentőségét nem lehet túlbecsülni. Ezt a Spring-ben nagyon szépen tovább gondolták, és (abszolút pozitív értelemben) egész meglepő lett az eredmény.

A következőt fogjuk megvalósítani:

  • A séma az Adatbázisok oldalon, a MySQL szakaszban leírtak szerinti lesz. Ez két táblát tartalmaz: az egyik a lakcím (Address), országgal, várossal, utcával, a másik pedig a személy (Person), névvel, életkorral, valamint hivatkozással a lakcímre. Egy személynek pontosan egy lakcíme lehet, és ugyanazon a lakcímen tetszőleges számú személy lehet bejelentve. (Ha még nem tettük meg, telepítsük fel és állítsuk be a MySQL adatbázist a leírtak szerint.)
  • Az adatbázisba behelyezünk 3 lakcímet, melyből 2 budapesti, egy szegedi, valamint és 4 személyt, melyek közül ketten budapesti lakótársak, egyvalaki egyedül él Budapesten, míg a másik egyedül él Szegeden.
  • Töröljük az adatbázis tartalmát.
  • Lekérdezzük az összes személyt.
  • Rákeresünk az egyik személyre név alapján.
  • Lekérdezzük az összes lakcímet. Mindegyik címnél megjelenítjük az ott lakókat is.
  • Lekérdezzük egy adott település lakcímet.
  • Lekérdezzük az adott településen lakók életkorát.

A JdbcTemplate használata

Lássuk először az alap JDBC-t! A probléma ezzel az, hogy igazából nagyon sok felesleges kód keletkezik. Ezt igyekszik a Spring minimalizálni. A példához hozzunk létre egy Spring Boot alkalmazást, pl. springbootjdbc néven.

pom.xml

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>springbootjdbc</artifactId>
    <version>1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>
 
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>          
    </dependencies>
 
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.1.1</version>
                </plugin>
            </plugins>
        </pluginManagement>
 
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>hu.faragocsaba.springbootjdbc.Main</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>                
            </plugin>
        </plugins>
    </build>
</project>

Két dolgot vegyünk benne észre:

  • spring-boot-starter-jdbc: a JDBC kapcsolathoz tehát létrehoztak egy kezdőcsomagot.
  • mysql-connector-java: a legjelentősebb adatbázis gyártók konnektorai elérhetőek. Később látni fogunk másfajta adatbázisra is példát.

A lényegi rész a következő:

package hu.faragocsaba.springbootjdbc;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
 
@Repository
public class RepositoryExample {
 
    @Autowired
    private JdbcTemplate jdbcTemplate;
 
    public void initialize() {
        jdbcTemplate.execute("DELETE FROM person");
        jdbcTemplate.execute("DELETE FROM address");
    }
 
    public Number insertAddress(String country, String town, String address) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(
            new PreparedStatementCreator() {
                @Override
                public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                    PreparedStatement ps = connection.prepareStatement("INSERT INTO address(country, town, street) VALUES(?, ?, ?)", new String[] {"id"});
                    ps.setString(1, country);
                    ps.setString(2, town);
                    ps.setString(3, address);
                    return ps;
                }
            },
            keyHolder
        );
        return keyHolder.getKey();
    }
 
    public void insertPerson(String name, int age, Number addressId) {
        jdbcTemplate.update("INSERT INTO person(name, age, addressid) VALUES(?, ?, ?)", name, age, addressId);
    }
 
    public List<String> getAddresses() {
        return jdbcTemplate.query("SELECT * FROM address",
            (rs, rowNum) -> {
                int id = rs.getInt("id");
                String country = rs.getString("country");
                String town = rs.getString("town");
                String street = rs.getString("street");
                List<String> persons = jdbcTemplate.query("SELECT * FROM person WHERE addressid = ?", new Object[] {id}, (personResultSet, personRowNum) -> personResultSet.getString("name") + " (" + personResultSet.getInt("age") + ")");
                return country + " - " + town + " - " + street + ": " + persons;
            }
        );
    }
 
    public List<String> getPersons() {
        return jdbcTemplate.query("SELECT * FROM person", (rs, rowNum) -> rs.getString("name") + " (" + rs.getInt("age") + ")");
    }
 
    public String findPersonByName(String name) {
        return jdbcTemplate.queryForObject("SELECT name, age FROM person WHERE name = ?", new Object[]{name}, (rs, rowNum) -> rs.getString(1) + " (" + rs.getInt(2) + ")");
    }
 
    public List<String> findAddressByTown(String town) {
        return jdbcTemplate.query("SELECT country, town, street FROM address WHERE town = ?", new Object[]{town}, (rs, rowNum) -> rs.getString(1) + " - " + rs.getString(2) + " - " + rs.getString(3));
    }
 
    public List<Integer> findAgeByTown(String town) {
        String queryStr = "SELECT person.age FROM Person person JOIN Address address ON person.addressid = address.id WHERE address.town = ?";
        return jdbcTemplate.query(queryStr, new Object[]{town}, (rs, rowNum) -> rs.getInt("age"));
    }
 
}

A kód magyarázata:

  • Egy @Repository annotációval ellátott komponenst hoztunk létre. Ha @Component lenne, akkor is működne. Láthatjuk, hogy felesleges kód nincs.
  • A lényeges rész a JdbcTemplate osztály: ennek segítségével tudjuk végrehajtani az SQL műveleteket. Az osztály részletes bemutatása túlmutatna az oldal keretein.
  • Az inicializálás (initialize()) során kiadjuk az SQL DELETE utasítást, melyhez a JdbcTemplate.execute() függvény használtuk.
  • Először nézzük a személy beszúrását (insertPerson())! Egy szokásos, megfelelően felparaméterezett SQL INSERT parancsot kell kiadni a JdbcTemplate.update() függvény segítségével. A paraméterek megadásának szokásos szintaxisát láthatjuk a példában, de több más módon is meg lehet adni, melyekre itt nem térünk ki.
  • A személy beszúrásához szükség van az cím azonosítójára. A lakcím beszúrása (insertAddress()) már trükkösebb, ugyanis vissza kell tudnunk adni az éppen beszúrt elem azonosítóját. Ne feledjük: az azonosítót a rendszer generálja, és utólag már 100%-os biztonsággal nem tudjuk lekérdezni. A kódrészlet viszont igen bonyolult lett.
  • A személyek lekérdezésénél (getPersons()) a SELECT-re láthatunk példát. Az eredményt a ResultSet-ből szedjük ki. A példában stringet hozunk létre belőle. Az egyes paramétereket a getString() ill. getInt() (ill. további hasonló) függvényekkel kérdezzük le. Ebben a példában nevesítjük a paramétert.
  • A személy megkeresése név alapján (findPersonByName()) szintén tartalmaz pár újdonságot. Az eljárás neve JdbcTeplate.queryForObject(), ami abban tér el a sima query()-től, hogy ennek egy elemű lehet csak a visszatérés eredménye. (A példában egyedinek tekintjük a személy nevét.) Láthatunk példát a query felparaméterezésére; tehát a paraméterek értékét egy objektum tömbben kell átadnunk. A példában nem a visszatérési kulcsok nevére, hanem azok sorszámára hivatkozunk, ami 1-től indul.
  • A címek lekérdezésénél (getAddresses()) mindegyik sorhoz egy belső lekérdezésben jtunk hozzá az adott címen lakók névsorához.
  • A cím megtalálása településnév alapján (findAddressByTown) hasonlít a fentire, viszont itt lényeges különbség, hogy a visszatérési érték akárhány elemet tartalmazhat. Így az eljárás, amit használunk, ismét a JdbcTemplate.query().
  • A település szerinti életkor megtalálásához (findAgeByTown()) SQL JOIN struktúrát használunk.

A főprogramban végrehajtjuk a törlést, majd a beszúrásokat követően lekérdezzük az adatokat:

package hu.faragocsaba.springbootjdbc;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private RepositoryExample repositoryExample;
 
    @Override
    public void run(String... args) throws Exception {
        repositoryExample.initialize();
        Number budapestPipacsId = repositoryExample.insertAddress("Hungary", "Budapest", "Pipacs utca 1");
        Number budapestKikeletId = repositoryExample.insertAddress("Hungary", "Budapest", "Kikelet utca 5");
        Number szegedTavaszId = repositoryExample.insertAddress("Hungary", "Szeged", "Tavasz utca 2");
        repositoryExample.insertPerson("Sanyi", 34, budapestPipacsId);
        repositoryExample.insertPerson("Kata", 32, budapestPipacsId);
        repositoryExample.insertPerson("Pista", 46, szegedTavaszId);
        repositoryExample.insertPerson("Anna", 28, budapestKikeletId);
        System.out.println(repositoryExample.getPersons());
        System.out.println(repositoryExample.findPersonByName("Sanyi"));
        System.out.println(repositoryExample.getAddresses());
        System.out.println(repositoryExample.findAddressByTown("Budapest"));
        System.out.println(repositoryExample.findAgeByTown("Budapest"));
    }
 
}

Már csak egy dolog hiányzik: a hozzáférés megadása. Ez legegyszerűbben az application.properties (pl. src/main/resources/application.properties) fájlba néhány sorral tudjuk megadni:

spring.datasource.url=jdbc:mysql://localhost:3306/testdb
spring.datasource.username=csaba
spring.datasource.password=farago

logging.level.org.springframework.jdbc.core=DEBUG

Én magam is meglepődtem azon, hogy ennyi tényleg elég az adatbázis műveletek futtatásához. A logging.level.org.springframework.jdbc.core=DEBUG sor eredménye az, hogy a naplófájlban megjelennek a ténylegesen végrehajtott SQL utasítások, ami pl. hibakeresésnél tud hasznos lenni. Ha most lefuttatjuk, akkor eredmény lényeges részei az alábbiak:

[Sanyi (34), Kata (32), Pista (46), Anna (28)]
Sanyi (34)
[Hungary - Budapest - Pipacs utca 1: [Sanyi (34), Kata (32)], Hungary - Budapest - Kikelet utca 5: [Anna (28)], Hungary - Szeged - Tavasz utca 2: [Pista (46)]]
[Hungary - Budapest - Pipacs utca 1, Hungary - Budapest - Kikelet utca 5]
[34, 32, 28]

Mindezt tényleges SQL utasításokkal kaptuk, melyről úgy tudunk a legegyszerűbben meggyőződni, hogy átírjuk pl. a jelszót.

A kapcsolat adatainak kézi megadása

A fenti példa egyszerű, talán túlságosan is az. Nem ad választ pl. arra az esetre, ha egyszerre két adatbázishoz szeretnénk csatlakozni. Az adatbázis kapcsolat "kézi" megadását az alábbi módon tudjuk elérni:

package hu.faragocsaba.springbootjdbc;
 
import javax.sql.DataSource;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
 
@Configuration
public class AppConfig {
 
    @Bean
    DataSource mysqlDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/testdb");
        dataSource.setUsername("csaba");
        dataSource.setPassword("farago");
        return dataSource;
    }
 
    @Bean
    JdbcTemplate mysqlJdbcTemplate(DataSource mysqlDataSource) {
        return new JdbcTemplate(mysqlDataSource);
    }
 
}

A példa persze túl egyszerűsített. A valóságban az értékeket konfigurációs fájlból vagy környezeti változóból kapjuk, a fent leírtaknak megfelelően. Valamint az alapértelmezett megoldás általában akkor nem jó, ha több adatbázishoz szeretnénk csatlakozni. Ez esetben több JdbcTemplate példányt hozunk létre, és azt az ugyancsak fent bemutatott Qualifier módszerrel tudjuk kezelni.

Az újabb MySQL verziók esetében a dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); sorra nincs szükség, de benne hagytam a kódban arra az esetre, ha mégis meg kellene adni.

H2 adatbázis a memóriában

A most bemutatott megoldás egyik legnagyobb ereje abban rejlik, hogy egyáltalán nincs a forrásban adatbázis specifikus rész; elvileg pusztán konfigurációval ki lehet cserélni az alatta levő adatbázist. Ebben a fejezetben a H2 adatbázisról lesz szó, ami beépülő, azaz nem kell telepíteni semmit, és a memóriában tartja az adatokat. Az adatok hosszú távú tárolására, ill. nagy mennyiségű adat tárolására ez a megoldás nem jó, de egyszerűbb programokhoz vagy teszteléshez megfelelő lehet.

Elvileg tehát nem kell belenyúlni a kódba, a példa kedvéért viszont mégis megteszem. Ugyanis azzal, hogy az adatbázis beépülőként a memóriában van, azt is jelenti, hogy minden leálláskor törlődik, és minden induláskor tiszta lappal indulunk. Így nincs szükség a törlésre, viszont inicializáláskor már eleve szúrhatunk be adatot, amit később meg tudunk nézni. Emiatt (is) úgy döntöttem, hogy egy külön példát hozok létre, springbooth2 néven. Ide viszont csak a különbségeket írom le.

Kezdjük a pom.xml-lel! A szokásos módosításokon túl a függőségek közül töröljük ki ezt:

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

és helyette szúrjuk be ezt:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

Az application.properties tartalma a következő legyen:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password

logging.level.org.springframework.jdbc.core=DEBUG

Másoljuk be a Main.java és JdbcExample.java forrásokat is (az AppConfig.java-t ne, mert az hibához vezetne). Tehát lecseréltük a függőséget és az elérési beállításokat. Ha egy másik, a MySQL-hez hasonló adatbázisra cseréltük volna le (pl. PostgreSQL), akkor valószínűleg ennyi elég is lenne. Viszont mivel itt memóriában levő adatbázisról van szó, a sémát létre kell hozni. Ezt legegyszerűbben úgy tudjuk megtenni, ha az src/main/resources/ könyvtárban létrehozunk egy schema.sql nevű fájlt, az alábbi tartalommal:

DROP TABLE IF EXISTS person;
DROP TABLE IF EXISTS address;
 
CREATE TABLE address (
    id INT NOT NULL AUTO_INCREMENT,
    country VARCHAR(100),
    town VARCHAR(100),
    street VARCHAR(200),
    PRIMARY KEY (id)
);
 
CREATE TABLE person (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(200),
    age INT,
    addressid INT,
    PRIMARY KEY (id),
    CONSTRAINT addressid FOREIGN KEY (addressid) REFERENCES address(id)
);

Ezt a Spring Boot automatikusan végrehajtja. Így már működnie kell! De hajtsunk végre még két apró módosítást! A src/main/resources/data.sql scriptet szintén automatikusan hajtja végre a Spring Boot, és ennek segítségével adatokat tudunk beszúrni, pl.:

INSERT INTO address(id, country, town, street) VALUES(1, 'Hungary', 'Budapest', 'Pipacs utca 1');
INSERT INTO address(id, country, town, street) VALUES(2, 'Hungary', 'Szeged', 'Tavasz utca 2');
 
INSERT INTO person(name, age, addressid) VALUES('Sanyi', 34, 1);
INSERT INTO person(name, age, addressid) VALUES('Kata', 32, 1);
INSERT INTO person(name, age, addressid) VALUES('Pista', 46, 2);

Hogy értelmet is adjunk a dolognak, a Main osztályban töröljünk ki pár sort: egyrészt az inicializálást (jdbcExample.initialize();), másrészt a már hozzáadott részeket. Így a fő függvény (a lekérdezések nélkül) az alábbira redukálódik:

        Number budapestKikeletId = repositoryExample.insertAddress("Hungary", "Budapest", "Kikelet utca 5");
        repositoryExample.insertPerson("Anna", 28, budapestKikeletId);

Ha lefuttatjuk a kódot, akkor eredményül ugyanazt kapjuk, mint fent.

A kód tehát tömör, felesleges részeket nem igazán tartalmaz, és vezérelt körülmények között működik.

További anyag

Ennek a szakasznak az elkészítésében - többek között - az alábbi oldalak segítettek:

JPA

A fenti példában láthattuk azt, hogy ebben a formában az adatbázis lekérdezés továbbra is nehézkes. A műveletek igen bonyolultak, különösen a táblák közötti kapcsolatok létrehozásánál, tovább a Java objektumokra való átalakítás tovább bonyolítaná a rendszert. A fenti példa létrehozása nekem több óra hosszáig eltartott: megtalálni a megoldásokat, javítani a stringként megadott, gyakran hibás SQL parancsokat stb.

A Java külső könyvtárak oldalon olvashatunk a Hibernate-ről, ami pont ezt a problémát orvosolja. A Spring-ben ezt gondolták tovább az alábbi módon. Ezt is példán keresztül mutatom be (springbootjpa).

A példában MySQL-t használunk, így a spring-boot-starter-data-jpa és mysql-connector-java függőségeket kell beállítani:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>          
    </dependencies>

Először hozzuk létre a JPA-hoz szükséges entitásokat:

package hu.faragocsaba.springbootjpah2;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
 
@Entity
public class Person {
 
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
 
    private String name;
 
    private Integer age;
 
    @ManyToOne
    @JoinColumn(name="addressid")
    private Address address;
 
    public Person() {}
 
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public Address getAddress() {
        return address;
    }
 
    public void setAddress(Address address) {
        this.address = address;
    }
 
    @Override
    public String toString() {
        return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
    }
 
}

Ill.:

package hu.faragocsaba.springbootjpah2;
 
import java.util.List;
 
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
 
@Entity
public class Address {
 
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
 
    private String country;
 
    private String town;
 
    private String street;
 
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "address")
    private List<Person> persons;
 
    public Address() {}
 
    public Address(String country, String town, String street) {
        this.country = country;
        this.town = town;
        this.street = street;
    }
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getCountry() {
        return country;
    }
 
    public void setCountry(String country) {
        this.country = country;
    }
 
    public String getTown() {
        return town;
    }
 
    public void setTown(String town) {
        this.town = town;
    }
 
    public String getStreet() {
        return street;
    }
 
    public void setStreet(String street) {
        this.street = street;
    }
 
    public List<Person> getPersons() {
        return persons;
    }
 
    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }
 
    public void addPerson(Person person) {
        this.persons.add(person);
    }
 
    @Override
    public String toString() {
        return "Address [id=" + id + ", country=" + country + ", town=" + town + ", street=" + street + ", persons=" + persons + "]";
    }
 
}

A két entitás a személyt és a címet valósítja meg, azok belső kapcsolatával együtt. Most jön a lényeg: a repository! Külön repository-t hozunk létre a két táblának. A címé az alábbi:

package hu.faragocsaba.springbootjpah2;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface AddressRepository extends CrudRepository<Address, Integer> {
    List<Address> findByTown(String town);
}

Egy elég üres interfésznek néz ki. A valóságban azzal, hogy a CrudRepository interfészből (!) származik az interfészünk, automatikusan legenerálódnak (többek között) az alábbi függvények:

  • count(): az entitások számát adja vissza
  • delete(T entity): adott entitás törlése
  • deleteAll(): az összes entitás törlése
  • deleteById(ID id): adott azonosítójú entitás törlése
  • findAll(): az összes entitás lekérdezése
  • findById(ID id): adott azonosítójú entitás lekérdezése
  • save(S entity): entitás mentése

Magyarán ezeket a függvényeket automatikusan használhatjuk, anélkül, hogy megvalósítanánk, sőt, akár csak deklarálnánk.

A másik lényeges dolog a findByTown(). Ezt sem kell megvalósítanunk: automatikusan legenerálja a SELECT * FROM Address WHERE Town = '…' SQL lekérdezést. A helyére pedig bekerül a függvény paramétere, a town. Mindezt anélkül, hogy akár csak egyetlen SQL utasítást is írnánk. Ez is egy szép példája (talán) a convention over configuration elvnek.

Tehát az elnevezés konvenciót betartva kapun SQL lekérdezéseket. A https://docs.spring.io/spring-data/data-jpa/docs/current/reference/html/#jpa.query-methods.query-creation oldalon felsorolt módokat tudjuk használni. Valójában az SQL kulcsszavak megfelelői ezek, pl. And, Or, Is, Between, LessThan, IsNull, IsNotNull, Like, OrderBy, In stb.

De mi van akkor, ha a lekérdezés annyira összetett, hogy ezekkel a szavakkal nem tudjuk megvalósítani? Pl. egy bonyolult JOIN? Erre használható a @Query annotáció, ahol megadjuk az SQL lekérdezést. A személy repository kódja az alábbi:

package hu.faragocsaba.springbootjpah2;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
 
public interface PersonRepository extends CrudRepository<Person, Integer> {
    List<Person> findByName(String name);
 
    @Query("SELECT person.age FROM Person person WHERE person.address.town = :town")
    List<Integer> findAgeByTown(@Param("town") String town);
}

A CrudRepository-nak köszönhetően az alap műveletek itt is megvannak. A findByName() a fentiek alapján már értelmezhető. A findAgeByTown() viszont egy példa arra, hogy SQL lekérdezéssel adjuk meg a műveletet. Ezt sem kell "igazi" Java kódban megvalósítani.

A CrudRepository mellett létezik még másik kettő:

  • PagingAndSortingRepository: a CrudRepository-ból szárazik, viszont lapozható lekérdezést is lehetővé tesz. Tehát ha a lekérdezés mérete nagy, akkor megmondhatjuk neki, hogy milyen sorba rendezés szerint, mekkora lapméretet használva hányadikat kérjük.
  • JpaRepository: a PagingAndSortingRepository-ból származik, és néhány további függvényt definiál.

A témáról bővebben a https://www.baeldung.com/spring-data-repositories oldalon olvashatunk.

Visszatérve a programunkhoz, a főprogram a következő lehet:

package hu.faragocsaba.springbootjpah2;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private PersonRepository personRepository;
 
    @Autowired
    private AddressRepository addressRepository;
 
    @Override
    public void run(String... args) throws Exception {
        personRepository.deleteAll();
        addressRepository.deleteAll();
 
        Address budapestPipacs = new Address("Hungary", "Budapest", "Pipacs utca 1");
        Address budapestKikelet = new Address("Hungary", "Budapest", "Kikelet utca 5");
        Address szegedTavasz = new Address("Hungary", "Szeged", "Tavasz utca 2");
        Person sanyi = new Person("Sanyi", 34);
        Person kata = new Person("Kata", 32);
        Person pista = new Person("Pista", 46);
        Person anna = new Person("Anna", 28);
        sanyi.setAddress(budapestPipacs);
        kata.setAddress(budapestPipacs);
        pista.setAddress(szegedTavasz);
        anna.setAddress(budapestKikelet);
 
        addressRepository.save(budapestPipacs);
        addressRepository.save(budapestKikelet);
        addressRepository.save(szegedTavasz);
        personRepository.save(sanyi);
        personRepository.save(kata);
        personRepository.save(pista);
        personRepository.save(anna);
 
        System.out.println(personRepository.findAll());
        System.out.println(personRepository.findByName("Sanyi").get(0).getAge());
        System.out.println(addressRepository.findAll());
        System.out.println(addressRepository.findByTown("Budapest"));
        System.out.println(personRepository.findAgeByTown("Budapest"));
    }
 
}

A kapcsolat kezelése a két tábla között jóval egyszerűbb, mint a JDBC példában; Java setterrel megoldottuk.

Egy dolog maradt még hátra: az application.property, amit a fent megadott módon állítsunk be. Ha mindent jól csináltunk, az eredmény a következő:

[Person [id=214, name=Sanyi, age=34], Person [id=215, name=Kata, age=32], Person [id=216, name=Pista, age=46], Person [id=217, name=Anna, age=28]]
34
[Address [id=105, country=Hungary, town=Budapest, street=Pipacs utca 1, persons=[Person [id=214, name=Sanyi, age=34], Person [id=215, name=Kata, age=32]]], Address [id=106, country=Hungary, town=Budapest, street=Kikelet utca 5, persons=[Person [id=217, name=Anna, age=28]]], Address [id=107, country=Hungary, town=Szeged, street=Tavasz utca 2, persons=[Person [id=216, name=Pista, age=46]]]]
[Address [id=105, country=Hungary, town=Budapest, street=Pipacs utca 1, persons=[Person [id=214, name=Sanyi, age=34], Person [id=215, name=Kata, age=32]]], Address [id=106, country=Hungary, town=Budapest, street=Kikelet utca 5, persons=[Person [id=217, name=Anna, age=28]]]]
[34, 32, 28]

Próbáljuk ki ezt a H2 adatbázissal is! Ehhez a következőket kell tennünk (én létrehoztam egy külön példát springbootjpah2 néven): egyrészt a MySQL függőség helyére a H2 függőséket tegyük a pom.xml-ben:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

Másrészt az application.properties fájlban az adatforrás URL-jét írjuk át a következőre:

spring.datasource.url=jdbc:h2:mem:testdb

Igazából tényleg csak ennyi; minden más változatlansága mellett, ha a fenti működött, akkor ennek is működnie kell. Vegyük észre még azt, hogy schema.sql sem kellett: az entitások alapján legenerálta. (Ha ezt nem szeretnénk, akkor használhatjuk a spring.jpa.hibernate.ddl-auto különböző értékeit a finomhangoláshoz.)

JMS

Hogy nézne ki egy nagyvállalati alkalmazási keretrendszer üzenetküldés nélkül? Természetesen a Spring is tartalmaz ilyet, bár ezt kivételesen nem vitték túlzásba. Az viszont az érdekes ezzel kapcsolatban, hogy a példák mind tele vannak boilerplate kóddal:

A többi jelentősebb szerző (pl. Mkyong, Jenkov stb.) nem is foglalkozik a témával, így nem volt egyszerű utána járni annak, hogy hogyan lehet ezt a legegyszerűbben kezelni. Pedig van megoldás!

Spring Boot esetében, ha a parent megoldást választjuk, akkor a következő függőséget kell beletenni:

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

Ez ne igazán szép, mert tartalmazza a queue szolgáltatót (ActiveMQ). Szerintem elegánsabb lenne mondjuk a spring-boot-starter-jms, vagy a talán még általánosabb spring-boot-starter-messaging, és az alapértelmezett megvalósítás lehetne az ActiveMQ, amit egy másik függőséggel felül lehetne írni, de a felsorolt starterek sajnos nincsenek.

A példa beleteszi még az activemq-broker függőséget is; a tapasztalatom szerint anélkül is működik.

A példákban az üzenet valamilyen objektum, melyekben meg kell valósítani a szerializációt. A lenti példában string küldését láthatjuk; legvégső soron úgyis azt kell küldenünk. A konverziós lépések nem JMS specifikusan, így azokat a példából kihagyom; a fent felsorolt linkeken meg lehet nézni.

Először lássuk a fogadó kódját!

package hu.faragocsaba.springbootjms;
 
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
 
@Component
public class MyReceiver {
 
    @JmsListener(destination = "myDestination")
    public void messageReceived(String myMessage) {
        System.out.println("Message received: " + myMessage);
    }
 
}

Valójában csak a @JmsListener annotációra van szükség, a függvény neve mindegy, hogy micsoda. A fenti példákban szerepel még egy containerFactory paraméter is, ill. annak egy megvalósítása a @Configuration osztályban, de a tapasztalatom szerint anélkül is működik; akkor meg minek bonyolítani?!

A küldő kódja az alábbi:

package hu.faragocsaba.springbootjms;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
 
@Component
public class MySender {
 
    @Autowired
    private JmsTemplate jmsTemplate;
 
    public void sendMessage() {
        jmsTemplate.convertAndSend("myDestination", "Hello, JMS world!");
    }
 
}

Valószínűleg a JdbcTemplate mintájára hozták létre a JmsTemplate könyvtárat, ami talán az egész üzenetküldő rendszerben a legeredetibb megoldás - persze pont a Template mint olyan kissé félrevezető.

A küldő függvénynek két paramétere van, ami teljesen logikus: az egyik az, hogy hova menjen, a másik meg az, hogy mi. (Elvileg elhagyható a küldő és a fogadó oldalon is a cél, és ez esetben az alapértelmezettre menne, de ez túlzottan lecsökkenti a lehetőségeket.) A függvény neve viszont kissé meglepő: convertAndSend(). Azt nem igazán sikerült megértenem, hogy egészen pontosan miért kell konvertálni. Ill. ezt még csak-csak értem, azt viszont végképp nem, hogy miért kell ezt a fejlesztő tudomására hozni.

Az alap send() függvénnyel is menne, de ott ezt a szintaxist kell használni:

        jmsTemplate.send("myDestination", s -> s.createTextMessage("Hello, JMS world!"));

Hm…, én a két elnevezést felcserélném. A TextMessage mellett az üzenet típusa még lehet ByteMessage, MapMessage, ObjectMessage. Az üzenetek konvertálására az alapértelmezett SimpleMessageConverter mellett használhatjuk még a következőket is MappingJackson2MessageConverter, MarshallingMessageConverter, MessagingMessageConverter. De mi most ebben az egyszerű példában ne használjuk.

A főprogram az alábbi:

package hu.faragocsaba.springbootjms;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.EnableJms;
 
@SpringBootApplication
public class Main implements CommandLineRunner {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
    @Autowired
    private MySender mySender;
 
    @Override
    public void run(String... args) throws Exception {
        mySender.sendMessage();
    }
 
}

A "könyv" azt írja, hogy annotálni kell az osztályt a @EnableJms annotációval; nálam enélkül is működött.

A fenti oldalakon a példákban olyan függvények is vannak, amelyektől elmegy a kedve a programozónak az alkotástól, de a tapasztalatom szerint azokra nincs szükség. Szóval végső soron eléggé letisztult ez, de azért még mindig nincs élére vasalva.

Spring webalkalmazások

Hello, Spring web!

A Spring egyik legnagyobb előnye a webalkalmazások egyszerűségében rejlik. Lássunk egy példát (springbootwebrest)! Függőségként vegyük fel a spring-boot-starter-web-et a pom.xml-ben:

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

Hozzuk létre a főprogramot:

package hu.faragocsaba.springbootwebrest;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
}

A főprogram lényegében teljesen üres. (Hamarosan láthatjuk, hogy arra sem lesz szükség, hogy "lényegében".) A lényeg pedig:

package hu.faragocsaba.springbootwebrest;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/rest")
public class HelloController {
 
    @GetMapping
    public String index() {
        return "Hello Spring Boot web world!";
    }
 
    @GetMapping("/hello/{name}")
    public String sayHello(@PathVariable("name") String name) {
        return "Hello, " + name + "!";
    }
 
    @GetMapping("/add")
    public String add(@RequestParam("a") int a, @RequestParam("b") int b) {
        return "" + a + " + " + b + " = " + (a + b);
    }
 
}

Először magam sem hittem el, hogy tényleg csak ennyi; ha elindítjuk, akkor elindul egy komplett webszerver, benne az alkalmazással. A példa néhány alapvető dolgot mutat be:

  • Az osztály @RestController annotációval van jelölve. Ez lehetne @Controller is, a Spring rendszernek mindegy; talán így jobban olvasható, különösen akkor, ha a programban rengeteg komponens van.
  • Az osztály @RequestMapping("/rest") annotációval is el van látva. Erre igazából nincs szükség; ezzel azt adjuk meg, hogy hol egyen a gyökér URL.
  • A @GetMapping a @RequestMapping(method = RequestMethod.GET) rövidítése (+ van még köztük néhány apró eltérés). Így ha egy böngészőből betöltjük a http://localhost:8080/rest oldalt, akkor a Hello Spring Boot web world! oldalt kapjuk eredményül.
  • A sayHello() példa a @PathVariable annotációra mutat példát, aminek segítségével az URL-ben található elérési útvonalból (path) tudunk információt kapni. Pl. a http://localhost:8080/rest/hello/Csaba eredménye: Hello, Csaba!
  • Az add() a query paraméterekre ad példát a @RequestParam annotáció segítségével: a http://localhost:8080/rest/add?a=2&b=3 eredménye ez: 2 + 3 = 5.
  • A többi HTTP lekérdezést is használni tudjuk, pl. a @PostMapping, @PutMapping stb.

war alkalmazás készítése

A Spring igazi ereje abban rejlik, hogy néhány sorral egy önmagában futtatható (self-contained) webalkalmazást készíthetünk, melyhez nem kell külön webszervert telepíteni. Ha valaki a hagyományos módszert választaná, arra is lehetőséget biztosít a Spring Boot.

Lássunk egy példát springbootwebwar! A pom.xml-ben vegyük fel ezt a sort: <packaging>war</packaging>. A build részt teljesen kivehetjük, esetleg a végső nevet megadhatjuk.

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>springbootwebwar</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>
 
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <finalName>${artifactId}</finalName>
    </build>
</project>

A HelloController maradjon változatlan. A "főprogram":

package hu.faragocsaba.springbootwebwar;
 
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
 
@SpringBootApplication
public class Main extends SpringBootServletInitializer {}

Meg sem kell írni! Indítsuk el a webalkalmazásoknál megszokott módon. Pl. a Tomcat esetében másoljuk a target/springbootwebwar.war fájlt a $TOMCAT_HOME/webapps/ könyvtárba, majd indítsuk a megfelelő bin/startup scripttel. Ugyanúgy fog működni, mint fent, annyi eltéréssel, hogy az URL elérési útvonalába bele kell tenni a megfelelő helyre a webalkalmazás nevét. Pl. a gyökér az alábbi lesz: http://localhost:8080/springbootwebwar/rest.

Thymeleaf

A Thymeleaf (https://www.thymeleaf.org/) egy gyakori kiegészítője a Spring-nek. Segítségével HTML oldalakat tudunk készíteni. Lássunk rögtön egy példát (springbootview)! A pom.xml-be az alábbi függőségeket vegyük fel:

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

A főprogram a szokásos minimalista:

package hu.faragocsaba.springbootview;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Main {
 
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
 
}

A fent elkészített REST controller alapján készítsük el az alábbit:

package hu.faragocsaba.springbootview;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
@Controller
public class HelloController {
 
    @GetMapping("/hello")
    public String hello(@RequestParam(name = "name", required = false, defaultValue = "world") String name, Model model) {
        model.addAttribute("name", name);
        return "hello";
    }
 
}

Fontos, hogy az annotáció itt @Controller legyen, és ne @RestController. Példát láthatunk az opcionális paraméterre, alapértelmezett értékkel, valamint a Model osztály használatára, melyben az adatokat kapja a HTML oldal. Itt egy attribútumot állítunk be, amit a HTML-ben a Thymeleaf segítségével kérdezünk majd le. A return "hello" hatására a hello.html oldal töltődik be. Ennek a tartalma (src/main/resources/templates/hello.html):

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

Ha elindítjuk és betöltjük a http://localhost:8080/hello?name=Csaba oldalt, akkor a Hello, Csaba!, ha pedig a http://localhost:8080/hello oldalt, akkor a Hello, world! felirat jelenik meg.

A Thymeleaf-ról egy 5 perces összefoglalót a https://www.thymeleaf.org/doc/articles/standarddialect5minutes.html oldalon olvashatunk.

TODO: kicsit részletesebb példa

Egy MVC alkalmazás

TODO: login
TODO: I18N
TODO: devtool
TODO: integrációs teszt
TODO: MVC
TODO: CSS
TODO: Docker

Spring Security

https://www.baeldung.com/security-spring

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License