Od Php Do J2EE

Dariusz Cieślak

Dariusz Cieślak. Zainteresowania zawodowe obejmują programowanie obiektowe i techniki zapewnienia jakości w projektach informatycznych. Zajmuje się systemami informacyjnymi działającymi w sieci Internet. Kontakt: cieslakd@aplikacja.info.

W dobie znaczącego wzrostu dostępności sieci Internet wzrasta zapotrzebowanie na usługi udostępniane poprzez sieć. Aplikacje internetowe już od wielu lat znajdują zastosowania w biznesie. Dla prostych systemów wystarczą proste mechanizmy (skrypty) jednak gdy złożoność systemu przekracza pewien próg konieczne staje się zastosowanie bardziej zaawansowanych technik.

W artykule postaramy się przybliżyć czytelnikowi platformę J2EE jako alternatywę dla innych popularnych technologii służących do tworzenia aplikacji WWW.

CGI

Pierwszym sposobem dodania dynamizmu do stron WWW było opracowanie interfejsu CGI (Common Gateway Interface). "Wspólny interfejs bramek" to opis sposobu dzięki któremu serwer WWW mógł wywołać zewnętrzny program w celu wygenerowania dla klienta strony (zwykle z zawartością HTML). Każde takie wywołanie wiąże się z koniecznością uruchomienia programu co dość mocno obciąża serwer (rysunek 1). Szary prostokąt opisuje fazę uruchomienia procesu, zielony -- rzeczywistą pracę skryptu. R1, R2, R3 to kolejne żądania HTTP. P1, P2, P3 oznaczają uruchamiane procesy. Po zakończeniu generowania odpowiedzi dla przeglądarki proces kończy pracę.


Rysunek 1. Procesy w CGI są tworzone dla każdego żądania

Ograniczenie wydajności CGI zostało przełamane przez opracowanie interfejsu FASTCGI, który tworzy trwałe procesy w pamięci i żądania przekierowywuje do już istniejących procesów (rysunek 2). Białe prostokąty to oczekiwanie procesu na kolejne żądanie.


Rysunek 2. Trwałe procesy w FASTCGI

Pomimo wad CGI jest nadal chętnie używane (zwłaszcza na kontach hostingowych) dzięki wysokiemu poziomowi bezpieczeństwa (aplikacja CGI znajduje się w odrębnym procesie niż serwer więc nie może uszkodzić danych procesu serwera). Odrębność procesu serwera od aplikacja umożliwia też uruchamianie aplikacji z prawami właściciela pliku CGI (tzw. suexec), co jest dobrym rozwiązaniem w środowisku wieloużytkowym.


Rysunek 3. Proces CGI uruchamiany dla każdego żądania

Interfejsy rozszerzeń serwera

Problem wydajności zwykłych skryptów CGI doprowadził do powstania (tym razem zależnych od konkretnego serera aplikacji) interfejsów rozszerzeń funkcjonalności serwera. Nazwy NSAPI (Netscape), ISAPI (Microsoft) to przykłady takich rozwiązań.


Rysunek 4. Roszerzenia serwera WWW w procesie serwera

Rozszerzenia były pisane w C/C++, co powodowało, że były niezwykle szybkie. Podstawową ich wadą jednak jest kłopotliwe rozwijanie takiego systemu i kwestie bezpieczeństwa - "złośliwe" rozszerzenie mogło wykradać krytyczne dane z systemu i w przypadku błędu mogło załamać pracę całego serwera WWW. Ponadto rozszerzenia nie są przenośne (różne API).

ASP/PHP

Naturalnym krokiem w dziedzinie aplikacji WWW było wprowadzenie języka skryptowego po stronie serwera. Język taki jest przenośny (kod źródłowy), bezpieczny (uruchamiany przez interpreter) i w większości przypadków wystarczająco efektywny do np. prezentacji danych z bazy (wąskie gardło stanowią w tym przypadku ograniczenia przepustowości sieci i motor bazy danych).

Jako przykład można podać najbardziej chyba popularny język skryptowy po stronie serwera: PHP lub komercyjny odpowiednik - ASP promowany przez Microsoft. Interpreter języka jest osadzony w procesie serwera WWW i uruchamiany dla specjalnie oznaczonych fragmentów strony. Unika się dzieki temu tworzenia za każdym razem procesu, co zwiększa szybkość odpowiedzi.

To jednak, co stanowi o sile tych języków (prostota i brak statycznej kontroli typów) zaczyna stanowić problem przy tworzeniu dużych systemów.

Java

Java powstała początkowo jako język do programowania urządzeń osadzonych (embeded) i prostych appletów (małych aplikacji działających w przeglądarce) mających urozmaicać statyczne strony WWW. Szybko jednak odkryto potencjał jaki drzemie w języku i korzyści jakie może przynieść zastosowanie Javy po stronie serwera. Dziś dla "ciężkich" aplikacji działających po stronie serwera coraz częściej jest wybierana Java.

Przenośność kodu Javy przełamuje granice systemów operacyjnych. Nie ma już problemu pisania aplikacji do wielu środowisk uruchomieniowych, również biblioteka tego języka jest ustandaryzowana i obszerna.

Język był od początku tworzony jako język bezpieczny i obiektowy. Te dwie cechy wyraźnie rzutują na konstrukcję języka -- jest łatwiejszy do poznania niż np. C++ (odpada wiele potencjalnych błędów np. wynikających z ręcznego zarządzania pamięcią). Fakt, że jest to język kompilowany ze statycznym typowaniem pozwala bezpiecznie rozwijać aplikacje.

W językach dynamicznych (bez statycznej kontroli typów) pisze się szybciej, ale język taki daje mniejsze wsparcie jeśli chodzi o kontrolę spójności systemu. Języki dynamiczne nadają się wspaniale do szybkiego opracowania aplikacji (RAD). Brak statycznej kontroli typów można kompensować poprzez zautomatyzowane testowanie w stylu eXtreme Programming.

Jedną z największych zalet przemawiających na korzyść Javy jako języka implementacji dużych systemów jest statyczne typowanie (ma ono chyba równie dużo zwolenników co przeciwników). Statyczne typowanie oznacza, że każda zmienna musi mieć z góry zadeklarowany typ i poprawność użycia tej zmiennej sprawdzana jest już na etapie kompilacji. Każdy, kto próbował modyfikować duży system pisany w języku dynamicznie typowanym wie jak łatwo wprowadzić do systemu błędy (nie ma etapu kompilacji -- błędny identyfikator można znaleźć dopiero przy uruchomieniu).

Java wymaga dużo więcej kodu deklaratywnego (np. jawne specyfikowanie interfejsów podczas gdy w PHP wystarcza dostarczenie tych samych zestawów metod), ale ta dodatkowa informacja pozwala działać kompilatorowi jeszcze przed uruchomieniem programu. Dzięki sprawdzaniu interfejsów nigdy nie dostaniemy w Javie błędu "niezdefiniowana metoda".

Serwlety

Serwlety to podstawowy mechanizm pozwalający na pisanie systemów WWW po stronie serwera w Javie. Została opracowane standardowe API (ang. Application Programming Interface) które separuje serwlety od mechanizmów konkretnego serwera WWW. Najnowsza wersja specyfikacji znajduje się adresem: http://java.sun.com/products/servlet.

Na listingu 1 znajduje się bardzo prosty serwlet, który wyświetla w przeglądarce znajome "Hello, World". Najpierw importujemy standardowe biblioteki Javy (dyrektywy import). Pozwolą one na używanie klas Http Servlet, Http Servlet Request, Http Servlet Response i Print Writer.

Listing 1. Prosty serwlet 'Hello World!'
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HelloWorld extends HttpServlet {

   public void doGet(
   HttpServletRequest req,
   HttpServletResponse res)
   throws ServletException, IOException {

      res.setContentType("text/html");
      PrintWriter out = res.getWriter();
      out.println("<html>");
      out.println("<head>");
      out.println("</head>");
      out.println("<body>");
      out.println("<p> Hello, world ! </p>");
      out.println("</body>");
      out.println("</html>");

   }
}

Kiedy przeglądarka wyśle żądanie typu GET, to kontener serwletów wywoła metodę obiektu klasy Hello World o nazwie doGet(). Pierwszy parametr typu Http Servlet Request niesie informacje o danych przesłanych z przeglądarki (argumenty, ciastka itp.) drugi natomiast służy do sformatowania odpowiedzi. W typ prostym przykładzie ustalamy nagłówek Content-type odpowiedzi wykorzystując metodę setContentType() i tworzymy odpowiedź w HTML-u poprzez serię wywołań metody println(). Program wygląda jak typowy skrypt CGI.

Plik z listingu 1 kompilujemy do pliku HelloWorld.class i taki plik umieszczamy w katalogu WEB-INF/classes. Aby serwer aplikacyjny mógł uruchomić serwlet musimy jeszcze podać pod jakim adresem serwet się znajduje. Do tego (między innymi) służy specjalny plik zwany deskryptorem wdrożenia który powinien znaleźć się w WEB-INF/web.xml

Na listingu 2 pokazano fragment pliku, który odpowiada za łączenie adresów z serwletami. Tag <servlet> definiuje nazwę dla serwletu. Korzystając z tej nazwy w tagu <servlet-mapping> przypisujemy serwlet do podanej ścieżki (lub ścieżek) URL (tag <url-pattern>).

Listing 2. Łączenie url z serwletem
<servlet>
  <servlet-name>hello</servlet-name>
  <servlet-class>HelloWorld</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>hello</servlet-name>
  <url-pattern>/hello</url-pattern>
</servlet-mapping>

Jak widać sama koncepcja jest bardzo prosta, jednak dzięki bogatej bibliotece możemy zrealizować nie tylko to wszystko co było możliwe dzięki technologii CGI/PHP. Serwlety dają dużo więcej możliwości.

J2EE

J2EE to skrót od Java 2 Enterprise Edition. Oznacza on zbiór współpracujących ze sobą technologii służących do tworzenia systemów internetowych. Tak jak technologia serwletów bazuje na standardzie i bibliotekach języka Java tak J2EE bazuje na (między innymi) standardzie serwletów. J2EE stanowi więc kolejną warstwę w naszej układance.

W skład J2EE wchodzi wiele technologii: m.in. serwlety, JSP, EJB, JDBC. Na początku może wydawać się przytłaczająca taka ilość skrótów ale należy pamiętać, że J2EE pretenduje do miana rozwiązania kompleksowego. Inne technologie są prostsze ponieważ niektóre zagadnienia (np. API dla baz danych) pozostawiały programiście. Powodowało to powstawanie różnych interfejsów. J2EE próbuje ująć typowe potrzeby aplikacji WWW w sposób ustandaryzowany.

Wydajność i niezawodność

Platforma J2EE przewyższa wydajnością standardowe CGI dzięki zlikwidowaniu czasu potrzebnego na tworzenie procesu przy każdym wywołaniu. Ponadto dzięki trwałości stan aplikacji może być przechowywany w pamięci (dane sesji), co eliminuje konieczność ciągłego zapisu/odczytu danych sesji z dysku. Dzięki temu, że język jest kompilowany osiąga się większą wydajność niż w przypadku PHP.

Aplikacje J2EE są z definicji przeznaczone do obsługi działań biznesowych w dużej skali (ang. enterprise). Aby zapewnić wysoką wydajność architektura J2EE uwzględnia możliwośc rozproszenia aplikacji na wiele serwerów (klastrowanie). Dzięki mechanizmowi serializacji Javy możliwe stało się przerzucanie obiektów pomiędzy serwerami w celu balansowania obciążenia systemu. W zależności od możliwości jakie daje serwer aplikacyjny możemy wyróżnić następujące style rozproszenia:

  • Brak klastrowania - najprostszy do realizacji, wszystkie serwlety działają wewnąrz jednej wirtualnej maszyny Javy
  • Obsługa klastrowania - żądania HTTP są dystrybuowane losowo (z zachowaniem równomiernego obciążenia) pomiędzy kilka serwerów, ale sesje są przywiązane do serwera na którym powstały
  • Obsługa klastrowania i wędrujących sesji - jak wyżej, ale sesje mogą być przenoszone pomiędzy serwerami (oczywiście przeźroczyście dla użytkownika)
  • Obsługa klastrowania, werujących sesji i unikania błędów - dane sesji są powielane, żeby zabezpieczyć się przed uszkodzeniem konkretnego serwera

Bazy danych

Dzisiejsze aplikacje nie mogą obyć się bez dostępu do baz danych. Za czasów aplikacji działających lokalnie wystarczyły bazy plikowe posiadające własne, niestandardowe formaty. Formaty te były obsługiwane przez nieustandaryzowane API. Dziś środowisko programowe powinno umożliwić dostęp do różnych baz danych.

Taki dostęp jest zapewniony przez ustandaryzowane API umożliwiające podłączenie się do baz danych -- JDBC (ang. Java Data Base Connectivity). Producent konkretnej bazy danych dostarcza sterownik (ang Driver) udostępniającą połączenia do bazy poprzez interfejs JDBC. Biblioteka taka ma postać pliku *.jar (format bibliotek dla języka Java) i instalacja sprowadza się do skopiowania tego pliku w określone miejsce na dysku. Najprostszy sposób podłączenia się do bazy został pokazany na listingu 3 (w przykładach będziemy posługiwać się bazą PostgreSQL, ale wymiana na konkretną bazę jest możliwa poprzez zmianę parametrów dla getConnection()).

Listing 3. Otwarcie połączenia do bazy danych
String dbUrl = "jdbc:postgresql://localhost/phpsolmag";
String dbUserName = "java1";
String dbPassword = "java1";

Class.forName("org.postgresql.Driver");
Connection conn = DriverManager.getConnection(
   dbUrl, dbUserName, dbPassword);

Listing 4. Struktura bazy do testów
DROP TABLE ExampleTable;
CREATE TABLE ExampleTable
(
  idExample INTEGER NOT NULL
    PRIMARY KEY,
  exampleColumn TEXT NOT NULL
);

Kiedy już mamy obiekt klasy Connection musimy utworzyć obiekt Statement by móc wywoływać zapytania SQL. Na listingu 5 pokazano zarówno wykonywanie zapytań aktualizujących dane jak i zapytań typu SELECT. (Strukturę bazy danych pokazano na listingu 4).

Listing 5. Wywoływanie zapytań SQL bez argumentów
static void testStatement(Connection conn, PrintStream out)
throws SQLException {

   Statement st = conn.createStatement();
   st.executeUpdate("DELETE FROM ExampleTable");

   st.executeUpdate(
      "INSERT INTO ExampleTable" +
      "(idExample, exampleColumn) " +
      "VALUES(12, 'a text')"
   );

   ResultSet rs = st.executeQuery(
      "SELECT * FROM ExampleTable");
   while (rs.next()) {
      out.println(rs.getString("exampleColumn"));
   }

   st.close();
}

Typowym błędem jaki popełniają programiści PHP jest składanie zapytania na podstawie danych otrzymanych z przeglądarki bez przetworzenia tych danych. Chodzi o zamianę znaku ' na \' itp. Samo tworzenie zapytania poprzez sklejanie łańcuchów jest błędogenne i takie zapytanie jest trudno po napisaniu przeanalizować. Aby w wygodny i bezpieczny sposób podstawiać argumenty do zapytań SQL w JDBC istnieje klasa Prepared Statement. Jest ona podobna do Statement, ale pozwala na wstępne skompilowanie przez bazę danych zapytania, co później przyspiesza jego wykonanie. Na Listingu 6 pokazano sposób korzystania z Prepared Statement.

Listing 6. Wywoływanie zapytań SQL z argumentami
static void testPrepared(Connection conn, PrintStream out)
throws SQLException {

   Statement st = conn.createStatement();
   st.executeUpdate("DELETE FROM ExampleTable");

   PreparedStatement stInsert = conn.prepareStatement(
      "INSERT INTO ExampleTable" +
      "(idExample, exampleColumn) " +
      "VALUES(?, ?)"
   );
   stInsert.setInt(1, 12);
   stInsert.setString(2, "a text");
   stInsert.executeUpdate();
   stInsert.close();

   PreparedStatement stSelect = conn.prepareStatement(
      "SELECT * FROM ExampleTable");
   ResultSet rs = stSelect.executeQuery();
   while (rs.next()) {
      out.println(rs.getString("exampleColumn"));
   }
   stSelect.close();
}

Bezpieczeństwo

W przypadku aplikacji sieciowych bezpieczeństwo można rozpatrywać jako:

  • uwierzytelnienie - możliwość weryfikacji tożsamości biorących udział w wymianie informacji
  • autoryzacja - udostępnienie zasobów systemu tylko wybranej grupie użytkowników
  • poufność - pewność, że przesyłane dane mogą odczytać tylko wybrani użytkownicy
  • integralność - możliwość sprawdzenia, czy informacja nie została zmieniona podczas transmisji

Ponieważ PHP jest językiem osadzonym w serwerze WWW (np. Apache), to do zapewnienia bezpieczeństwa aplikacji możemy przerzucić na barki serwera WWW. Np. ograniczenie dostępu do plików zaczynających się na "hello" tylko do grupy użytkowników o nazwie "admin" można osiągnąć poprzez następujący wpis w pliku .htaccess:

   <Files "hello*">
      AuthType "Basic"
      AuthName "Administratorzy"
      AuthUserFile "/home/user/.htpasswd"
      AuthGroupFile "/home/user/.htgroup"
      require group admin
   </Files>

W powyższym przykładzie dane o użytkownikach są składowane w pliku /home/user/.htpasswd a wiązanie użytkowników do grup w pliku /home/user/.htgroup.

A jak ograniczenie dostępu do zasobów wygląda w serwlecie ? We wcześniej wspomnianym pliku web.xml można zadeklarować ochronę niektórych składowych aplikacji (listing 7).

Listing 7. Definiowanie reguł dostępu w pliku web.xml
<security-constraint>
  <web-resource-collection>
    <web-resource-name>
      Hello Application
    </web-resource-name>
    <url-pattern> /hello* </url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name> admin </role-name>
  </auth-constraint>
</security-constraint>

<login-config>
  <auth-method> BASIC </auth-method>
  <realm-name> Administratorzy </realm-name>
</login-config>

<security-role>
  <role-name> admin </role-name>
</security-role>

Poprzez tag <web-resource-collection> definiujemy co ma podlegać ochronie. Podobnie jak w przypadku .htaccess będziemy chronić odwołania do zasobów zaczynających się na "hello".

Tag <auth-constraint> określa jakie ograniczenia powinny być nałożone na dostęp do wcześniej określonych zasobów. W tym przypadku żądamy, by tylko użytkownicy grupy admin mieli dostęp do zabezpieczanych zasobów.

Następnie określamy (poprzez tag <login-config>) w jaki sposób użytkownik ma być autoryzowany. W tym przypadku użyto "BASIC", co odpowiada sposobowi konfiguracji we wcześniejszym przykładzie z .htaccess.

Wnikliwi czytelnicy na pewno zauważą, że brakuje tu dwóch elementów: nie zostało określone skąd należy pobrać informację o użytkownikach (hasła) i przynależność użytkowników do ról. Brak tej informacji wynika z koncepcji leżącej u podstaw architektury J2EE: To nie dostawca komponentów określa lokalizację bazy użytkowników. Zadanie to należy do wdrożeniowca który instaluje i konfiguruje aplikację w środowisku docelowym. Ma on wtedy możliwość decydowania gdzie i pod jaką formą są informacje o użytkownikach (może to być np. baza LDAP lub zwykłe pliki z hasłami).

Ta właściwość (wydzielenia zagadnień bezpieczeństwa) staje się ważna kiedy zauważymy, że przeciętny użytkownik nie korzysta już tylko z jednego systemu informacyjnego w firmie. Zwykle korzysta z kilku systemów gdzie może mieć różne hasła. Przechowywanie informacji o użytkownikach w jednym miejscu ułatwia zarządzanie kontami a dla użytkowników oznacza konieczność pamiętania tylko jednego hasła. Dzięki standaryzacji zagadnień związanych z bezpieczeństwem dostawcy produktów i narzędzi J2EE (inne role zdefiniowane w J2EE) mogą przygotować wygodne narzędzia wspomagające instalację a niezależne od konkretnej aplikacji.

Śledzenie sesji

Listing 8. Przykład śledzenia sesji przez serwlet
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SessionExample extends HttpServlet {

   public void doGet(
   HttpServletRequest req,
   HttpServletResponse res)
   throws ServletException, IOException {

      HttpSession session = req.getSession();

      Integer counter = (Integer)
         session.getAttribute("counter");
      if (counter == null) {
         counter = new Integer(1);
      } else {
         counter = new Integer(
            counter.intValue() + 1);
      }
      session.setAttribute("counter", counter);

      res.setContentType("text/html");
      PrintWriter out = res.getWriter();
      out.println("<html>");
      out.println("<head>");
      out.println("</head>");
      out.println("<body>");
      out.println("<p> Hello, world ! </p>");
      out.println("<p> Your visits = "
         + counter + " </p>");
      out.println("</body>");
      out.println("</html>");

   }
}

Listing 9. Czas ważności sesji w deskryptorze wdrożenia
<session-config>
  <session-timeout> 60 </session-timeout>
</session-config>

W sieci

  • http://fastcgi.com - biblioteka FASTCGI
  • http://java.sun.com - informacje na temat Javy

Systemy szablonów

Odpowiednia organizacja pracy pozwala na rozdzielenie pracy pomiędzy prorgamistów (tworzą kod w języku programoawania) a grafików (projektują stronę HTML). Osadzenie kodu HTML i kodu Javy w jednym pliku utrudnia odpowiedni podział pracy. Dlatego programiści chętnie sięgają po systemy szablonów, które ułatwiają separację kodu (tzw. kontroler i model) od stron HTML (tzw. widok).

Dla języka PHP jednym z popularniejszych rozwiązań jest system szablonów Smarty. [przykład]

Serwlety od dawna posiadają system szablonów zwany JSP (Java Server Pages). Ideologicznie jest zbliożony do "czystego" PHP (lub ASP) - kod Javy jest osadzony w stronie HTML za pomocą specjalnych znaczników. JSP jest kompilowane przez kontener serwletów w momencie żądania strony (jeśli nie ma skompilowanej wersji lub plik JSP został zmieniony). Na podstawie pliku JSP powstaje zwykły serwlet który obsługuje żądanie użytkownika.

WebMacro - przykład

Podsumowanie

W artykule został pokazany fragment jednej z podstawowych technologii na której bazuje J2EE - serwletów. J2EE składa się z wielu innych komponentów. Poznanie ich pozwala na przspieszenie pracy i ulepszenie komunikacji pomiędzy programistami (techniki nie są budowane od zera).

J2EE jest dojrzałą platformą do tworzenia aplikacji sieciowych. Wymaga jednak większej dyscypliny i obszerniejszej wiedzy niż zwykłe skrypty PHP. Daje w zamian efektywne środowisko dla zlożonych systemów.

(...) Nie ma bowiem łatwych odpowiedzi. Nie istnieje nic takiego jak najlepsze rozwiązanie - zarówno jeśli chodzi o narzędzia, jak i języki czy systemy operacyjne. Są jedynie systemy, które mogą być bardziej odpowiednie w konkretnych okolicznościach.

I tu właśnie do gry wchodzi pragmatyzm. Nie należy przywiązywać się do żadnej określonej metody, ale mieć na tyle rozległą wiedzę i doświadczenie, by w danej sytuacji wybrać dobre rozwiązanie. (...)

Andrew Hunt, David Thomas "Pragmatyczny Programista"