Jáva szervletek és Java Server Pages

Paller Gábor

1. A HTTP protokol alapjai

A HTTP protokollt webes kliensek és szerverek beszélik egymással, tipikusan böngészők és webszerverek. A HTTP egy kapcsolat nélküli, kérés-válasz protokol, amelyet egy megbízható transzportréteg, tipikusan TCP felett használnak. A protokol szolgáltatásai az OSI Session rétegének felelnek meg.

A HTTP kérés a következő részekből áll:
A HTTP válasz ehhez eléggé hasonló
A HTTP parancs a célcím azonosítóval és a HTTP státusz mindig jelen van, ezen felül a HTTP válasz bizonyos fejlécei majdnem mindig szerepelnek. A többi rész opcionális.

A HTTP-t legegyszerűbb példákon keresztül megérteni (a karakter a soremelés-kocsivissza karaktereket reprezentálja, a túl hosszú sorok a következő sorban folytatódnak). A kérés-válasz egy Netscape 7.1 böngésző és egy Apache webszerver között zajlik.

GET /test/example.html HTTP/1.1¶
Host: localhost¶
User-Agent: Mozilla/5.0 (Windows;U; Windows NT5.0; en-US; rv:1.4) Gecko/20030624
Netscape/7.1 (ax)¶
Accept: application/x-shockwave-flash,text/xml,application/xml,
application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,video/x-mng,image/png,
image/jpeg,image/gif;q=0.2,*/*;q=0.1¶
Accept-Language: en-us,en;q=0.5¶
Accept-Encoding: gzip,deflate¶
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7¶
Keep-Alive: 300¶
Connection: keep-alive¶


Érdemes odafigyelni az utolsó fejléc után levő CR-LF jelre. A fejlécblokk végét egy üres sor jelzi. Ezután következne a kérés tartalom, de ebben a kérésben ilyen nincsen, mert a GET parancshoz tartalom nem tartozik. Lássuk ezek után a választ!

HTTP/1.1 200 OK¶
Date: Fri, 26 Sep 2003 12:11:49 GMT¶
Server: Apache¶
Last-Modified: Fri, 26 Sep 2003 12:08:03 GMT¶
ETag: "2041b-54-66790ec0"¶
Accept-Ranges: bytes¶
Content-Length: 84¶
Connection: close¶
Content-Type: text/html; charset=ISO-8859-1¶

<html><head><title>Example</title></head>¶
<body><h1>Example</h1></body></html>¶

A válaszban már volt státusz, fejlécek és tartalom is. A legfontosabb fejléc a Content-Type, ez mondja meg a böngészőnek, mi módon kell megjeleníteni a tartalmat. A Content-Type az u.n. MIME (Multipurpose Internet Mail Extensions) típusra utal, ez ebben az esetben text/html, tehát egy HTML oldalról van szó. Fontos kiemelni, hogy a HTTP akármilyen tartalom átvitelére alkalmas, tehát nem kell szövegesnek lennie. Meglehetősen tipikus pl. HTTP felett image/jpeg (JPEG kép) tartalmat átvinni. Másik triviális, de fontos megállapítás, hogy a tartalom típusát a szerver mondja meg, tehát teljes szabadsága van abban, milyen célcímhez milyen tartalmat rendel. A célcím a tartalom típusát nem határozza meg, tehát pl. a http://www.server.com/index.html cím mögött nyugodtan lehet egy kép.

Nézzünk most egy másik példát, ahol van kérés tartalom. Itt egy másik parancs, a POST szerepel, amelyik a kéréssel tartalmat is küld. A kérés tartalomnak ugyanúgy van típusa, mint a válasznak.

POST /cgi-bin/test-cgi HTTP/1.1¶
Referer: http://localhost/test/phbook1.html
Content-Type: application/x-www-form-urlencoded¶
Content-Length: 55¶

firstname=John&familyname=Doe&phonenumber=%2B1777111111¶

A válasz szerkezete hasonló az előző példában látotthoz. Habár ebben a példában egy HTML form-ot küldtünk, ami szöveges formátumú kérés tartalmat jelent, a kérés tartalom lehetne akármilyen, akár bináris típus is.

Messze a legtöbbet használt HTTP parancsok a GET és a POST. Ezen felül még az OPTIONS, HEAD, PUT, DELETE, TRACE és CONNECT parancsok léteznek.

A HTTP protokol megváltozik, ha proxy szerverekkel együtt használják. A proxy szerver a tűzfalba van építve és csak bizonyos protokoll (pl. HTTP) átvitelére alkalmas. A kliensek a célszerver helyett a proxyhoz kapcsolódnak, a proxy értelmezi a kérést és a tűzfal túloldalán kapcsolódik a valódi célszerverhez és megismétli a kérést. Hogy ezt meg tudja csinálni, a kérésnek tartalmaznia kell a teljes célcímet (/test/example.html helyett http://www.server.com/test/example.html).

Példa kérés-válasz proxyval.

GET http://172.24.169.107/test/example.html HTTP/1.1¶
Host: 172.24.169.107¶
Keep-Alive: 300¶
Proxy-Connection: keep-alive¶


HTTP/1.1 200 OK¶
Date: Wed, 01 Oct 2003 12:27:56 GMT¶
Proxy-Connection: close¶
Via: 1.1 proxy1 (NetCache NetApp/5.3.1R4), 1.1 proxy2 (NetCache NetApp/5.2.1R1D4)¶

… válasz tartalom …

2. Jáva szervletek

A webszerverek a kezdetek óta tudnak dinamikus tartalmat generálni. Ez azt jelenti, hogy egy kérés érkezésekor a kiszolgált tartalmat nem egy statikus fájlból veszik, hanem a kérés egy programhoz kerül, ami dinamikusan generálja a választ. A legősibb ilyen megoldás a Common Gateway Interface (CGI), ami egy processzt indít az operációs rendszerben és annak adja át a kérést, de hasonló rendszerek készültek pl. Perl-hez és a népszerű nagy adatbázisokhoz. A Jáva válasza a problémára a Jáva szervlet. A Jáva szervletet a JSR-53 specifikálja.

A Jáva szervlet egy Jáva osztály, amely egy bizonyos szülőosztályból (javax.servlet.http.HttpServlet) származik és regisztrálják a webszervernél. A webszerver ezek után tudja, hogy bizonyos célcím kérése esetén a szervletet kell meghívni. A szervlet ilyenkor megkapja a kérést és a kérés alapján generálhatja a választ. A webszerver továbbra is elvégzi a web-kiszolgálással kapcsolatos teendőket (HTTP kezelés, TCP kapcsolat kezelés, URL-leképezés, terhelésmegosztás, stb.) A webszerver szervleteket futtató modulját szervlet konténernek is nevezik, jelezvén, hogy a szervlet konténer teremti meg a szervlet futási feltételeit.

A szervlet a következő állapotokon megy keresztül.
A következő példa egy minimális szervletet mutat.

public class MinimalServlet extends javax.servlet.http.HttpServlet {

// Init metódus. Itt lehetne elhozni a konfigurácios paramétereket
  public void init( ServletConfig conf ) throws ServletException {
    super.init( conf );
  }

// GET kérések kezelése
  public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {
    res.setContentType( "text/html" );
    PrintWriter out = new PrintWriter( res.getWriter() );
    out.println( "<body>Hello, GET!</body>" );
  } 

// POST kérések kezelése
  public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {
    res.setContentType( "text/html" );
    PrintWriter out = new PrintWriter( res.getWriter() );
    out.println( "<body>Hello, POST!</body>" );
  } 

// Szervlet destroy. Meghívódik, amikor a szervletet elpusztitja a webszerver
// (pl. a webszerver leáll)
  public void destroy() {}
}

A szervleteket lehet telepíteni direktben a szervlet konténer megfelelő könyvtáraiba, de ez a módszer ellenjavalt. Mindenekelőtt konténerenként, sőt azok verzióiként változik, hova mit kell tenni, hogy a szervlet regisztrálva legyen. Ezen kínlódások elkerülése végett a szervleteknek (pontosabban: szervleteket tartalmazó web alkalmazásoknak) is van csomagolási formátuma, a WAR formátum. A WAR egy közönséges JAR formátum, aminek kötött könyvtárszerkezete van, így a szervlet konténer megtalálhatja a szervletek regisztrálásához szükséges információkat. Mindenekelőtt a WAR-ba tetszőleges fájl vagy könyvtár elhelyezhető, ezeket statikus tartalomként fogja a webszerver kiszolgálni. Pl. a WAR-ban levő dir/content.html fájl elérhető a http://szervercím/webappcím/dir/content.html címen. A WAR-ban levő WEB-INF könyvtárnak (szigorúan nagybetűvel!) különleges szerepe van. A struktúra így néz ki:

WEB-INF/
  web.xml
  classes/
  lib/

A web.xml leírófájl definiálja a szervleteket, erre később visszatérek. A classes könyvtár alatt vannak a szervletekhez tartozó osztályok (ha package-ben vannak, akkor persze ennek megfelelő struktúrában), a lib könyvtár alatt pedig a szervlet osztályok által igényelt könyvtárak JAR formában. Úgy is fel lehet fogni, hogy az archívumban levő szervletek futása alatt a classes könyvtár és a lib könyvtárban levő JAR fájlok rajta vannak a CLASSPATH-en.

Nézzük most a web.xml fájlt! A web.xml sokféle opciót leírhat, most csak a legfontosabbakra, a szervlet osztály meghatározására és a szervlet URL-hez rendelését nézzük meg. Példa:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC
   "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
   "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
   <description>Alkalmazasleiro az MinimalServlet-hez</description>
   <servlet>
      <servlet-name>Servlet1</servlet-name>
      <servlet-class>MinimalServlet</servlet-class>
      <init-param>
         <param-name>someConfigName</param-name>
         <param-value>someConfigValue</param-value>
         <description>Example configuration variable</description>
      </init-param>
   </servlet>
   <servlet-mapping>
      <servlet-name>Servlet1</servlet-name>
      <url-pattern>/MiniServletURL</url-pattern>
   </servlet-mapping>
</web-app>


web.xml egy XML fájl, ennek megfelelő formátumleíróval kezdődik. (megjegyzés: a web.xml alapértelmezett kódolása - mint minden XML fájlnak - UTF-8. Ez az ékezetes betűknél okoz problémát. Ha nincs kéznél UTF-8-at támogató szövegszerkesztő, inkább ne tegyünk ékezetes betűt a web.xml-be.) Minden egyes <servlet> elem egy szervletet definiál. A <servlet-name> nevet ad a szervletnek, hogy hivatkozni lehessen rá, a
<servlet-class> definiálja a szervletosztályt (a package-dzsel együtt, de a példában nincs package), az <init-param> elemben pedig egy konfigurációs paramétert lehet definiálni, amit az init() kap meg a ServletConfig egyeden keresztül. <init-param>-ból is tetszőleges számú lehet.

A szervlet csak akkor elérhető, ha célcímhez van rendelve. Korábbi vagy fejlesztési célú konténerek megengedik szervletek automatikus regisztrációját, amikor akármelyik osztályt a konténer webalkalmazás könyvtárából meg lehet próbálni szervletként meghívni. Ez persze nagyon veszélyes biztonsági szermpontból, ezért az elterjedt konténerek csak címhez regisztrált szervlethez adnak hozzáférést. Ez történt a <servlet-mapping> elemben, ahol a szervletet a /MiniServletURL címhez rendeltük. Megjegyzendő, hogy a leképezés egyszerű reguláris kifejezés is lehet, pl. *.jsp, ekkor a webalkalmazás címe alatt levő összes illeszkedő címre (.jsp-vel végződő célcímre) meghívódik a szervlet. A HttpServletRequest-ből a szervlet kiveheti a kérés teljes célcímét.

A konténer szabadon dönthet, milyen stratégiát követ a teljes cím szervlethez rendelésekor, szokásos például a http://<szerver cím>/<webalkalmazás név>/<url-pattern>, ahol a webalkalmazás név alapértelmezésben a WAR neve, az url-pattern pedig az <url-pattern> elemben megadott célcím leképezés.

Minden webalkalmazáshoz egy ServletContext objektum van rendelve. Ez lehetővé teszi, hogy egy webalkalmazásba tartozó szervleteknek közös konfigurációs paraméteri legyenek (<context-param> elem a web-app elem alatt),  a ServletContext az egész webalkalmazásban (tehát a webalkalmazás más szervletei által is látható) változókat tárolhat és a ServletContext-en keresztül elérhetők a webalkalmazás statikus állományai.

A webalkalmazás állapotának változásaihoz eseménykezelőket lehet rendelni. Jelenleg 4 eseménykezelő létezik.
A konténer az eseménykezelő osztályokat az általuk implementált interfész (pl. javax.servlet.ServletContextListener) alapján azonosítja. Az eseménykezelőket a web.xml-ben adjuk meg és a konténer példányosítja őket.

<web-app>
  <listener>
     <listener-class>com.acme.MyConnectionManager</listener-class>
   </listener>
    ....
</web-app>


3. Java Server Pages (JSP)

Amint azt minimális szervletünknél is észrevehettük, ha a szervlet HTML lapot generál, a szervlet kódja szükségszerűen tele lesz HTML darabokkal. Ez egyfelől nehezen készíthető el, másfelől nehezen tartható karban. A népszerű template-nyelvek (PHP, ASP) mintájára létrehozták a JSP nyelvet. A JSP a szervlet technológiára épít.

A JSP kifordítja a szervletet: a JSP-nél a HTML (vagy XML) oldalba ágyazzuk a Jáva kóddarabokat, amelyek az oldalt dinamikussá teszik. Egy HTML oldal (.jsp kiterjesztéssel) tehát érvényes JSP oldal. A legfontosabb kifejezések, amelyeket az oldalba helyezhetünk a következők:
A JSP feldolgozó a JSP lap első hivatkozásakor a lapot Jáva szervletté fordítja, azt Jáva bájtkódra fordítja és ez a szervlet hajtódik végre. Ennek megfelelően a szervlet szolgáltatásai elérhetőek, a JSP fordító olyan kódot fordít, ami ismert nevű objektumokon keresztül teszi elérhetővé pl. a HttpServletRequest vagy HttpServletResponse objektumokat. A JSP lapban tehát leírhatunk ilyet:

<% String param = request.getParameter( "parm" ) %>

vagy

<% out.println( "Date is: "+ new Date().toString() ) %>

mert biztosak lehetünk benne, hogy a request vagy az out változó ismert lesz a JSP fordító által generált program adott pontján.

A JSP-t tartalmazó webalkalmazást ugyanúgy WAR-ba csomagolhatjuk, mint a szervleteket (és persze a JSP-k és a szervletek keverhetők). A JSP lapokat egyszerűen a többi statikus fájl közé kell tenni a WAR-ban. A JSP lap előfordítható, ekkor a JSP-ből generált szervlet kerül a WAR-ba (a szokásos WEB-INF/classes könyvtárba) és a <servlet-mapping> elemmel biztosítjuk, hogy a JSP-re vonatkozó kérés a szervletnél kössön ki. Pl.:

<web-app>

  <servlet>
    <servlet-name>HelloWorld </servlet-name>
    <servlet-class> _jsp_HelloWorld_XXX_Impl.class </servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>HelloWorld </servlet-name>
    <url-pattern>/HelloWorld.jsp </url-pattern>
  </servlet-mapping>
</web-app>