Contents

[OLD] Java 101: Enum + Servlets

[This is a post from my old website. Outdated packages and libraries. Viewer discretion is advised ;-)]

ENUM

Typ enum, dodany w wersji 1.5, służy do definiowania stałych. Kiedy mamy z nim do czynienia, to wiemy że mamy do wyboru ograniczony zbiór możliwych opcji. Przykładowo, definiując enum poraRoku z góry wiemy, że będzie on przyjmować wartości jedynie spośród ZIMA, WIOSNA, LATO i JESIEN.

Każdy enum domyślnie rozszerza klasę java.lang.Enum, dlatego nie może rozszerzać żadnej innej klasy. Może natomiast implementować interfejsy. Enumy możemy definiować również jako klasy wewnętrzne.

Konstruktor może być protected (domyślnie) albo private. Nie przywołujemy go bezpośrednio. Weźmy nasz enum poraRoku:

1
2
3
4
5
6
7
8
9
    class EnumCwiczenie {
      enum poraRoku {
        ZIMA, WIOSNA, LATO, JESIEN
      }

      public static void main(String[] args) {
        System.out.println(poraRoku.LATO + " to okres strasznej duchoty.");
      }
    }

Efekt: LATO to okres strasznej duchoty.

Ciało klasy (typu) enum może zawierać metody i inne pola. Domyślnie w trakcie kompilacji dodawana jest metoda values(), zwracająca tablicę wszystkich wartości naszego enum w kolejności ich deklaracji w kodzie. Zobaczmy jak możemy zawrzeć więcej informacji w stałych enum:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    class EnumCwiczenie {
      enum poraRoku {
        ZIMA("Gwiazdka"), WIOSNA("Wielkanoc"), LATO("Noc Kupaly"), JESIEN("Zaduszki");

        private final String waznaData;

        poraRoku(String waznaData) {
          this.waznaData = waznaData;
        }

        String getWaznaData() {
          return waznaData;
        }
      }

      public static void main(String[] args) {
        System.out.println(poraRoku.LATO + " to okres strasznej duchoty.");
        System.out.println("W trakcie tej pory roku ma miejsce " + poraRoku.LATO.getWaznaData() + ".");

        for(poraRoku pora : poraRoku.values()) {
          System.out.println("W trakcie pory " + pora + " ma miejsce " + pora.getWaznaData() + ".");
        }
      }
    }

Efekt:

1
2
3
4
5
6
    LATO to okres strasznej duchoty.
    W trakcie tej pory roku ma miejsce Noc Kupaly.
    W trakcie pory ZIMA ma miejsce Gwiazdka.
    W trakcie pory WIOSNA ma miejsce Wielkanoc.
    W trakcie pory LATO ma miejsce Noc Kupaly.
    W trakcie pory JESIEN ma miejsce Zaduszki.

Pełen zbiór domyślnych metod znaleźć można w dokumentacji.

Kiedy stosować?

Korzystając z enuma, mamy zagwarantowane istnienie tylko jednej instancji stałej (wygodny punkt wyjścia dla wzorca Singleton). Ponadto, możemy korzystać z operatora ==. Zawarta w klasie Enum metoda equals() działa dokładnie tak samo jak ten operator, ale jest metodą, więc możemy otrzymać wyjątek NullPointerException, zamiast wartości false jak w przypadku porównania enum przy pomocy == z null.

Nie trzeba implementować interfejsu Serializable by móc serializować enumy. Ponadto, dokonując serializacji enuma, de facto serializowana jest jego nazwa, zwracana przez wbudowaną metodę name(), a przy deserializacji przywoływana jest metoda valueOf() naszego enuma, zwracająca stałą o tej nazwie. W ten sposób nie musimy serializować wartości wszytkich pól enuma.

Enumy pozwalają także na wygodniejsze stosowanie wyrażeń switch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    poraRoku jednaPoraRoku = poraRoku.WIOSNA;
    switch(jednaPoraRoku) {
      case LATO:
        System.out.println("Cieplo!");
        break;
      case ZIMA:
        System.out.println("Zimno!");
        break;
      case WIOSNA:
        System.out.println("W sam raz!");
        break;
      case JESIEN:
        System.out.println("Mokro!");
        break;
    }

Efekt: W sam raz!

Nie zawsze stosowanie typu enum jest korzystne. Zajmują one więcej miejsca w pamięci (powód dla którego ich stosowanie nie jest rekomendowane przez zespół Androida). Jeżeli nasze stałe nie mają żadnych dodatkowych pól, to prawdopodobnie mniej zasobów będzie zajmować zwyczajowe public static final. Z drugiej strony, enumy są łatwym sposobem obsługi stałych, które mają posiadać dodatkowe pola (a zatem więcej informacji) i własne metody.

Czyli, jak zawsze, stosować w sposób przemyślany ;).

Prosta aplikacja webowa: Servlets, Tomcat

Mówiąc w skrócie, servlety to klasy pomagające serwerowi w odpowiadaniu na żądania ze strony klienta. Jako że najczęściej żądania są w protokole HTTP, to pisząc servlet zwykle rozszerzamy klasę javax.servlet.http.HttpServlet, i nie bawimy się w implementację samego interfejsu javax.servlet.Servlet.

Zwróćcie uwagę na początek tych nazw: javax, nie zwykłe java. Standardowa edycja Javy nie zawiera servletów, Jeżeli używaliście wcześniej platformy Javy SE, to będziecie musieli ściągnąć brakujące biblioteki, albo po prostu przerzucić się na Javę EE, która je (i inne) zawiera. Przykładowo, jeśli korzystacie z Eclipse’a, macie do wyboru kilka wersji programu. Zainstalujcie wersję dla Javy EE i będziecie mieć spokój ;).

Potrzebny jest też serwer oraz (akurat dla potrzeb takich servletów, jakie napiszemy) baza danych. W poniższych przykładach wykorzystany jest Tomcat oraz MySQL. Nie będziemy krok po kroku przerabiać ich instalacji, w internecie bez problemu można znaleźć instrukcje do każdego systemu operacyjnego. Za pierwszym razem może to trochę czasu zająć, szczególnie jeśli się nie miało wcześniej do czynienia z linią komend, ale warto tę chwilę poświęcić. Założenie jest zatem takie, że Tomcat jest zainstalowany, a na dysku utworzona została baza danych MySQL.

javax.servlet.http.HttpServlet zawiera szereg metod odpowiadającym żądaniom HTTP. Skorzystamy z metod doGet() oraz doPost(). Różnica pomiędzy GET a POST polega na widoczności przesyłanych danych. Żądanie GET zawiera przesyłane do serwera informacje w adresie URL (można więc podejrzeć je nawet w historii przeglądarki), stąd dane muszą być zapisane za pomocą znaków ASCII. Przeglądarki i serwery nakładają ograniczenia na maksymalną długość adresu URL (od 2048 w górę), więc i z tej perspektywy jesteśmy ograniczeni. Dla żądania POST format danych nie jest tak istotny.

Napiszemy bardzo prostą aplikację webową: pamiętnik operujący na lokalnym serwerze.

Przygotowania*

(*Eclipse, wszystko można zrobić bez pomocy IDE)

Potrzebny jest nam nowy projekt, skonfigurowany do działania na serwerze. W Eclipse klikamy **File **-> **New **-> Dynamic Web Project. Po podaniu nazwy projektu, obok pola Target runtime klikamy przycisk New Runtime…, wybieramy wersję Tomcata, klikamy Finish. Kreator projektu również zamykamy klikając Finish. Ok, mamy lokalny serwer, na którym będziemy odpalać nasze servlety.

Teraz baza danych. Upewnij się, że masz włączony domyślną perspektywę w Eclipse (_Window _-> Perspective -> Open perspective -> Java EE). U dołu okna IDE kliknij w Data Source Explorer i prawym przycikiem myszy na Database Connections. Kliknij New, z listy wybierz MySQL. Po naciśnięciu Next wpisz dane do swojej bazy danych (nazwa pod polem Database, pełen URL, login i hasło). Jeżeli po naciśnięciu Test Connection pokazuje się komunikat o niepowodzeniu, sprawdź wybrany sterownik. W naszym wypadku jest to JDBC Driver, ściągnąć go można tutaj. a zmienić naciskając na ikonę trójkąta po prawej strony od listy sterowników, wskazując na miejsce, gdzie tenże .jar znajduje się na dysku.

/images/oldblog/20-07_servlet_databasedriver.png{: .align-center}

W naszym wypadku baza danych nazywa się diary, a przygotowana wcześniej tabela ENTRIES, z następującymi kolumnami:

1
2
3
4
5
6
7
    CREATE TABLE ENTRIES(
        id INT NOT NULL AUTO_INCREMENT,
        title VARCHAR(200),
        date DATETIME,
        content LONGTEXT,
        PRIMARY KEY ( id )
    );

Servlet pobierający informacje z bazy danych

Tak jak wyżej napisano, servlety służące do obsługi żądań HTTP powinny rozszerzać klasę javax.servlet.http.HttpServlet. A więc:

1
2
3
    @WebServlet("/DiaryArchive")
    public class DiaryArchive extends HttpServlet {
    }

Kilka słów o @WebServlet. Pojawiła się ona w specyfikacji Servlet 3.0. Wcześniej dla każdej aplikacji sieciowej obsługiwanej przy pomocy servletów należało tworzyć deskryptor w pliku web.xml, w którym m.in. umieszczaliśmy informacje na temat wszystkich servletów. Dzięki temu kontener servletów (w naszym przypadku Tomcat) wiedział, który servlet powinien odpalić na dany typ żądania ze strony klienta. Adnotacja @WebServlet ułatwia nam pracę, takowego pliku przygotowywać nie trzeba (Tomcat ją obsługuje od wersji 7.0).

W dokumentacji wymieniony jest szereg atrybutów adnotacji, ale podawać trzeba jedynie urlPatterns/value, wskazując URL pod którym nasz servlet będzie aktywny.

Nasz pierwszy servlet będzie służyć do odczytywania z bazy danych wpisów z pamiętnika. Żadnych specjalnych danych do serwera nie przesyłamy, tylko żądanie wyświetlenia wpisów, więc skorzystamy z żądania GET, a więc przesłaniamy metodę doGet(). Od razu ustalimy typ odpowiedzi (będzie to wyświetlany w przeglądarce html) oraz przygotujemy przypiszemy zmienną out do obiektu PrintWriter wziętego z HttpServletResponse. Ten posłuży nam do przesłania tekstu (kodu html) do klienta:

1
2
3
4
5
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
          throws ServletException, IOException {
                    response.setContentType("text/html");
                    PrintWriter out = response.getWriter();
    }

Czas na małe parsowanie. Najpierw część kodu html, do którego nie potrzebujemy informacji z bazy danych:

1
2
3
4
5
6
7
8
9
    String title = "My Db Diary";
    String docType =  "<!DOCTYPE html>\n";

    out.println(docType + 
        "<html>\n" +
        "<head><title>" + title + "</title></header>\n" +
        "<body bgcolor = \"#f3f3f3\">\n" +
        "<h1 align = \"center\">" + title + "</h1>\n" +
        "<h3 align = \"center\"><a href=\"http://localhost:8080/DiaryServlet/DiaryMain.html\">Add new entry</a></h3>");

Od razu zawarliśmy w nim URL, pod którym będzie działać nasz drugi servlet. Na razie wejście pod ten adres wywołałoby jedynie błędy.

Pozostaje nam połączenie się z bazą danych, pobranie wpisów i sparsowanie ich do kodu HTML. Najpierw kilka stałych:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    private static final String DB_URL = "jdbc:mysql://localhost:3306/diary";  // Adres naszej bazy MySQL
    private static final String USER = "root"; // Login do bazy
    private static final String PASS = "root"; // Hasło do bazy

    try {	
      // Rejestrujemy sterownik.
      Class.forName(JDBC_DRIVER);

            // Podłączamy się do naszej bazy MySQL
      Connection con = DriverManager.getConnection(DB_URL, USER, PASS);

            // Przygotowujemy zapytanie do bazy oraz obiekt klasy ResultSet z danymi zwrotnymi.
      Statement stmt = con.createStatement();
      String sql = "SELECT id, date, title, content FROM ENTRIES";
      ResultSet rs = stmt.executeQuery(sql);

            // Parsujemy każdy kolejny wiersz pobrany do obiektu ResultSet do kodu HTML.
      while(rs.next()) {
        int id = rs.getInt("id");
        Timestamp ts = rs.getTimestamp("date");
        String entryTitle = rs.getString("title");
        String entryContent = rs.getString("content");

        out.println("<h3 align = \"center\">" + id + ": " + ts.toString() + "</h3>");
        out.println("<h2 align = \"center\">" + entryTitle + "</h2><br>");
        out.println(entryContent + "<br>");
      }
      out.println("</body></html>");
      
            // Zamykamy połączenia z bazą danych (dla przejrzystości przykładu zakładamy, że nie wystąpią w tym miejscu wyjątki).	
      rs.close();
      stmt.close();
      con.close();
    } catch(SQLException se) {
      se.printStackTrace();
    } catch(Exception e) {
      e.printStackTrace();
    }

Pierwszy servlet gotowy. Gdy klikniemy Run -> Run as… -> Run on Server powinniśmy ujrzeć coś takiego:

/images/oldblog/20-07servlet_effect1.png{: .align-center}

Zwróćcie uwagę, że adres URL to połączony adres bazy danych oraz adresu podanego w adnotacji @WebServlet.

Czas na drugi servlet, pozwalający na dodawanie nowych wpisów do bazy. Jak widać podany adres servleta nie musi pokrywać się z nazwą klasy:

1
2
3
4
5
6
7
    @WebServlet("/DiaryNew")
    public class UploadEntryServlet extends HttpServlet {
            private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
            private static final String DB_URL = "jdbc:mysql://localhost:3306/diary";
            private static final String USER = "root";
            private static final String PASS = "root";
    }

Tym razem skorzystamy z żądania POST, bo inaczej cała treść wpisu w żądaniu GET musiałaby wejść w adres URL. Przesłaniamy metodę doPost():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Tym razem pobieramy informacje z obiektu HttpServletRequest 
        String entryTitle = request.getParameter("title");
        String content = request.getParameter("entry");
        // Jedna z kolumn w bazie jest typu DATETIME, potrzebujemy więc obiektu Timestamp z aktualną datą.
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        
        try {
          Class.forName(JDBC_DRIVER);
          
          Connection con = DriverManager.getConnection(DB_URL, USER, PASS);
          
          // Przygotowujemy dane do wysłania do bazy.
          String sql = "INSERT INTO ENTRIES (content, date, title) values (?, ?, ?)";
          PreparedStatement statement = con.prepareStatement(sql);
          statement.setString(1, content);
          statement.setString(3,  entryTitle);
          statement.setTimestamp(2, timestamp);
          
          // Wysyłamy przygotowane dane do bazy i zamykamy połączenia.
          statement.executeUpdate();
          statement.close();
          con.close();
        } catch(SQLException se) {
          se.printStackTrace();
        } catch(Exception e) {
          e.printStackTrace();
        }
        // Po wysłaniu wpisu do bazy, przejdź pod URL pierwszego servleta i pokaż zawartość pamiętnika 
        response.sendRedirect("DiaryArchive");
      }

Gdybyśmy spróbowali odpalić ten servlet na serwerze, wyskoczy nam błąd 405. Nie wysłaliśmy bowiem żądania POST, więc sprawdzane jest (po adresie URL) żądanie GET, a tego nasz servlet nie obsługuje. Musimy przygotować plik .html do obsługi naszego servleta. Umieścimy go w projekcie w folderze WebContent/WEB-INF:

/images/oldblog/20-07servlet_htmlplace.png{: .align-center}

Przesyłane są informacje zawarte pomiędzy znacznikami <form> i </form>, odzyskiwane w kodzie servleta poprzez atrybut name:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
   <!DOCTYPE html>
   <html>
   <head>
   <meta charset="UTF-8">
   <title>My Db Diary</title>
   </head>
   <body>
   <h3 align="center">Read your diary: <a href="http://localhost:8080/DiaryServlet/DiaryArchive">HERE</a></h3>
   <h3>New Entry:</h3>
         <form action = "DiaryNew" method = "post">
            Title: <input type = "text" name = "title"/> <br/>
            Entry: <br/> <textarea rows="20" cols="90" wrap="hard" id="entryfield" name="entry"></textarea> <br/>
            <input type="submit" value="Submit"/>
         </form>
   </body>
   </html>

Po odpaleniu zobaczymy coś takiego:

/images/oldblog/20-07servlet_effect2.png{: .align-center}

I to tyle! Mamy prosty (prostacki ;)) pamiętnik przetrzymywany w bazie danych, z którym łączymy się Javą poprzez serwer lokalny. Póki nie wyłączymy serwera ani bazy danych możemy łączyć się z pamiętnikiem przez dowolną przeglądarkę.

Pełen kod dostępny na GitHubie.