7.3. JSP

Ievads

Mērķi

  • Iepazīties ar JSP lapu pamatkonstrukcijām un "iebūvētajiem mainīgajiem"
  • Iepazīties ar JSP un "Java beans" komponenšu sadarbību
  • Autorizācija un navigācijas joslas
  • Failu augšupielāde
  • Datubāzu piekļuve, savienojumu dīķis

JSP un servletu salīdzinājums

Servletus labi izmantot, ja lapas ģenerēšanai vajag daudz programmēt:

  • atklāti manipulēt ar HTTP statusa kodiem un hederiem
  • uzstādīt cepumiņus un/vai manuāli uzturēt sesiju
  • piegādāt lappuses saspiestā formā
  • tieši piekļūt datubāzēm
  • dinamiski ģenerēt resursus binārā formā, piemēram, attēlus
  • veikt fiksētu, labi parametrizējamu uzdevumu, piemēram, izdarīt XSLT transformācijas

JSP labi izmantot, ja vajag atdalīt prezentāciju no dinamiskas satura ģenerēšanas:

  • HTML dokumentā tikai daži fragmenti ir jāģenerē dinamiski (piemēram, failu iekļāvumi (SSI), dinamisks lappuses datums utml.)
  • Gribam garantēt, ka prezentācijas slānis ir atdalīts no aplikācijas loģikas slāņa

JSP apstrāde

Pirmais jaunizveidotas JSP lapas pieprasījums gjenerē un kompilē atbilstošu Javas klasi - servletu. Visi turpmākie pieprasījumi izmanto šo servletu, kura gjenerētais izejas kods un kompilācijas rezultāts ir atrodami servletu konteinera darba direktorijās. Gjenerētais servleta izejas teksts reizēm palīdz atklāt kompilācijas un izpildes laika kļūdas (jo kļūdu paziņojumi uzrāda rindiņu numurus gjenerētajā servletā, nevis programmētāja rakstītajā JSP lapas tekstā).

JSP apstrādes secība OC4J konteinerā

Šablona teksta apstrāde

Šablona teksts - HTMLa sagatave, kuru iekļauj JSP lapā. To mēdz veidot ar dažādiem HTML dizaina rīkiem; šablona teksts nonāk klienta lapā bez izmaiņām.

Visas lietas, kas neattiecas uz šablona tekstu ierobežo ar <%...%>. Ja HTML dokumenta rezultātā vajag dabūt <%, tad var rakstīt <\%.

JSP komentārs, kurš nenonāk klienta HTML dokumentā:

<%-- JSP komentārs --%>

HTML komentāri nonāk klienta dokumentā bez izmaiņām:

<!-- HTML komentārs -->

JSP koda iebūvētie mainīgie

request
tekošās lapas HttpServletRequest
response
tekošās lapas HttpServletResponse
out
PrintWriter instance, lai sūtītu izvadi klientam (precīzāk - buferizēts JspWriter)
session
tekošās sesijas HttpSession (JSP, ja vien nav īpašas direktīvas, vienmēr izveido sesiju). JSP var noglabāt tur datus, kuri kopīgi vienai klienta sesijai.
application
ServletContext objekts, analoģisks getServletConfig().getContex(). JSP var noglabāt tur datus, kuri kopīgi visai aplikācijai.
this
Tekošā servleta instance. To lieto reti.

Uz mainīgajiem session un application var izsaukt parametra uzlikšanas un nolasīšanas metodes.

public void setAttribute(String name, Object value); 
public Object getAttribute(String name);

JSP izteiksmes <%= ... %>

Servera laiks: <%= new java.util.Date() %> <br />
Laiks citā formātā: 
<%= (new java.text.SimpleDateFormat("yyyy-MM-dd"
  + " HH:mm:ss")).format(new java.util.Date()) %>

Izteiksmi izrēķina, pārveido par stringu un ievieto HTML lapā.

Klienta pieprasījums nāk no: 
<%= request.getRemoteHost() %>

Vēl daži izteiksmju piemēri:

Jūsu sesijas ID: <%= session.getId() %>
Formas parametra "test" vērtība: 
  <%= request.getParameter("test") %>

Lai uzstādītu "test" parametru, vai nu jāveido HTML forma, vai arī jāizsauc augšminētā JSP lapa ar apmēram šādu URL:

http://localhost:8888/piemeri/piemers.jsp?test=XXX

JSP skriptleti: <% ... %>

Sarežģītākām programmēšanas konstrukcijām, kuras nevar uzrakstīt ar vienu izteiksmi, domāti t.s. skriptleti jeb sīkscenāriji.

<%
String queryData = request.getQueryString(); 
out.println("GET pieprasījuma strings: " + queryData); 
%>

Vai arī direktīva, kura uzstāda citu MIME tipu (noklusētais ir text/html):

<% response.setContentType("text/plain"); %>

Servletu programmētājam, kurš izsauc līdzīgu komandu ir jāņem vērā, ka HTTP pieprasījuma hederi (t.sk. "Content-Type" hederis) ir jāuzstāda PIRMS tiek sūtīts lapas saturs (t.sk. pirms "out.println" komandas). Toties JSP programmētājs var paļauties uz to, ka JspWriter buferizē izvadi, tātad šādus hederus JSP lapās var uzstādīt patvaļīgās vietās.

JSP deklarācijas: <%! ... %>

Deklarāciju redzmības līmenis ir servleta klases ķermenis (ārpus _jspService metodes)

<%! private int accessCount = 0; %>
Pieprasījumu skaits kopš servletu 
  konteinera pārstartēšanas:  
  <%= ++accessCount %>

(Nestatisks) mainīgais "accessCount" ir kopīgs vairākām klientu sesijām, jo tās izveido tikai vienu servleta klases instanci.

JSP importa direktīva: <%@ page import="..." %>

Bez īpašas deklarēšanas JSP lapās ir pieejamas šādas pakotnes:

java.lang.*
javax.servlet.*
javax.servlet.jsp.*
javax.servlet.http.*

Ja vajadzīgas citas pakotnes, jālieto importa direktīva. Piemēram,

<%@ page import="java.util.*" %>

Ja tiek izmantotas pakotnes, kuru nav Javas standarta API, programmētājam tās jāiekopē servletu konteineram specifiskā vietā. OC4J gadījumā ir divas iespējas:

  • c:\oc4j\j2ee\home\lib - to redzēs visas aplikācijas
  • aplikacija\WEB-INF\lib - to redzēs tikai viena aplikācija

JSP MIME tipa direktīva: <%@ page contentType="..." %>

Pēc noklusēšanas JSP izvada MIME tipu "text/html" (t.i. HTML failu), lietojot ISO-8859-1 kodējumu (t.i. Rietumeiropas kodu tabulu). Citos gadījumos vajag MIME direktīvu (Content-type hedera uzstādīšana):

<%@ page contentType="text/plain" %>
<%@ page contentType="text/html; charset=windows-1257" %>

Varam lietot arī skriptletus. Piemēram:

<% response.setContentType("text/plain") %>

JSP Pavediendrošības direktīva:

JSP programmētājam ieteicams rūpēties par skriptletu koda pavediendrošumu pašam. Tā kā vairāki klienti vienlaikus izmanto to pašu servleta instanci, dažādi pavedieni var viens otru pārtraukt jebkurā koda vietā. Piemēram šāds kods nav pavediendrošs:

<%! private int idNum = 0; %>
<% 
String userID = "userID" + idNum; 
out.println("Tavs ID ir " + idNum); 
idNum = idNum + 1; 
%>

Viens no risinājumiem būtu iekļaut kritisko posmu synchronized blokā:

<%! private int idNum = 0; %>
<% 
synchronized(this) {
    String userID = "userID" + idNum; 
    out.println("Tavs ID ir " + idNum); 
    idNum = idNum + 1; 
}
%>

Tas nozīmē, ka attiecīgajā servletā pavediens, kurš sācis pildīt kritisko posmu, kura marķēta ar "this", nelaidīs šajā posmā (un arī citos tāpat marķētos posmos) nevienu citu pavedienu. Objekta "this" vietā var lietot arī jebkādu citu servleta klases līmenī redzamu objektu.

Ja programmētājs nevēlas pats garantēt koda pavediendrošumu, tad jāraksta direktīva:

<%@ page isThreadSafe = "false" %>

Šajā gadījumā servlets īsteno SingleThreadModel - saliek klientus rindā un laiž tos servletā pa vienam. Šādā gadījumā servletu konteiners var izveidot vairākas servleta instances (baseinu), kuru kopīgi izmanto klienti. T.i. pazeminās ātrdarbība un parādās atšķirība starp statiskajiem un nestatiskajiem servleta mainīgajiem.

JSP sesijas uzturēšanas direktīva

Ja nevēlamies JSP lapu klientus vārdzināt ar cepumiņiem, varam neuzstādīt sesiju:

<% page session="false" %>

Šādā gadījumā JSP iebūvēto mainīgo session nevar lietot.

JSP kļūdu apstrādes lapas direktīva

<%@ page errorPage = "Relatīvs URL" %>

Uz šo lapu pāriet tad, ja JSP lapā rodas nenoķerts izņēmums. Piemēram, JSP lapa connect.jsp mēģina pieslēgties datubāzei pēc norādītajiem formas parametriem, bet nepārbauda šo parametru pareizību. Liels risks rasties kļūdai, bet to apstrādājam citā JSP lapā:

<%@ page errorPage="dbconnect_error.jsp" %>
<%
String hostname = request.getParameter("hostname"); 
String user = request.getParameter("user"); 
String password = request.getParameter("password"); 
String port = request.getParameter("port"); 
String database = request.getParameter("database"); 

String drivername = "org.gjt.mm.mysql.Driver";
Class.forName(drivername).newInstance();
String url = "jdbc:mysql://" + hostname + ":" 
  + port + "/" + database; 
Connection con = DriverManager
  .getConnection(url, user, password);
session.setAttribute("connection", con); 
response.sendRedirect("tables.jsp?sql=" 
  + URLEncoder.encode("SHOW TABLES")); 
%>

Cita JSP lapa - dbconnect_error.jsp apstrādā izņēmumu, kurš nonāk tur kā iebūvēts mainīgais exception. Lai varētu izmantot šādu mainīgo un kalpot par kļūdu apstrādes lapu, vispirms ir jāuzliek īpaša "isErrorPage" direktīva:

<%@ page contentType="text/html; charset=windows-1257" %>
<%@ page isErrorPage="true" %>
<p>DB konektēšanās mēģinājums nosprāga ar šādu kļūdu: 
  <%= exception %></p>
<p>Kļūda radās sekojošā vietā:</p>
<pre>
<% exception.printStackTrace(new PrintWriter(out)); %>

JSP direktīva faila iekļaušanai: <%@ include file="..."%>

Pirms servleta izejas koda ģenerācijas var lapā iekopēt koda gabalu no cita faila (t.i. notiek faila statiskā iekļaušana).

<%-- navbar.jsp labots 2002-11-22 --%>
<%@ include file="/navbar.jsp" %>
<hr />Šai lapai specifisks saturs...

Problēma: Ja navbar.jsp mainās, tad tos JSP, kuri viņu lieto, automātiski servletu konteiners var nepārģenerēt. Tāpēc ieteicams vienmēr faila iekļaušanas direktīvai priekšā pierakstīt pēdējo iekļaujamā faila izmaiņas datumu, un to izlabot ar rokām ikreiz, kad iekļaujamais fails mainās.

XML sintakse JSP konstrukcijām

Īsais pierakstsXML pieraksts
<%= ... %><jsp:expression>...</jsp:expression>
<% ... %><jsp:scriptlet>...</jsp:scriptlet>
<%! ... %><jsp:declaration>...</jsp:declaration>
<%@ page import="java.util.*" %><jsp:directive.page import="java.util.*"/>

Javas pupiņas

Javas Bean komponenti jeb pupiņas (Java Beans) ir Javas klases, kuras apmierina dažas papildu prasības tā, lai tās varētu instrumentēt (t.i. apstrādāt dažādos rīkos un ietvaros).

Beans komponentu specifikāciju sk. http://java.sun.com/beans/docs/ EJB (Enterprise Java Beans) nav tas pats, kas Javas pupiņas - EJB konteiners izvirza komponentiem pavisam citas prasības nekā jebkāds Java Beans konteiners.

Java Bean komponentes pazīmes:

  • Beans komponentei (pupiņai) jābūt noklusētam vai atklāti uzrakstītam bezargumentu konstruktoram (to izsauc, kad JSP pirmo reizi inicializē pupiņu)
  • Pupiņas klasei nedrīkst būt publiskie instanču mainīgie. To vietā jālieto piekļuves metodes - t.i. jāizmanto iekapsulēšana. (Var arī automātiski veikt blakusefektus, ja instanču mainīgo vērtības mainās. (To lieto, piemēram, veidojot Java Beans grafiskos komponentus - setPosition() izraisa notikuma apstrādi, kura pārzīmē atbilstošo grafisko komponenti.
  • Teiksim, ka Javas pupiņai ir īpašība xxx, ja tai ir atbilstošs privāts instances mainīgais xxx un sekojošas piekļuves metodes:
    1. ja xxx ir int, vai jebkurš cits primitīvais vai referenču tips, izņemot boolean, tad piekļuves metodes ir sekojošas - public int getXxx() un public void setXxx(int x)
    2. ja xxx ir boolean, tad piekļuves metodes ir public boolean isXxx() un public void setXxx(boolean x)

    Java Bean komponentam drīkst nebūt atsevišķas setXxx() metodes. Šajā gadījumā atbilstošo īpašību "xxx" sauc par tikai-lasāmu īpašību (read-only property).

Javas pupiņu lietošana JSP lapās

Ar īpašiem tagiem pupiņas var inicializēt vai uzzināt jau esošās (ar jsp:useBean) un to vērtības var uzstādīt un nolasīt (attiecīgi ar jsp:setProperty un jsp:getProperty). Piemēri šo tagu lietošanai:

<%-- vai nu izveido jaunu pupiņas "mybeans.Vehicle" 
instanci ar vārdu "v", vai arī savāc un izmanto 
kādu veco --%>
<jsp:useBean id="v" class="mybeans.Vehicle" />

<%-- ievērojiet apostrofus "value" atribūtam - 
tas ir veids, kā iekļaut JSP izteiksmi 
šī atribūta vērtības definēšanai, ja tajā ir pēdiņas --%>
<jsp:setProperty name="v" property="vards" 
value='<%= request.getParameter("vards") %>'/>

<jsp:getProperty name="v" property="maxLoad"/>

Sakompilētai klasei Vehicle jābūt aplikācijas WEB-INF/classes direktorijā.

Bean komponentu redzamība (bean scopes)

Ja vairākas JSP lapas, vai vairāki citi klienti lieto tās pašas JSP lapas, ir lietderīgi redzamības apgabalu norādīt jau pupiņu radot.

  • page - Tā ir noklusētā vērtība - redzamība tikai šajā JSP lapā (atbilstošajā servletā) - uzvedas apmēram kā servleta doGet() metodes lokāls mainīgais.
  • request - Pupiņa pievienojas atbilstošam ServletRequest objektam - redzama vienas lapas vienā pieprasījumā.

    session - Pupiņa pievienojas atbilstošam HttpSession objektam - redzama visās viena klienta izsauktās JSP lapās

  • application - Pupiņa pievienojas ServletContext objektam - redzama visā aplikācijā.

Piemērs:

<jsp:useBean id="counter" class="coreservlets.AccessCountBean"
scope="application">

Tagu bibliotēkas

JSP ļauj lietotājam ieviest savus tegus, kuri izsauc Javas kodu ar noteiktiem parametriem, un kuriem priekšā ir noteikts prefikss. Tegu bibliotēku mehānisms var būt ērtāks, salīdzinot ar pupiņām, jo katra tega izsaukums var ņemt vērā ne vien tegā nodotos parametrus, bet arī tegu savstarpējos iekļāvumus (nesting).

Pietiekami plašas tegu bibliotēkas ļauj definēt jaunas XML-veidīgas valodas ar vēlamo semantiku. Piemērs: vadības konstrukciju pieraksts ar lietotāja definētiem tegiem drukā 20 monētas metienu simulāciju:

<csajsp:repeat reps="20">
  <csajsp:if>
    <csajsp:condition>
      <%= Math.random() > 0.5 %>
    </csajsp:condition>
    <csajsp:then><b>Heads</b><br /></csajsp:then>
    <csajsp:else><b>Tails</b><br /></csajsp:else>
  </csajsp:if>
</csajsp:repeat>

Datubāzes tagu bibliotēka MySQL gadījumā

Darbības pirms JSP lapas darbināšanas:

  • Iekopēt MySQL draiveri savā "c:\oc4j\j2ee\home\lib"
  • Pārstartēt OC4J
  • Izveidot MySQL lietotāju "dzonis" ar paroli "dzonis" (sk. apakštēmu)
  • Izveidot MySQL datubāzi "demo" ar tabulu "cilveks"
<%@ taglib uri="/WEB-INF/sqltaglib.tld" prefix="sql" %>
<html>
<head>
<title>SQL vaicājuma tegi</TITLE>
</head>
<body>
<hr />
<sql:dbOpen URL="jdbc:mysql://localhost:3306/demo"
user="dzonis" password="dzonis" connId="con1">
</sql:dbOpen>
<sql:dbQuery connId="con1">
select * from cilveks
</sql:dbQuery>
<sql:dbClose connId="con1" />
<hr />
</body>
</html>

Datubāzes tegu bibliotēka Oracle gadījumā

Šajā piemērā pirmais vaicājums izveido HTML tabulu ar datiem no DB tabulas "cilveks". Otrais vaicājums izveido java.sql.ResultSet objektu, kuru vēlāk izmanto, lai izveidotu izvēļu sarakstu ar "SELECT" elementu.

<%@ taglib uri="/WEB-INF/sqltaglib.tld" prefix="sql" %>
<html>
<head>
<title>SQL vaicājuma tegi</TITLE>
</head>
<body>
<hr />
<sql:dbOpen 
URL="jdbc:oracle:thin:@olimpus.alise.lv:1521:itndev01"
user="javatest" password="javatest" connId="con1">
</sql:dbOpen>

<sql:dbQuery connId="con1">
select * from cilveks
</sql:dbQuery>

<sql:dbQuery connId="con1" 
    output="jdbc" queryId="myquery">
select id,vards from cilveks
</sql:dbQuery>

<form>
<p>Izvēlies cilvēku: <select>
<sql:dbNextRow queryId="myquery">
<% out.println("<option value='" 
    + myquery.getInt(1) + "'>" 
    + myquery.getString(2) + "</option>"); 
%>
</sql:dbNextRow>
</select></p>
</form>

<sql:dbClose connId="con1" />
<hr />
</body>
</html>

Savienojumu dīķis

Datubāzu savienojuma atvēršana patērē daudz resursu. Tos ir vērts atkalizmantot. Savienojumu dīķis jeb baseins (connection pool) veic šādas darbības:

  • Konekciju pirmsizveide (preallocation)
  • Pārvalda eksistējošās konekcijas
  • Darbības laikā izveido jaunas konekcijas
  • Liek gaidīt uz konekciju
  • Aizver konekcijas

Savienojumu dīķis: Konekciju pirmsizveide

Pieņemam, ka gan neaizņemto, gan aizņemto konekciju glabāšanai tiek izmantoti Vector objekti. Kodu raksta konekciju baseina klases konstruktorā:

availableConnections = new Vector(initialConnections); 
busyConnections = new Vector(); 
for (int = 0; i < initialConnections; i++) {
    availableConnections.addElement(makeNewCOnnection()); 
}

Savienojumu dīķis: Pārvalda eksistējošās konekcijas

  • Ja pieprasa konekciju un ir pieejama neaizņemta konekcija, to ieliek aizņemto konekciju sarakstā un atgriež
  • Pirms konekciju atgriež, pārbauda, vai tai nav notecējis derīguma termiņš (timeout).
public synchronized Connection getConnection() 
        throws SQLException {
    if (!availableConnections.esEmpty()) {
        Connection existingConnection = 
            (Connection)availableCOnnections.lastElement(); 
        int lastIndex = availableConnections.size() - 1; 
        availableCOnnections.removeElementAt(lastIndex);
        if (existingCOnnection.isClosed()) {
            notifyAll();
            return(getConnection()); 
        }
        else {
            busyConnections.addElement(existingConnection);
            return(existingConnection);
        }
    }
}

Savienojumu dīķis: Jaunu konekciju veidošana

Ja nav neizmantotu (idle) konekciju, bet baseina limits nav sasniegts, mēģina veidot jaunas konekcijas

if ((totalCOnnections() < maxConnections) &&
        !connectionPending) { // Pending - kāds jau veido konekciju
    makeBackgroundConnection(); 
}
try {
    wait(); // Baseina kods šajā pavedienā sevi aptur
}
catch(InterruptedException ie) {}
return(getConnection()); // mēģina vēlreiz

Nav efektīvi veidot konekciju sinhroni, jo tas var prasīt vairākas sekundes pie noslogota Web servera, turklāt pa šo laiku cits pavediens var atbrīvot savu konekciju. Tāpēc jaunu konekciju veido asinhroni (jeb "fonā"). No wait() pavediens pamostas gan tad, ja pats izveidojis konekciju, gan tad, ja kāds cits to atbrīvojis.

Konekciju baseins: Gaida, kad konekcijas kļūs pieejamas

Ja nav neizmantotu konekciju un ir sasniegts baseina limits.

try {
    wait(); 
}
catch (InterruptedException ie) {}
return (getConnection()); 

Savienojumu dīķis: Slēdz savienojumus

Savienojumi aizveras pirms drazu savākšanas, bet baseins var par to parūpēties arī pats:

public synchronized void closeAllConnections() {
    closeCOnnections(availableConnections);
    availableCOnnections = new Vector(); 
    closeConnections(busyConnections); 
    busyConnections = new Vector(); 
}

Nevienu no konekcijām šai laikā nedrīkst izmantot, lai piekļūtu datubāzei.

Kā servleti lieto konekciju baseinu

2 metodes - init(), kura inicializē servleta klases instanci un doGet(), kura apstrādā kārtējo pieprasījumu.

init()
Konstruē konekciju baseina instanci (vai iegūst to no fabrikas, ja baseins atbilst "Factory" vai "Singleton" projektēšanas šablonam)
doGet()
Iegūst konekciju no baseina, izdara ar šo konekciju vajadzīgo pieprasījumu un atbrīvo konekciju.

Servleti, kuri nelieto konekciju baseinu rezervē un atbrīvo konekcijas doGet() kodā, t.i. katrs HTTP pieprasījuma pavediens veido citu konekciju.

Var veidot 1 konekcijas baseinu - rezervēt 1 statisku konekciju init() metodē, bet doGet() metodē pārbaudīt, vai tā nav notecējusi un vajadzības gadījumā izveidot no jauna. Programmētājam tad jāsinhnronizē atbilstošie koda gabali. 1 konekcijas baseins bieži sniedz ātrdarbības uzlabojumu, tomēr visefektīvākie parasti ir nedaudz lielāki baseini.

Kā JSP lapas lieto konekciju baseinu

  • Baseinu parasti lieto ar konekcijas piesaisti, izmantošanu un atbrīvošanu katrā individuālajā JavaBeans metodē, kurai vajag datubāzi
  • Var izveidot uz pavedienu baseinu orientētu tegu bibliotēku