Servlettien käyttö Tietokantasovellusten harjoitustyössä

Harri Laine (11.6.2002)

Tietokannoista

Tässä ohjeessa käsitellään vain servletteihin liittyviä erityispiirteitä. Harjoitustöissä voidaan käyttää joko koneessa kontti.helsinki.fi toimivaa Oracle 8.0.5-tietokantaa, koneessa bodbacka.cs.helsinki.fi (ehkä jatkossa db.helsinki.fi) toimivaa Oracle 9.1 -tietokantaa tai koneessa db.cs.helsinki.fi käyttäjän omaa PostgreSQL- tai MySQL kantaa.

Yleistä servleteistä

Keskeisiä asioita servleteistä

Servletti muodostuu yhdestä pääluokasta ja mahdollisista apuluokista. HTML-sivulta servlettiä kutsutaan oletusarvoisesti pääluokan nimellä (servletille voi konfigurointitiedostossa antaa jonkin muunkin nimen). Servlettien ajoympäristönä toimii www-palvelinohjelmistoon kytketty servlettimoottori (servlet engine), joka pitää sisällään Java-ajoympäristön. HY/TKTL:n tapauksessa kyseessä on Apache www-palvelimeen on kytketty jserv servlettimoottori. Servletti ladataan ajoympäristöön kun sitä ensimmäisen kerran kutsutaan.

TKTL:llä servlettejä voi ajaa vain koneessa db.cs.helsinki.fi. Tällä koneella servlettejä haluava luo itselleen servlettipuitteet (= hakemiston, jonne servletit sijoitetaan ja ajoympäristön käynnistys- ja alasajoskriptit) ajamalla skriptin

Puitteet luovat ajoympäristön käynnistystä varten konfigurointitiedostot alihakemistoon jserv/etc. Ajoympäristön keskeiset konfigurointitiedot, mm javan CLASSPATH määrittely ja ympäristön elinaika, ovat tiedostossa environment. Tiedostossa on kommentoituna käskyt Oracle tai PostgreSQL -tietokanta-ajurin liittämiseksi CLASSPATH:iin. Jos haluat ottaa ajurin käyttöön käy editoimassa kommenttimerkki (#) pois käskyn edestä. CLASSPATH:iin ei ole syytä sisällyttää tarpeettomia ajureita, sillä ajurikirjastot sisältävät samannimisiä luokkia ja usean kirjaston mukanaolo edellyttää tarkenteita luokkaviitauksessa. Jos haluat CLASSPATH:iin muita muutoksia kirjaa ne myös tähän tiedostoon. Jos haluat jättää ajoympäristön käyntiin pidemmäksi kuin oletusajaksi (esim. työn tarkastusta varten) on lifetime muuttujan arvoa muutettava.

Servlettiympäristö käynnistetään komennolla start-servlet Käynnistysskripti pitää ajoympäristön pystyssä oletusarvoisesti 10 tuntia. Jos haluat ympäristön alas ennen sitä, voit tehdä alasajon skriptillä stop-servlet. Katso ylläpidon servlet-ohje". Ajoympäristö on ajettava alas esimerkiksi, kun jotain servlettien käyttämää apuluokkaa, tai CLASSPATH:ia on muutettu ja muutokset halutaan näkyviin. Servlettiluokan itsensä muuttuessa servlettiajuri osaa ladata uuden koodin automattisesti.

Ajoympäristö luo serveletin pääluokasta (servlettiluokasta) lataamisen yhteydessä yhden ilmentymän (olion). Tämä on ainoa ilmentymä, joka servlettiluokasta luodaan. Olio säilyy hengissä kunnes ajoympäristö suljetaan tai servlettiluokan koodia lataushakemistossa muutetaan. Jotta ajoympäristö osaisi ladata servletit, on servlettien class-tiedostot sijoitettava ympäristön konfiguroinnin yhteydessä määriteltyyn paikkaan, lataushakemistoon. Puitteiden luontiskripti määrittelee lataushakemiston käyttäjän alihakemistoksi jserv/servlets. Servlettiin viitataan url:lla http://db.cs.helsinki.fi/s/username/servlet_class_name. Esimerkiksi http://db.cs.helsinki.fi/s/laine/PgTesti käynnistää koneen db.cs.helsinki.fi hakemistoon /home/laine/jserv/servlets/ sijoitetun luokan PgTesti.class koodia suorittavan servletin käyttäjän laine servlettiympäristössä.

Servlettien kääntämiseen tarvittavat CLASSPATH muunnokset saa tehtyä skriptillä setup servlet, joka käyttää asetuksiin hakemistossa jserv/etc olevaa environment tiedostoa.

Servletympäristö kuvat

Servletin palvelut toteutetaan servlettiluokan palvelumetodien doPost tai doGet avulla. Saadessaan servlettiin kohdistuvan palvelupyynnön servlettiajuri käynnistää palvelumetodin erillisenä rinnakkaisena säikeenä. Se kumpi metodeista goGet tai doPost käynnistetään riippuu kutsusta. Jos kutsu annetaan HTML-linkin kautta tai html-lomakkeen method-attribuutin arvona on 'get', käynnistetään doGet-palvelu ja muutoin doPost -palvelu. Kumpikin näistä metodeista on syytä toteuttaa saman sisältöisenä, eli varsinainen koodi kirjoitetaan vain toiseen näistä ja toinen määritellään kutsumaan sitä.

Palvelinmetodit pitää koodata säieturvallisiksi, eli niitä on kyettävä ajamaan rinnakkain ilman, että ne sotkevat toistensa tietoja. Yksinkertaisimmin tämän saa aikaan käyttämällä metodeissa vain metodin sisäisiä paikallisia muuttujia ja olioita sekä hoitamalla tarvittava tilatiedon välitys eri servlettien välillä www-sivujen ja selaimen kautta, esimerkiksi lomakkeen piilokenttinä tai pipareina (cookie).

Servlet API

Servlettien rajapinta on määritelty javax.servlet- ja javax.servlet.http-pakkauksissa, jotka on otettava mukaan ohjelmaan import:lla. Pakkaukset sisältyvät JSDK (jsdk.jar) kirjastoon, joka pitää liittää CLASSPATH:iin. Kirjastosta on eri versioita, viimeisin on versio 2.3. Tässä ohjeesa esitettävät asiat toimivat myös vanhempien versioiden yhteydessä. Laitoksen nykyinen jserv-servelettiympäristö on JSDK versio 2.0 yhteensopiva. Polku ajuriin on määritelty tiedostossa ../jserv/etc/environment. Tämän ajurin dokumentaatiota ei enää löydy, joten lähinnä lienee version 2.1 dokumentti.

Kaikkien servlettiluokkien (servletin pääluokkien) tulee periä luokka HttpServlet. Tätä kautta periytyvät palvelumetodien doGet, doPost ja service hahmot, esimerkiksi

    public void doPost(
       HttpServletRequest req, HttpServletResponse resp) 
       throws ServletException, IOException { 
    }

Kaikilla palvelumetodeista on kaksi parametria req ja resp. Req-parametrina annettava HttpServletRequest luokan olio tarjoaa palvelut servletin kutsun yhteydessä annettujen parametriarvojen käsittelyyn. Resp-olio hoitaa servletin tulokset takaisin www-selaimelle.

Alkutoimet palvelumetodeissa

Palvelumetodin alkutoimiin kuuluvat

Tulosten käsittelyyn liittyvät alkutoimet

Ensimmäisenä alkutoimenpiteenä suoritetaan yleensä tulosten käsittelyyn liittyvät alkutoimet: Seuraavassa yksi tapa tehdä nämä alustukset:
   resp.setContentType("text/html"); 
   PrintWriter pw = res.getWriter();
Näiden lisäksi voidaan alkuvaiheessa tulostaa www-sivun otsaketiedot. Jos usea servletti tuottaa samat otsaketiedot kannattaa määritellä joko luokkahierarkiassa HttpServletin ja varsinaisen servlettiluokan väliin tai luokkahierarkiasta erilleen apuluokka hoitamaan otsakkeet.

Palvelupyynnön yhteydessä annettujen parametrien purkaminen

Servletin kutsuparametrit annetaan HTML:ssä samalla tavoin kuin CGI-ohjelmia kutsuttaessa. Ohjelma saa parametrien arvot käyttöönsä palvelumetodin req-parametrin kautta. Parametrin arvoa pyydetään req-oliolta parametrin nimellä
    String arvo = req.getParameter("parametrin_nimi");
Pyyntö tuottaa merkkijonomuotoisen arvon, joka tarvittaessa on erikseen munnettava vaikkapa kokonaisluvuksi. Ellei annetun nimistä parametria löydy on tuloksena arvo null. Moniarvoisten parametrien käsittelyyn on oma metodi getParameterValues, joka tuottaa tuloksenaan merkkijonotaulukon.

Tietokantayhteyden avaus

Jos kyseessä on tietokantaa käyttävä servletti liittyy alkutoimiin myös tietokantayhteyden muodostus. Jos käytössä on yhteysallas, jonne varastoidaan auki pidettäviä tietokantayhteyksiä, voidaan yhteys luoda varaamalla yhteys yhteysaltaasta. Harjoitustöissä, kun suoritusnopeus ei ole tärkeää, voidaan jokaisen palvelumetodin alussa perustaa uusi yhteys.

Yhteyden luomiseksi on ladattava tietokantakohtainen ajuri ja otettava se käyttöön sekä luotava sen avulla yhteys. Seuraava funktio createConnection palauttaa uuden tietokantayhteyden. Jos sovelluksessa on useita tietokantaa käyttäviä servlettejä, kannattaa HttpServlet-luokan ja servlettiluokkien väliin yleistyshierarkiaan luoda vaikkapa luokka HttpDataBaseServlet, joka tarjoaa tietokantayhteyden muodostus- ja sulkupalvelut.

   public Connection createConnection(
           String driverName, 
           String dataBase, 
           String  user, String password) {
     try {
         Class.forName(driverName);
     } catch (ClassNotFoundException ex)
         return null;
     }
     Connection conn= null;
     try {
          conn= DriverManager.getConnection(dataBase, user, password);
     } catch (SqlException sex) {
          conn=null;
     }
     return conn;
   }
Yhteys koneella bodbacka.cs.helsinki.fi olevaan Oracle 9 -kantaan test saadaan aikaan seuraavasti:
   conn=  createConnection("oracle.jdbc.OracleDriver",
      "jdbc:oracle:thin:@bodbacka.cs.helsinki.fi:1521:test", 
      user,password);
Ajurin löytämiseksi on CLASPATH:sta löydyttävä
     /opt/jdbc/oracle/classes12.zip
Kun yhteys halutaan palvelimella kontti olevaan Oracle 8 -kantaan tktb, on kutsu seuraava:
   conn = createConnection("oracle.jdbc.driver.OracleDriver",
      "jdbc:oracle:thin:@kontti.helsinki.fi:1522:tktb", 
      user,password);
Tällöin CLASSPATH:ssa on oltava
      /opt/jdbc/classes12_01.zip
Koneella db.cs.helsinki.fi olevaan kayttäjän username postgeSQL-kantaan saadaan yhteys kutsulla:
   conn = createConnection("org.postgresql.Driver",
             "jdbc:postgresql://localhost:portnumber/username",
             username,password);
Tässä portnumber on asennusskriptin antama porttinumero ja username kayttäjän unix-käyttäjätunnus.

Tietokanta-ajuriin osoittava polku on liitettävä CLASSPATH:iin. Ajurit Oraclelle ja PostgreSQL:lle löytyvät hakemistosta /opt/jdbc. Oracle 8.1-thin ajuri JDK 1.2:lle on nimeltään classes12_01.zip ja siihen liittyvä kansallismerkistöpakkaus nls_charset12_01.zip. Oracle 9.1-thin ajuri JDK 1.2:lle on nimeltään classes12.zip tai classes12.jar. PostgreSQL ajuri on nimeltään jdbc7.0-1.2.jar.

Toiminnan runko

Rungon toiminnallisuus riippuu siitä, mitä palvelinmetodin on tarkoitus tehdä. Yleensä palvelinmetodi ainakin tuottaa html-muotoisen tulossivun. Sivulle kirjoitetaan tekstiä käyttämällä alustustoimien yhteydessä res-olioon kytkettyä kirjoitinoliota (yllä PrintWriter pw).

Wenla-projekti on tuottanut kirjasto-olioita www-sivujen tuottamisen helpottamiseksi. Kirjastoa ei ole juurikaan kokeiltu käytännössä, mutta sieltä voi ainakin katsoa esimerkkejä.

Tietokantaoperaatiot

Tietokantaoperaatioita varten luodaan tyypillisesti Statement- tai PreparedStatement-olio. CallableStatement oliota voi käyttää tietokantaproseduurien ja funktioiden käynnistykseen. PreparedStetement ja CallableStatement perivät Statement luokan. Jatkossa kaikkien luokkien olioita kutsutaan Statement-olioiksi. Tietokantaoperaatioihin liittyvät Statement-oliot luodaan tietokantayhteysolion (Connection -olio) avulla. Ne on aina suljettava ohjelman lopuksi. Lähes kaikki tietokantaoperaatiot voivat aiheuttaa SQLException poikkeuksen, joka on pyydystettävä ohjelmassa. Tietokantaoperaatioita ei ole välttämätöntä upottaa suoraan palvelumetodien koodiin. Usein saattaa olla kätevämpää käyttää jotain apuluokkaa. Seuraavassa esimerkki tietokantasilmukasta:

   Statement stm=null;
   ResultSet rs=null;
   String kysely= 
     "select nimi from opiskelija where hetu like \'0101%\' order by nimi";
      // tammikuun 1. päivänä syntyneet   
   try  {
      stm= conn.createStatement();
      rs= stm.executeQuery(kysely);
      while (rs.next()) {
         pw.println(rs.getString("nimi")+"<br>");
      }
    } catch (SQLException e) {
         databaseError(e.getErrorCode(), e.getMessage(), pw);
         // databaseError on tässä itse tehty virheilmoitusmetodi
    } finally {
        // finally takaa, että tietokantaoperaatioihin liittyvä 
        // close suoritetaan myös virhetilanteessa 
        // Jos try osasta tullaan pois return lauseella tätä ei suoriteta
        // moista ei siis pidä tehdä
        try {
          if (rs!=null)
              rs.close();
          if (stm!=null)
              stm.close();
        } catch (SQLException ee) {}
          // kursorin sulkemisen ei 
   }

Lopputoimet

Palvelumetodin lopuksi on vapautettava tietokantaresurssit ja päätettävä HTML-sivu. Tietokantaresurssien vapauttamiseen sisältyy kaikkien ResultSet ja Statement (ja sen aliluokkien) olioiden close-metodien kutsuminen. JDBC-manuaalissa todetaan, 'ettei tämä ole yleensä välttämätöntä vaan java-roskienkerääjä hoitaa asian joskin jotkut tietokantapalvelimet vaativat, että resurssit vapautetaan eksplisiittisesti close metodeja kutsumalla'. Ainakin Oraclen kohdalla tilanne on tällainen ja close täytyy suorittaa jokaisessa ohjelman päättymishaarassa. Jos se jätetään suorittamatta resusseja jää palvelinpäässä auki ja koko palvelin saattaa hyytyä. Myös tietokantayhteys pitää sulkea lopuksi, esim erillisellä metodilla
  public void endConnection(Connection conn) {
     try {
          conn.close();
     catch (SQLException e) {}
  }

Esimerkki