Kategória: Enterprise Java.
Table of Contents
|
Áttekintés
Az EJB alkalmazások tipikusan az n-rétegű alkalmazások közé tartoznak. Az ilyen alkalmazások futtatásához ún. EJB konténerre van szükség, és ezeket alkalmazás szervereknek nevezzük. Az alkalmazás szerverek általában tartalmaznak web konténert is, fordítva viszont ez többnyire nem igaz: pl. a Tomcat vagy a Jetty nem alkalmas arra, hog EJB alkalmazásokat futtasson.
Az EJB Enterprise Java Beans rövidítése. A lényege ennek az, hogy a futtatása teljesen a futtató rendszerre van bízva, mint például az erőforrás optimalizálás, a párhuzamos futtatásból adódó problémák kezelése, tranzakciókezelés stb. Amint azt a következő példában látni fogjuk, ez egy igazi "nehézsúlyú" (heavyweight) technológia, még úgy si, hogy az évek folyamán igyekeztek - amennyire csak lehet - egyszerűsíteni. Minden jel arra mutat, hogy ez a technológia ebben a formában - ha nem is fog hamarosan teljesen kikopni, de - túl van már a csúcson. Ugyanakkor érdemes megismerkedni az alapjaival és az elvekkel, mert az más nagyvállalati rendszerekben is megtalálható.
Egy példa
Ha valaki azt állítja, hogy az EJB alkalmazások készítése könnyű, az bizony nem mond igazat. Az interneten nem találtam olyan példát, ami a létező legegyszerűbbre le van csupaszítva, de még minden része meg van ahhoz, hogy működjön. A példák mindegyike vagy túl volt komplikálva, vagy csak felületesen írta le a lényeget, de működő, kipróbálható kódot nem biztosított. Innen-onnan összeollózott töredékekből (https://github.com/nuzayats/eartest, https://www.baeldung.com/ejb-intro) állítottam össze a lenti példát, amelyet igyekeztem egyrészt leegyszerűsíteni annyira, amennyire érdemes, másrészt a teljesség igényével minden részletet benne hagyni.
Nagy általánosságban egy EJB alkalmazás a következőképpen néz ki:
- Interfész: ez többnyire (bár nem kötelezően) külön komponensbe kerül. A külön komponens lényege az, hogy ha távolról szeretnénk meghívni az EJB komponenst, akkor a szerver és a kliens is elérje azt. Ez fizikailag egy jar fájlt jelent, amelyet elérhetővé teszünk a példában a megvalósítás és a kliens számára is, fordítási és futási időben egyaránt. Tetszőleges számú ilyen interfész fájl lehet egy alkalmazásban, és a tipikus alkalmazás sokat tartalmaz.
- Megvalósítás: ez tartalmazza a tulajdonképpeni EJB implementációt. Általában minden interfész fájlhoz tartozik egy megvalósítás fájl. Fizikailag ezek is jar fájlok, a csomagolás viszont - ahogy látni fogjuk - nem az alapértelmezett jar, hanem ejb lesz.
- Kliens: ebben az esetben ez egy webalkalmazás lesz, ami a fent megismert war. Ez fog hivatkozni a fenti interfészre és meghívni annak függvényét, és a keretrendszer gondoskodik majd a komponensek összedrótozásáról. (Ezt az összedrótozást hívja a szakirodalom függőség befecskendezésének (Dependency Injection, DI) vagy a kontroll megfordításának (Inversion of Control, IoC).) Technikai korlátozást jelent az, hogy egy EJB alkalmazásban legfeljebb egy war lehet, így a logikailag különálló részeket is kénytelenek vagyunk egybe gyúrni.
- Keret: ha vegyünk egy nem is olyan túl nagy EJB alkalmazást, mondjuk 10 interfésszel, 10 megvalósítással, egy webalkalmazással és néhány külső könyvtár függőséggel, akkor a részeket igen nehézkes egyben tartani. E probléma kezelésére alkották meg az ear (Enterprise ARchive) formátumot: ez gyakorlatilag a fentieket tartalmazza egyetlen fájlban. Ideális esetben egy EJB alkalmazás tehát gyakorlatilag egyetlen ear kiterjesztésű fájl, ami elvileg mindent tartalmaz, amire szükség van az alkalmazás futtatásához.
Ahogy a fent vázolt példában is láthatjuk, nagyon sok komponensnek kell együttműködnie egy webalkalmazás során. Ez felveti a verziózás problémáját. Verziója lehet a következőknek:
- Az egyes komponenseknek, tehát az interfészeknek, megvalósításoknak, a webalkalmazásnak, ill. magának az ear csomagnak. Elképzelhető, hogy az egyik komponensen módosítunk, míg a másikon nem, és "elcsúsznak" a verziók.
- Ezek a komponensek számos külső könyvtárat használnak. Ha az alkalmazás kellően szerteágazó és kellő hosszú ideig létezik, nagy az esély arra, hogy az egyik komponens ugyanannak a könyvtárnak az egyik, a másik a másik verziót használja, és ezek a könyvtárak között további függőségek lehetnek.
- Magának a Java EE-nek és van verziója (az írás pillanatában 8.0), és az EJB-nek is (az írás pillanatában 3.2).
- A Maven beépülőknek is van verziójuk, és az nem jó, ha az egyik EJB komponenst az egyik, a másikat a másik verziók készíti el.
- Az egyéb beállításokról, mint pl. egységes groupId, Java fordítási verzió, karakterkódolás, fájlnév konvenció (pl. az eredmény tartalmazza-e a verziószámot) stb. már nem is beszélve.
Emiatt célszerű (bér végső soron nem kötelező) ún. szülő (parent) pom.xml fájlt létrehozni, amelyből a többiek származnak. Ez biztosíthatja a többé-kevésbé egységes verziókezelést. Pl. a gyerek komponensek automatikusan öröklik a groupId-t és a verziót. A külső függőségeket elég felsorolni ebben a szülő pom.xml fájlban, melyeket a leszármazottak öröklik. Ill. ha nem szeretnénk, hogy mindegyik örökölje, akkor a <dependencyManagement> technikát alkalmazva felsoroljuk az összes külső függőséget a pontos verzióval, és a leszármazottaknak elég csak hivatkozniuk az adott függőségre verziószám nélkül, ugyanazt fogják kapni mint az alkalmazás többi része. Hasonlóan a szerkesztés beépülők verzióit is ebben a szülő pom.xml-ben adhatjuk meg.
A fordítás sorrendje is számít: először az interfészt kell lefordítani, majd a megvalósítás és a webalkalmazás mehet elvileg párhuzamosan (mindegy a sorrend), végül az egész összecsomagolása csak úgy történhet, ha mindegyikkel megvagyunk. Ehhez is kell egy "összefogó" pom.xml, amelynek a csomagolás típusa pom. Ez utóbbi a szülő pom.xml-lel összevonható; az már ízlés kérdése, hogy ezt megtesszük-e. Az alábbi példában össze lesz vonva, hogy így is csökkentsem az egyébként rettenetesen sok fájlt.
Egy ilyen hosszú bevezető után már kezdhet kialakulni bennünk a kép. Lássuk a példát! A lenti fájlokat akár kézzel, akármilyen fájlszerkesztővel létre tudjuk hozni; mégis, célszerű valamilyen integrált fejlesztőeszközt használni. Az Eclipse-en keresztül mutatom be. Érdemes az Eclipse-ben tiszta lappal indítani, és a Fejlesztőeszközök oldalon leírtak alapján beállítani.
A szülő pom
Először hozzunk létre egy új Maven projektet: File → New → Maven Project (vagy Other…, és ott válasszuk ki). Archetype-ot ne válasszunk (azokkal sosincs szerencsém…). A groupId nálam hu.faragocsaba, az artifactId értéke ejbexample, a verzió bármi (pl. 1.0), a Packaging pedig pom. Kiterjeszteni és szépíteni ráérünk később, és meg is fogjuk tenni, mist viszont kivételesen lépjünk tovább, és hozzuk létre az alprojektetet. Hozzunk létre 4 Maven modult (jobb kattintás az imént létrehozott projekten → New → Other → Maven → Maven Module) az alábbiak szerint. Egyik esetben se használjunk archetype-ot (kapcsoljuk be a skip-et). Ha így hozzuk létre, akkor az ejbexample pom.xml fájlja automatikusan a szülő lesz. A jövetkezőket hozzuk létre:
- ejbexample-api, a csomagolás típusa jar,
- ejbexample-impl, a csomagolás típusa ejb,
- ejbexample-war, a csomagolás típusa war,
- ejbexample-ear, a csomagolás típusa ear.
Az Eclipse kissé logikátlanul jeleníti meg a projekteket. Ha megnézzük fájl szinten a dolgot, akkor az ejbexample alatt található mind. Az Eclipse-ben is ott is látjuk, de magukat a komponenseket az ejbexample projekttel egy szinten. Lássuk először az ejbexample pom.xml-ét! Ide bele kellett, hogy kerüljön egy <modules> rész az egyes modulokkal. Némi formázás, valamint a <build> és a <dependencyManagement> részek hozzáadásával az alábbit kapjuk!
ejbexample/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>ejbexample</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>ejbexample-api</module>
<module>ejbexample-impl</module>
<module>ejbexample-war</module>
<module>ejbexample-ear</module>
</modules>
<build>
<finalName>${project.artifactId}</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-ear-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<defaultLibBundleDir>lib</defaultLibBundleDir>
</configuration>
</plugin>
<plugin>
<artifactId>maven-ejb-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<ejbVersion>3.2</ejbVersion>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Vegyük át ismét a lényeget:
- A csomagolás típusa pom. Ennek önmagában nem lesz eredménye.
- A modules rész sorolja fel azokat a modulokat, amelyeket végrehajt.
- A build szekcióban megadjuk, hogy hogyan nézzen ki a végeredmény (a modul neve, verziószám nélkül); ezt öröklik a leszármazottak.
- A build-en belül a pluginManagement szekcióban azt adjuk meg, hogy melyik típusú build pontosan melyik verziót alkalmazza, és hogyan viselkedjen. Ebben a példában talán nem túl nyilvánvaló a jelentősége; ha sok EJB komponens lenne, akkor is csak egyszer kell megadni a megfelelő beépülőt. Itt adjuk meg az EJB verziót is.
- A dependencyManagement szekcióban soroljuk fel a függőségeket, ami jelen esetben egyetlen elemből áll. Itt adjuk meg a Java EE verzióját (8.0). Ha nem a dependencyManagement-en belül lenne, hanem közvetlenül a project alatt, akkor automatikusan örökölné az összes leszármazott. Ha az alkalmazás több tucat komponensből állna, és a függőségek uniója szintén több tucat lenne, akkor nagyon sok felesleges függőség jönne létre. Ez a megoldás viszont biztosítja azt, hogy mindegyik komponens ugyanazt a verziót használja, de nem hoz be felelleges függőségeket.
Ha a környezet generált egyéb könyvtárakat (pl. src), azokat töröljük le, nem lesz rájuk szükség. Ezzel a szülő komponens kész.
Az interfész
Az ejbexample-api megvalósítása következik. Először lássuk a pom.xml-t!
ejbexample/ejbexample-api/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>
<parent>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample</artifactId>
<version>1.0</version>
</parent>
<artifactId>ejbexample-api</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
</dependency>
</dependencies>
</project>
A lényege:
- A parent szakaszban megadtuk a szülőre való hivatkozást. Onnan örökli a fent felsorolt dolgokat. Láthatjuk, hogy nincs külön groupId és verzió, azokat is örökli.
- Az artifactId-t megadtuk.
- A csomagolás (packaging) az alapértelmezett jar, amit nem feltétlenül kell megadni, de mivel a többi komponensnél megadjuk, a teljesség igényével itt is megjelenik.
- A függőségeknél megjelenik a javaee-api, de a verziószámot nem adjuk meg, azt az őstől örökli.
Maga az interfész egyetlen függvényt tartalmaz.
ejbexample/ejbexample-api/src/main/java/hu/faragocsaba/ejbexample/api/EjbExampleApi.java
package hu.faragocsaba.ejbexample.api;
import javax.ejb.Local;
@Local
public interface EjbExampleApi {
String sayHello();
}
A @Local annotációról lesz még szó; röviden azt jelenti, hogy ezt (ill. a megvalósítást) lokálisan, csak az adott alkalmazásból tudjuk meghívni. A másik lehetőség a @Remote. A függvény maga paraméter nélküli (lehetne paraméteres is), a visszatérési értéke pedig string (lehetne más is).
A megvalósítás
A példában (és a legtöbb esetben a valóságban is) a megvalósítás elkülönül az interfésztől. A megvalósítás jelen esetben az ejbexample-impl fájlban történik. Először itt is lássuk a pom.xml-t!
ejbexample/ejbexample-impl/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>
<parent>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample</artifactId>
<version>1.0</version>
</parent>
<artifactId>ejbexample-impl</artifactId>
<packaging>ejb</packaging>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
</dependency>
<dependency>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
A lényege:
- Itt is megadtuk a szülő komponenst.
- A csomagolás típusa ejb; ennek következtében a maven-ejb-plugin beépülő fogja végrehajtani a build folyamatot.
- A függőségek között it is szerepel a javaee-api, verziószám nélkül.
- Szintén a függőségek között található a hivatkozás az interfészre. A gyakorlatban sok interfész és hozzájuk tartozó megvalósítás van; itt elég felsorolni a saját interfészét, valamint azokat, amelyeket használ; felesleges függőség nem jelenik meg. A ${project.version} az aktuális projektverziót jelöli, így verzióváltás esetén nem kell átírni. (Ezt nem tudjuk elkerülni a szülőre való hivatkozásnál, így sajnos egy újabb verzió kiadásánál mindegyik komponens pom.xml fájljához hozzá kell nyúlnunk.)
Most lássuk magát a megvalósítást!
ejbexample/ejbexample-impl/src/main/java/hu/faragocsaba/ejbexample/impl/EjbExampleImpl.java
package hu.faragocsaba.ejbexample.impl;
import javax.ejb.Stateless;
import hu.faragocsaba.ejbexample.api.EjbExampleApi;
@Stateless
public class EjbExampleImpl implements EjbExampleApi {
public String sayHello() {
return "Hello world from EJB!";
}
}
Maga a megvalósítás semmi extra; a @Stateless annotáció jelent csak újdonságot. Erről ugyancsak lesz még szó részletesebben; ezzel jelezzük a fordítónak (ill. inkább a futtatónak), hogy ez egy EJB, azon belül is állapot nélküli, azaz stateless. A másik lehetőség nem túl meglepő módon a @Stateful, ahol a rendszer elmenti az EJB állapotát, és a következő híváskor a korábbi részeredmény is rendelkezésre áll.
Kitérő: XML konfiguráció
Ebben a megoldásban két annotációt adtunk meg: az interfészben a @Local-t, a megvalósításban pedig a @Stateless-t. Ezt a megoldást invazívnak hívjuk (invasive), ami azt jelenti, hogy hozzá kellett nyúlnunk a forráshoz annak érdekében, hogy EJB-t kreáljunk belőle. Elméletileg ezt meg tudjuk oldani nem invazív módon is (non-invasive), XML konfigurációval. Az már ízlés kérdése, hogy melyiket használjuk, a személyes preferenciám az annotáció. Ha valaki az XML konfigurációs megoldást preferálja, azoknak álljon itt a megfelelő konfigurációs fájl. Ez esetben a fenti annotációkat töröljük ki
ejbexample/ejbexample-impl/src/main/resources/META-INF/ejb-jar.xml (nem része a példának!)
<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_2.xsd"
version="3.2">
<enterprise-beans>
<session>
<ejb-name>EjbExampleImpl</ejb-name>
<business-local>hu.faragocsaba.ejbexample.api.EjbExampleApi</business-local>
<ejb-class>hu.faragocsaba.ejbexample.impl.EjbExampleImpl</ejb-class>
<session-type>Stateless</session-type>
</session>
</enterprise-beans>
</ejb-jar>
A kliens
A kliens egy webalkalmazás: ejbexample-war. Lássuk először a pom.xml-t!
ejbexample/ejbexample-war/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>
<parent>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample</artifactId>
<version>1.0</version>
</parent>
<artifactId>ejbexample-war</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
</dependency>
<dependency>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
A lényeg:
- Itt is a fentiekhez hasonlóan beállítjuk a szülő komponenst.
- A csomaglás típusa war, így az örökölt maven-war-plugin beépülővel fog a build folyamat végrehajtódni.
- A javax.servlet-api függőség nincs, helyette a javaee-api-t használunk, ami tartalmazza a servletet is.
- Függőségként megjelenik a ejbexample-api, és vegyük észre, hogy az ejbexample-impl nincs közte. Ennek ellenére - ahogy azt látni fogjuk - mégis végre fog hajtódni a megvalósítás.
A megvalósítás maga egy servlet.
ejbexample/ejbexample-war/src/main/java/hu/faragocsaba/ejbexample/servlet\EjbExampleClient.java
package hu.faragocsaba.ejbexample.servlet;
import java.io.IOException;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import hu.faragocsaba.ejbexample.api.EjbExampleApi;
@WebServlet(urlPatterns = "/ejbexampleclient")
public class EjbExampleClient extends HttpServlet {
private static final long serialVersionUID = 1L;
@EJB
private EjbExampleApi ejbExampleApi;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write(ejbExampleApi.sayHello());
}
}
A servlet része már valószínűleg ismerős, az újdonság a @EJB annotáció. Ezzel jelöljük a futtatónak, hogy oda szeretnénk kérni az EjbExampleApi egy megvalósítását. A példányosítás (tehát az, hogy ha még nincs példány, akkor létrehozzon egyet, ha van, akkor azt újra felhasználja, vagy annak ellenére, hogy van, hozzon létre újat) még a futtató környezet felelőssége. Ha nem alkalmazás szerver futtatná, akkor a ejbExampleApi.sayHello() soron egy NullPointerException váltódna ki, de így nem fog. Ez a mechanizmus a már említett Dependency Injection, ill. Inversion of Control.
A komponensek összekapcsolása
Most már meg vannak a darabkák: össze kell őket kapcsolni. Ehhez a ejbexample-ear komponenst használjuk, ami egyetlen pom.xml-ből áll. (Ha a generálsá során az IDE egyéb könyvtárakat hozott volna létre, azokat töröljük ki.)
ejbexample/ejbexample-ear/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>
<parent>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample</artifactId>
<version>1.0</version>
</parent>
<artifactId>ejbexample-ear</artifactId>
<packaging>ear</packaging>
<dependencies>
<dependency>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-api</artifactId>
<version>${project.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-impl</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-war</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-ear-plugin</artifactId>
<configuration>
<modules>
<jarModule>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-api</artifactId>
</jarModule>
<ejbModule>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-impl</artifactId>
</ejbModule>
<webModule>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-war</artifactId>
</webModule>
</modules>
</configuration>
</plugin>
</plugins>
</build>
</project>
A függőségeket itt is megadjuk, majd sajnos külön fel kell sorolni a beépülőnél is. (A pom.xml nem a tömörségéről híres…) Figyeljük meg a csomagolás típusát: ear. Igazából lenne valahogy összevonni a szülő pom.xml-t ezzel, de mivel annak a csomagolási típusa kötelezően pom, ennek pedig ear, emiatt technikailag nem lehetséges.
Indítás
Az elkészített programot fordítsuk le a szokásos módon (mvn clean install) az ejbexample főkönyvtárból kiadva. Az eredmény ez lesz: ejbexample/ejbexample-ear/target/ejbexample-ear.ear.
Az indításhoz alkalmazás szerverre van szükségünk. Én a WindFly nevű alkalmazás szervert használom a példákhoz, aminek a korábbi neve JBoss volt. Töltsük le a https://wildfly.org/ oldalról (Downloads → a legfelső Application Server Distribution sorban levő zip fájlt töltsük le), és tömörítsük ki egy tetszőleges könyvtárba (pl. c:\programs\wildfly-18.0.1.Final\). Hozzuk létre a JBOSS_HOME környezeti változót, melynek tartalma a megfelelő könyvtár legyen, és győződjünk meg arról, hogy a JAVA_HOME rendesen be van állítva. Az eredmény ear fájlt másoljuk be a standalone/deployments könyvtárba (pl. c:\programs\wildfly-18.0.1.Final\standalone\deployments\ejbexample-ear.ear), majd indítusk el az alkalmazás szervert a bin\standalone.bat futtatásával. Nyissuk meg egy böngészőből a http://localhost:8080/ejbexample-war/ejbexampleclient/ oldalt. Ha minden jól csináltunk, akkor eredményül ezt kapjuk: Hello world from EJB!
Szerintem ez volt idáig a legbonyolultabb hello world alkalmazás!
Debuggolás
A hibakeresés alapvető fontosságú, és ez a nagyvállalati Java esetén nem magától értetődő. Elvileg két módszer lehetséges. Az egyik az, hogy a fejlesztő környezetben szerverként beépítjük az alkalmazás szervert, a másik pedig az, hogy kívülről csatlakozunk hozzá.
Az első módszer elvileg a következő: Eclipse-ben nyissuk meg a Server lapot (Window → Show View → Other… → Server → Servers), majd új szerver létrehozás (alapból felkínálja, ill. ha már van létrehozva, akkor jobb kattintás → New → Server), válasszuk ki a megfelelő típust (WindFly 18) a host name legyen localhost, adjunk egy akármilyen nevet, és adjuk meg az elérési útvonalat. Majd jobb kattintás a szerver nevén → Add and Remove Programs… → ott adjuk hozzá, amit szeretnénk. Elvileg indíthatjuk normál és debug módban is. Nálam ez nem működött. Ha valaki próbálkozni szeretne ezzel a megoldással, annak javaslom a https://www.baeldung.com/eclipse-wildfly-configuration oldalt.
A másik megoldás az a távolból történő csatlakozás. Ehhez nyissuk meg a bin/standalone.bat fájlt. Ott látunk egy ilyen sort: set DEBUG_MODE=false; ezt írjuk át erre: set DEBUG_MODE=false. Alatta láthatjuk a portot (set DEBUG_PORT_VAR=8787). Így indítsuk el az alkalmazás szervert. Majd az Eclipse-ben tegyük a következőt: Run → Debug Configurations… → kattintsunk duplán a Remote Ejb Application-re, válasszuk ki a Browse-re kattintva a megfelelő projektet (sajnos egyszerre csak egyet tudunk), a host legyen localhost, a port 8787, majd kattintsunk a Debug-ra. Az Eclipse-ben most már beállíthatunk breakpointokat, és ha kiváltjuk az eseményt, ami hatására az lefut (pl. betöltük a megfelelő oldalt), akkor meg fog állni.
A debuggolás itt kicsit másképp működik mint alkalmazás szerver nélkül; pl. a böngészőnek is elengedi a kérést, ha egy ideig nem kap választ, de ez belül is lehetséges. Tehát nem állhatunk egyetlen ponton a végtelenségig.
Local vs Remote
A fenti példában az interfészen a @Local annotáció szerepel. Ez azt jelenti, hogy csak az adott webalkalmazásból tudjuk meghívni (technikailag: az adott ear fájlban található forrásokból), távolból (egy tehát egy másik webalkalmazásból adott konténeren belül, vagy akár távolból) nem. Ahhoz, hogy távolból is elérhető legyen az EJB, a @Local annotációt erre kell cserélnünk: @Remote.
Programozástechnikailag szerver oldalon mindössze ennyi a változtatás (felületesen mondhatnánk azt is, hogy "könnyű"; egyezzünk ki mondjuk egy "egyszerűben"). Felmerülhet persze a kérdés, hogy ha tényleg csak ennyi a különbség, akkor miért vezették be egyáltalán a @Local annotációt; elég lenne mindent @Remote-tal annotálni, hiszen az a másikkal felülről kompatibilis. Igazából két fő oka is van annak, hogy a @Local-nak is van létjogosultsága:
1. Elképzelhető, hogy egy interfészt csak belülről szeretnénk elérni, és nem szeretnénk megmutatni a nagyvilágnak.
2. Az interfész kiajánlása a nagyvilág felé drága művelet. Ha van 1000 interfészünk, de csak 10-et szeretnénk kívülről is elérhetővé tenni, akkor mind az ezret kiajánlani aránytalanul erőforrás igényesebb a másik megoldásnál.
Lássunk most egy Remote interfész példát! A szerver kódja tehát egyetlen apró kivétellel megegyezik a fentiekkel; ez a kivétel pedig - ahogy már volt róla szó - az interfészen a @Local annotáció helyett a @Remote, a következőképpen:
package hu.faragocsaba.ejbexample.api;
import javax.ejb.Remote;
@Remote
public interface EjbExampleApi {
String sayHello();
}
Ha ezt elindítjuk, akkor a http://localhost:8080/ejbexample-war/ejbexampleclient/ oldalon ugyanazt kell látnunk. A kliens oldal viszont itt már trükkösebb. Először lássunk egy általános példát, amelyben nem (vagy nem feltétlenül) az adott alkalmazás szerverben futó alkalmazás éri el az EJB-t, hanem akárhonnan el tudjuk érni. Ehhez két függőséget kell felvenni: egyrészt magát az interfészt, másrészt pedig az adott alkalmazásszerver kliens könyvtárát, jelen esetben a következőképpen:
<dependencies>
<dependency>
<groupId>org.wildfly</groupId>
<artifactId>wildfly-ejb-client-bom</artifactId>
<version>18.0.1.Final</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>hu.faragocsaba</groupId>
<artifactId>ejbexample-api</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
A hívás JNDI lekéréssel történik, a következőképpen:
package hu.faragocsaba.ejbstandaloneclient;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import hu.faragocsaba.ejbexample.api.EjbExampleApi;
public class EjbStandaloneClient {
public static void main(String[] args) throws NamingException {
Properties jndiProperties = new Properties();
jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory");
jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
jndiProperties.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
jndiProperties.put("jboss.naming.client.ejb.context", true);
Context ctx = new InitialContext(jndiProperties);
EjbExampleApi ejbExampleApi = (EjbExampleApi)ctx.lookup("ejb:ejbexample-ear/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi");
System.out.println(ejbExampleApi.sayHello());
}
}
A ctx.lookup sorban a paramétert legegyszerűbben a WildFly naplójából tudjuk kiolvasni. Indításkor nálam a következőket írja ki:
java:global/ejbexample-ear/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi
java:app/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi
java:module/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi
java:jboss/exported/ejbexample-ear/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi
ejb:ejbexample-ear/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi
java:global/ejbexample-ear/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl
java:app/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl
java:module/EjbExampleImpl
Itt az 5. sorban levő részt kell odamásolni. Ha szokásos módon elkészítjük a programot is elindítjuk, akkor ha mindent jól csináltunk, és a valóban a helyi gépen fut a WildFly a szerverrel, akkor egy hosszabb naplózási bevezető után a Hello world from EJB! szöveget kell látnunk. (A példa elkészítésében a https://www.baeldung.com/wildfly-ejb-jndi oldal segített.)
Ha ugyanabban a WildFly-ban fut a kliens mint a szerver, akkor megoldhatjuk a fenti módon is, de itt van egy egyszerűsítési lehetőségünk: használhatunk annotációt. Készítsünk az ejbexample-war mintájára egy önálló webalkalmazást, legyen a neve mondjuk ejbclient-war, tartalmazza a fenti függőségeket (javaee-api és ejbexample-api), a teljes kód feleljen meg az eredetinek, egyetlen eltéréssel: az @EJB annotációnak paraméterként adjuk meg az elérési útvonalat, jelen esetben a következőképpen:
@EJB(lookup = "ejb:ejbexample-ear/hu.faragocsaba-ejbexample-impl-1.0/EjbExampleImpl!hu.faragocsaba.ejbexample.api.EjbExampleApi")
private EjbExampleApi ejbExampleApi;
Fordítsuk le és másoljuk target/ejbclient-war.war fájlt a standalone/deployments könyvtárba, indítsuk el ha nem fut, majd töltsük be a http://localhost:8080/ejbclient-war/ oldalt. Ha mindent jól csináltunk, ismét megjelenik a Hello world from EJB! szöveg.
Stateless vs. Stateful Session Bean
Áttekintés
Kétféle session bean-t különböztetünk meg: állapotmenteset (stateless) és olyat, ami megjegyzi az állapotát (stateful). Az alkalmazás szerver szempontjából az állapotmentes az egyszerűbb, ugyanis ebben az esetben jóval egyszerűbb megvalósítani a skálázhatóságot vagy a terheléseloszlást. Ez esetben elképzelhető ugyanis az, hogy fizikailag - a pillanatnyi terheléstől függően - egy-egy felhasználói folyamat (session) interakcióit más és más szerver szolgálja ki. Az, hogy tényegesen hány ilyen session bean jön létre összesen az a konténerre van bízva, ami a terheléstől függően növelhető vagy csökkenheti az állapotmentes session bean-ek számát. Ilyen értelemben egy ilyen bean-nek két állapota van:
- Nem létező (does not exist): ez azt is jelenti, hogy ha már nincs szükség egy stateless session bean-re (pl. mert már egy jó ideje tartósan lecsökkent a terhelés), akkor nem egyszerűen "jegeli" a konténer, hanem törli.
- Kész (ready): a konténer tetszőleges számú stateles session bean példányt tarthat ebben az állapotban. Nyilván minél nagyobb ezek száma, a kliensek kiszolgálása annál gyorsabb (mivel nem a híváskor kell létrehozni), ugyanakkor a memóriahasználat is értelemszerűen annál nagyobb, így a sateless session bean-ek számának megfelelő meghatározása az alkalmazásszerverek egy kritikus része.
Ezzel ellentétben az állapotot megjegyző változat - ahogy a nevében is benne van - megjegyzi a korábbi állapotot. Ebből tehát kliensenként egyet kell létrehozni. Ez jelentősen lecsökkenti az erőforrás gazdálkodást, mivel problémás átvinni egyik alkalmazás szerverről a másikra, és elképzelhető, hogy több alkalmazás szerver esetén aránytalan lesz a "beragadt", de még élő stateful session bean-ek száma. Mivel a "beragadsára" is gondolni kell, az állapot átmenet itt egy fokkal bonyolultabb.
- Nem létező: mint fent.
- Kész: mint fent, azzal a különbséggel, hogy ennek megmaradnak az attribútum értékei (a másiknál erre nem lehet számítani).
- Passzív (passive): ha a kliens még nem fejezte be a folyamatot, de két lépés között hosszú idő telik el, akkor annak érdekében, hogy optimalizálja az erőforrásait, az alkalmazás szerver ebbe az állapotba teszi (kb. szerializálja és lemezre menti, ahonnan szükség esetén előhívja). Ha a kliens várakozása meghalad egy határt (timeout), akkor az alkalmazás szerver törli a stateful session bean-t. Ha ez nem következne be, akkor egy idő után az összes erőforrást a tartósan beragadt stateful session bean-ek használnák fel.
A web alapvetően állapotmentes, így azt legjobban a stateless session bean modellezi.
A stateless session bean
A bevezető példa egy stateless session bean volt. Lássunk most mégis egy másikat is, ami csinál is valamit: ez a téma klasszikusa, az összeadó. Folytathatjuk a bevezetőben elkezdett példát, de újat is készíthetünk. (Abban az esetben ha folytatjuk, és a példában a servlet a gyökérre válaszolt (@WebServlet(urlPatterns = "/")), akkor azt módosítsuk.) Az interfészhez (ejbexample-api) adjuk hozzá a következőt:
package hu.faragocsaba.ejbexample.api;
import javax.ejb.Local;
@Local
public interface Adder {
int add(int a, int b);
}
A megvalósítás (ejbexample-impl):
package hu.faragocsaba.ejbexample.impl;
import javax.ejb.Stateless;
import hu.faragocsaba.ejbexample.api.Adder;
@Stateless
public class AdderImpl implements Adder {
public int add(int a, int b) {
return a + b;
}
}
A valóságban képzeljük el azt, hogy az összeadás helyett itt egy igen bonyolult, erőforrás igényes művelet van.
A kliens oldalon hozzunk létre egy HTML oldalt (/ejbexample-war/src/main/webapp/add.html):
<html><body>
<form name="add" action="add" method="GET">
a = <input type="number" name="a"><br>
b = <input type="number" name="b"><br>
<input type="submit" value="Calculate">
</form>
</body></html>
Végül ugyancsak az ejbservlet-war komponensben servletként valósítsuk meg az EJB hívást:
package hu.faragocsaba.ejbexample.servlet;
import java.io.IOException;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import hu.faragocsaba.ejbexample.api.Adder;
@WebServlet(urlPatterns = "/add")
public class Add extends HttpServlet {
private static final long serialVersionUID = 1L;
@EJB
private Adder adder;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
int a = Integer.parseInt(request.getParameter("a"));
int b = Integer.parseInt(request.getParameter("b"));
response.getWriter().write("" + a + " + " + b + " = " + adder.add(a, b));
}
}
A http://localhost:8080/ejbexample-war/add.html oldalt betöltve megadhatunk két számot, majd a Calculate gombra kattintva eredményül az összeget kapjuk.
Amint arról volt szó, az egyes EJB-knek állapotaik és állapot átmeneteik vannak. Létre tudunk hozni olyan függvényeket, amelyek akkor hajtódnak végre, amikor kiváltódik egy átmenet. Megfelelő annotációval kell ellátni. Módosítsuk a bevezető példát az alábbi módon:
@Stateless
public class EjbExampleImpl implements EjbExampleApi {
public String sayHello() {
return "Hello world from EJB!";
}
@PostConstruct
public void construct() {
System.out.println("EjbExampleImpl::postCostruct()");
}
}
A stateless session bean esetében két ilyne függvény tdunk létrehozni:
- @PostConstruct: akkor hívódik meg, miután létrejött a példány. Ha lefuttatjuk a példaprogramot, akkor naplóbejegyzésben megjelenik a megfelelő szöveg. Figyeljük meg, hogy többszöri újratöltést követően nem íródik ki újra, azaz fizikailag ugyanazt a példányt használja újra.
- @PreDestroy: akkor fut le, mielőtt megszünteti az alkalmazásszerver a példányt. Ez akkor következik be, ha tartósan lecsökkent a terhelés.
A stateful session bean
A stateful session beanre egy növelő példát valósítunk meg: a megadott számokat sorban hozzáadja az aktuális összeghez. Itt az interfész a következő (ejbexample-api):
package hu.faragocsaba.ejbexample.api;
import javax.ejb.Local;
@Local
public interface Incrementer {
int increment(int a);
}
A megvalósítás (ejbexample-impl):
package hu.faragocsaba.ejbexample.impl;
import javax.ejb.Stateful;
import hu.faragocsaba.ejbexample.api.Incrementer;
@Stateful
public class IncrementerImpl implements Incrementer {
private int sum = 0;
public int increment(int a) {
sum += a;
return sum;
}
}
A kliens (ejbexample-war) most egy servlet lesz, nincs külön html:
package hu.faragocsaba.ejbexample.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import hu.faragocsaba.ejbexample.api.Incrementer;
@WebServlet(urlPatterns = "/increment")
public class Increment extends HttpServlet {
private static final long serialVersionUID = 1L;
@EJB
private Incrementer incrementer;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String aStr = request.getParameter("a");
int a = 0;
if (aStr != null) {
a = Integer.parseInt(aStr);
}
PrintWriter out = response.getWriter();
int incremented = incrementer.increment(a);
out.println("<html><body>");
out.println("Actual sum = " + incremented + "<br>");
out.println("<form action=\"increment\" method=\"GET\">");
out.println("a = <input type=\"number\" name=\"a\"><br>");
out.println("<input type=\"submit\" value=\"Increment\">");
out.println("</form>");
out.println("</body></html>");
}
}
Az első betöltésnél még nincs a paraméter, amivel növeljük az összeget, ezt le kell kezelnünk. A http://localhost:8080/ejbexample-war/increment oldalt betöltve majd számokat beírva megkapjuk az addig beírtak összegét.
Ugyanezt a példát stateless formában nem tudjuk megvalósítani, mivel nem lehetünk biztosak abban, hogy a következő lépésben ugyanaz a szerver fogja kiszolgálni a kérésünket.
A stateful session bean esetén a következő annotációkat használhatjuk az állapot átmenetek kezelésére:
- @PostConstruct: ld. stateless.
- @PreDestroy: ld. stateless.
- @PrePassivate: akkor hívódik meg, amikor sokat kell várni a kliensre, és az állapot lementődik egy másodlagos tárolóba.
- @PostActivate: akkor hívódik meg, amikor a másodlagos tárolóból visszakerül a memóriába.
MDB
Az üzenet vezérelt bean (Message Driven Bean, MDB) olyan EJB, ami egy üzenet hatására aktiválódik. Ez tehát alapvetően egy aszinkron aktiválási mód: a hívó (azaz üzenet küldő) nem blokkolódik, amíg a hívott (az üzenet fogadója) feldolgozza az üzenetet. Persze ez azt is jelenti, hogy a kommunikáció alapvetően egyirányú.
Amint arról már volt szó, JMS (Java üzenet szolgáltatás, Java Messaging Service) kétféle modellt definiál:
- Queue (cső): ebben az esetben pontosan egy kliens dolgozza fel az üzenetet Ha több kliens várakozik, akkor a feldolgozás nem meghatározott.
- Topic (téma): ez esetben akárhány kliens feldolgozhatja, de alapvetően csak akkor, ha a küldés pillanatában "figyelnek". (Ez alól is léteznek kivételek, pl. létezik az ún tartós (durable) feliratkozás fogalma, ami azt jelenti hogy a kliens akkor is megkapja utólag az üzenetet, ha annak keletkezésének a pillanatában nem futott.)
A megvalósítása igencsak nehézkes; elég sok időm ráment, mire sikerült egy épkézláb működő példát összehozni, a neten ugyanis nem találtam ilyet. Az alábbi oldalak segítettek a példa elkészítésében:
- https://www.baeldung.com/ejb-message-driven-beans
- https://www.tutorialspoint.com/ejb/ejb_message_driven_beans.htm
- http://www.mastertheboss.com/javaee/ejb-3/developing-mdb-with-jboss-as-7
- https://developers.redhat.com/quickstarts/eap/helloworld-mdb/
- https://cleanprogrammer.net/how-to-configure-jms-in-wildfly/
- https://stackoverflow.com/questions/32473361/javax-naming-namenotfoundexception-connectionfactory-with-wildfly-9-0-1-final
- https://wildfly.org/news/2017/10/03/Messaging-features/
- https://github.com/wildfly/quickstart/tree/master/helloworld-mdb
- https://docs.jboss.org/author/display/WFLY10/Message+Driven+Beans+Controlled+Delivery
- https://theopentutorials.com/tutorials/java-ee/ejb3/mdb/mdb-example/
- https://docs.oracle.com/javaee/6/tutorial/doc/bnbpk.html
Először lássunk a fogadót! Az egyszerűség érdekében én ejbexample-impl projektbe tettem bele; ez esetben nem kellett újabb függőséget felvennem:
package hu.faragocsaba.ejbexample.mdb;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
@MessageDriven(name = "MdbExample", activationConfig = {
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
@ActivationConfigProperty(propertyName = "destination", propertyValue = "java:/jms/queue/MdbExample"),
@ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge")
})
public class MdbExample implements MessageListener {
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("Received message: " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
A küldő az ejbexample-war komponensbe került:
package hu.faragocsaba.ejbexample.servlet;
import java.io.IOException;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/send")
public class MdbClient extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String message = request.getParameter("message");
if (message == null) {
message = "Hello MDB world!";
}
Connection connection = null;
try {
Context context = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory) context.lookup("/ConnectionFactory");
Queue queue = (Queue)context.lookup("java:/jms/queue/MdbExample");
connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer publisher = session.createProducer(queue);
connection.start();
TextMessage textMessage = session.createTextMessage(message);
publisher.send(textMessage);
response.getWriter().println("Message sent: " + message);
} catch (Exception e) {
response.getWriter().println("Error sending message: " + e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
Ezt persze lehetne szebben is megoldani, pl. a try with resources megoldással, de a példának ebben a formában is megteszi. A küldő és a fogadó oldalon is találhatunk jóváhagyásra vonatkozó információt (Session.AUTO_ACKNOWLEDGE ill. Auto-acknowledge). Az üzenetek küldése tranzakcióban történik. Ha a tranzakció sikertelen, akkor a fogadást vissza kell utasítani, a konzisztens állapot fenntartása érdekében. Pl. ha az üzenet azt tartalmazza, hogy "utalj át tízezer forintot erre és erre a számlára", de a számlámon csak ötezer forint van, akkor jelezni kell a küldőnek, hogy nem tudtuk feldolgozni, tehát pl. ott is álljon vissza a küldés előtti állapot vagy esetleg próbálkozzon a küldéssel később. Ez egy nagy területet nyit meg: pl. általában be lehet állítani azt, hogy ez esetben legfeljebb hányszor ill. milyen gyakorisággal próbálkozzon a kliens az újraküldéssel, ill. még számos egyéb adat megadható. Ezek túlmutatnak jelen leírás keretein.
Kipróbálni a fenti példát még nem tudjuk, ugyanis a WildFly alapból nem ismeri az MDB-t. Ahhoz, hogy használni tudjuk, ún. full módban kell indítani, valamint a queue-t is be kell állítani. A WildFly az ActiveMQ üzenetkezelő rendszert használja, melyről már volt szó. A $JBOSS_HOME/standalone/configuration/standalone-full.xml fájlt kell szerkesztenünk. A <jms-queue name="MdbExample" entries="java:/jms/queue/MdbExample"/> sort kell beleírnunk a következőképpen:
<server xmlns="urn:jboss:domain:10.0">
[...]
<profile>
[...]
<subsystem xmlns="urn:jboss:domain:messaging-activemq:8.0">
<server name="default">
[...]
<jms-queue name="DLQ" entries="java:/jms/queue/DLQ"/>
<jms-queue name="MdbExample" entries="java:/jms/queue/MdbExample"/>
<connection-factory name="InVmConnectionFactory" entries="java:/ConnectionFactory" connectors="in-vm"/>
[...]
</server>
</subsystem>
[...]
</profile>
[...]
</server>
Indítás:
standalone.bat -c standalone-full.xml
Most hajtsuk végre a deploy műveletet a szokásos módon, majd nyissuk meg egy böngészőből a http://localhost:8080/ejbexample-war/send?message=apple oldalt. Ha mindent jól csináltunk, a naplóban megjelenik a Received message: apple szöveg.
A stateless session beanekhez hasonlóan az MDB-knek is két logikai állapotuk van: nem létező és kész. Az alkalmazásszerver tetszőleges számú MDB-t létrehozhat, a pillanatnyi terhelés függvényében.
Az állapot átmeneteket ugyanazokkal az annotációkkal tudjuk vezérelni, mint a stateless session beanek esetén: @PostConstruct és @PreDestroy.
Konténer vezérelt perzisztencia
A Hibernate bemutatásánál mi magunk gondoskodtunk az EntityManager betöltéséről és a tranzakciókezelésről. Az Enterprise Java-ban ezt is rábízhatjuk az alkalmazás szerverre, ehhez viszont komoly előkészületeket kell tennünk. (Megjegyzés: a lentieket számos forrásból kellett "összegereblyéznem", mivel nem találtam egyetlen teljes, működő példát sem. Több napom ráment, mire kiderítettem, és így sem tökéletes; majd látni fogjuk, miért. Szóval egy kifejezetten macerás részhez készüljünk most fel.)
Az adatbázis létrehozása
Mielőtt hozzákezdünk, olvassuk el ill. ismételjük át a következő oldalak bizonyos részeit:
- Adatbázisok: olvassuk el a MySQL adatbázis szakaszt, és hozzuk létre az ottani adatbázist. Azt fogjuk ebben a példában használni.
- Java külső könyvtárak: fussuk át az Adatbázis kezelés részt, és azon belül olvassuk el részletesebben a JCA és ORM szakaszt.
Ezen a ponton tehát van egy működő MySQL adatbázisunk.
Az alkalmazásszerver beállítása
Következő lépésben fel kell készíteni a WildFly alkalmazás szervert az adatbázis kapcsolatra. Ehhez az alábbi lépéseket kell végrehajtanunk.
A MySQL meghajtó letöltése
Az adatbázis kapcsolat létrehozásához szükség van adatbázis specifikus meghajtó könyvtárra. A MySQL esetében ez a MySQL Connector. Ezt töltsük le az alábbi oldalról: https://dev.mysql.com/downloads/connector/j/. A legfrissebbet közvetlenül letölthetjük, a korábbi verziókat pedig az archívumban (Archives) találjuk. A legördülő menüben a platformfüggetlen (Platform independent) verziót töltsük le, azon belül a zip verziót. A letöltéshez be kell jelentkeznünk Oracle azonosítónkkal, amit ingyenesen létre tudunk hozni. A letöltéskor még több kérdést is feltesz, szóval igencsak megnehezítik a dolgunkat.
Az eredmény egy zip fájl, melynek neve mysql-connector-java-8.0.15.zip, a megfelelő verzióval természetesen. Ebből kell kimásolnunk a mysql-connector-java-8.0.15\mysql-connector-java-8.0.15.jar fájlt valahova, pl. a d:\ gyökérbe. A zip fájl többi részére nincs szükségünk.
A meghajtó telepítése
Az alkalmazásszerverben is be kell állítani a MySQL kapcsolatot. Ezt kétféleképpen tehetjük meg: egyrészt a jboss-cli program segítségével, másrészt kézzel. A jboss-cli módszer a biztosabb, de leírom azt is, hogy az egyes parancsok hatására mi történik, így rendelkezésünkre áll majd az információ ahhoz, ha kézzel szeretnénk beállítani.
Indítsuk el a WildFly-t! Mivel a korábbi, üzenetküldést is tartalmazó példát folytatjuk, én teljes módban hajtottam végre:
$JBOSS_HOME\bin>standalone.bat -c standalone-full.xml
Egy másik konzolból indítsuk el a jboss-cli programot, majd kapcsolódjunk a futó JBoss-hoz:
$JBOSS_HOME\bin>jboss-cli.bat
You are disconnected at the moment. Type 'connect' to connect to the server or 'help' for the list of supported commands.
[disconnected /] connect localhost:9990
[standalone@localhost:9990 /]
Adjuk hozzá a korábban letöltött és kicsomagolt meghajtót a követkeő parancsokkal:
[standalone@localhost:9990 /] module add --name=com.mysql.driver --dependencies=javax.api,javax.transaction.api --resources=d:\mysql-connector-java-8.0.15.jar
[standalone@localhost:9990 /] :reload
{
"outcome" => "success",
"result" => undefined
}
Ennek eredményeképpen létrejön a $JBOSS_HOME\modules\com\mysql\driver\main\ könyvtár, ami két fájlt tartalmaz. Az egyik a mysql-connector-java-8.0.15.jar, a másik pedig a module.xml a következő tartalommal:
<?xml version='1.0' encoding='UTF-8'?>
<module xmlns="urn:jboss:module:1.1" name="com.mysql.driver">
<resources>
<resource-root path="mysql-connector-java-8.0.15.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
</dependencies>
</module>
Ezt elvileg kézzel is létrehozhatjuk, de pl. nálam úgy pont nem működött.
A következő parancsokkal tudjuk ellenőrizni a telepített meghajtókat (nem kötelező kiadni):
[standalone@localhost:9990 /] /subsystem=datasources:installed-drivers-list
[standalone@localhost:9990 /] /subsystem=datasources:read-resource(recursive=true)
A meghajtó és a szerver beállítása
A meghajtót be is kell állítani a megfelelő konfigurációs fájlban:
[standalone@localhost:9990 /] /subsystem=datasources/jdbc-driver=mysql/:add(driver-module-name=com.mysql.driver,driver-name=mysql,jdbc-compliant=false,driver-class-name=com.mysql.jdbc.Driver)
{"outcome" => "success"}
A szervert a következőképpen állítsuk be (a megfelelő azonosítót és jelszót megadva):
[standalone@localhost:9990 /] data-source add --name=testdb --jndi-name=java:/jdbc/testdb --driver-name=mysql --connection-url=jdbc:mysql://127.0.0.1:3306/testdb --user-name=csaba --password=farago
[standalone@localhost:9990 /] :reload
{
"outcome" => "success",
"result" => undefined
}
Ennek eredményeképpen a $JBOSS_HOME\standalone\configuration\standalone-full.xml fájl tartalma a következőképpen változott:
<server xmlns="urn:jboss:domain:10.0">
...
<profile>
...
<subsystem xmlns="urn:jboss:domain:datasources:5.0">
<datasources>
...
<datasource jndi-name="java:/jdbc/testdb" pool-name="testdb">
<connection-url>jdbc:mysql://127.0.0.1:3306/testdb</connection-url>
<driver>mysql</driver>
<security>
<user-name>csaba</user-name>
<password>farago</password>
</security>
</datasource>
<drivers>
...
<driver name="mysql" module="com.mysql.driver">
<driver-class>com.mysql.jdbc.Driver</driver-class>
</driver>
</drivers>
</datasources>
</subsystem>
...
</profile>
...
</server>
Az új sorok a <datasource jndi-name="java:/jdbc/testdb" pool-name="testdb">…</datasource> és <driver name="mysql" module="com.mysql.driver">…</driver>.
A beállítás során figyeljük a logokat, ill. célszerű újra indítani, és megnézni, hogy ekkor látunk-e hibát benne. Nekem igen hosszú ideig tartott elérnem azt, hogy legalább itt ne jöjjön elő hiba.
A példaprogram
A keret
Ezen a ponton elvileg van egy előkészített adatbázisunk és egy előkészített alkalmazás szerverünk. Készítsük el a példaprogramot! A fent megkezdettet folytatjuk. Mivel a perzisztencia része a javaee-api könyvtárnak, plusz könyvtárt nem kell hozzáadnunk. Valamint a MySQL specifikus részeket elfedi az alkalmazás szerver ill. a Hibernate, azt sem kell hozzáadnunk. Ezzel az alkalmazásunk továbbra is kicsi marad, ráadásul nem lesz benne adatbázis specifikus kód.
Az interfész
Az interfész az ejbexample-api komponensbe kerül:
package hu.faragocsaba.ejbexample.api;
import java.util.List;
import javax.ejb.Local;
@Local
public interface JpaExampleApi {
void clearDb();
void addPersons();
void updatePersons();
List<String> getPersons();
}
Az interfész nem tartalmazza az entitásokat. Nem célszerű kiajánlani ezeket a külvilág felé, az maradjon csak belülről látható. Ha mégis ki szeretnénk ajánlani, akkor ún. üzleti objektumokat (businnes object, BO) érdemes készítenünk. Ez szélsőséges esetben akár teljes egészében megegyezhet az entitásokkal, bár a gyakorlatban kisebb-nagyobb mértékben eltér tőle. Jelen példában teljesen általános Java osztályokat tartalmaz az interfész: listát, Sringet. A clearDb() megvalósítása törölni fogja az adatbázist, az addPersons() néhány személyt ad hozzá, címmel együtt, az updatePersons() ezeket módosítja (itt láthatunk majd módosítást és törlést is), a getPersons() pedig a végeredményt adja vissza.
Az entitások
Az entitások az ejbexample-impl komponensbe kerülnek:
package hu.faragocsaba.ejbexample.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String name;
private Integer age;
public Person() {}
public Person(String name, Integer age, Address address) {
super();
this.name = name;
this.age = age;
this.address = address;
}
@ManyToOne
@JoinColumn(name="addressid")
private Address address;
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 + ", address=" + address + "]";
}
}
Ill.:
package hu.faragocsaba.ejbexample.entity;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String country;
private String town;
private String street;
public Address() {}
public Address(String country, String town, String street) {
super();
this.country = country;
this.town = town;
this.street = street;
}
@OneToMany(mappedBy="address")
private List<Person> persons;
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 + "]";
}
}
A megvalósítás
A megvalósítás szintén az ejbexample-impl komponensbe kerül.
package hu.faragocsaba.ejbexample.impl;
import java.util.ArrayList;
import java.util.List;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import hu.faragocsaba.ejbexample.api.JpaExampleApi;
import hu.faragocsaba.ejbexample.entity.Address;
import hu.faragocsaba.ejbexample.entity.Person;
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class JpaExampleImpl implements JpaExampleApi {
@PersistenceContext(unitName = "JpaExample")
private EntityManager entityManager;
public void clearDb() {
entityManager.createQuery("DELETE Person").executeUpdate();
entityManager.createQuery("DELETE Address").executeUpdate();
}
public void addPersons() {
Address viragUtca = new Address("Hungary", "Szeged", "Virág utca 2");
entityManager.persist(viragUtca);
Address napUtca = new Address("Hungary", "Budapest", "Nap utca 5");
entityManager.persist(napUtca);
entityManager.persist(new Person("Gyuri", 42, viragUtca));
entityManager.persist(new Person("Sanyi", 38, viragUtca));
entityManager.persist(new Person("Béla", 50, napUtca));
}
public void updatePersons() {
List<Address> addresses = entityManager.createQuery("SELECT a FROM Address a WHERE a.town = :town").setParameter("town", "Szeged").getResultList();
Address address = addresses.get(0);
address.setTown("Kecskemét");
Person person = address.getPersons().get(0);
entityManager.remove(person);
}
public List<String> getPersons() {
List<Person> persons = entityManager.createQuery("SELECT p FROM Person p").getResultList();
List<String> result = new ArrayList<String>();
for (Person person: persons) {
result.add(person.toString());
}
return result;
}
}
Talán kissé erőltetett a példa, de láthatunk benne hozzáadást, módosítást, törlést és lekérdezést is. Ebben a példában kihasználjuk azt, hogy mindegyik entitás vezérelt (managed) állapotban van, azaz bármilyen módosítás rajta megjelenik az adatbázisban. Ha nem ez lenne az állapot, akkor az entityManager.merge(object) függvénnyel tudnánk azt vezéreltté tenni. Ez esetben az esetleges módosítások is elvesznek, és felveszi az objektum az adatbázisban található értékeket.
A perzisztencia leíró
Még létre kell hozni a kapcsolatot a kód és az adatbázis között. Az ejbexample-impl komponensben hozzuk létre az src/main/resources/META-INF/persistence.xml fájlt az alábbi tartalommal:
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="JpaExample" transaction-type="JTA">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<jta-data-source>java:/jdbc/testdb</jta-data-source>
<properties>
<property name="hibernate.archive.autodetection" value="class"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
Nincs adatbázis specifikus rész benne; a java:/jdbc/testdb hivatkozással oldja fel az alkalmazás szerver. A Hibernate "natív" használatával ellentétben itt nem kell felsorolni <class> tag-ekben az osztályokat, az az alkalmazás szerver automatikusan megteszi. Ám ha az entitások nem az ejbexample-impl komponensben lennének, hanem mondjuk az ejbexample-api-ban, akkor fel kellene sorolnunk.
A kliens
A kliens az ejbexample-war komponensbe kerül:
package hu.faragocsaba.ejbexample.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import hu.faragocsaba.ejbexample.api.JpaExampleApi;
@WebServlet(urlPatterns = "/jpa")
public class JpaClient extends HttpServlet {
private static final long serialVersionUID = 1L;
@EJB
private JpaExampleApi jpaExample;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
jpaExample.clearDb();
jpaExample.addPersons();
jpaExample.updatePersons();
PrintWriter out = response.getWriter();
out.println("Persons:");
for (String person: jpaExample.getPersons()) {
out.println(person);
}
}
}
Itt tehát sorra meghívjuk a függvényeket, majd kiírjuk az eredményt.
Telepítsük az alkalmazást a megszokott módon. Ha mindent jól csináltunk, a http://localhost:8080/ejbexample-war/jpa oldal betöltésekor az alábbi eredményt kell kapnunk:
Persons:
Person [id=38, name=Sanyi, age=38, address=Address [id=16, country=Hungary, town=Kecskemét, street=Virág utca 2]]
Person [id=39, name=Béla, age=50, address=Address [id=17, country=Hungary, town=Budapest, street=Nap utca 5]]
A komplexitásból már szint következik, hogy kicsi annak az esélye, hogy egyből működjön.
Állapot átmenetek
Az EJB-khez hasonlóan az entitásoknak is vannak állapot átmenetei, melyet a következő annotációval ellátott eljárásokkal tudunk vezérelni:
- @PrePersist: akkor hívódik meg, mielőtt az entitás az adatbázisba kerülne.
- @PostPersist: akkor hívódik meg, miután az entitás bekerült az adatbázisba.
- @PreRemove: akkor hívódik meg, mielőtt az entitás törlődne az adatbázisból.
- @PostRemove: akkor hívódik meg, miután az entitás törlődött az adatbázisból.
- @PreUpdate: akkor hívódik meg, mielőtt az entitás módosul az adatbázisban.
- @PostLoad: akkor hívódik meg, miután egy adatbázis rekord betöltődött egy entitásba.
Elméleti áttekintés
A példa után nézzünk némi elméleti áttekintést.
Az entitások életciklusa
Az entitásoknak is van egy jól meghatározott életciklusa. A lehetséges állapotok az alábbiak:
- New (új): ebbe az állapotba kerül az objektum, amikor létrehozzuk. De amíg nem tud róla az entityManager, addig nem is tudja kezelni.
- Managed (vezérelt): ez az az állapot, amikor az entityManager vezéreli az entitást, azaz ha pl. megváltozik valamelyik attribútuma, akkor gondoskodik arról, hogy a változás az adatbázisban is megjelenjen, ill. ha egy másik komponens változtatja meg, akkor az adott programban is megjelenik a változás. A következő módokon kerülhet egy entitás ebbe az állapotba:
- az új entitás példány az entityManager.persist(object) hívás hatására;
- adatbázis lekérdezéssel, ha a lekérdezés az entityManager-en keresztül történik, pl. egy entityManager.createQuery(…).getResultList() hívás hatására.
- leválasztott entitások esetén az entityManager.merge(object) hívás hatására. Ez esetben a mezőket beállítja azokra az értékekre, amelyek az adatbázisban vannak.
- Detached (leválasztott): előfordulhat, hogy egy entitás "kikerül" az entityManager "látóköréből", azaz lecsatolódik. Ez történhet pl. rétegek közötti szerializálás és deszerializálás hatására. Innen az entityManager.merge(object) hatására kerülhet ismét vezérelt állapotba.
- Removed (eltávolított): eltávolításkor kerül ebbe az állapotba, és ez egy köztes állapot a létező és a ténylegesen törölt között. Amiatt van jelentősége, mert a törlés fizikailag a tranzakció végén következik be.
Deklaratív tranzakció demarkáció
A megvalósítás osztálya a @TransactionAttribute(TransactionAttributeType.REQUIRED) annotációval is el van látva. Ezt az annotációt az egyes függvényekre is rátehetjük, felülírva az osztály szintű annotációt. Ezzel a deklaratív tranzakció demarkációt valósíthatjuk meg. A példában ennek önmagában nincs jelentősége, mert ez az alapértelmezett, viszont fontos megismernünk, hogy milyen lehetőségek vannak még, és ezek mit jelentenek:
- REQUIRED: a műveletnek tranzakción belül kell végrehajtódni. Ha már van tranzakció, akkor azon belül történik a futás, ha nincs, akkor létrehoz egyet.
- SUPPORTS: ha egy tranzakción belül vagyunk, akkor a művelet annak része lesz, egyébként tranzakción kívül fog lefutni.
- MANDATORY: a tranzakció kötelező, ha tehát nem tranzakción belül történt a hívás, akkor hibával leáll.
- NEVER: az előző fordítottja: akkor áll le hibával, ha van tranzakció.
- NOT_SUPPORTED: ha van tranzakció, akkor azt leállítja, és tranzakción kívül hajtja végre a lépéseket.
- REQUIRES_NEW: mindenképpen új tranzakciót hoz létre. Ha már fut egy tranzakció, akkor azt felfüggeszti; ha nem, létrehoz egyet.
Ez kisebb rugalmasságot ad, mintha mindezt kézzel hajtanánk végre, így a kódot ennek megfelelően kell szervezni. Pl. egy eljárásnak teljes egészében benne kell lennie egy tranzakcióban. A konténer vezérelt tranzakció kezelésben a "kézi" megoldás nem engedélyezett, így nem tudjuk meghívni pl. a commit(), a rollback() vagy a getUserTransaction() függvényeket.
Integrációs teszt
Elvileg lehetséges integrációs tesztet végrehajtani beágyazott (embedded) WildFly alkalmazásszerverrel. Erről itt találunk leírást: http://www.mastertheboss.com/jboss-frameworks/arquillian/arquillian-tutorial. A példaprogram csak 4.11-es JUnit verzióval működött, a sokkal elterjedtem 4.12-vel nem, és nem sikerült működésre bírnom. Ráadásul a WildFly-nak csak a 8-as verzióját támogatta. Néhány nekifutásra nem működött, és mivel ezt nem tartom annyira lényegesnek, részletesebben nem jártam utána.
EJB szerverek
A fentiekben a WildFly alkalmazásszervert használtuk. Ezen kívül még van néhány másik alkalmazás szerver is:
- GlassFish: ez valójában a referencia megvalósítás, melyről az Oracle lemondott, és Jakarta EE néven fut tovább, az Eclipse Foundation gondozásában. A weboldala, letöltéssel a következő: https://projects.eclipse.org/projects/ee4j.glassfish/. Indítása: GLASSFISH_HOME/bin/asadmin.bat start-domain —verbose}}. Alkalmazás telepítése: GLASSFISH_HOME/bin/asadmin.bat deploy app.ear}}. (Az alkalmazásnak a teljes elérési útvonalát meg kell adni.) Ha a fenti alkalmazással szeretnénk kipróbálni, akkor be kell állítani az adatbázist. (Nem jártam utána.)
- JBoss: valójában ez a WildFly elődje (ill. előző neve). Ahogy fent már volt róla szó, a https://wildfly.org/ oldlaon érhető el.
- WebLogic: az Oracle igazán nehézsúlyú alkalmazásszervere a maga közel 1 GB méretével. Nem próbáltam ki.
- WebSphere: az IBM alkalmazásszervere. A neten csak kipróbálási verziót találtam, szóval számomra azonnal megmérettetett és könnyűnek találtatott. Mindenesetre ha valaki szeretne próbálkozni: https://www.ibm.com/cloud/blog/websphere-trial-options-and-downloads.