Enterprise Java

Fő kategória: Java.

Áttekintés

Az alapprobléma

Ha egy tipikus vállalati program Java program elér egy bizonyos méretet, akkor szinte törvényszerű, hogy az alábbi problémák nagy többségébe belefutunk:

  • többszálúság, ill. leginkább az ezzel kapcsolatos problémák orvoslása,
  • skálázhatóság, magyarán az, hogy kis és nagy igénybevétel mellett is megbízhatóan működjön a rendszer,
  • perzisztencia, azaz az adatok tartós tárolása, ill. az ezzel kapcsolatos problémák kezelése (pl. kapcsolódás az adatbázishoz, a Java objektumok relációs adattáblákra való leképezése stb.),
  • tranzakció kezelés, tehát mikor történjen commit ill. rollback, és mi történjen mondjuk ez rollback esetén,
  • távoli elérés, ill. általánosabban megfogalmazva bármiféle kommunikáció két távoli rendszer között, az ezzel kapcsolatos problémák kezelése,
  • névszolgáltatás, azaz az erőforrások kezelése, a lekérdezés biztosítása,
  • üzenetkezelés, ami az üzenetek küldését és fogadását is jelenti, általában többféle módon,
  • biztonság, tehát annak a biztosítása, hogy illetéktelenek ne férhessenek hozzá az adatokhoz
  • és a sort még hosszan lehetne sorolni.

Az Enterprise Java (magyarul kb. nagyvállalati Java) ezekre a problémákra nyújt megoldást. Az elkészített program nem önmagában fut, hanem egy futtató környezetben, amely kezeli a fenti problémákat, vagy teljesen elfedve azt a fejlesztő elől (pl. a többszálúsággal kapcsolatos problémák kezelése), vagy arra a minimumra csökkentve a szükséges kódot, amit feltétlenül muszáj megadni (pl. az adatbázis kapcsolat felépítéséhez többnyire elég megadni az alap elérési adatokat, és mondjuk a kapcsolat létrehozásának a részleteivel általában nem kell törődnie a programozónak).

Szerintem nem egyértelmű a határ a Standard és az Enterprise Java között. Szigorú értelemben az Enterprise Java nem más mint egy specifikáció halmaz, amelyet a Java Community Process gondoz, és megtalálható a https://www.jcp.org oldalon. E szerint tehát az Enterprise Java az, ami megfelel a specifikációknak (ill. annak egy részének). Kevésbé szigorú értelemben viszont (szerintem) ide lehet sorolni mindazt, amely a fenti problémákra (vagy azok egy részére) igyekszik megoldást találni mégpedig úgy, hogy a program nem önmagában fut, hanem egy keretrendszerben. Ezen az oldalon e tágabb értelmezést használom, sőt, ide gyűjtöm azokat a rendszereket is, melyek önálló alkalmazásokként futnak, és interfészt nyújtanak a tőlük függetlenül futó Java alkalmazások felé.

Az Enterprise Java közös jellemzője tehát az, hogy a program nem önmagában fut, hanem bele kell tenni egy "konténerbe" és a futtató rendszer futtatja. Itt tipikusan nem a public static void main(String[] args) függvény indul el, hanem a belépési pont a konténertől (és egyéb eseményektől) függ. Klasszikusan kétféle konténert különböztetünk meg: a web konténert és az EJB konténert, és ez alapján különböztetjük meg a webszervereket és alkalmazás szervereket.

Architektúrák

Mielőtt belemennénk a részletekbe, érdemes kicsit áttekinteni az architektúra típusokat, és az ezekkel kapcsolatos általános tudnivalókat. Alapvetően a szoftverarchitektúrákat a következő csoportokba soroljuk:

  • 1 rétegű: ebben az esetben minden egy helyen van: az adatforrás, az üzleti logika és a megjelenés is. Egyrétegű architektúrának tekinthető az összes, személyi számítógépen futó alkalmazás, melyhez nem szükséges hálózati kapcsolat.
  • 2 rétegű: ezeket hívjuk vastag kliens architektúráknak. Az üzleti logika és a megjelenítés a kliens számítógépeken fut, az adatforrás viszont közös. Ez az architektúra üzemeltetési nehézségekhez vezethet, ugyanis ha frissíteni szeretnénk a központi komponenst, akkor egyszerre kell frissítenünk az összes klienst. Ilyen architektúrájúak a hálózatot használó alkalmazások.
  • 3 rétegű: ezek az úgynevezett vékony kliens architektúrák. Az előzővel ellentétben itt tipikusan elkülönül az üzleti logika és a megjelenítés: az üzleti logika egy központi szerveren fut, míg a grafikus felület a leggyakrabban egy böngésző. Ez jelentősen megkönnyíti a frissítést, mert nem szükséges frissíteni a kliens gépeken az alkalmazást.A webalkalmazások képezik ennek az architektúrának egy jelentős hányadát.
  • N rétegű: elkülönül a webes réteg és az üzleti logika réteg. Ebben az esetben 4 vagy 5 rétegről beszélünk, melyek az alábbiak:
    • Kliens réteg: leggyakrabban ez itt is a böngésző, de vastag kliens is lehetséges.
    • Web réteg: megfelel a 3 rétegű alkalmazások webes rétegének, viszont ez kevesebb üzleti logikát tartalmaz, a fő feladata a felhasználói interakciók kezelése és a megjelenítés. Az adatforráshoz általában nem közvetlenül nyúl, hanem az alatta levő rétegen keresztül.
    • Üzleti logika réteg: ide konténer által vezérlet komponensek kerülnek, melyek többnyire skálázhatóak, távolról elérhetőek, többféle módon.
    • Integrációs réteg: ebbe kerülnek azok a komponensek, amelyek az adatok összegyűjtéséért felelősek, pl. amelyek az adatbázis kapcsolatért felelősek. (Ami miatt N rétegű architektúráról beszélünk, és nem 4 vagy 5 rétegűről, az emiatt van: ez ugyanis logikailag része lehetne az előzőnek, de el is különülhet attól)
    • Erőforrás réteg: itt található a tényleges adat, pl. egy adatbázisban.

Webalkalmazások

Áttekintés

A klasszikus web szerverek az alap webes technológiákat szolgálják ki: tipikusan HTML oldalakat (amelyek mögött esetleg lehet PHP) tudunk segítségével letölteni. Talán a legelterjedtebb ilyen rendszer az Apache HTTPD (https://httpd.apache.org/). Ennek a részleteibe itt nem megyünk bele.

Számunkra ebben a témában érdekesebbek azok a web szerverek, amelyek a Java Servlet specifikációt is megvalósítják. Ez technikailag azt jelenti, hogy Java webalkalmazásokat tudunk készíteni, melynek a kiterjesztése .war, amit egy meghatározott könyvtárba másolva tudunk futtatni.

Javaslom, hogy a példákat az Apache Tomcat webszerverrel próbáljuk ki. Ehhez töltsük le a webszervert a http://tomcat.apache.org/ oldalról. Én a 9.0.29-es verziót használom, zip formában töltöttem le, és a c:\programs\apache-tomcat-9.0.29\ könyvtárba csomagoltam ki. Használat:

  • A war fájlt a webapps könyvtárba kell másolni.
  • Indítás: bin/startup.bat

Futás közben a webalkalmazás kicsomagolódik a nevének megfelelő könyvtárba. Az alapértelmezett port a 8080. Ha pl. a webalkalmazás neve example.war, akkor a http://localhost:8080/example/ alatt értjük el azt. Ha felülírjuk a war fájtl, akkor elvileg automatikusan megtörténik a kicsomagolás, bár ha biztosra szeretnénk menni, akkor érdemes előtte magát a könyvtára letörölni.

Fejlesztés során célszerű a kedvenc fejlesztőkörnyezetünkben beállítani a Tomcatet. Pl. Eclipse-ben a következőképpen tudjuk ezt megtenni: View → Show View → Other… → Server → Servers → jobb kattintás → New → Server. Itt felül válasszuk ki a verziót (pl. Tomcat v9.0 Server), majd adjuk meg a pontos elérési útvonalat. Programokat a következőképpen tudunk hozzáadni: jobb kattintás a megfelelő szerver példányon → Add and Remove… → ott bal oldalon találjuk a webalkalmazásokat, amelyeket nyilakkal tudjuk átmozgatni jobbra. A Servers fülön elindíthatjuk normál és hibakereső (debug) módban is; ez utóbbi esetben debuggolni tudjuk a programunkat úgy, hogy az az alkalmazásszerverben fut.

Egy fontos művelet az hogy mikor történjen az eredmény publikálása (publish). Az alapértelmezett beállítás az automatikus, ami azt jelenti, hogy bármely kis változtatás maga után vonja a publikálást. Ez kis programok esetén kétségkívül hasznos, ha viszont a program egyre nagyobbá válik, és pl. egy publikálás mondjuk egy jelentősebb adatbázis lekérdezést vált ki, akkor megfontolandó az automatikus publikálás kikapcsolása. Ezt a következőképpen tudjuk megtenni: dupla kattintás a webszerver nevén → Publishing → Never publish automatically. Ez esetben a publikálásról a fejlesztőnek kell gondoskodnia, amit a következőképpen tud megtenni: jobb kattintás a webszerveren: Publish.

Egy klasszikus servlet web alkalmazás

A web alkalmazások készítésénél a következőkre kell figyelni:

  • A csomagolás (packaging) war kell, hogy legyen, az alapértelmezett jar helyett.
  • Fordítás során szükségünk van a következő függőségre: javax.servlet % javax.servlet-api % 4.0.0.
  • Ahhoz, hogy minél egyszerűbb legyen az elérése, érdemes gondoskodni arról, hogy ne legyen benne az eredményben verziószám.
  • A szerkesztést a maven-war-plugin beépülő segítségével kell végrehajtanunk.

Példaként készítsük el a következő pom.xml fájlt:

<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>webapp</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.1</version>
            </plugin>
        </plugins>
    </build>
</project>

A web.xml a webalkalmazások központi eleme: itt adhatjuk meg, hogy hogyan fusson. Ennek a helye: src/main/webapp/WEB-INF/web.xml. Példaként hozzuk létre az alábbi tartalommal.

<?xml version="1.0" encoding="UTF-8"?> 
<web-app>
    <servlet> 
         <servlet-name>Greet</servlet-name> 
         <servlet-class>hu.faragocsaba.webapp.GreetServlet</servlet-class> 
    </servlet> 
    <servlet-mapping> 
         <servlet-name>Greet</servlet-name> 
         <url-pattern>/greet</url-pattern> 
    </servlet-mapping> 
</web-app>

Ezze két dolgot deklaráltunk:

  • A belépési pont a hu.faragocsaba.webapp.GreetServlet osztály.
  • A böngészőből a greet URL-en érhetjük el.

A forrás a következő (src/main/java/hu/faragocsaba/webapp/GreetServlet.java):

package hu.faragocsaba.webapp;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class GreetServlet extends HttpServlet {
    @Override
    public void init() {
        System.out.println("GreetServlet initialized");
    }

    @Override
    public void destroy() {
        System.out.println("GreetServlet destroyed");
    }

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        System.out.println("GreetServlet.doGet() called.");
        PrintWriter out = response.getWriter();
        out.println("Hello world!");
    }
}

A servlet a HttpServlet osztályból származik. Itt alapvetően két dolgot adhatunk meg:

  • Milyen kód fusson le betöltéskor (init()) ill. a végén (destroy). Ezek egyébként nem kötelező elemek, kihagyhatjuk.
  • Mi hajtódjon végre adott művelet hatására, pl. doGet() egy HTTP GET kérés esetén.

Tegyük a következőt:

  • Fordítsuk le hagyományosan a mvn clean install segítségével.
  • Az eredmény ez: target/webapp.war. A fájlt magát másoljuk be a Tomcat webapps könyvtárába (az én esetemben tehát létrejön egy c:\programs\apache-tomcat-9.0.29\webapps\webapp.war fájl).
  • Indítsuk el a Tomcat webalkalmazást (pl. c:\programs\apache-tomcat-9.0.29\bin\startup.bat).
  • A böngészőbe írjuk be a megfelelő URL-t, jelen esetben a következőt: http://localhost:8080/webapp/greet. Ha mindent jól csináltunk, akkor megjelenik a Hello world! felirat.

Servlet annotációval

Az xml konfigurálást sokan nem szeretik, igazából én sem; ha van választási lehetőségem, akkor ugyanazt szívesen készítem el kódban. A 6-os Java verziótól kezdve lehetőség van webalkalmazást web.xml nélkül, annotációk segítségével létrehozni. A fenti példa átírása web.xml nélkülire a következőképpen történik: a pom.xml maradjon meg, a web.xml fájlt töröljük ki, a GreetServlet-ben pedig az osztályt pedig lássuk el a következő annotációval:

@WebServlet(name = "GreetServlet", urlPatterns = "/greet")
public class GreetServlet extends HttpServlet {
    ...
}

HTTP GET paraméterek lekérdezése

A HttpServletRequest tartalmazza a kérés minden részletét: az URL paramétereket, a HTTP fejléc (header) információkat, a sütiket (cookie) stb. Ebben a lépésben HTML-t adunk vissza. Folytassuk a fenti példát! Módosítsuk a doGet() függvényt az alábbi módon:

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><body><h1>");
        String name = request.getParameter("name");
        out.println("Hello, " + name + "!");
        out.println("</h></body></html>");
    }

A válasz típusát HTML-re állítjuk, és lekérdezzük a name paraméter értékét. Nyissuk meg a következő oldalt: http://localhost:8080/webappget/httpgetexample?name=Csaba. Ha mindent jól csináltunk, akkor megjelenik nagy vastag betűkkel a "Hello, Csaba!" felirat. Ha megnyitjuk az oldal forrását (tipikusan Ctrl+U), a következőt látjuk:

<html><body><h1>
Hello, Csaba!
</h></body></html>

Egy HTTP POST példa

Most nézzük meg, hogy hogyan működik a HTTP POST! A fenti példát folytatjuk. A forrásban mindössze a doGet függvényt kell átírni erre: doPost, ugyanis a request.getParameter() függvényt kell használnunk a GET és a POST esetben is. Tehát a következő legyen a GreetServlet.java-ban:

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ...
    }

POST üzenetet tipikusan HTML formok segítségével tudunk létrehozni. Hozzuk létre az src/main/webapp/index.html fájtl az alábbi tartalommal:

<html><body><form name="greetForm" action="greet" method="POST">
Your name: <input type="text" name="name"><input type="submit" value="Submit">
</form></body></html>

A fent leírtak szerint fordítsuk és telepítsük a programot. Töltsük be egy böngésző segítségével a http://localhost:8080/webapp/ oldalt (mivel a html fájl neve index.html automatikusan azt fogja betölteni). Ha mindent jól csináltunk, egy formanyomtatványt látunk, ahova beírhatjuk a nevünket, és ha megnyomjuk a Submit gombot, akkor üdvözöl minket.

JSP példa

A JSP a Java Server Pages rövidítése. Ez valójában HTML-be ágyazott Java. A https://www.ntu.edu.sg/home/ehchua/programming/java/JSPByExample.html oldal segített nekem gyorsan és hatékonyan átismételni a lényeget.

Lássunk egy példát! A Tomcat webapps könyvtárába hozzunk létre egy alkönyvtárat (pl. jsp), azon belül pedig egy .jsp kiterjesztésű fájlt (pl. add.jps). A példában tehát (nálam) a fájl teljes elérési útvonala ez: c:\programs\apache-tomcat-9.0.29\webapps\jsp\add.jsp, a tartalma pedig legyen a következő:

<html><body>
<%= 2+3 %>
</body></html>

Láthatjuk, hogy ez egy (többé-kevésbé) szabályos HTML kód, viszont tartalmaz egy <%= … %> részt. Oda került a Java kód. Ha elindítjuk a Tomcatet és betöltjük a http://localhost:8080/jsp/add.jsp oldalt, akkor eredményül ezt kapjuk: 5. A háttérben viszont a JSP-ből servlet generálódott, jelen esetben a következő: c:\programs\apache-tomcat-9.0.29\work\Catalina\localhost\jsp\org\apache\jsp\add_jsp.java. Ennek a számunkra lényeges része a következő:

      out.write("<html><body>\r\n");
      out.print( 2+3 );
      out.write("\r\n");
      out.write("</body></html>\r\n");

Ezen az egyszerű példán valószínűleg nem látszik, de az első letöltés mindig lassúbb mint a későbbiek, ugyanis ekkor generálódik le a Java kód, ill. fordul is le, ld. a .java melletti .class-t.

A JSP-ben a szokásos HTML tag-eken túl az alábbiakat használhatjuk:

  • <%= … %>: ide Java kifejezéseket írhatunk, ami egy out.print(…); metódus paramétere lesz. A fenti példa is ezt a formát használja.
  • <% … %>: ide Java kódot írhatunk.
  • <%@ page|include … %>: Java direktívákat írhatunk ide.
  • <%— … —>: JSP megjegyzések.

A JSP definiál néhány objektumot, melyek közül a legfontosabbak:

  • request: HTTP kérés.
  • response: HTTP válasz.
  • out: a HTTP válasz kimenete.
  • stb.

Az alábbi példán keresztül láthatjuk ezek használatát, melyet a fenti módon tudunk beüzemelni:

<%@ page contentType="text/html" %>
<%@ page import="java.util.*" %>
<html><body>
Server: <%= request.getServerName() %>
<br>
Fruits:
<ul>
  <%
    List<String> fruits = Arrays.asList("apple", "orange", "banana");
    for (String fruit: fruits) {
  %>
  <%-- Prints the fruits one by one. --%>
  <li><%= fruit %>
  <%
    }
  %>
</ul>
</body></html>

Webalkalmazást is készíthetünk, ill. inkább az a tipikus: a kód tartalmaz egyszerre Java kódot és JSP-t is. Írjuk át a fenti, üdvözlő példát tisztán JSP-re!

  • pom.xml: hagyjuk meg úgy, ahogy van.
  • web.xml: erre most nincs szükség.
  • Java kód: erre sincs szükség.
  • src/main/webapps/index.jsp: ez igazából maradhatna index.html is, mivel nincs benne JSP kód, de a példa legyen teljesen JSP. A fenti index.html-hez képest egyetlen dolgot kell átírnunk: az action="greet" helyére ezt kell írni: action="greet.jsp". A teljes forrás:
<html><body><form name="greetForm" action="greet.jsp" method="POST">
Your name: <input type="text" name="name"><input type="submit" value="Submit">
</form></body></html>
  • src/main/webapps/greet.jsp: ide írjuk a fenti doPost()-ban megvalósított logikát:
<html><body><h1>
Hello, <%= request.getParameter("name") %>!
</h1></body></html>

Folyamat vezérlés

Angolul session handling. A webes világ egyik legnagyobb kihívását az jelenti, hogy a folyamat állapot mentes. Az egyes kattintásokat a szerver nem jegyzi meg, és semmi sem garantálja azt, hogy a következő kattintást pont ugyanaz a fizikai szerver fogja kiszolgálni. Mégis, a webes élményhez hozzátartozik az az élmény, mintha egy folyamatban vennénk részt: pl. bejelentkezünk egy webáruházba, árukat teszünk a virtuális kosárba, fizetünk, majd kijelentkezünk.

Ennek kezelésére több módszer létezik, melyek közül talán az egyik legelterjedtebb a böngésző sütik (cookie) használata. Ezek valójában kulcs-érték párok. Hacsak nem tiltotta le a felhasználó, akkor amit a szerver a válaszban elküld, az a kliens a következő kérésben elküldi a szervernek, és így tudja a szerver beazonosítani a klienst. Túl sok információt nem tudunk egy sütiben tárolni. Valamint a sütiknek vannak lejárati idejük.

Ennek a szakasznak a megírásában ez az oldal segített nekem sokat: https://www.journaldev.com/1907/java-session-management-servlet-httpsession-url-rewriting.

Lássunk egy példát, mely a következőről szól:

  • Először meg kell adnunk a nevünket. Ez eltárolódik egy böngésző sütiben.
  • Majd beírhatunk egy számot, amit elküldve hozzáadja a nevünkhöz kötött összeghez. Ez kezdetben 0, és ha többször egymás után elküldünk számokat, akkor mindig az aktuális összeg jelenik meg.
  • Az összeg nem jelenik meg a sütiben, azt szerver oldalon tároljuk, és a név alapján kapcsoljuk össze az adatokat.
  • Ki tudunk lépni, és egy másik névvel belépni. Ekkor nullázódik a számláló.
  • Ha újra az eredeti névvel lépünk be, akkor folytatódik az előző.
  • Ha egy másik böngészővel ugyanazt a nevet adjuk meg, amit máshol már használtunk, akkor folytatja az összeadást.

Hozzunk létre egy webalkalmazást, melynek neve legyen mondjuk webappsession, a pom.xml tartalma legyen ugyanolyan mint a fentiek (értelemszerűen az artifactId-n kívül). Először készítsük el az összeg szerver oldali lementését! Egy teljes programban ezt adatbázisban kellene tároljunk, és ennek következtében lényegében párhuzamosan futó webszerverek is elérnék a süti alapján ugyanazt az információt; az egyszerűség érdekében most az adatokat egy asszociatív tömbben tároljuk, a memóriában. Ehhez elkészítünk egy osztályt, ami ezt kezeli, és mivel azt szeretnénk, hogy csak egy példány legyen belőle, az egyke (singleton) tervezési minta segítségével valósítjuk meg (src/main/java/hu/faragicsaba/webappsession/Sums.java):

package hu.faragocsaba.webappsession;

import java.util.*;

public class Sums {
    private static Sums instance = new Sums();

    public static Sums getInstance() {
        return instance;
    }

    private Map<String, Integer> sums = new HashMap<String, Integer>();

    private Sums() {}

    public int getSum(String user) {
        Integer actualSum = sums.get(user);
        if (actualSum == null) {
            sums.put(user, 0);
            return 0;
        } else {
            return actualSum;
        }
    }

    public void add(String user, int value) {
        sums.put(user, getSum(user) + value);
    }
}

A lényegi részt, ahol a süti kezelése is történik, JSP-ben készítjük el, mégpedig egyetlen fájlt túlterhelve. Talán nem túl elegáns, de annak a célnak megfelel, hogy egy helyen lássuk a folyamat vezérlés lényegét (src/main/webapp/index.jsp):

<html><body>
<%
hu.faragocsaba.webappsession.Sums sums = hu.faragocsaba.webappsession.Sums.getInstance();

String user = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if (cookie.getName().equals("user")) {
            String isLogout = request.getParameter("isLogout");
            if ("true".equals(isLogout)) {
                cookie.setMaxAge(0);
                response.addCookie(cookie);
            } else {
                user = cookie.getValue();
            }
        }
    }
}

if (user == null) {
    String name = request.getParameter("name");
    if (name == null) {
%>
<form name="nameForm" action="index.jsp" method="POST">
Your name: <input type="text" name="name"><input type="submit" value="Submit">
</form>
<%
    } else {
        user = name;
        Cookie userCookie = new Cookie("user", name);
        response.addCookie(userCookie);
    }
}
if (user != null) {
    String numberStr = request.getParameter("number");
    if (numberStr != null) {
        sums.add(user, Integer.parseInt(numberStr));
    }
%>
Hello, <%=user%>!<br>
Actual sum: <%=sums.getSum(user)%><br>
<form name="addForm" action="index.jsp" method="POST">
Enter a number to add: <input type="number" name="number"><input type="submit" value="Submit">
</form>
<form action="index.jsp" method="post">
<input type="submit" value="Logout">
<input type="hidden" name="isLogout" value="true">
</form>
<%
}
%>
</body></html>

A kódban elvileg csak a süti kezelés az újdonság, de lássuk kicsit részletesebben!

  • A sütiket a request.getCookies() segítségével kérjük le, amely azokat egy tömbben adja vissza.
  • A válaszba a response.addCookie() hívással tudunk sütit betenni. Ez megjelenik a következő kérésben.
  • Sütit törölni nem lehet, viszont - ahogy arról már szó volt - a sütiknek van egy lejárati idejük. A trükk a következő: ha a lejárati időt 0-ra állítjuk be, a böngésző azonnal törli, mivel egyből lejár.
  • Belépéskor nem kér jelszót. A gyakorlatban nyilván erre szükség van, ráadásul kódoltan célszerű azt továbbítani, és olyan módszereket alkalmazni, hogy ne lehessen ellopni a süti tartalmát. Ezek fontos szempontok, de túlmutatnak ezen a fejezeten.
  • A kilépésnél, mivel ugyanaz az index.jsp kezel mindent, egy trükköt kell alkalmaznunk: a példában egy rejtett mezőbe helyeztünk egy név-érték párt (isLogout=true), ami a többinél nem szerepel, így onnan tudjuk, hogy ez most a kilépés, hogy ez benne van egy rejtett mezőben. (A gyakorlatban erre a trükkre nincs szükség, mert ott a vezérlést pl. egy logout.jsp vagy egy neki megfelelő servlet kapja meg, ami egyszerűen végrehajtja a kiléptetést.) A rejtett mező egyébként - a süti mellett - szintén alkalmas folyamat információk átadására: a válasz HTML-be ugyanis tehetünk ilyen adatokat, ami a következő kérésnél benne lesz a paraméterek között.

A példát fordítsuk le a telepítsük a szokásos módon, majd a fent megadott instrukciókkal próbáljuk ki. (Ha újraindítjuk a szervert, akkor törlődik a memória. Ez esetben a süti még megmarad a böngészőben). Érdemes megvizsgálni a böngészőben a sütit: pl. Google Chrome-ban F12 → Application → Cookies → http://localhost:8080, és ott láthatjuk a user süti értékét. (A süti szerverre vonatkozik, így a többi alkalmazás sütijét itt nem látjuk.)

Ha megnéztük a sütiket, akkor feltűnhetett az, hogy a mi user sütink mellett ott van egy JSESSIONID, amit nem állítottunk be. Ez JSP esetén automatikusan létrejön, servlet esetén pedig a request.getSession() hívás hatására (request.getSession(false) visszaadja a sessiont, ha van, de nem hozza létre). Valójában létezik egy HttpsSession megoldás is a folyamatok kezelésére, amely egy réteg a fent leírtak felett, és ez hozza létre az említett sütit, generált tartalommal. Így valójában a sütik közvetlen kezelése nélkül is számon tudjuk tartani a folyamat azonosítót. Erre hamarosan létunk egy példát.

A fenti példa nem működik akkor, ha a böngészőben le van tiltva a süti. Próbáljuk ki: töröljük a localhost:8080-ra vonatkozó sütiket, és erre a szerverre tiltsuk is le. Az azonosító megadása után még megkapjuk az aktuális részeredményt (mert pont akkor átadtuk a felhasználónevet), de hozzáadni már nem tudunk. Ez esetben más módszert kell alkalmaznunk, pl. az URL-ben küldhetjük azt el. Ehhez használhatjuk a response.encodeURL(url) függvényt. Ez utóbi megoldás amiatt is jó, mert csak akkor teszi az URL-be a JSESSIONID értékét, ha a sütik le vannak tiltva, egyébként a süti megoldást tartalmazza.

Módosítsuk a fenti példát úgy, hogy egyrészt nem a sütiket használjuk közvetlenül, hanem a HttpSession-t, másrészt használjuk az URL kódolást! Minden maradjon az eredeti állapotában, csak az index.jsp-t módosítsuk a következőre:

<html><body>
<%
hu.faragocsaba.webappsession.Sums sums = hu.faragocsaba.webappsession.Sums.getInstance();

String isLogout = request.getParameter("isLogout");
if ("true".equals(isLogout)) {
    session.setAttribute("user", null);
}

String user = (String)session.getAttribute("user");
if (user == null) {
    String name = request.getParameter("name");
    if (name == null) {
%>
<form name="nameForm" action=<%=response.encodeURL("index.jsp")%> method="POST">
Your name: <input type="text" name="name"><input type="submit" value="Submit">
</form>
<%
    } else {
        user = name;
        session.setAttribute("user", name);
    }
}
if (user != null) {
    String numberStr = request.getParameter("number");
    if (numberStr != null) {
        sums.add(user, Integer.parseInt(numberStr));
    }
%>
Hello, <%=user%>!<br>
Actual sum: <%=sums.getSum(user)%><br>
<form name="addForm" action=<%=response.encodeURL("index.jsp")%> method="POST">
Enter a number to add: <input type="number" name="number"><input type="submit" value="Submit">
</form>
<form action=<%=response.encodeURL("index.jsp")%> method="post">
<input type="submit" value="Logout">
<input type="hidden" name="isLogout" value="true">
</form>
<%
}
%>
</body></html>

Vegyük észre, hogy a Cookie nem fordul elő, valamint három helyen is szerepel a action=<%=response.encodeURL("index.jsp")%>.

  • Először próbáljuk ki úgy, hogy nincsenek letiltva a sütik: elvileg nem szabad változást tapasztalnunk az előzőhöz képest.
  • Majd töröljük a meglevő sütiket, tiltsuk is le azokat legalább a localhost:8080-on, és úgy próbáljuk ki. Ez esetben az URL fejlécben láthatjuk a süti értékét, kb. a következőképpen: http://localhost:8080/webappsession/index.jsp;jsessionid=4DA1FC3170897AB56E51C57A3B237519, és a webalkalmazás letiltott sütik mellett is működik.

Figyelők

Angolul listener. A webalkalmazások során különböző események váltódnak ki, például elindul egy kérés, beállítanak egy attribútumot stb. Ezekre az eseményekre lehet figyelőket írni. Lássunk egy példát, melyben minden lekérdezés elején és végén kiírjuk a kliens IP címét. Az alap tetszőleges webalkalmazás lehet, melyhez adjuk hozzá az alábbit:

package hu.faragocsaba.webapplistener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class MyServletRequestListener implements ServletRequestListener {
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("ServletRequest initialized. Remote IP: " + servletRequestEvent.getServletRequest().getRemoteAddr());
    }

    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println("ServletRequest destroyed. Remote IP: " + servletRequestEvent.getServletRequest().getRemoteAddr());
    }
}

Annotáció nélkül a web.xml-be a következőképpen tudjuk deklarálni a figyelő osztályt (melyből tetszőleges számút fel tudunk sorolni):

<web-app ...>
    ...
    <listener>
        <listener-class>hu.faragocsaba.webapplistener.MyServletRequestListener</listener-class>
    </listener>
    ...
</web-app>

Néhány további figyelő:

  • ServerContextListener: ezzel a webalkalmazás indulását (contextInitialized(ServletContextEvent)) és befejezését (contextDestroyed(ServletContextEvent)) tudjuk elkapni. Például itt tudjuk felépíteni ill. lezárni az adatbázis kapcsolatot, ha nem szeretnénk ezt megtenni minden kérés során.
  • ServletContextAttributeListener: webalkalmazás szintű attribútum változások során hívódnak meg a függvények (attributeAdded(ServletContextAttributeEvent), attributeReplaced(ServletContextAttributeEvent), attributeRemoved(ServletContextAttributeEvent)).
  • ServletRequestListener: a lekérdezések során hívódnak meg, ahogyan a fenti példában is láthattuk.
  • ServletRequestAttributeListener: akkor váltódnak ki, ha a lekérdezéshez kapcsolódó attribútum létrejön (attributeAdded(ServletRequestAttributeEvent)), megváltozik (attributeReplaced(ServletRequestAttributeEvent)) vagy törlődik (attributeRemoved(ServletRequestAttributeEvent)).
  • HttpSessionListener: a HTTP session létrejötte (sessionCreated(HttpSessionEvent)) és befejezése (sessionDestroyed(HttpSessionEvent)) alkalmával váltódik ki.
  • HttpSessionBindingListener: függvényei akkor hívódnak, ha egy session-höz hozzáadunk (request.getSession().setAttribute(…)valueBound(HttpSessionBindingEvent)) vagy abból törlünk (request.getSession().removeAttribure(…)valueUnbound(HttpSessionBindingEvent)) egy objektumot. Itt az érték valamilyen objektum.
  • HttpSessionAttributeListener: a függvényei akkor hívódnak, ha a session attribútumai értéket kapnak (attributeAdded(HttpSessionBindingEvent)), módosulnak (attributeReplaced(HttpSessionBindingEvent)) vagy törlődnek (attributeRemoved(HttpSessionBindingEvent)). Itt az érték string.
  • HttpSessionActivationListener: ha egy session átkerül egyik VM-ből egy másikba, akkor egy sessionWillPassivate(HttpSessionEvent) ill. a másik oldalon egy sessionDidActivate(HttpSessionEvent) esemény váltódik ki, melyre a session-höz kapcsolódó objektumok kaphatnak értesítést.
  • AsyncListener: aszinkron események során hívódnak meg az itt definiált függvények. Ez a fentiektől annyiban eltér, hogy egy lekérés során kell beállítanunk, és azon belül szolgál visszahívó (callback) függvényekként. Példát láthatunk ezen az oldalon: https://examples.javacodegeeks.com/enterprise-java/servlet/java-servlet-asynclistener-example/.

Ennek a szakasznak az elkészítésében a https://www.journaldev.com/1945/servletcontextlistener-servlet-listener-example oldal segített.

Szűrők

Angolul filter. Szűrők segítségével - ahogy a nevéből is következtethetünk - megszűrhetjük a kéréseket. Ez a következőket jelentheti:

  • Eldönthetjük, hogy egyáltalán megkaphatja-e a kliens a választ. Például itt történhet egy generikus jogosultság ellenőrzés; ahelyett tehát, hogy minden egyes oldalon külön ellenőriznénk azt, hogy a bejelentkezett felhasználó jogosult-e végrehajtani azt a műveletet, ezt központilag tudjuk a szűrők segítségével kezelni.
  • Módosíthatunk a válaszon. Pl. egységes fejléccel ill. lábléccel láthatjuk el az oldalakt.
  • Vagy csak "csendben" végrehajthatunk egy műveletet, pl. naplózást.

Lássunk egy példát! Induljunk ki egy tetszőleges fenti webalkalmazásból; én a JSP példát vettem alapul. Hozzunk létre egy szűrőt a megfelelő helyen, a következőképpen:

package hu.faragocsaba.webappfilter;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

@WebFilter("/*")
public class TestFilter implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("RequestLoggingFilter.init() called");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("RequestLoggingFilter.doFilter() called");
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpServletRequest = (HttpServletRequest)request;
            System.out.println("Requested URL: " + httpServletRequest.getRequestURL());
        }
        PrintWriter out = response.getWriter();
        out.println("Hello from TestFilter!");
        chain.doFilter(request, response);
        out.println("Goodbye from TestFilter!");
    }

    public void destroy() {
        System.out.println("RequestLoggingFilter.destroy() called");
    }
}

Igazából ennyi; a @WebFilter("/*") annotációval megadtuk azt, hogy ez minden bejövő kérésre vonatkozik. Természetesen finomhangolhatjuk is ezt.

A példában kiírunk pár bejegyzést a standard kimenetre (a Tomcat konzoljára), valamint ellátjuk az összes oldalt egy fejléccel és egy lábléccel. (Most ne törődjünk azzal, hogy ezzel elrontunk egy többé-kevésbé jól felépített HTML oldalt.) Tesztelni a fent leírtak szerint tudjuk; érdemes közben figyelni a Tomcat naplóját.

Annotáció nélkül szükségünk van a web.xml-re, úgy egy kicsit bonyolultabb:

<?xml version="1.0" encoding="UTF-8"?> 
<web-app> 
    <filter>
        <filter-name>TestFilter</filter-name>
        <filter-class>hu.faragocsaba.webappfilter.TestFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>TestFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
</web-app>

Ahogy a példákból már következtethetünk, néhány fontos dolog a szűrőkkel kapcsolatban:

  • Tetszőleges számú szűrőt adhatunk hozzá egy webalkalmazáshoz. Ha az annotációt részesítjük előnyben (ahogyan én is), akkor ezeket az osztályokat @WebFilter annotációval kell ellátnunk, egyébként a web.xml-ben kell felvennünk a <filter> ill. <filter-mapping> alá.
  • Finomhangolhatjuk azt, hogy melyik szűrő mely tartalmakat szűrje, amit a @WebFilter annotáció paramétereként, ill. a web.xml fájlban az <url-pattern> tag-ek között adhatunk meg. Így elképzelhető, hogy egyes tartalmakat több szűrő is szűr, míg másokat akár egy sem.
  • A fenti példában van egy chain.doFilter(request, response); hívás. Ha ez nem lenne ott, akkor a lekérdezés sohasem teljesülne. Ez adja tovább a láncon a hívást, tehát a következő szűrőnek, vagy ha már elfogytak, akkor magának az erőforrásnak. Amint a fenti példában is láthatjuk, még egyszer átmegy a válasz mindegyik szűrőn, így ismét bele tudunk nyúlni a válaszba.

Taglib

A HTML nem kiterjeszthető: amit a specifikáció leír, attól elvileg a böngészők nem térhetnek el. Ez sok esetben komoly megkötést jelent, ráadásul a többi népszerű szöveges formátum (pl. XML, JSON) kiterjeszthetőek. A JSP lehetővé teszi a kiterjeszthetőséget, azaz úgymond új tag-ek létrehozását. Ezek valójában JSP tag-ek, amelyek tiszta HTML-re változnak, mire a válasz megérkezik a böngészőhöz, mégis, a fejlesztő szinte úgy használhatja, mintha a HTML tag lenne. Ezeket hívjuk tag könyvtáraknak; angolul: tag library, rövidítve taglib.

Erre a technológiára épül számos keretrendszer, melyre látni fogunk majd példát, mi magunk ritkán hozunk létre taglibet. Ennek ellenére hasznos megtanulni a mikéntjét, és most erre látunk egy példát. Először elkészítünk egy olyan projektet, ahol a használat helye megegyezik a definiálás helyével, majd egy olyan megoldást is, ahol ez a kettő elkülönül.

A pom.xml-ben vegyük fel az alábbi függőséget (a javax.servlet-api mellé):

        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
            <scope>provided</scope>
        </dependency>

A taglib deklarálása egy tld (Tag Library Descriptor) kiterjesztésű fájlban történik, melyet a WEB-INF könyvtárba kell helyezni, pl. (src/main/webapp/WEB-INF/mytaglib.tld):

<taglib>
   <tlib-version>1.0</tlib-version>
   <jsp-version>2.2</jsp-version>
   <short-name>My TLD</short-name>
   <uri>http://faragocsaba.hu/mytld</uri>

   <tag>
      <name>mytag</name>
      <tag-class>hu.faragocsaba.mytaglib.MyTag</tag-class>
      <body-content>scriptless</body-content>
      <attribute>
         <name>name</name>
      </attribute>
   </tag>
</taglib>

A tlib-version az adott tag library verziója. Az URI csak egy hivatkozás, nem valódi URL. Itt egyetlen tag-et deklaráltunk, melynek a neve mytag (tetszőleges számút deklarálhatnánk). A példa tartalmaz attribútumot, valamint tartalmazhat szöveges törzset (body) is.

Valósítsuk meg a saját tag-et a következőképpen:

package hu.faragocsaba.mytaglib;

import java.io.IOException;
import java.io.StringWriter;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;

public class MyTag extends SimpleTagSupport {
    private String name;

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

    @Override
    public void doTag() throws JspException, IOException {
        JspWriter out = getJspContext().getOut();
        if (name == null) {
            out.println("Hello my tag world!");
        } else {
            out.println("Hello " + name + "!");
        }

        StringWriter sw = new StringWriter();
        JspFragment body = getJspBody();
        if (body != null) {
            body.invoke(sw);
            getJspContext().getOut().println("<br><b>" + sw.toString() + "</b><br>");
        }

        out.println("End of my tag.");
    }
}

A példában lekezeljük azt az esetet is, ha van name paraméter, és azt is, ha nincs, és ugyanígy a törzzsel is.

Végül a használatra egy példa (src/main/webapp/index.jsp):

<%@ taglib prefix="fcs" uri="http://faragocsaba.hu/mytld" %>
<html><body>
    The following lines are rendered by a custom tag:<hr>
    <fcs:mytag name="Csaba">This is the body of the custom tag.</fcs:mytag>
    <hr>This is JSP again.
    <hr>
    <fcs:mytag/>
</body></html>

Figyeljük meg a fcs prefixet az első sorban, valamint magában a tag-ben is. Töltsük be a fent megadott módon a példát, és ha mindent jól csináltunk, akkor egy pár soros, vízszintes elválasztókkal elválasztott oldalt látunk.

Tipikusabb az az eset, ha a taglib megvalósítása és használata elkülönül. Lássuk most ezt is! A megvalósítás (mytaglib) pom.xml fájlja az alábbi:

<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>mytaglib</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

A csomagolás tehát jar, és nincs külön build beépülő. A fenti példával ellentétben itt nem a WEB-INF, hanem a META-INF könyvtárba kell tennünk a tld fájlt, így hosszuk létre a src/main/resources/META-INF/mytaglib.tld fájlt, a fenti tartalommal. A MyTaglib.java helye és tartalma legyen ugyanaz mint fent. Hajtsuk végre a fordítást és telepítést a mvn clean install paranccsal, és győződjünk meg arról, hogy az eredmény belekerült a Maven repository-ba, pl. c:\Users\[username]\.m2\repository\hu\faragocsaba\mytaglib\1.0\mytaglib-1.0.jar.

Most valósítsuk meg a klienst! Ez már "rendes" webalkalmazás lesz, így vehetjük a fentieket alapul, de ehhez adjuk hozzá függőségként a fent megvalósított taglib jar fájlt:

        <dependency>
            <groupId>hu.faragocsaba</groupId>
            <artifactId>mytaglib</artifactId>
            <version>1.0</version>
        </dependency>

Az index.jsp helye és tartalma legyen pont ugyanaz mint a fenti. Figyeljük meg, hogy a mytaglib-1.0,jar az eredményben megjelenik a WEB-INF/lib/ könyvtárban. Indítsuk el a szokásos módon. Ha mindent jól csináltunk, a http://localhost:8080/mytaglibclient/ oldalon megjelenik az elvárt tartalom.

JSTL és EL

Az előző szakaszban arról volt szó, hogy mi magunk is tudunk JSP kiegészítő tag-eket készíteni amely - a fejlesztő szempontjából - kb. úgy működik, mintha új HTML tag-eket alkotnánk. Valójában nem véletlenül alakult ki az igény az új tag-ek iránt: elég sok általános probléma van, melyek általános megoldást igényelnek. zt az igényt kielégítve alkották meg a a Java szabvány tag könyvtárat, angolul Java Standard Tag Library, rövidítve JSTL.

Ezen az oldalon csak egy áttekintést és egy példát látunk a JSTL-re; az egyes tag-ek ismertetése túlmutat az oldal keretein. A netes források közül a https://www.javatpoint.com/jstl ajánlom, mely részletesen, példákkal illusztrálva mutatja be az egyes JSTL tag-eket. Lássuk az áttekintést!

A jstl & jstl & 1.2 függőséget hozzá kell adni a projekthez.

  • Alap tag-ek (core tags): ezek a tag-ek a leggyakoribb programozási struktúrákat vezeti be, mint pl. feltételkezelés, ciklus stb. Használatához a következő sort kell a forrás elejére írni: <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>. A szokásos prefix tehát c:.
    • c:out: a kifejezés értékét írja ki, hasonlóan a <%=…%> struktúrához. A formátuma <c:out value = "${…}"/>, az értékhez pedig Expression Language (EL) formázott kifejezést kell írnunk. Az EL bemutatása szintén túlmutat az oldal keretein. Segítségével a szokásos műveleteket hajthatjuk végre, pl. alapműveletek, összehasonlítások stb. A lenti példa is tartalmaz EL kifejezést. A https://www.javatpoint.com/EL-expression-in-jsp oldalon találunk egy jó összefoglalót róla.
    • c:import: letölt egy URL-t, melynek tartalmát egy változóba teszi.
    • c:set: beállítja egy változó értékét.
    • c:remove: törli a változót.
    • c:catch: kivételek elkapására szolgál.
    • c:if: feltételes vérehajtás.
    • c:choose, c:when, c:otherwise: a switch … case … default JSP megvalósulása.
    • c:forEach: alap ciklus; ez a klasszikus for ciklusnak felel meg.
    • c:forTokens: elemeken történő végigiterálás; valójában ez felel meg a klasszikus programozási nyelvek foreach struktúrájának.
    • c:url, c:param: egy URL tartalmát egy változóba tölti; paramétereket is megadhatunk.
    • c:redirect: átirányítás.
  • Függvény tag-ek (function tags): string műveleteket valósít meg. Használatának szokásos fejléce: <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> .
    • fn:contains(): megállapítja, hogy egy string tartalmaz-e egy másik stringet.
    • fn:containsIgnoreCase(): ugyanaz, mint az előző, de a kisbetű/nagybetű eltérés nem számít.
    • fn:indexOf(): visszaadja azt, hogy egy alstring hol kezdődik.
    • fn:startsWith(): annak ellenőrzése, hogy egy string egy adott stringgel kezdődik-e.
    • fn:endsWith(): annak ellenőrzése, hogy egy string egy adott stringgel végződik-e.
    • fn:escapeXml(): escape (\) karakterekkel látja el azokat a karaktereket, amelyeket különben XML tag-ként értelmezne.
    • fn:trim(): törli az elejéről és a végéről a szóköz karaktereket.
    • fn:split(): felbontja a stringet adott karakter mentén.
    • fn:toLowerCase(): kisbetűssé alakítja a stringet.
    • fn:toUpperCase(): nagybetűssé alakítja a stringet.
    • fn:substring(): adott kezdeti és vég pozíció alapján visszaadja az alstringet.
    • fn:substringAfter(): adott alstring utáni részt adja vissza.
    • fn:substringBefore(): adott alstring előtti részt adja vissza.
    • fn:length(): visszaadja a string hosszát.
    • fn:replace(): a stringben lecserél alstringeket más alstringekre.
  • Formázó tag-ek (formatting tags): szöveg formázó függvények. Fejléc: <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> .
    • fmt:parseNumber: ha a szöveg számot tartalmaz, akkor kinyeri a számot.
    • fmt:setTimeZone: beállítja az időzónát.
    • fmt:timeZone: időzónával kapcsolatos formázások.
    • fmt:formatNumber: számot formáz.
    • fmt:parseDate: dátum formátummá alakítja a dátumot tartalmazó szöveget.
    • fmt:setBundle: betölti és elmenti az erőforrás csomagot.
    • fmt:bundle:erőforrás csomagot (ResourceBundle) hoz létre.
    • fmt:message: nemzetköziesített üzenetet formát.
    • fmt:formatDate: dátumot formáz.
  • XML tag-ek (XML tags): XML formázó függvényeket tartalmaz. Fejléc: <%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x" %>. Használatához szükség van a xalan és a xerces függőségekre. A függvények hasonlítanak a fent bemutatott alapfüggvényekre, azzal a különbséggel, hogy itt XPath kifejezéseket használhatunk.
    • x:out: hasonló a {{c:out}} tag-hez, de itt XPath kifejezéseket adhatunk meg.
    • x:parse: XML elemzés.
    • x:set: egy XPath kifejezés eredményének értékére állíthatunk be egy változó értékét.
    • x:choose, x:when, x:otherwise: az XML elemzés során fellépő switch … case … default.
    • x:if: XPath kifejezés
    • x:transform: egy XML formázása ("átalakítása") egy XSL alapján.
    • x:param: a fenti transzformációt tudjuk ennek segítségével felparaméterezni.
  • SQL tag-ek (SQL tags): egyszerűbb adatbázis lekérdezésekhez használatos függvények, Fejléc: <%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %>.
    • sql:setDataSource: adatforrás beállítása.
    • sql:query: SQL SELECT lekérdezés.
    • sql:update: módosítás.
    • sql:param: az SQL műveletek paraméterének beállítása.
    • sql:dateParam: SQL dátum paraméter beállítása.
    • sql:transaction: belső tranzakció egy meglévő adatbázis kapcsolaton belül.

A példa nagyvonalakban megfelel a fentieknek; a javax.servlet-api mellé vegyük fel a következő függőséget:

        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

Hozzunk létre egy JSP fájlt (pl. src/main/webapp/index.jsp) az alábbi tartalommal:

<%@ taglib uri = "http://java.sun.com/jsp/jstl/core" prefix = "c" %>
<html>
   <body>
       <c:set var="factorial" scope="session" value="${1}"/>  
       <c:forEach var="j" begin="1" end="5">  
           <c:set var="factorial" scope="session" value="${factorial*j}"/>  
       </c:forEach>
       <c:out value="${factorial}"/> 
   </body>
</html>

Ez a példa az 5 faktoriálisát számolja ki, ami tartalmaz néhány alapműveletet, változót és kifejezést is. Megjegyzés: nálam az Eclipse hibát írt ki a legbelső set utasításnál, melynek okára nem jöttem rá, de a példa működik.

web.xml

Az src/main/webapp/WEB-INF/web.xml fájl (egészen pontosan: ami a keletkező .war fájlban a WEB-INF/web.xml) a webalkalmazások fő konfigurációs fájlja, XML formátumban. A Java EE 6-tól kezdve döntőrészt kiváltható annotációkkal, mégis, szükség esetén érdemes tudnunk a részleteit. A hivatalos(nak tűnő) specifikáció igen jó alapot nyújt: https://docs.oracle.com/cd/E13222_01/wls/docs100/webapp/web_xml.html.

Példát már láthattunk fent az alábbiakra:

  • <servlet>
  • <servlet-mapping>
  • <listener>
  • <filter>
  • <filter-mapping>

Számos egyéb elem van még, melyek közül talán az alábbi kettővel érdemes megismerkednünk:

  • <welcome-file-list>: itt adhatjuk meg azt, hogy alapból mely fájl töltődjön be. Ez tipikusan az index.html vagy index.jsp, de felüldefiniálhatjuk.
  • <error-page>: azt adhatjk meg, hogy az egyes HTTP hibakódon esetén melyik oldal jelenjen meg.

Lássunk egy példát! A projekt neve nálam webxml. A pom.xml felépítése legyen a szokásos (javax.servlet-api függőség, maven-war-plugin a szerkesztéshez).

src/main/webapp/WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?> 
<web-app>
    <welcome-file-list>
        <welcome-file>hello.html</welcome-file>
    </welcome-file-list>
    <error-page>
        <error-code>404</error-code>
        <location>/error.html</location>
    </error-page>
</web-app>

src/main/webapp/hello.html:

<html><body>
Hello, world!
</body></html>

src/main/webapp/error.html:

<html><body>
Missing URL.
</body></html>

Próbálkozzunk az alábbiakkal:

Modell-nézet-vezérlő

A modell-nézet-vezérlő (model-view-controller, MVC) egy szoftvertervezési szerkezeti minta. A lényege az, hogy érdemes az alkalmazást 3 jól elkülönülő részre bontani:

  • Modell (model): az adatok betöltését megvalósító komponens. Tipikusan az adatbázis kapcsolatárt felelős, de ide kerülnek azok a részek is, amelyek más adatforrásból gyűjtik az adatokat.
  • Nézet (view): az adatok megjelenítéséért felelős. Ez jelen esetben a HTML oldalak legenerálását jelenti.
  • Vezérlő (controller): a kapcsolatot teremti meg a modell, a nézet és a felhasználói interkaciók között.

Ez a minta leginkább a webalkalmazások területén elterjedt, annyira, hogy a webes keretrendszereket szokás MVC keretrendszereknek is nevezni. Technikailag a nézet nézet tipikusan (br nem feltétlenül) valamilyen HTML oldal (pl. JSP), a vezérlő servlet, a modell pedig valamilyen, a webalkalmazásokon kívül eső technológia, pl. alap Java, valamilyen adatbázis könyvtár használatával.

Lássunk egy egyszerűsített példát! Az név legyen mondjuk mvc; ennek megfelelően készítsük el az alkalmazás vázát, a pom.xml-lel együtt.

Modell

Az alkalmazás "adatbázisa" egy kulcs-érték párokból álló asszociatív tömb, melyben a kulcs egy egész, az érték pedig valamilyen gyümölcs. A példát most ne bonyolítsuk tényleges adatbázis kapcsolattal, "bedrótozva" készítsünk elő pár adatot (src/main/java/hu/faragocsaba/mvc/Model.java):

package hu.faragocsaba.mvc;

import java.util.HashMap;
import java.util.Map;

public class Model {
    private Map<Integer, String> fruits = new HashMap<Integer, String>();

    public Model() {
        fruits.put(1, "apple");
        fruits.put(2, "peach");
        fruits.put(3, "orange");
        fruits.put(4, "banana");
        fruits.put(5, "grape");
    }

    public String getFruit(int id) {
        return fruits.get(id);
    }

}

Nézet

A felhasználó a webes felületen megadhat egy számot, és eredményül a neki megfelelő gyümölcsöt kapja. A program belépési pontja egy sima HTML oldal (src/main/webapp/index.html):

<html><body>
<form action="controller" method="post">
ID = <input type="number" name="id" min="1" max="5">
<input type="submit" value="Submit">
</form>
</body></html>

Itt a controller a kontroller neve, ez bármi lehet.

Az eredményt az alábbi JSP oldal jeleníti meg (src/main/webapp/view.jsp):

<html><body>
Fruit: <%= request.getAttribute("fruit") %>
</body></html>

Vezérlő

A fentiek alapján először szedjük össze, hogy mit kell végrehajtania vezérlőnek:

  • Az index.html oldalról egy HTTP POST kérésben érkezik egy id paraméter, ezt ki kell olvasni.
  • A modellből le kell kérni az id azonosítójú gyümölcs nevét.
  • A visszaadott értéket be kell állítani a kérés fruit attribútumaként.
  • A kérést továbbítani kell a view.jsp felé.

Mindez servletként megvalósítva (/mvc/src/main/java/hu/faragocsaba/mvc/Controller.java):

package hu.faragocsaba.mvc;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
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(name = "Controller", urlPatterns = "/controller")
public class Controller extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private Model model = new Model();

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        String id = request.getParameter("id");
        String fruit = model.getFruit(Integer.parseInt(id));
        request.setAttribute("fruit", fruit);
        RequestDispatcher dispatcher = request.getRequestDispatcher("view.jsp");
        dispatcher.forward(request, response);
    }
}

Egyúttal megismerkedtünk a servletben történő kérés továbbítással is. Telepítsük a szokásos módon, töltsük be a megfelelő oldalt (http://localhost:8080/mvc/), írjunk be egy 1 és 5 közötti számot, és kattintsunk a Submit gombra.

Webalkalmazás keretrendszerek

Pusztán servetet használva elméletileg bármilyen webalkalmazást el tudunk készíteni. A probléma ezzel az, hogy a fejlesztés ebben a formában rendkívül lassú, a kód nehezen áttekinthető, nehezen karbantartható, és nincsenek olyan eszközük, amelyek egyfajta egységesítést kényszerítene ki. Ráadásul egy bizonyos bonyolultságon túl macerás mindent az alap Tomcat-re építeni. Ezekre a problémákra adnak megoldást a webalkalmazás keretrendszerek (web application framework). Számos ilyen könyvtár létezik, melyekről részletesen a Java keretrendszerek oldalon olvashatunk. Kb. úgy viszonyul a servlet a webalkalmazás keretrendszerekhez, mint az assembly a magas szintű programozási nyelvekhez: elvileg lehet ez utóbbiak nélkül élni, gyakorlatilag nem érdemes.

Az egyes webes keretrendszerek összehasonlítása is igen nehéz, mivel többféle van belőlük, és az egyes kategóriák között nem éles a határ:

  • Tisztán Java alapú web keretrendszerek: ezek azok, amelyek tag könyvtárakat alkalmaznak, amelyek beépülnek a HTML oldalba, ill. HTML részek generálódnak belőle. Ezeket gyakran MVC könyvtáraknak hívjuk. Többnyire tartalmaznak AJAX megoldást is, ami JavaScript-et is generál a HTML kódba. Ez dinamikusan változtatja a HTML tartalmat, azaz az oldal teljes újratöltése nélkül változik a tartalom. Ez oyan hatást kelt, mintha normál alkalmazás lenne, nem pedig egy weboldal. Ilyen pl. a JSF.
  • Olyan Java keretrendszerek, melyek tartalmaznak kisebb-nagyobb méretű webes felületet: ebbe a kategóriába azokat a megoldásokat sorolom, amelyeknek a célja elsődlegesen nem új tag-ek létrehozása, de tartalmaznak ilyet. Ezek a rendszerek tipikusan önállóan futó programok, azaz tartalmaznak valamiféle webszervert. Ide sorolom pl. a Springet.
  • JavaScript keretrendszerek: a fentiek tipikusan olyan könyvtárak, amelyek során a fejlesztés szerver oldalon történik, ami HTML kódot (ill. hozzá tartozó CSS-t és JavaScript-et) generál. Az ebbe a kategóriába tartozó keretrendszerek viszont a közvetlen frontend fejlesztést teszik lehetővé. Vannak közöttük olyanok, amelyek a funkcionalitást egyszerűsítik, olyanok, amelyek magasabb szintű elemelet definiálnak, ill. célirányosan egyéb könyvtárak is. Fontos, hogy itt a fejlesztő közvetlenül fejleszti a HTML oldalt JavaScript segítségével. Példák: jQuery, AngularJS, ExtJS.
  • PHP alapú webes keretrendszerek: a PHP sokban hasonlít a JSP-hez (egészen pontosan fordítva: a JSP a PHP-hoz), ugyanis a PHP is gyakorlatilag olyan HTML oldal, amely tartalmaz olyan részeket, amelyek szerver oldalon értékelődnek ki. Az eredménye ennek is HTML (más nem is lehetne), de az oldal bizonyos részei (vagy akár az egész) letöltéskor generálódnak. Közvetlenül PHP-ban fejleszteni kb. olyan, mint közvetlenül JSP-ben: lehet, de inkább érdemes olyan keretrendszerekkel megismerkedni, amelyek felgyorsítják a fejlesztés folyamatát, mert kész megoldásokat nyújtanak gyakran felmerülő problémákra. Az egyik legnépszerűbb PHP keretrendszer a Drupal.
  • ASP.NET: a Microsoft világ .NET-re épülő webes keretrendszere, mely a fenti lista minden kategóriájából kilóg.

A Java világban elsősorban az első kettő kategóriát tartjuk fókuszban, melyről a Java keretrendszerek oldalon olvashatunk részletesebben. A JavaScript és PHP témákat a Web fejlesztés oldal érinti, míg az ASP.NET helye a C#, .NET oldal.

Beépülő webszerver

A fenti példák futtatásához szükségünk volt webszerverre, melyhez a Tomcat-et használtuk. A tapasztalatom szerint az ilyen külső függőség mindig potenciális problémaforrás: pl. ha nem pont ugyanazt a verziót használjuk, esetleg egy konfigurációs változtatást elfelejtünk ledokumentálni, akkor előfordulhat, hogy az egyik számítógépen másképp fut mint a másikon. Én személy szerint - ha csak lehet - az önmagában kerek egész (self-contained) programok híve vagyok. Az Enterprise Java világa persze alapvetően nem erről szól, de azért bizonyos esetekben ez is lehetséges. A webalkalmazások esetében pl. az egyik legnépszerűbb beépülő webszerver a Jetty.

Mindenekelőtt a Jetty egy önmagában működő web szerver, és ilyen értelemben a Tomcat versenytársa.Letölthető a https://www.eclipse.org/jetty/ oldalról: Downloads → itt a .zip-et töltsük ki, csomagoljuk ki egy tetszőleges könyvtárba. A webalkalmazásokat (tehát a .war fájlokat) itt is a webapps könyvtárba kell másolnunk. A webszervert a start.jar indításával tudjuk futtatni (java -jar start.jar, vagy duplán kattintva az ikonra).

Sokkal izgalmasabb viszont a beépülő (embedded) megoldás! Lássunk erre egy példát!

A pom.xml-be a jetty-server és jetty-servlet függőségeket kell felvennünk:

<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>jetty</artifactId>
    <version>1.0</version>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.4.26.v20200117</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>9.4.26.v20200117</version>
        </dependency>
    </dependencies>
</project>

Vegyük észre, hogy a <packaging> hiányzik, azaz az alapértelmezett jar lesz.

Készítsük el a servletet, pl.:

package hu.faragocsaba.jetty;

import java.io.IOException;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.getWriter().println("Hello, Jetty world!");
    }
}

Annotáció vagy web.xml nem kell, mert ez nem webalkalmazás. A servletet kódból indítjuk, a következőképpen:

package hu.faragocsaba.jetty;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletHandler;

public class JettyServer {
    public static void main(String[] args) throws Exception {
        Server server = new Server();
        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8090);
        server.setConnectors(new Connector[] {connector});
        ServletHandler servletHandler = new ServletHandler();
        server.setHandler(servletHandler);
        servletHandler.addServletWithMapping(HelloServlet.class, "/hello");
        server.start();
    }
}

Ha elindítjuk, akkor egy böngészőben a http://localhost:8090/hello oldalt betöltve tudjuk kipróbálni.

Ezzel kapcsolatban igazán "telitalálat" dokumentációt nem találtam. Az alábbiak alapján gereblyéztem össze az információkat:

Egységtesztelés

A beépülő Jetty webszerver lehetővé teszi a webalkalmazások egységtesztelését is. A kockák nagyrészt már rendelkezésre állnak, rakjuk őket össze! Készítsünk egy webalkalmazást, amely tartalmaz egy servletet, másrészt egy olyan egységtesztet, amely elindít egy beépülő Jetty webkonténert, végrehajtja a lekérdezést, majd ellenérzi az eredményt!

A pom.xml felépítése megfelel a szokásos webalkalmazásoknak, de tartalmazza az egységteszteléshez, valamint a Jetty futtatáshoz szükséges komponenseket is:

<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>webappunittest</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.4.26.v20200117</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>9.4.26.v20200117</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.1</version>
            </plugin>
        </plugins>
    </build>
</project>

A servlet kód lehet bármelyik fenti vagy pl. a következő (src/main/java/hu/faragocsaba/webappunittest/HttpGetServlet.java):

package hu.faragocsaba.webappunittest;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/")
public class HttpGetServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter out = response.getWriter();
        String name = request.getParameter("name");
        out.println("Hello, " + name + "!");
    }
}

Próbáljuk ki: a szokásos módon fordítsuk le, telepítsük, és ellenőrizzük, pl. http://localhost:8080/webappunittest/?name=Csaba. Ha működik, készítsük el az egységtesztet! (Igazából ez rossz sorrend! Elvileg először kell megírnunk az egységtesztet, utána a kódot, és ha működik az egységteszt, csak azt követően ellenőrizzük a teljes működést. De most - utoljára! - kivételt teszünk.)

Az egységteszt kódja az alábbi (src/test/java/hu/faragocsaba/webappunittest/HttpGetServletTest.java):

package hu.faragocsaba.webappunittest;

import static org.junit.Assert.assertTrue;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletHandler;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class HttpGetServletTest {
    private static Server server;

    @BeforeClass
    public static void setup() throws Exception {
        server = new Server();
        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8090);
        server.setConnectors(new Connector[] {connector});
        ServletHandler servletHandler = new ServletHandler();
        server.setHandler(servletHandler);
        servletHandler.addServletWithMapping(HttpGetServlet.class, "/webappunittest");
        server.start();
    }

    @AfterClass
    public static void teardown() throws Exception {
        server.stop();
    }

    @Test
    public void testGreet() throws Exception {
        HttpClient client = HttpClientBuilder.create().build();
        HttpGet method = new HttpGet("http://localhost:8090/webappunittest?name=Csaba");

        HttpResponse httpResponse = client.execute(method);

        String response = EntityUtils.toString(httpResponse.getEntity());
        assertTrue(response.contains("Hello, Csaba!"));
    }
}

A setup() indítja a web konténert, és mivel ez drága művelet, csak egyszer indul el, és ha lenne több egységteszt, akkor ugyanazt a példányt használnák. Az org.apache.http csomag néhány osztályát használjuk a lekérdezéshez.

EJB alkalmazások

Á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:

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. TODO: utána járni.

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.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License