Webalkalmazások készítése Javában

Kategória: Enterprise Java.

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

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