Servlettien käyttö Tietokantasovellusten harjoitustyössä

Harri Laine (30.1.2003)

Servlettien koodaus

Servletin palvelut toteutetaan servlettiluokan palvelumetodien doPost tai doGet avulla. Saadessaan servlettiin kohdistuvan palvelupyynnön servlettiajuri käynnistää palvelumetodin erillisenä rinnakkaisena säikeenä, ts. samaa palvelumetodia voi samanaikaisesti olla käynnissä useita rinnakkain. 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 (servlet.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ä. jserv toimii version 2.0 kanssa.

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 pitäisi servlettien suoritusta varten kopioida hakemistoon SERVLET/WEB-INF/lib/ (edellä SERVLET tarkoittaa servlettihakemistoa). Ajuri löytyy löytyy paikasta
     /opt/jdbc/oracle/classes12.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 wanna-postgres asennusskriptin antama porttinumero ja username kayttäjän unix-käyttäjätunnus.

Tietokanta-ajuri PostgreSQL:lle on tiedostossa /opt/jdbc/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