Část #2: Modulární systémy ve Spring Frameworku

V této části seriálu si rozebereme problematiku 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.

Refresh zřetězených aplikačních kontextů

Spring Framework má jednu skvělou vlastnost, kterou v prostředí web aplikací řada z nás asi nevyužívá. A tou je refresh aplikačních kontextů. V případě, že je konfigurační soubor umístěn na nějakém editovatelném místě (tím je myšleno třebas někde na filesystemu, url ale ne na classpath) je možné po úpravě konfiguračního XML souboru zavolat na aplikačním kontextu refresh, který nám bezpečně refreshne definici bean v něm.

Tento proces má svůj lifecycle. Po zavolání refresh se celý aplikační kontext znovu zčista zinicializuje. Vzniknou tedy nové instance bean, znovu se na nich provede injecting a opětovně jsou zavolány lifecycle akce (jako např. afterPropertiesSet apod.). Aplikační kontext o této události také informuje své okolí vysláním události ContextRefreshedEvent. Staré beany by měl (pokud jsme si je někde neuvážlivě neuložili do statické proměnné) garbage collector vyhodit. Při zavolání refreshe je dokončen lifecycle původních bean v kontextu (např. v případě DisposableBean je zavolána metoda destroy) a naopak v případě nově vytvořených bean jsou zavolány odpovídající lifecycle callback metody (afterPropertiesSet například).

Hlídání změn konfiguračních souborů žádný aplikační kontext neprovádí. Ani jsem nenašel žádnou třídu uvnitř Springu, která by podobnou záležitost prováděla. Tzn. hlídání změn konfiguračních souborů si musíme zajistit vlastními silami (a jelikož je to záležitost triviální, nebudu se tu o ní zbytečně rozepisovat :-) ).

Jak je tomu v případě, že máme celý strom aplikačních kontextů? Pokud provedeme refresh kontextu vespod stromu (dejme tomu tedy pouze modulu na nejnižší úrovni) je vše v pořádku. Když ovšem provedeme refresh kořenového kontextu, dostaneme celou struktutru do nekonzistentního stavu - beany podřízených kontextů budou mít stále nasetovány odkazy na původní beany parent kontextu, které v něm byly ještě před refreshem (refresh vytvořil nové, znovu nakonfigurované instance bean). V případě, že se jedná o tzv. DisposableBean - teda beany, které si hlídají uzavření kontextu, aby mohly uvolnit systémové zdroje (otevřené kurzory, konekce atd.), dojde pravděpodobně při dalším volání metod na beanách podřízených kontextů k chybě - tyto beany budou mít totiž odkazy na již disposnuté beany z rodičovského kontextu.

Řešení není zase tak složité jak by se mohlo na první pohled zdát. Celý problém lze vyřešit zavedením listeneru, který je dostupný i podřízeným kontextům (využíváme vlastnosti, kdy v podřízených kontextech vidíme beany nadřízeného kontextu). Tento listener naslouchá na event ContextRefreshedEvent, který je vyhazován refreshi kontextu a obsahuje list registrovanými objekty, kteří si přejí být notifikovány, když se daný kontext znovu inicializuje. Do tohoto listeneru si zaregistruji i objekty, které mi zajistí následný refresh podřízených kontextů. Pokud tedy provedu refresh root kontextu, kaskádovitě se provedou postupně refreshe i dalších navázaných aplikačních kontextů. Po této kompletní reinicializaci máme ve všech kontextech konzistentní beany s platnými odkazy na dalšími beany.

Tento listener by mohl vypadat například takto:


/**
 * Listens to root application context reload event. Performs refresh of local application context
 * so the beans in local context will be linked properly to new parent ones.
 */
public class GenericRefreshingListener implements ApplicationListener, ApplicationContextAware {
	private static Log log = LogFactory.getLog(GenericRefreshingListener.class);
	private final List childApplicationContexts = new ArrayList();
	private final List notifiedObjects = new ArrayList();
	private ApplicationContext ctxListenerIsDeclaredIn;
	private static final String RELOADING_LISTENER_BEAN_NAME = "reloadingListener";
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		ctxListenerIsDeclaredIn = applicationContext;
	}
	/**
	 * Handle an application event.
	 *
	 * @param event the event to respond to
	 */
	public void onApplicationEvent(ApplicationEvent event) {
		if(event instanceof ContextRefreshedEvent) {
			ContextRefreshedEvent refreshEvent = (ContextRefreshedEvent)event;
			AbstractRefreshableApplicationContext rootContext =
					(AbstractRefreshableApplicationContext)refreshEvent.getSource();
			//we react only to event fired by context this listener is declared in (the root one)
			if(rootContext == ctxListenerIsDeclaredIn) {
				//switch all children to new parent
				synchronized(childApplicationContexts) {
					for(int i = 0; i < childApplicationContexts.size(); i++) {
						try {
							AbstractRefreshableApplicationContext childContext =
									(AbstractRefreshableApplicationContext)childApplicationContexts.get(i);
							//try to refresh - this is the critical part of process - exception could occur there
							if(log.isInfoEnabled()) {
								log.info("Calling refresh on child context: " + childContext.toString());
							}
							//refresh all beans in childContext
							childContext.refresh();
						}
						catch(Throwable ex) {
							//overgo any exception - just log it, and continue with refreshing other ctxs
							if(log.isErrorEnabled()) {
								log.error("Failed to refresh child context - error occured.", ex);
							}
						}
					}
				}
				//notify all objects that wanted to
				synchronized(notifiedObjects) {
					for(int i = 0; i < notifiedObjects.size(); i++) {
						try {
							IRefreshNotifier notifier = (IRefreshNotifier)notifiedObjects.get(i);
							if(log.isInfoEnabled()) {
								log.info("Calling refresh on registered object: " + notifier.toString());
							}
							notifier.onRefresh(refreshEvent);
						}
						catch(Throwable ex) {
							//overgo any exception - just log it, and continue with notifying other objects
							if(log.isErrorEnabled()) {
								log.error("Failed to notify refresh listener - error occured.", ex);
							}
						}
					}
				}
			}
		}
		if(event instanceof ContextClosedEvent) {
			ContextClosedEvent closedEvent = (ContextClosedEvent)event;
			//switch all children to new parent
			synchronized(childApplicationContexts) {
				for(int i = 0; i < childApplicationContexts.size(); i++) {
					try {
						AbstractRefreshableApplicationContext childContext =
								(AbstractRefreshableApplicationContext)childApplicationContexts.get(i);
						//try to refresh - this is the critical part of process - exception could occur there
						if(log.isInfoEnabled()) {
							log.info("Calling close on child context: " + childContext.toString());
						}
						//refresh all beans in childContext
						childContext.close();
					}
					catch(Throwable ex) {
						//overgo any exception - just log it, and continue with refreshing other ctxs
						if(log.isErrorEnabled()) {
							log.error("Failed to close child context - error occured.", ex);
						}
					}
				}
			}
			//notify all objects that wanted to
			synchronized(notifiedObjects) {
				for(int i = 0; i < notifiedObjects.size(); i++) {
					try {
						IRefreshNotifier notifier = (IRefreshNotifier)notifiedObjects.get(i);
						if(log.isInfoEnabled()) {
							log.info("Calling close on registered object: " + notifier.toString());
						}
						notifier.onClose(closedEvent);
					}
					catch(Throwable ex) {
						//overgo any exception - just log it, and continue with notifying other objects
						if(log.isErrorEnabled()) {
							log.error("Failed to notify refresh listener - error occured.", ex);
						}
					}
				}
			}
		}
	}
	public synchronized void addChildApplicationContext(AbstractRefreshableApplicationContext childContext) {
		if(!childApplicationContexts.contains(childContext)) childApplicationContexts.add(childContext);
	}
	public synchronized void removeChildApplicationContext(AbstractRefreshableApplicationContext childContext) {
		childApplicationContexts.remove(childContext);
	}
	public synchronized void addNotifiedObject(IRefreshNotifier notifier) {
		if(!notifiedObjects.contains(notifier)) notifiedObjects.add(notifier);
	}
	public synchronized void removeNotifiedObject(IRefreshNotifier notifier) {
		notifiedObjects.remove(notifier);
	}
	/**
	 * Hooks child context to parent reload event listenning.
	 *
	 * @param rootContext
	 * @param childContext
	 */
	public static void registerChildForRefreshingEvent(
			AbstractRefreshableApplicationContext rootContext, AbstractRefreshableApplicationContext childContext) {
		if(rootContext.isActive()) {
			GenericRefreshingListener reloadingListener =
					(GenericRefreshingListener)rootContext.getBean(RELOADING_LISTENER_BEAN_NAME);
			reloadingListener.addChildApplicationContext(childContext);
		}
		else {
			if(log.isErrorEnabled()) {
				log.error("Cannot register " + childContext.toString() +
						" for refreshing events: root context is closed!");
			}
		}
	}
	/**
	 * Unhooks child context to parent reload event listenning.
	 *
	 * @param rootContext
	 * @param childContext
	 */
	public static void unregisterChildForRefreshingEvent(
			AbstractRefreshableApplicationContext rootContext, AbstractRefreshableApplicationContext childContext) {
		if(rootContext.isActive()) {
			GenericRefreshingListener reloadingListener =
					(GenericRefreshingListener)rootContext.getBean(RELOADING_LISTENER_BEAN_NAME);
			reloadingListener.removeChildApplicationContext(childContext);
		}
		else {
			if(log.isInfoEnabled()) {
				log.info("Cannot unregister " + childContext.toString() +
						" from refreshing events: root context already closed!");
			}
		}
	}
	/**
	 * Hooks child context to parent reload event listenning.
	 *
	 * @param rootContext
	 * @param notifier
	 */
	public static void registerNotifierForRefreshingEvent(
			AbstractRefreshableApplicationContext rootContext, IRefreshNotifier notifier) {
		if(rootContext.isActive()) {
			GenericRefreshingListener reloadingListener =
					(GenericRefreshingListener)rootContext.getBean(RELOADING_LISTENER_BEAN_NAME);
			reloadingListener.addNotifiedObject(notifier);
		}
		else {
			if(log.isErrorEnabled()) {
				log.error("Cannot register " + notifier.toString() +
						" for refreshing events: root context is closed!");
			}
		}
	}
	/**
	 * Unhooks child context to parent reload event listenning.
	 *
	 * @param rootContext
	 * @param notifier
	 */
	public static void unregisterNotifierForRefreshingEvent(
			AbstractRefreshableApplicationContext rootContext, IRefreshNotifier notifier) {
		if(rootContext.isActive()) {
			GenericRefreshingListener reloadingListener =
					(GenericRefreshingListener)rootContext.getBean(RELOADING_LISTENER_BEAN_NAME);
			reloadingListener.removeNotifiedObject(notifier);
		}
		else {
			if(log.isInfoEnabled()) {
				log.info("Cannot unregister " + notifier.toString() +
						" from refreshing events: root context already closed!");
			}
		}
	}
}

Listener automaticky provede refresh zaregistrovaných dětských kontextů a dále dokáže notifikovat i libovolný objekt, který implementuje rozhraní IRefreshNotifier s callback metodami.


/**
 * Can be implemented by any object, that want to be notified when context is
 * refreshed. This interface is meant to be used by object, that are outside
 * spring context itself (beans inside it can react to ContextRefreshed event
 * directly).
 *
 * Could be used for example by servlet Filters, that are instantiated by web
 * server itself and we have no control over them.
 */
public interface IRefreshNotifier {
	/**
	 * Callback method executed on application context refresh.
	 */
	void onReload(ApplicationEvent event);
	/**
	 * Callback method executed on application context closing.
	 */
	void onClose(ApplicationEvent event);
}

Upozornění: tento kód neprošel unit testy - pro naše použití máme poněkud odlišný mechanismus reloadu (uvedený příklad z něj vychází - nepsal jsem to celé z ruky), proto berte příklad spíš jen jako inspiraci, spíš než kód, který byste šoupli do produkce ;-)

V řadě případů je prostě skvělé mít možnost rekonfigurovat webaplikaci, aniž bychom museli restartovat celý web kontext aplikace na úrovní serveru - a to jak ve vývojové fázi, tak i při drobných dolaďovačkách při nasazování systému.

Refresh odkazů v objektech technologií třetích stran

Dalším problémem, který s refreshem kontextů ve webové aplikaci může souviset jsou objekty třetích knihoven, které jsme se Springem integrovali. Tady musíme být uvážliví. Uvedu dva příklady technologií, se kterými jsme již tuto záležitost řešili a pro své použití i vyřešili.

Apache Struts

První technologií jsou Apache Struts - Spring má v sobě již zabudované třídy pro propojení se Strutsy. Jedná se jednak o ContextLoaderPlugin a potom také o DelegatingActionProxy. V souvislosti s tímto nám postačuje jen vyřešit reload podřízeného kontextu při refreshi předka - toto jsem rozebral již v předchozí části. Víc řešit již nemusíme, jelikož třída DelegatingActionProxy si vždy znovu sahá do aktuálního aplikačního kontextu pro spring beanu, na kterou deleguje provedení Struts akce. Díky kaskádovému refreshi dostaneme konsistentní beanu.

Java Server Faces

Druhou technologií jsou Java Server Faces. Zde probíhá integrace mj. na úrovni DelegatingVariableResolver, který se stará o injektování Spring bean do managovaných bean JSF. Tady máme problém v tom, že se nám reference na objekty dostávají do objektů, které jsou ve správě JSF. Nám postačovalo jednoduché řešení - které patří mezi best practices JSF - managované beany JSF mít rozdělené na tzv. servisní a modelové. Modelové beany mohou být ukládány v session, jsou serializovatelné a jsou to jen jednoduché POJO, které se na žádné Spring beany neodkazují. Servisní beany jsou typicky bezstavové, jsou striktně ukládány do request nebo none scope a mohou libovolně obsahovat reference na Spring beany nebo na modelové beany. Jelikož jsou tyto servisní beany vytvářeny pro každý request znovu, je vždy v tento moment vyvoláván i DelegatingVariableResolver, který vždy získá reference na aktuální a konzistentní beany ve Spring kontextu.

Pokud by bylo třeba mít servisní beany s odkazy na spring beany mít ukládané v session scope, napadlo mne jediné řešení. Upravit DelegatingVariableResolver tak, aby místo injektování referencí na Spring beany injektoval pouze dynamické proxy (s použitím např. CGLIB), které by byly implementované tak, že každé volání na proxy by převedly na volání beany ze Spring kontextu, kterou by si vždy čerstvě vyzvedly. Do tohoto řešení jsem se ale nepouštěl, jelikož jsme jej, díky zavedení výše uvedného pravidla, nakonec nepotřebovali.

Problém s registrací listenerů

Pokud se budete trochu více hrabat ve správě listenerů ve spring kontextech, narazíte na jednu věc, která mne trochu zarazila a pár věcí zkomplikovala. Ve standardním aplikačním kontextu (chování je definováno v AbstractApplicationContext, ze kterého většina - ne-li všechny - aplikační kontexty dědí) není možné přidat nový listener po refreshi aplikačního kontextu.

Máte jen tri možnosti:

  1. aplikační listenery definovat přímo v konfiguraci springu - tyto listenery jsou automaticky registrovány standardními BeanPostProcessory po vytvoření příslušné beany v kontextu
  2. aplikačně listenery registrovat před samotným refreshem kontextu - což vyžaduje vytvořit kontext nastavením flagu refresh=false a pak provést refresh manuálně
  3. zaregistrovat si vlastní multicaster, který bude umožňovat registraci nových listenerů do běžícího kontextu (nenarazil jsem na jediný důvod, proč by to nemělo být možné / správné)

Osobně jsem nechtěl příliš ohýbat stávající chování a proto jsem se přidržel první možnosti a definuji výše uvedený listener standardně v konfiguraci parent kontextu. Dětské kontexty se do toho listeneru registrují přes statickou metodu, které předají odkaz na parent kontext, ve kterém listener "žije". Statické metody se již postarají o to, aby uvedený listener nalezly podle jeho jména a zaregistrovaly nový objekt.

Prvotní myšlenku, že pro každý nový dětský kontext (resp. libovolný objekt, který si přeje být notifikován) vytvořím novou instanci listeneru a registruji ji za běhu do aplikačního kontextu, jsem nakonec opustil.

Co bude v dalším díle?

V přechozím díle jsme si ukázali, jak jednotlivé moduly separovat a propojit ve stromu. V tom dnešním, jak strom udržet konzistentní a refreshovatelný za běhu aplikace. Další díl bude o tom, jak jednotlivé moduly mezi sebou propojit - respektive, jak zajistit interakci mezi jednotlivými moduly.

Díl #3: Vystavení “interface” bean modulů