Beans introspection - základy Springu

Je tomu už drahně let, co jsem používal k populaci JavaBean Commons-BeanUtils z rodiny Apache Jakarta. Od chvíle, kdy stavím svoje aplikace nad Springem, pozbývá používání této knihovny smysl - naopak bylo by bláhové se této knihovny držet, když Spring nabízí již ve svém základu mnohem víc. Prostým logickým úsudkem lze odvodit, že Spring coby IoC kontejner bude obsahovat promyšlenou logiku pro injektování dat do Java Bean. Nicméně v dokumentaci o tom najdete jen poměrně krátkou kapitolu Validation. Proto jsem se rozhodl vyextrahovat ze svého kódu pár příkladů, které standardní Spring dokumentaci trochu rozvádí do podrobností.

BeanWrapper

Pro jednoduchou populaci dat do JavaBean je možné použít BeanWrapper - respektive BeanWrapperImpl jako jeho jedinou implementaci. S pomocí tohoto objektu je možné jednoduše do jakéhokoliv objektu dodržujícího pravidla JavaBean nasetovat property, nebo jeho property číst. Tyto operace lze předvést jednoduše na následujícím příkladě (kompletní zdrojové kódy lze stáhnout na konci článku):


//create custom pojo instance
User myJavaBean = new User();
//create wrapper
BeanWrapper wrapper = new BeanWrapperImpl(myJavaBean);
//use simple population
wrapper.setPropertyValue("login", "novoj");
wrapper.setPropertyValue("password", "heslo");
//check it
assertEquals("novoj", myJavaBean.getLogin());
assertEquals("heslo", myJavaBean.getPassword());
//use collected values population
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("address.street", "U řeky 1");
pvs.addPropertyValue("address.town", "Královec");
pvs.addPropertyValue("address.country", "Česká republika");
wrapper.setPropertyValues(pvs);
//check it
assertEquals("U řeky 1", myJavaBean.getAddress().getStreet());
assertEquals("Královec", myJavaBean.getAddress().getTown());
assertEquals("Česká republika", myJavaBean.getAddress().getCountry());
//populate list data
wrapper.setPropertyValue("tags[0]", "redTeam");
wrapper.setPropertyValue("tags[1]", "management");
//check it
assertTrue(myJavaBean.getTags().contains("redTeam"));
assertTrue(myJavaBean.getTags().contains("management"));
//populate even map properties
wrapper.setPropertyValue("properties[blogger]", Boolean.FALSE);
wrapper.setPropertyValue("properties[degree]", "university");
//check it
assertEquals(Boolean.FALSE, myJavaBean.getProperties().get("blogger"));
assertEquals("university", myJavaBean.getProperties().get("degree"));
//let's look at more difficult types - PropertyEditors come handy
wrapper.setPropertyValue("email", "Otec Fura (novotnaci@gmail.com)");
//check it
assertEquals("Otec Fura", myJavaBean.getEmail().getName());
assertEquals("novotnaci@gmail.com", myJavaBean.getEmail().getAddress());
//will throw exception
try {
   wrapper.setPropertyValue("notExistingProperty", "doesn't matter");
   fail("Exception expected!");
} catch(BeansException ex) {
   //that is ok
}

Jedinou instanci BeanWrapperu je možné použít pro populaci libovolného počtu různých java bean - beanwrapper si nedrží žádné stavové údaje, které by ho vázaly k jediné JavaBeaně. Údaje je možné setovat jednotlivě nebo více naráz (pomocí PropertyValues). Dále je možné nasetovat data do kolekcí - stačí mít getter vracející instanci kolekce (pozor kolekce musí existovat - instanci nemůže wrapper vytvořit sám protože nezná konkrétní implementaci) a o zbytek se již postará Spring sám. Obdobně funguje populace do mapy - opět stačí mít getter vracející instanci java.util.Map (opět je třeba zajistit aby se nevrátila null hodnota).

Tuto funkcionalitu jste schopni dosáhnout i s pomocí Commons-BeanUtils. Co je však v případě Commons-BeanUtils nepříjemné je ten fakt, že při práci s indexovanými nebo map property vyžadují specifické deklarace metod (např. public void setIndexedProperty(int index, Object value); nebo public void setMapProperty(String key, Object value);). Spring dokáže pracovat se standardními gettery vracejícími Collection nebo Mapu.

Kromě populace můžete samozřejmě použít BeanWrapper i k zjištění informací o JavaBean - nicméně tím se v tomto článku nechci zabývat.

Dosud v článku nezaznělo nic extra zajímavého - vydržte a čtěte dál, teprve začínáme ;-)

DataBinder - hrajeme si s chybovými hlášeními

DataBinder se vám bude hodit v případě, že máte hrst neznámých dat, které chcete napopulovat do vaší JavaBean. Strukturu své JavaBean a její omezení / pravidla znáte - nicméně netušíte, co se může nacházet ve vstupních datech. Při použití BeanWrapperu byste brzy narazili na nějakou exception (např. pokud by se ve vstupních datech nacházela položka, ke které by v JavaBean nebyla odpovídající property nebo pokud by se nepodařila konverze na cílový typ). Opracování těchto stavů byste si museli zajišťovat sami a bylo by to poměrně dost kódování.

Pro tento usecase poskytuje Spring třídu DataBinder. Tato chytrá třída vám jednoduše umožní:

  • ignorovat hodnoty, pro které nemá JavaBean odpovídající settery (setIgnoreUnknownFields(true))
  • ignorovat hodnoty, které nelze napopulovat z důvodu null hodnot v property (např. u indexovaných nebo map property, kdy getter vrací null) (setIgnoreInvalidFields(true))
  • nastavit property, jejichž hodnota nesmí být změněna i kdyby ve vstupních hodnotách byla odpovídající položka (setDisallowedFields(seznam property))
  • nastavit property, které musí být ze vstupních hodnot napopulovány (setRequiredFields(seznam property))
  • zjistit chyby, ke kterým při populaci došlo a odpovídajícím způsobem je zobrazit uživateli

Uvedené vlastnosti jsou předvedeny v následujícím příkladě:


//create custom pojo instance
User myJavaBean = new User();
myJavaBean.setLogin("novoj");
myJavaBean.setPassword("password");
myJavaBean.setEmail(new EmailAddress("novotnaci@gmail.com", "Otec Fura"));
myJavaBean.setProperty("degree", "high school");
myJavaBean.setProperty("blogger", Boolean.TRUE);
//create data binder
DataBinder binder = new DataBinder(myJavaBean);
binder.setIgnoreUnknownFields(true);
binder.setIgnoreInvalidFields(false);
binder.setDisallowedFields(new String[] {"login", "address.*"});
binder.setRequiredFields(new String[] {"password", "properties[degree]"});
//create input values map
MutablePropertyValues pvs = new MutablePropertyValues();
//will be ignored - is in disallowed fields
pvs.addPropertyValue("login", "newLogin");
//will be populated
pvs.addPropertyValue("password", "newPassword");
//would trigger exception if ignoreUnknownfields == false
pvs.addPropertyValue("nonExistingField", "doesn't matter");
//will result in error in binding ressult
pvs.addPropertyValue("email", Boolean.FALSE);
//will result in required error in result if commented out
//pvs.addPropertyValue("properties[degree]", "university");
//bind values at one single shot
binder.bind(pvs);
//check errors
BindingResult result = binder.getBindingResult();
assertEquals(2, result.getErrorCount());
assertEquals("novoj", myJavaBean.getLogin());
assertEquals("newPassword", myJavaBean.getPassword());
assertEquals("Otec Fura", myJavaBean.getEmail().getName());
assertEquals("novotnaci@gmail.com", myJavaBean.getEmail().getAddress());
assertEquals("high school", myJavaBean.getProperties().get("degree"));

Při populaci (bind) z DataBinderu nikdy nevyletí vyjímka. Při problematických situacích DataBinder vytváří chybové hlášky, které lze po skončení populace získat pomocí binder.getBindingResult(). V tomto objektu jsou potom dostupné instance MessageSourceResolvable reprezentující vzniklé chyby. Strategii tvorby chyb lze poměrně detailně ovlivnit (pokud byste to vůbec potřebovali) nastavením implementací rozhraní MessageCodesResolver a BindingErrorProcessor.

Tato třída je používána především ve Spring-MVC části Springu, nicméně i v případě, že pro webovou vrstvu využíváte jiné frameworky (my například Stripes), narazíte na řadu use-case, kdy se vám možnost populace libovolných dat do libovolné JavaBeany s detailní kontrolou nad tímto procesem může hodit. V našem případě to je například při načítání konfigurace aplikace z konfiguračních souborů.

Tuto funkcionalitu budete v Commons-BeanUtils těžko hledat.

PropertyEditor - hrajeme si s daty

Co činí BeanWrapper / DataBinder zajímavějším, je možnost populovat do typových property (např. Integer, Boolean) řetězcové hodnoty (String). Spring k tomu využívá tzv. PropertyEditory, které jsou v původním standardu JavaBean. Pokud je setovaná hodnota String a odlišuje se od typu požadovaného setterem, pokusí se Spring najít odpovídající PropertyEditor, který by k jejímu převodu mohl použít. Více o PropertyEditorech se dozvíte buď v dokumentaci Springu, nebo v článku českého bloggera Vlasty Vávrů.

Principy jsou v obou odkazovaných dokumentech popsány poměrně podrobně a proto je tu nebudu opakovat. Co bych však chtěl zdůraznit je to, že přestože je možné Springu říci, pro jaký typ má použít jaký PropertyEditor, existuje i jednodušší cesta. Stačí implementaci PropertyEditoru umístit do stejné package jako je deklarace původního objektu, který property editor konvertuje (dále je nutné zachovat základ názvu class a přidat na konec slůvko "Editor"). Takové editory budou Springem nalezeny automaticky, aniž bychom museli kdekoliv cokoliv registrovat. Je to, řekl bych, nejjednodušší způsob, jak PropertyEditory zavádět. Pro vytváření nových PropertyEditorů využijte Spring předka PropertyEditorSupport, který vám ušetří mnoho práce.

V Commons-BeanUtils podobný problém řeší tzv. Convertory.

ResourceLoader - řekněte Springu ke má co hledat

A v poslední kapitolce bych chtěl naťuknout ResourceLoadery ve Springu. Je to věc, která by možná zasloužila vlastní příspěvek, ale vezmu to letem světem. ResourceLoader je abstrakce Springu, která se stará o vytvoření instancí implementací rozhraní Resource, které je další abstrakcí Springu nad libovolnými binárními zdroji. Zni to složitě ale princip je báječně jednoduchý. Rozhraní Resource jsme adoptovali do našich projektů a osobně si tento krok nemůžu vynachválit. Resource se svým charakterem podobá třídě java.io.File - je nezávislá na existenci zdroje, na který se odkazuje, umožňuje získat InputStream a konverzi na URL. Resource může však reprezentovat libovolný objekt binárního charakteru, java.io.File rozhraní se k tomu už příliš nehodí. V našem CMS systému používáme kromě standardních Spring resource implementací také vlasní implementace reprezentující soubory uložené v databázi a virtuálních úložištích.

ResourceLoader umožňuje Springu převést cestu (ve formátu String) na výslednou Resource implementaci. Ve většině případů si asi vystačíte se standardním ResourceLoaderem (DefaultResouceLoader, který vám umožní zpřístupnit zdroje na classpath, filesystému a síti).

Pokud byste však chtěli vytvořit vlastní abstrakci pro umístění resourců, není problém vytvořit si vlastní ResourceLoader nebo i implementaci Resource rozhranní. V následujícím příkladě si vytvoříme specializovaný ResourceLoader, který bude rozeznávat "protokol" corpWeb, který bude hledat zdroje na web stránkách naší společnosti. V rámci "protokolu" se již budeme odkazovat relativně. Takováto jednoduchá implementace může vypadat následovně:


/**
 * Custom resource loader - remember, you can have also your custom Resource types. This is our aim.
 *
 * @author Jan Novotný
 * @version $Id: $
 */
public class CustomResourceLoader extends DefaultResourceLoader {
	private static final String CORP_WEB_PREFIX = "corpWeb:";
	public Resource getResource(String location) {
		if (location.startsWith(CORP_WEB_PREFIX)) {
			try {
				return new UrlResource(new URL("http", "www.fg.cz", location.substring(CORP_WEB_PREFIX.length())));
			}
			catch(MalformedURLException e) {
				return super.getResource(location);
			}
		} else {
			return super.getResource(location);
		}
	}
}

Použití vlastní instance ResourceLoaderu v kombinaci s BeanWrapperem (obdobně i DataBinderem) je velmi přímočará. Následující test nám potvrzuje předpokládané chování. V první části testu použijeme standardní Spring DefaultResourceLoader. Ten sice vytvoří jakousi instanci Resource (konkrétně ClassPathResource), ale ta samozřejmě nebude odkazovat na existující zdroj, protože na classpath takový zdroj není. Pokud zaregistrujeme vlastní ResourceLoader (druhá část testu), vrátí se naprosto jiná implementace Resource rozhraní, která již dokáže najít existující zdroj dat.


//create custom pojo instance
User myJavaBean = new User();
//create wrapper
BeanWrapper wrapper = new BeanWrapperImpl(myJavaBean);
//bind custom resource
wrapper.setPropertyValue("photo", "corpWeb:/img/u/logo_title.gif");
//because default resource editor doesn't know corpWeb
//location handling it will use classpath resource
assertNotNull(myJavaBean.getPhoto());
//that mean, that such resource won't be accessible
assertFalse(myJavaBean.getPhoto().exists());
//but when we register custom resource loader being able to
//process corpWeb location situation changes
wrapper.registerCustomEditor(
   Resource.class,
   new ResourceEditor(
      new CustomResourceLoader()
   )
);
//bind custom resource
wrapper.setPropertyValue("photo", "corpWeb:/img/u/logo_title.gif");
//check that photo exists
assertNotNull(myJavaBean.getPhoto());
assertTrue(myJavaBean.getPhoto().exists());

ResourceLoader je základním konceptem celého Springu. Zde si z něj ukazujeme jen malou část - jednoduché použití, které je však součástí nejužšího jádra Spring Frameworku.

Závěrem

V článku srovnávám Spring především s Commons-BeanUtils. Jsem si vědom toho, že populačních knihoven je velká spousta - namátkou mohu zmínit např. OGNL, která je používána frameworkem Struts 2 (a o které se také proslýchá, že není z nejrychlejších ;-) ). Přidržel jsem se však toho, co detailně znám a nechtěl jsem se pouštět do srovnávání s knihovnami, o kterých jsem si pouze něco přečetl. Pokud budete mít zajímavé postřehy z použití konkurenčních řešení, neváhejte a pište své komentáře.

Kompletní zdrojové kódy lze stáhnout zde.

Pozn.: pokud programujete web aplikaci, nezapomeňte do web.xml přidat deklaraci listeneru IntrospectorCleanupListener (viz. dovětky k článku PermGenSpace problem? No problem!)