Hlavní navigace

Struts, komentovaný příklad: obrnění aplikace

27. 1. 2004
Doba čtení: 12 minut

Sdílet

Deklaratorní zachycování výjimek, chyby při zpracování formuláře, chyby v akcích, validator plug-in a ošetřování chyb jak na straně serveru, tak na straně klienta. Je toho poměrně hodně, takže pojďme na to.

Možná si všimnete, že jsem od posledního dílu načichl některými počeštěnými výrazy běžných anglických termínů (např. rámec místo frameworku). Přes vánoce se mi totiž podařilo přečíst knihu Programujeme Jakarta Struts, a i když mi tato pojmenování zpočátku trhala uši (nebo spíše oči), dnes jsem je již přijal víceméně za svá. Doufám ale, že se přes to lehce přenesete.

Bylo by jistě velice nepříjemné, pokud by aplikace čas od času zkolabovala bez jakéhokoli varování nebo pouze s nějakou nesmyslnou zprávou (případně s ještě více frustrujícím výpisem zásobníku, z čehož ovšem znalý uživatel alespoň něco pochopí). V dnešním pokračování se pokusím nastínit, jaké možnosti nám rámec Struts dává pro ošetřování chybových stavů. Ty se jak známo v Javě zpracovávají pomocí zachytávání výjimek.

Deklaratorní zachytávání výjimek

Tento způsob ošetřování chybových stavů se ve Struts objevil od verze 1.1 a umožňuje tvůrcům obsluhu výjimek pomocí konfiguračního souboru, tedy bez přímého zásahu do programového kódu. Je to často velice výhodné, protože změní-li se z nějakého důvodu logika obsluhy těchto chybových stavů, není třeba celou aplikaci rekompilovat, pouze se poeditují příslušné konfigurační soubory.

Tímto místem je v rámci Struts, jak již víme, soubor struts-config.xml. Výjimky v něm můžeme deklarovat na dvou úrovních. Buďto globálně pro celou aplikaci, nebo zvlášť pro každou akci.

Ukažme si nejprve, jak vypadá taková deklarace na globální úrovni. Najděme v souboru struts-config.xml sekci global-exceptions a připišme do ní definici potenciální výjimky:

<global-exceptions>
  <exception
    key="exception.fatal"
    path="/calculate.jsp"
    scope="request"
    type="java.lang.Exception" />
</global-exceptions> 

Jednotlivé položky postupně znamenají:

  • key udává klíč zprávy ze zdrojových svazků, která bude použita jako informace o chybě.
  • path je stránka, na kterou má být přesměrován výstup. V tomto případě by asi bylo vhodnější odkázat uživatele na samostatnou stránku, která může podle arogance autora obsahovat cokoli od poslání uživatele někam až po kultivovanou omluvu s nabídkou náhradního řešení.
  • scope označuje prostor, ve kterém bude chybová zpráva uchována. Výchozí hodnota je request, takže vlastně nemusela být uvedena, další možností je session.
  • type je plně určený typ chyby, ke které se tato definice váže. V tomto konkrétním případě se snažíme zachytit co nejobecnější výjimku, která by měla koncového uživatele ochránit před smrtící modrou obrazovkou (myslím tu tomcatovskou).

Stejně dobře bychom na stejné úrovni mohli ošetřit výjimku java.lang.Num­berFormatExcep­tion, která by samozřejmě byla zpracována před rodičovskou výjimkou java.lang.Excep­tion.

Pokud bychom nyní naši aplikaci znovu sestavili (ant dist) a obnovili v kontejneru (redeploy), k žádné havárii při zadání nesprávného tvaru čísel nedojde. Ovšem nedojde ani k informování uživatele o příčině problému. K tomu potřebujeme do stránky calculate.jsp přidat jeden řádek, který zajistí výpis chybové zprávy, jak ukazuje kus následujícího kódu:

...
<body bgcolor="white">
  <h4><bean:message key="calculate.title"/></h4>

  <html:errors />
... 

Tag <html:errors/> se postará o zobrazení chybových zpráv, jsou-li nějaké.

Stejným způsobem můžeme deklarovat výjimky v rámci každé akce. Ty jsou zachyceny pouze pro akci, u které jsou definovány, a platí, že stejná definice výjimky na úrovni akce má přednost před globální definicí. Oba přístupy se tedy dají vhodně kombinovat, pokud v akci potřebujeme upravit předdefinované standardní chování.

<action-mappings>
  <action
    path="/solve"
    type="numbers.NumbersAction"
    name="NumbersForm"
    scope="request">
    <exception
      key="exception.number.badFormat"
      path="/calculate.jsp"
      type="java.lang.NumberFormatException" />
  </action>
</action-mappings> 

Zachycení výjimky v rámci akce

Další možností je ošetření chyb přímo v programovém kódu. To můžeme provést buď při zpracování akce (tedy např. ve třídě odvozené odorg.apache.strut­s.action.Acti­on), nebo při zpracování formuláře (o tom dále).

Nejjednoduššeji toho při zpracování akce dosáhneme překrytím metody execute ve třídě odvozené od již dříve zmíněné třídy Action. Je třeba provést několik málo kroků. Podívejme se na následující kód:

package numbers;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.*;

public class NumbersAction extends Action
{
  public ActionForward execute(ActionMapping       mapping,
                               ActionForm          form,
                               HttpServletRequest  request,
                               HttpServletResponse response)
  {
    NumbersForm numbers = (NumbersForm) form;
    try {
      String n1 = numbers.getN1();
      String n2 = numbers.getN2();
      CalculateBean calculator =
        new CalculateBean(Integer.parseInt(n1), Integer.parseInt(n2));
      ResultBean result = calculator.solve();
      request.setAttribute("resultBean", result);
    }
    catch (NumberFormatException e) {
      ActionErrors errors = new ActionErrors();
      errors.add(ActionErrors.GLOBAL_ERROR,
        new ActionError("error.number.notInteger"));
      saveErrors(request, errors);
    }
    return mapping.findForward("calculate");
  }
} 

Náš nebezpečný kód jsme obalili klasicky do bloku try … catch. Pokud dojde k výjimce deklarovaného typu, je uskutečněno několik kroků. Je vytvořen objekt uchovávající informace o nalezených chybách (ActionErrors) a do něj jsou metodou add uloženy jednotlivé chyby (ActionError). Pomocí prvního parametru můžeme specifikovat jakousi kategorii chyby (je to dále využitelné u tagu<html:e­rrors>, kde pomocí parametru property můžeme definovat pouze kategorii chyb, o které chceme informovat) – zde jsme použili standardní předdefinovaný typ GLOBAL_ERROR.

Druhý parametr představuje samotnou chybu. Té předáváme klíč zprávy v lokalizačním souboru a případně další parametry, které jsou v této zprávě použity. Nakonec jsou chyby uloženy v objektu uživatelské relace (request).

Samozřejmě, toto byl (snad) nejjednodušší možný způsob. Vidíte, že je možno vracet více chyb najednou (v jednom objektu ActionErrors), a tudíž uživatele upozornit na všechny potenciální chyby najednou.

Zachycení problémů při kontrole formuláře

Dalším místem, kde můžete zachytit potenciální problémy, je kontrola formuláře ještě před zpracováním příslušnou akcí. V tom případě potřebujeme trošičku poeditovat konfigurační soubor struts-config.xml a překrýt metodu validate třídy org.apache.strut­s.action.Acti­onForm.

Nejprve se podívejme na konfigurační soubor. Sekci s definicí naší akce přepíšeme následujícím způsobem:

<action
  path="/solve"
  type="numbers.NumbersAction"
  name="NumbersForm"
  scope="request"
  validate="true"
  input="/calculate.jsp">
  <exception
    key="exception.number.badFormat"
    path="/calculate.jsp"
    type="java.lang.NumberFormatException" />
</action> 

Tím říkáme, že si přejeme, aby tento formulář byl před pokračováním zpracování akce zkontrolován na případné chyby (validate=„true“), a také, na které stránce se nachází formulář, k němuž je třeba se vrátit, pokud se nějaké problémy vyskytnou (input=„calcu­late.jsp“).

Nyní se podívejme, jak se změní dříve vytvořená třída NumbersForm:

package numbers;

import org.apache.struts.action.*;
import javax.servlet.http.HttpServletRequest;

public class NumbersForm extends ActionForm {
  private String n1;
  private String n2;

  public String getN1() { return n1; }
  public String getN2() { return n2; }

  public void setN1(String number) { this.n1 = number; }
  public void setN2(String number) { this.n2 = number; }

  public ActionErrors validate(ActionMapping mapping,
      HttpServletRequest request) {
    ActionErrors errors = new ActionErrors();
    try {
      Integer.parseInt(n1);
      Integer.parseInt(n2);
    }
    catch (NumberFormatException e) {
      errors.add(ActionErrors.GLOBAL_ERROR,
        new ActionError("error.number.notInteger.inForm"));
    }
    return errors;
  }
} 

Zpracování chyb ve formuláři je podobné jako zpracování chyb v rámci akce. Nejprve je vytvořen objekt chyb (ActionErrors) a do něj jsou uloženy informace o jednotlivých chybách. Tento objekt pak ale nemusíme ukládat do prostoru požadavku, o to se rámec postará sám. Musíme pouze tento objekt vrátit.

Pokud žádné chyby nejsou nalezeny, můžeme vrátit buď prázdný objekt ActionErrors, nebo null. Obojí má stejný význam.

Pokud jsou nějaké chyby detekovány, vrátí se akce zpět na původní formulář. Ovšem hodnoty, které jsme do něj předtím zadali, zůstanou vyplněny. Uživatel tak není nucen znovu vypisovat celý formulář. Akce nebude pokračovat, dokud formulář nebude vyplněn bez chyb a neprojde kontrolou.

Plug-in Validate

Od verze 1.1 se do standardní distribuce Struts dostaly dva moduly. Jedním z nich je plug-in Validate, sloužící pro ověřování formulářových dat. Ověřování položek většiny formulářů je totiž velice podobné, ne-li stejné. To vás nutí kopírovat části kódu ve všech formulářových třídách. Modul Validate by tomu měl zabránit.

Tak jako u předchozích částí naší aplikace je ve struts-blank.war archivu vše předpřipraveno, jenom to použít. Modul je do aplikace připojen v souboru struts-config.xml, v sekci<plug-in>. Pokud se ho rozhodnete nepoužívat, měli byste jej odtud odstranit a můžete smazat i některé knihovny v lib adresáři. Ušetříte nějaké místo ve výsledném .war archivu.

K modulu dále patří dva konfigurační soubory. Prvním z nich je validation-rules.xml. Ten obsahuje popis ověřovacích pravidel, a dokud vám budou stačit základní pravidla (a to ve většině případů budou), nemusíte se jím nijak zvlášť zabývat. Použitelná pravidla jsou tato:

  • required – platí, pokud ověřované pole obsahuje nějaké znaky kromě bílých,
  • min(max)length – platí, pokud pole obsahuje více(méně) nebo stejný počet znaků, než je požadováno,
  • mask – platí, jestliže souhlasí se zadaným regulárním výrazem,
  • byte, short, integer, long, float, double – platí, jestliže lze pole převést na daný primitivní typ,
  • date – platí, jestliže představuje platné datum, formát data můžeme upřesnit,
  • range – platí, jestliže je číslo v zadaném rozsahu,
  • creditcard – platí, jestliže může být platným číslem kreditní karty,
  • email – platí, jestliže může být emailovou adresou.

No, ale dost teorie. Slíbil jsem, že se jí nebudeme moc zabývat, podrobnosti si určitě snadno dohledáte sami. Pojďme opět na náš příklad. Nejprve potřebujeme upravit soubor NumbersForm.java:

package numbers;

//import org.apache.struts.action.*;
import org.apache.struts.validator.ValidatorForm;
import javax.servlet.http.HttpServletRequest;

public class NumbersForm extends ValidatorForm {
  private String n1;
  private String n2;

  public String getN1() { return n1; }
  public String getN2() { return n2; }

  public void setN1(String number) { this.n1 = number; }
  public void setN2(String number) { this.n2 = number; }
} 

Nebudeme už potřebovat metodu validate, kterou nahradí modul Validator. Naše formulářová třída už také není potomkem ActionForm, ale ValidatorForm (která je potomkem ActionForm). V tomto případě budeme mapovat ověřování přímo na formulář. Mohli bychom také použít mapování na akci (která zná správný formulář – můžeme totiž použít různá ověřovací pravidla pro jeden formulář podle toho, která akce s ním pracuje). V tom případě bychom použiliValida­torActionForm. Pokud bychom použili dynamicky definovaný formulář, pak ještě s předponou Dyna, tedy v „nejsložitějším“ případě DynaValidatorAc­tionForm.

Nyní můžeme aplikaci zkompilovat a sestavit (ant compile dist) a případná chyba bude zachycena až v akci. To proto, že jsme pro náš formulář nestanovili žádná pravidla, takže ověřením prošel.

Pravidla pro náš formulář můžeme stanovit v konfiguračním souboru validation.xml v adresáři WEB-INF. Řekněme, že v našem formuláři chceme ověřit první políčko tak, že musí obsahovat nějakou hodnotu:

<form-validation>
  <formset>
    <form name="NumbersForm">
      <field property="n1"
        depends="required">
        <arg0 key="item.N1"/>
      </field>
    </form>
  </formset>
</form-validation> 

Toto je výsek konfiguračního souboru bez hlavičky. V tagu form musíme pomocí parametru name definovat název formuláře, který musí odpovídat některému názvu ze sekce form-beans souborustruts-config.xml. Dále říkáme, že v poli (field) s názvem n1 musí být zapsána nějaká hodnota (depends=„requ­ired“).

Pro každý takový test je v lokalizačním souboru zpráv (application.pro­perties) definována chybová zpráva, která bude v případě problému automaticky uložena ve standardním objektu ActionErrors, se kterým jsme se seznámili už dříve. Pro testrequired má tento tvar (který je možno samozřejmě změnit):

errors.required={0} is required. 

Do zprávy tedy můžeme předat jeden parametr. My jsme použili item.N1, což je pojmenování pole ve stejném lokalizačním souboru. Pokud bychom chtěli vypsat přímo název pole bez použití souboru zpráv, použili bychom

<arg0 key="Argument1" resource="false"/> 

No a to je vše, co je nutné udělat, aby formulář nebyl zpracován, dokud první pole neobsahuje nějaký údaj (který prozatím nemusí být smysluplný v rámci naší miniaplikace). Nic už není nutné kompilovat, stačí sestavit a nahrát do kontejneru. Smysluplnost zajistíme jedním dalším slovem:

...
<field property="n1" depends="required,integer"/>
... 

Od tohoto okamžiku musí být údaj navíc celé číslo, takže jsme se jednoduše dostali na úroveň testů, které jsme předtím zajišťovali programově. Krásné, že? Navíc pokud bude pole prázdé, test na integer už neproběhne – ani nemá smysl.

Podívejme se ještě na jeden složitější test:

<form-validation>
  <global>
  <constant>
    <constant-name>number-type</constant-name>
    <constant-value>^1\d{2,3}$</constant-value>
  </constant>
</global>

  <formset>
    <form name="NumbersForm">
      <field property="n1"
        depends="required,mask">
        <msg name="mask" key="number.notPropper"/>
        <arg0 key="item.N1"/>
        <var>
          <var-name>mask</var-name>
          <var-value>${number-type}</var-value>
        </var>
      </field>
    </form>
  </formset>
</form-validation> 

Co se změnilo. Nyní chceme, aby číslo bylo navíc tří nebo čtyřciferné a začínalo jedničkou. Zadefinovali jsme si globální konstantu pojmenovanou number-type, která tento tvar popisuje regulárním výrazem. Přidali jsme test porovnání s regulárním výrazem (depends=„mask“) a v sekci var jsme tomuto testu přiřadili náš regulární výraz (mohli jsme jej stejně dobře definovat přímo v tagu var-value, ale takhle ho můžeme použít na více místech). Navíc jsme tomuto testu přiřadili alternativní chybovou zprávu ze souboru zpráv.

Nakonec bych chtěl ukázat ještě jednu věc. Modul Validator v sobě obsahuje podporu javascriptů, takže všechny tyto testy lze provést i na straně klienta. A jelikož je to opět předpřipravené, nemusíte o javaskriptech skoro nic vědět. Pokud má uživatel javascripty vypnuté, nevadí, nakonec je stejně provedena kontrola i na straně servru.

...
<html:form action="solve.do"
  onsubmit="return validateNumbersForm(this);">
...
</html:form>
<html:javascript formName="NumbersForm"/>
</body>
... 

Toto jsou změny, které musíte provést v souboru calculate.jsp, aby vše fungovalo. Není jich moc, že? Nyní, pokud máte javascripty v prohlížeči zapnuté, vyskočí na vás v případě problémů varovné okno. Ale jelikož já vím o javascriptech velice málo, nebudu se o tom dále rozepisovat. Podrobnosti nechám na vás.

Závěrečné slovo autora

Snad má takový článek pro začátečníky smysl, mistři prominou nepřesnosti. Vašich příspěvků v diskusi si vážím, důkazem budiž to, že jsem se v tomto pokračování vyhnul veškerému experimentování s barvičkama, které nedopadlo zrovna dobře :o). Zvláštní objekt pro předávání dat jsem sice neudělal, ale nechtěl jsem ztratit návaznost na minulý díl a v tomto mini příkladě to snad není tak velký prohřešek. Každopádně jsem si tuto radu vzal k srdci.

Článek je sice opět poněkud delší a dokonce bylo i kde dělit, ale přišlo by mi to líto. Takhle je vše na jednom místě a navíc je stejně určen relativně specializované skupině čtenářů, kterým to (snad určitě) nebude vadit.

Tip do článku - TOP100

Na závěr ještě jedna výzva. Já na tomto místě víceméně končím. Asi se k seriálu časem vrátím, až vstřebám další podpůrné technologie (modul Tiles, JSTL, filtry). Ale pokud by někdo chtěl pokračovat, z mé strany není problém, spíš to uvítám. Jen mě prosím kontaktujte, abychom nepracovali souběžně na stejné věci.

cisla.war – archiv konečné aplikace, lze rozbalit i zipem.