Struts

Wprowadzenie

Java Servlets

Servlety reprezentują programy Javy, działające po stronie serwera WWW. Umożliwiają generowanie dynamicznych stron WWW. Mają następujące zadania:

Java Server Pages

Pisanie HTMLa w olbrzymiej liczbie println() bardzo szybko stało się męczące. Odpowiedzią na ten problem są Java Server Pages. Pliki JSP to dokumenty tekstowe podobne do plików HTMLowych. Oczywiście pozwalają one zamieścić kod Javy w pliku. JSP pozwala wymieszać statycznie napisany HTML z dynamicznie wygenerowanym przez servlet. Właściwie cały pomysł polega na umieszczeniu kodu w dokumencie HTML, co jest przeciwnością wcześniejszych servletów, gdzie HTML był zaszyty w kod Javy. Dlatego mówi się, że powstanie JSP wywróciło pisanie servletów na drugą stronę.

Java Beans

Java Beans to nic innego jak klasy, trzymające się konwencji nazewniczej zdefiniowanej przez Suna. Konwencja ta dotyczy nazw metod, atrybutów i sposobu dostępu do nich.

Logika biznesowa

Logika biznesowa jest sercem aplikacji. To ona wykonuje operacje na danych. Wszystkie czynności i procesy mają tutaj swoją podstawę.

W czym Struts może pomóc?

Rozdzielenie logiki od interfejsu

Wiele osób pisze aplikacje w PHP lub Perlu, zaszywając zapytania SQL i logikę biznesową bezpośrednio w kodzie HTML. Wyobraźmy sobie taką sytuację:
<html><head><title>Important title</title></head>
<body>
<someScript>
dbConnection = openDBConnection(someDB)
resultSet = dbConnection.executeQuery('select bookName from books')
loop over resultSet{
print (resultSet.field('bookName') + '<br>')
}
</someScript>
</body></html>

Można również w ten sposób pracować używając JSP i servletów. A teraz wyobraźmy sobie, że mamy 70 takich dialogów i chcemy dodać w tabeli books dodać nowe pole, mówiące, że książka została usunięta. Pewnie czeka nas mnóstwo roboty.

Co byśmy zyskali separując logikę od interfejsu:

Centralne sterowanie

Aby kontrolować interakcje między interfejsem a logiką potrzebna jest centralna jednostka. Przez taki byt przechodziłyby wszystkie ważne procesy, niezależnie od tego która część logiki lub która część interfejsu byłaby w proces zaangażowana.

Jeśli implementując aplikację rezygnujemy z centralnego sterowania i łączymy interfejs bezpośrednio z logiką narażamy się na niedogodności:

Co zyskujemy wykorzystując centralne sterowanie:

MVC a Struts

MVC

Przypomnijmy sobie MVC i wyobraźmy sobie taką sytuację:

Struts

W Struts ten schemat wygląda tak:

Dodatkowo mamy możliwość użycia bibliotek rozszerzających technologię, której używamy. Nie musimy używać JSP, możemy użyć Tilesów lub JavaFaces. Beansy możemy rozszerzyć na przykład o Hibernate'a lub EJB.

Czy w ogóle tego potrzebujemy?

Jeśli piszemy aplikację składającą się z kilku stron, to projektowanie jej zgodnie z paradygmatem MVC nie jest niezbędne (chociaż ładne). Możemy wtedy sięgać do logiki biznesowej prosto z interfejsu, w razie modyfikacji logiki, przedefiniować cały interfejs i nie martwić się o poprawność przesyłanych między interfejsem a logiką danych. Wcześniej czy później pewnie uda nam się doprowadzić aplikację do działania. Natomiast kiedy mamy dużą aplikację mającą kilkadziesiąt dynamicznie generowanych stron, rozbudowaną logikę i bazę danych Controller staje się niezbędny i może oszczędzić olbrzymie ilości czasu.

Struts

Skąd nazwa?

Strut znaczy (między innymi) podpórka. I właśnie w ten sposób widzą Strutsy jego autorzy: jako niewidzialny dla użytkownika zestaw podpórek podtrzymujący dużą budowlę jaką jest aplikacja.

Konfiguracja

Centralna kontrola

Zakres widoczności

W aplikacji opartej na WWW JavaBeans mogą być przechowywane w kilku kolekcjach "atrybutów". Każda z kolekcji różni się regułami decydującymi o żywotności i widoczności beansów w niej przechowywanych. Reguły te nazywane są scope. JSP definiują następujące poziomy: Ważne, żeby pamiętać, że strony JSP i servlety współdzielą kolekcje beanów. Na przykład jeśli napiszemy:
MyCart mycart = new MyCart(...);
request.setAttribute("cart", mycart);
Bean jest widoczny w stronie, do której się przekierujemy. Możemy go użyć na przykład tak:
<jsp:useBean id="cart" scope="request"
class="com.mycompany.MyApp.MyCart"/>

Actions

Każdej akcji w logice biznesowej wykonywanej na zlecenie interfejsu powinniśmy przypisać (zamapować) klasę rozszerzająca klasę Action. Aby przetwarzać żądanie zgodnie z własnymi upodobaniami należy przedefiniować metodę execute(...):
public final class WelcomeAction extends Action {
    public ActionForward execute(
        ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response)
        throws Exception {

	...
}
Mapujemy klasy w pliku struts-config.xml w węźle action-mappings, najważniejsze atrybuty to:

ActionForms

Dla zapewnienie łączności między między interfejsem a modelem (dokładnie w tym kierunku) używamy klas rozszerzających ActionForms. Są to beany tworzone i wypełniane automatycznie, gdy użytkownik wysyła formularze. Są one przechowywane w otoczeniu sesji (domyślnie) lub żądania. Jeśli przechowujemy je w sesji należy koniecznie pamiętać o przedefiniowaniu metody reset(). Pisząc ActionForm należy pamiętać o:

Najważniejsze atrybuty <form-bean> w struct-config.xml to:

ExceptionHandlers

Można zdefiniować własne obiekty obsługujące wyjątki wyrzucane przez obiekty Action. Należy zacząć od napisania podklasy org.apache.struts.action.ExceptionHandler i przedefiniować metodę execute. Metoda ta powinna obsłużyć wyjątek i zwrócić przekierowanie (ActionForward), aby Struts wiedział gdzie ma się udać. Następnie zamapować obsługę wyjątków w struts-config.xml, na przykład tak:

<global-exceptions>
    <exception 
      key="some.key" 
      type="java.io.IOException" 
      handler="com.yourcorp.ExceptionHandler"/>
</global-exceptions>

Zaawansowane mapowanie

Uniwersalne mapowania

Najłatwiej wytłumaczyć to na przykładzie. Oto zastosowanie wzorców do mapowania wszystkich akcji zaczynających się od /edit:

<action    
    path="/edit*"
    type="org.apache.struts.webapp.example.Edit{1}Action"
    name="{1}Form"
    scope="request"
    validate="false">
    <forward 
        name="failure" 
        path="/mainMenu.jsp"/>
    <forward 
        name="success" 
        path="/{1}.jsp"/>
</action>

"*" pozwala na zamapowanie wszystkich URI typu /editUser ale już nie /editUser/add. Do ciągu znaków dopasowanego do "*" możemy się odwoływać przez {1}. Generalnie wzorce mamy takie:

W mapowaniach do wartości dopasowanych do części wzorca możemy się odwoływać przez {n} (n=1..9), cały URI jest dostępny pod {0}.

Struts - rozszerzenia

Validator

Zamiast przesyłania do danych z formularzy do akcji i walidowania ich w kontrolerze mamy możliwość użycia Validatora, który sprawdzi poprawność danych zanim zostaną one wysłane. Jeśli nie są one zgodne z zadanymi wytycznymi, to akcja nie zostanie podjęta. Plugin validatora musimy zadeklarować w pliku struts-config.xml.

<plug-in className="org.apache.struts.validator.ValidatorPlugIn">
    <set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validator/validation.xml" />
</plug-in>

We właściwościach "pathnames" podajemy ścieżki do plików określających sposób walidacji. Reguły wyglądają mniej więcej tak:

<field property="age" depends="required,integer,intRange">
      <arg0 key="employee.age"/>
      <arg1 name="intRange" key="${var:min}" resource="false"/>
      <arg2 name="intRange" key="${var:max}" resource="false"/>
      <var><var-name>min</var-name><var-value>18</var-value></var>
      <var><var-name>max</var-name><var-value>65</var-value></var>
</field>
Warto obejrzeć plik validation.xml z przykładu. Atrybuty depends są zdefiniowane w pliku validator-rules.xml.

Biblioteka tagów Bean.

Tagi Bean służą do operacji na beanach, definiowania nowych w dowolnym zasięgu, zmiany wartości pól w beanie, pobierania wartości z beanów oraz tworzenia zmiennych dowolnego typu dostępnych później w skryptletach i innych tagach. Funkcjonalność części tego tagliba jest podobna do obecnej w standardzie JSP, jednak w wielu obszarach ją rozszerza. Większość tagów przyjmuje atrybut "id", który określa nazwę tworzonej zmiennej. Pozostałe atrybuty mogą być stosowane zamiennie, "name" oznacza źródłowy bean, "property" jego pole, a "value" wartość którą chcemy przypisać (czyli wyklucza się on z dwoma poprzednimi). Przykład:
<bean:define id="imie" name="osoba" property="imie" scope="request" toScope="session"/>
Oznacza umieszczenie w sesji pod kluczem "imie" pola "imie" z beana "osoba" znajdującego się w requeście. Kolejny przykład powoduje pobranie ciastka "klient" i umieszczenie go pod zmienną cust:
<bean:cookie id="cust" name="klient"/>
Za pomocą opisywanej biblioteki można wypisywać na wyjście JSPa.
<bean:write name="cust" property="adresy[2].ulica" scope="session"/>
<bean:message key="message.hello" arg0='<%= user.getFullName() %>'/>

Logic

Kolejnym ciekawym taglibem dostępnym ze Strutsami jest Logic. Ma ona wiele cech wspólnych z JSTLem, zawiera tagi zachowujące się jak instrukcje warunkowe w językach programowania oraz pętle.
<logic:tagWarunkowy value="name" cookie="client">
  Klient zalogowany.
</logic:tagWarunkowy>
Tagi dla warunków to np. present, equal, lessThan, match.
<% 
  java.util.List lista = new java.util.ArrayList();
  lista.add(new Integer(12));
  lista.add(new Integer(5));
%>

<logic:iterate id="element" collection="<%= lista %>">
 Mamy tu dostęp do zmiennej element
</logic:iterate>


<logic:iterate id="element" name="kolekcja">
 Mamy tu dostęp do zmiennej element
</logic:iterate>
W pierwszym przypadku atrybutowi "collection" przypisujemy kolekcję ze zmiennej na stronie, a w drugim tag sam będzie szukać w różnych zasięgach obiektu o nazwie "kolekcja".

Message resources

Plik propertiesowy definiowany w struts-config.xml'u w tagu
<message-resources parameter="nazwa.properties"/>
Zakładając, że ma on postać:
file.request=Plik do wysłania
button.ok=Wyślij
button.cancel=Anuluj
Można z niego korzystać następująco:
<table class="form">
<tr><td><bean:message key="file.request"/></td></tr>
<tr><td><html:file property="plik"/></td></tr>

    [...]

<html:submit><bean:message key="button.ok"/></html:submit>
<html:cancel><bean:message key="button.cancel"/></html:cancel>
Konsekwętnie unikając pisania jakiegokolwiek tekstu w plikach JSP a korzystając z opisywanego mechanizmu łatwo wprowadzać nowe wersje językowe, wystarczy przetłumaczyć jeden plik.

Tiles

<logic:notPresent name="USER">
  <logic:forward name="login" />
</logic:notPresent>


<tiles:insert page="/common/layout.jsp">
        <tiles:put name="title" value="Strona główna" />
        <tiles:put name="menu" value="/common/menu.jsp" />
        <tiles:put name="body" value="/mainPageBody.jsp" />
</tiles:insert>
Plik layout może wyglądać następująco:
<html lang = "pl">
<body>
  <table width="100%" border="0">
    <tr>
        <td class="logo" width="15%">Logo </td>
        <td class="header"> <tiles:getAsString name="title" /> </td>
    </tr>
    <tr>
        <td class="menu"> <tiles:insert attribute="menu" /> </td>
        <td class="body"> <tiles:insert attribute="body" /> </td>
    </tr>
  </table>
</body>
</html>
Powyższy przykład dobrze prezentuje zastosowanie tagów logic oraz tiles. Pierwszy plik zawiera zbiera kawałki strony znajdujące się w różnych JSPach, na przykład element z menu jest wspólny dla wszystkich stron, może być zatem umieszczony w osobnym pliku.
Drugi plik zawiera zebranie wszystkich części w całość.

Dyna Action Form

Pewnym ułatwieniem przy tworzeniu formularzy i akcji są DynaFormBeany - tworzone na podstawie zapisu w deskryptorze, istniejące jedynie w pamięci typy beanów formularzy.
W struts-config.xml'u definicja takiego elementu wygląda następująco:
   <form-bean name="addrEditForm"
              type="org.apache.struts.action.DynaActionForm">
              <form-property name="addr_name" type="java.lang.String"/>
              <form-property name="company_name" type="java.lang.String"/>
              <form-property name="nip" type="java.lang.String"/>
              <form-property name="first_name" type="java.lang.String"/>
              <form-property name="last_name" type="java.lang.String"/>
              <form-property name="street" type="java.lang.String"/>
              <form-property name="home" type="java.lang.String"/>
              <form-property name="flat" type="java.lang.String"/>
              <form-property name="city" type="java.lang.String"/>
              <form-property name="code" type="java.lang.String"/>
              <form-property name="country" type="java.lang.String"/>
              <form-property name="phone" type="java.lang.String"/>
              <form-property name="panstwo" type="org.apache.struts.util.ImageButtonBean"/>
              <form-property name="dalej" type="org.apache.struts.util.ImageButtonBean"/>
   </form-bean>
W JSP nie ma żadnej różnicy w używaniu zdefiniowanego w pełni ręcznie FormBeana od DynaFormBeana. W akcji natomiast nie można pobrać bezpośrednio pola z beana (bo nie ma takiego), zatem trzeba skorzystać z klasy PropertyUtil:
ImageButtonBean dalej = (ImageButtonBean) PropertyUtils.getSimpleProperty(form, "dalej");
String first_name = (String) PropertyUtils.getSimpleProperty(form, "first_name");

Pluginy do Eclipsa

Wtyczkami do Eclipsa, które są godne polecenia i ułatwiają pracę ze Strutsami sa: Pozostałe, które miałek okazję testować były niezbyt stabilne albo wręcz nieużyteczne.

Źródła


Autorzy: Maciek Wiśniewski i Piotrek Witusowski