Překonaný ResourceBundle, Spring MessageSource vítězí v prvním kole KO

Tento článek mám ve WordPressu rozepsaný snad už rok. Jeho původní název zněl "ResourceBundle - stačí Javě beze změny?". Plno věcí, které jsme původně jako Java vývojáři dělali my, postupně uzpůsobujeme tak, aby je mohli dělat web designeři. Na prezentační vrstvu zcela jistě patří lokalizované texty a zprávy, pro které standardně používáme ResourceBundly Javy, které se načítají z property souborů. Ideální model pro web developery je iterace: navrhnu stránku, vložím text do property bundlu, uložím, reloadnu stránku a kouknu jak to vypadá. V tomhle jednoduchém scénáři jsme však narazili hned na několik problémů.

Standardní PropertyResourceBundle se obvykle získává takto (to je mmch. cesta zmíněná v dokumentaci ResourceBundle):


ResourceBundle myResources =
   ResourceBundle.getBundle("MyResources", currentLocale);

Takto načítaný property soubor ("MyResources.properties") musíme mít na classpath (kde samozřejmě není editovatelný) a musíme jej mít ve Ascii formátu zkonvertovaný utilitou Native2Ascii. Dalším problémem je sekvence, ve které se hledají konkrétní lokalizované varianty bundlu. Díky tomu, že Java původně vznikla hlavně pro desktopové aplikace se do posloupnost hledání bundlů dostává i bundle pro systémové locale stroje, na kterém aplikaci spouštíme. V případě volání metody getBundle s Locale("cs","CZ") se na anglickém Ubuntu hledá property bundle v této posloupnosti:

  1. messages_cs_CZ.properties
  2. messages_cs.properties
  3. messages_en_US.properties
  4. messages_en.properties
  5. messages.properties

Tuto věc jsem ještě před rokem nevěděl, a to možná také proto, že není nijak zmíněná v dokumentaci Javy.

Tak se nám těch problémů nakonec sešlo docela dost - na to, že řešíme takovou hloupost jako je lokalizace - že?

ReloadableResourceBundleMessageSource

Naštěstí je tu Spring - jak jinak. Je to prozatím jediné místo, kde jsem našel zmíněný problém pořešený natolik dobře, že likvidace všech zmíněných problémů nám nakonec zabere jen pár minut.

To řešení se jmenuje ReloadableResourceBundleMessageSource. Jeho použití má řadu předností:

  • basenames - property akceptující array stringů reprezentující základ cesty k properties souborům (v základu je podporován i XML formát); property ze všech těchto souborů se sloučí a vy můžete žádat o texty z libovolného z nich
  • defaultEncoding, respektive fileEncodings - výchozí kódování property souborů - buď hromadně pro všechny uvedené (defaultEncoding), nebo jednotlivě pro každý zvlášť (fileEncodings); od této chvíle už nepotřebujete Native2Ascii
  • fallbackToSystemLocale - property, která určuje jestli se při výpočtu variant souborů má počítat i se systémovým locale nebo ne; nastavením na false se zbavíte problému s nelogickou posloupností v prostředí web aplikací
  • cacheSeconds - definuje čas v sekundách po který jsou obsahy property bundlů cachovány; na produkci se hodí hodnota -1 (napořád), při vývoji naopak 0 (nikdy)
  • propertiesPersister - interface pro načítání / ukládání messagí do / z cílového formátu (v základu properties a XML, ale díky rozhraní si můžete dopsat cokoliv - třeba i načítání z MS Excelu)
  • resourceLoader - implementace rozhraní pro přístup k souborům reprezentujícím message bundly; díky resource loaderu můžete jednoduše zajistit načtení obsahu souboru třebas z databáze

Jediná Spring třída nám v podstatě řeší všechny problémy naráz. Pokud budu mít ve svém aplikačním kontext následující definici:


<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
			<value>file:/projekt/neco/messages</value>
			<value>file:/projekt/neco/exceptions</value>
			<value>file:/projekt/neco/disclaimer</value>
		</list>
	</property>
<property name="defaultEncoding" value="UTF-8"/>
<property name="fallbackToSystemLocale" value="false"/>
<property name="cacheSeconds" value="0"/>
</bean>

... získám přesně tu funkcionalitu, kterou pro hladký vývoj potřebuji. Property soubory jsou uložené v textových souborech ve formátování UTF-8 na filesystému, při změně libovolného z nich se mi změna okamžitě projeví i v obsahu web stránky. Navíc se mi konečně hledání správného property bundlu chová tak jak očekávám, tzn. můžu bez obav vytvořit "default" property bundle s pojmenováním "neco.properties" a vím, že pokud bude chtít lokalizované zdroje, které zatím nemám k dispozici (třeba thajštinu), tak se použije obsah právě tohoto "default" bundlu a nikoliv (záhadně) odlišná lokalizace dle nativního locale Javy na serveru.

Jak se ke zprávám dostanu z kódu

Dostáváme se k základům Springu, ale pokud by někdo přeci jen nevěděl ...

Pokud dodržíme jmennou konvenci a beanu nazvu "messageSource" není třeba již nic moc složitého provádět. Stačí když ve svých beanách implementuji rozhraní MessageSourceAware, Spring sám mi do nich injektuje referenci na připravený MessageSource se zprávami.

MessageSource sice již obsahuje potřebné metody k získání zpráv, ale často se hodí i použití helper wrapperu MessageSourceAccessor, který sadu metod rozšiřuje. Ten navíc obsahuje i integraci s třídou LocaleContextHolder, která nám zase zajišťuje přístup ke správnému locale pro aktuální request (v kombinaci s DispatcherServlet nebo libovolným filtrem / servletem, který si sami napíšete).

MessageSourceResolvable - když je jeden klíč pro zprávu málo

Další skvělou vlastností MessageSource Springu je možnost hledat lokalizovanou zprávu pod několika klíči naráz v předem definované posloupnosti. Na podobnou funkcionalitu narazíte i ve web frameworku Stripes. Na první pohled by se mohlo zdát jako zbytečnost - vždyť pokud přeci potřebuju lokalizovaný text, tak přeci vždy vím jaký. A to právě není tak úplně pravda ...

Pokud píšete nějakou obecnější věc, může se hodit seskupovat lokalizované zprávy do konkrétních skupin obecnosti. V případě aplikační chyby třeba definovat nejen konkrétní klíč pro danou chybu "exception.myBusinessClass.errorCode", ale i obecnější kód "exception.general" - popř. i více kódů pro jemnější členění. Uživatel vaší knihovny se pak může rozhodnout které kódy využije (přeloží) a které nikoliv. Pokud nepotřebuje detailní rozlišení pro chybová hlášení, může se rozhodnout přeložit pouze hlášení "exception.general" a má lokalizaci vyřešenou.

Osobně považuji tento mechanismus za geniální - v našem prostředí ho využíváme například pro validační hlášení. Každé validační chybové hlášení hledáme pomocí MessageSourceResolvable pod těmito klíči:

  • validation.lokaceStranky.lokaceKomponenty.typValidace (validation.blogs.createBlog.title.required)
  • validation.lokaceStranky.typValidace (validation.blogs.createBlog.required)
  • validation.typValidace (validation.required)

Takto je potom možné mít obecnější validační hlášení pro celý web, ale pokud je potřeba pro konkrétní stránku toto hlášení uzpůsobit, stále je tu ta možnost.

Ještě nekončíme - co se znovupoužitelnými knihovnami?

Je tu ještě jeden problém, který jsem dosud nezmínil. Co když chceme mít obecnou knihovnu, která potřebuje produkovat hlášení prezentované uživateli? Rádi bychom, abychom ji mohli dát na classpath a bez jakékoliv dodatečné práce ji mohli hned používat (tzn. knihovna měla sama v sobě zabudovaná použitelná lokalizovaná hlášení). Na druhou stranu bychom ale zase chtěli, aby nám zůstala možnost výběrově některé z hlášení při specifickém nasazení změnit.

Pro tyto účely můžeme využít hierarchického členění MessageSourců (HierarchicalMessageSource), které se principem podobá mechanismu který platí i pro samotné aplikační kontexty. Pokud zajistíme, aby "systémová" lokalizace knihovny byla nastavena jako parentMessageSource pro námi používaný MessageSource docílíme požadovaného chování. V našem MessageSource potom můžeme předefinovat jen pár lokalizovaných hlášení, o která nám jde a zbytek bude delegován na nadřízený MessageSource, kde vždy nalezneme původní systémová hlášení knihovny.

Podpora pro hnízdění MessageSourců je již implementována v aplikačním kontextu Springu - konkrétně v AbstractApplicationContext. Pokud ve svém podřízeném aplikačním kontextu nadefinujete beanu "messageSource", která bude implementovat HierarchicalMessageSource a nebude mít nastavený parentMessageSource, Spring sám mu nastaví tento parentMessageSource na MessageSource nadřízeného aplikačního kontextu. Tahle věta je možná trošku oříšek, ale jednodušeji jsem to zapsat nedokázal - v případě nejasností se stačí kouknout do třídy AbstractApplicationContext, metody initMessageSource().

Závěrem

Škoda, že podobná podpora, jakou nalezneme ve Springu není součástí standardní Javy. V ní se po víc jak deseti letech vývoje (JDK 1.6) objevil pouze nový konstruktor, který umožní načíst obsah property souboru pomocí dodaného Readeru. Alespoň už nemusíme nutně používat Native2Ascii. Není to ale přeci jenom trochu málo?