Část #1: Modulární systémy ve Spring Frameworku
V tomto díle si povíme něco o aplikačních kontextech, jejich vlastnostech a možnosti jejich řetězení do stromové struktury. Tato část je základem principem celého modulární skladby, jejíž detaily vám budu v následujících dílech popisovat. Jak jsem již uváděl v předmluvě, nejedná se o nic světoborného, jen o základní principy Springu.
Aplikační kontexty
Celý Spring je postaven na aplikačních kontextech. Aplikační kontext se dá nejlépe přiblížit ke class loaderům v Javě, má i řadu podobných vlastností. Aplikační kontexty mohou tvořit strom, kontexty v nižších úrovních se odkazují na kontexty nadřazené. Z nižších kontextů jsou viditelné beany v kontextech nadřazených, z nižších kontextů jsou ApplicationEventy posílány ke zpracování i do nadřazených kontextů. Pouze post processorové faktory (BeanPostProcessor a BeanFactoryPostProcessor) jsou pro každý kontext definovány zvlášť.
Aplikační kontext je něco víc jak BeanFactory - krom jiného se stará o:
- inicializaci Multicasteru (ten se stará o distribuci událostí), buď defaultním objektem SimpleApplicationEventMulticaster nebo vámi definovaným objektem, který je nalezen v konfiguraci pod názvem "applicationEventMulticaster"
- automatické nalezení bean ApplicationListenerů v konfiguraci a jejich zaregistrování do Multicasteru
- zaregistruje JVM shutdown hook, na který se můžete navěsit v případě, že chcete uvolnit nějaké zdroje, pokud je zvenčí vyvoláno ukončení JVM
- postprocessing vašich bean - konkrétně splnění kontraktů rozhraní ApplicationContextAware, ApplicationContextAwareProcessor, ResourceLoaderAware, ApplicationEventPublisherAware, MessageSourceAware
- poskytuje podporu pro AOP
Dokumentace Springu mluví rozdílech aplikačního kontextu a bean factory takto:
"Users are sometimes unsure whether a BeanFactory or an ApplicationContext is best suited for use in a particular situation. A BeanFactory pretty much just instantiates and configures beans. An ApplicationContext also does that, and it provides the supporting infrastructure to enable lots of enterprise-specific features such as transactions and AOP."
V celé řadě případů si vystačíte pouze s jedním hlavním aplikačním kontextem. Někdy však můžete zcela nevědomky používat dva - například Springový plugin do Strutsů funguje tak, že pro každý Strutsový modul vytváří vlastní aplikační kontext s definicí action bean a navěšuje jej na hlavní WebApplicationContext uložený v ServletContextu (viz. metoda: createWebApplicationContext třídy ContextLoaderPlugIn).
Také WebApplicationContext nemusí být tím hlavním. Pokud do web.xml do init parametru přidáte parametr s názvem "parentContextKey" a jako hodnotu doplníte klíč pod kterým se v BeanFactoryLocator nalézá jiný aplikační kontext, bude tento aplikační kontext dosazen jako parent kontext toho webového (viz. třída ContextLoader, metoda loadParentContext). Dosadit parent kontext pro web aplikační kontext můžete také velmi jednoduše přepsáním této třídy a předefinováním metody loadParentContext.
Kouzlo řetězení Spring aplikačních kontextů
Řetězení aplikačních kontextů můžeme použít k oddělení kontextů jednotlivých částí aplikace s možností vysdílet jejich společné části. Představte si, že vyvíjíte dvě aplikační knihovny, které spolu co se týče business logiky nijak nesouvisí - například nějaká reportovací knihovna a knihovna pro správu uživatelů. Obě dvě knihovny se vyvíjejí nezávisle a obě jsou postavené na Springu. Pak máte nějakou web aplikaci, ve které chcete oba "moduly" využít. Vzhledem k tomu, že oba moduly byly vyvíjeny nezávisle, může se jednoduše stát, že budou mít některé beany stejné názvy a při startu by vám Spring vyhořel (a je ještě řada dalších věcí, které by mohly způsobit problémy).
Ideální by bylo umožnit oběma modulům žít dále ve svých oddělených aplikačních kontextech. Co však s beanami, které bychom chtěli mít společné? Stačí nám k tomu definovat rootovský aplikační kontext, který bude obsahovat definici společných bean a oběma modulům nastavit tento "root" kontext jako jejich parent kontext.
Pozn.: společnými beanami jsou myšleny např. MailSender, DataSource, TransactionManager a podobné beany, které potřebuje ke svému chodu každý modul (nebo alespoň větší část modulů) a které chceme zároveň konfigurovat a mít pouze jednou v celé aplikaci.
Z jednotlivých modulů se vyjmou deklarace sdílených bean a přesunou se do extra konfiguračního souboru, který využijeme pouze pro testy. V reálné aplikaci tyto beany vždy dostaneme "svrchu" od root aplikačního kontextu. Pokud bychom narazili na problém rozdílného pojmenování sdílených bean v root kontextu oproti názvům, které očekává modul, můžeme jednoduše aliasem vytvořit beanu s potřebným názvem pro konkrétní modul.
Tuto kompozici používá například open source Flash server Red5 a nyní i CMS FG Forrest ;-) .
Jak inicializovat root context
Root context je typicky singletonem - ukládáme jej do statické proměnné a existuje pouze jednou v celé aplikaci. Je to obdobná strategie, kterou používá ContextSingletonBeanFactoryLocator, který bychom mohli k této operaci využít. Pro vlastní inicializaci je asi nejvhodnější statický konstruktor. Pro inspiraci uvádím příklad kódu:
public class RootContextLocator {
private static ApplicationContext rootContext;
static {
//ve statickém konstruktoru inicializujeme konfiguraci
rootContext = new ClasspathXmlApplicationContext("classpath:META-INF/rootApplicationContext.xml");
}
public static ApplicationContext getRootApplicationContext() {
return rootContext;
}
}
Kdekoliv v aplikaci je pak možné díky statické metodě získat přístup k root kontextu. Existence jediné instance tohoto kontextu je zajištěna právě statickým konstruktorem třídy RootContextLocator.
Root kontext bude obvykle poměrně hubený - bude obsahovat jen pár společných bean. S postupem času si ukážeme, že hlavním cílem root kontextu je, stát se jakousi postavou ve středu dění (z anglického "man in the middle" ;-) ), která se stará především o koordinaci podřízených kontextů a sloužící k jejich vzájemnému propojení.
Tímto způsobem můžeme také vytvořit root kontext, který bude jedinečný (společný) pro různé web aplikace i EJB v rámci jednoho EARu. V případě, že bude RootContextLocator třída nahrávaná EAR root classloaderem (více o classloaderech v J2EE) a můžeme tedy sdílet základní beany i mezi různými web aplikacemi a EJB.
V souvislosti s J2EE připomínám, že aplikační servery mohou používat více JVM pro stejný EAR. V takovém případě bude root kontext fyzicky vícekrát (to je ostatně společný problém všech singletonů v J2EE prostředí) a proto je třeba tento fakt brát v úvahu. Což tedy znamená, že stavové informace je vhodné ukládat do bean, o jejich replikaci se nám aplikační server postará (např. beany uložené v session) nebo si takové údaje ukládat např. do databáze a držet se spíše stateless bean (myšleno Spring bean).
Další věcí, kterou je dobré si uvědomit, že v J2EE v podstatě nesmíte skoro nic. O propagaci datasourců a dalších věcí do web aplikací a EJB se stará sám kontejner a asi by nebylo nejvhodnější se mu plést do práce paralelním pašováním datasourcu z root spring kontextu (osobně si myslím, že ten by se ani k datasourcu v JNDI při inicializaci ještě ani nemohl dostat).
Upozorňuji, že v J2EE nejsem kovaný, takže pokud byste se hodlali pouštět do něčeho podobného, ověřte si chování na nějakém prototypu.
Každopádně podobnou architekturu už si vyzkoušeli ve zmíněném Red5 serveru - viz. mail jednoho z vývojářů.
Co bude v další části seriálu?!
V další části seriálu bude rozebrána problematika refreshe stromu aplikačních kontextů. Toto je skvělá vlastnost Springu, která je často nedoceněná a málo používaná. Díky ní je možné jednoduše zahodit všechny současné instance bean definované v aplikačním kontextu a provést kompletní reinicalizaci kontextu s aktuální konfigurací (tak můžeme elegantně změnit chování aplikace bez nutnosti restartu serveru). S refreshem aplikačního kontextu se dá vyřešit poměrně dost věcí i v produkčním prostředí - navíc netrpí problémem PermGenSpace jako při reloadu kontextu celé aplikace na serveru. V situaci, kdy máme ale celý strom aplikačních kontextů se nám situace poměrně komplikuje - refresh se totiž stromem sám od sebe nezpropaguje.