Část #4: Modulární systémy ve Spring Framework

Aplikační události jsou jedním ze základních stavebních kamenů Springu a proto by bylo škoda se ochudit o tuto skvělou vlastnost na rozhraní modulů. Je zřejmé, že nebudeme chtít otevřít všechny aplikační události svému okolí, nicméně u řady událostí bychom chtěli umožnit ostatním modulům reagovat. Jako příklad uvedu interakci mezi modulem pro správu uživatelů a notifikačním modulem - notifikační modul se stará o rozesílání emailových notifikací v reakci na konkrétní aplikační události (samozřejmě obecně - konfigurovatelně). To je typická ukázka stavu, kdy chceme, aby uživatelský modul dokázal emitovat třebas událost “založení nového uživatele” tak, aby notifikační modul mohl reagovat odesláním emailu.

Přeposílání eventů z jednoho modulu do jiného modulu

Další lahůdkou je možnost posílání událostí z oddělených aplikačních kontextů. Standardně se vám multicaster postará o vyvolání listenerů na úrovni aplikačního kontextu, ze kterého pochází beana, která událost vyvolala (respektive ApplicationEventPublisher, který použijete pro distribuci události). Dále je událost propagována do nadřazených kontextů. Nedostane se vám tedy do aplikačních kontextů, které nejsou ve stromě kontextů nad tím vaším.

V praxi se ovšem může hodit, mít možnost na události "modulu" reagovat v jiném modulu. Některé události v podstatě mohou tvořit část rozhraní daného modulu (jistě většina událostí bude pouze pro vnitřní účely, některé by se ale hodilo propagovat vně a zařadit je jako součást rozhraní).

Jak tedy zajistit propagaci těchto událostí i do modulů na stejné úrovni? Opět k tomu můžeme využít BeanPostProcessor. Zavedeme nové rozhraní s názvem ExternalEventListener, které budou implementovat ty listenery, které budou chtít naslouchat událostem okolních modulů. Rozhraní ExternalEventListener má metodu getExternalEventClasses, která vrací pole objektů Class těch externích událostí, kterým chtějí naslouchat.


/**
 * Module listeners can implement this interface whenever they need to listen to events fired in other modules.
 * By default events fired in module are listenable only in module itself and in parent (root) context.
 */
public interface ExternalEventListener {
	/**
	 * Method returns class specifications of event types, that should be forwarded to this module
	 * by root context.
	 *
	 * BEWARE: do not listen to events that are fired by your module, this would cause this event
	 * to be forwarded too, and you'll receive it two times!!!
	 *
	 * @return
	 */
	Class[] getExternalEventsTypes();
}

Dále zavedeme marker rozhraní PublicEvent, které budou implementovat aplikační události, které modul považuje za veřejné (tzn. zařazuje je do svého rozhraní).


/**
* Marker interface that could be implemented by ApplicationEvent classes to declare, that this i module public event, that could
* be catched and processed by listeners of other modules.
*/
public interface PublicEvent {
}

V kořenovém kontextu zaregistrujeme aplikační listener s názvem ExternalEventPropagateListener. Listenerem v root contextu proběhnou všechny události (tedy i události jednotlivých modulů). Tento listener udržuje mapu aplikačních kontextů (respektive libovolných jiných objektů), které si přejí "přeposlat" externí události. Listener si eviduje vždy seznam Class aplikačních událostí, o které má daný objekt zájem a v případě, že takovou událost zachytí, provede přeposlání události danému registrovanému objektu. Před vlastní registrací je prověřeno, zda modul skutečně požaduje přeposílání pouze veřejných událostí - pokud nikoliv, je vyhozena vyjímka IllegalArgumentException.


/**
 * Forwarding listener / publisher that is registered to listen to events in one application context,
 * and if they conform to predefined types publishes them into second application context where they
 * otherwise would not be visible.
 */
public class ExternalEventPropagateListener implements ApplicationListener, DisposableBean {
	private static Log log = LogFactory.getLog(ExternalEventPropagateListener.class);
	public static final String FORWARDING_LISTENER_BEAN_NAME = "externalEventPropagateListener";
	private final List forwards = new ArrayList();
	private final Set currentlyProcesedEvents = new HashSet();
	public void destroy() throws Exception {
		forwards.clear();
	}
	/**
	 * Handle an application event.
	 *
	 * @param event the event to respond to
	 */
	public void onApplicationEvent(ApplicationEvent event) {
		//avoid inifinite loops
		synchronized(currentlyProcesedEvents) {
			if(!currentlyProcesedEvents.contains(event)) {
				currentlyProcesedEvents.add(event);
				try {
					for(int i = 0; i < forwards.size(); i++) {
						ForwardMappingHolder holder = (ForwardMappingHolder)forwards.get(i);
						for(int j = 0; j < holder.getExternalEventClasses().length; j++) {
							Class externalEventClass = holder.getExternalEventClasses()[j];
							if(externalEventClass.isAssignableFrom(event.getClass())) {
								if(log.isDebugEnabled()) {
									log.debug("Forwarding event " + event.toString() + " to child context " + holder.getContextToForwardedTo().toString());
								}
								//observing event class matches - foward event
								holder.getContextToForwardedTo().publishEvent(event);
								//continue checking next holder
								break;
							}
						}
					}
				}
				finally {
					currentlyProcesedEvents.remove(event);
				}
			}
		}
	}
	public synchronized void addEventForwarding(AbstractRefreshableApplicationContext childContext, Class[] wantedEventClasses) {
		for(int i = 0; i < wantedEventClasses.length; i++) {
			Class wantedEventClass = wantedEventClasses[i];
			if (!PublicApplicationEvent.class.isAssignableFrom(wantedEventClass)) {
				String msg = "Cannot register listener to a " + wantedEventClass.getName() + ". This class does not " +
						"implement PublicApplicationEvent interface and thus cannot be externaly observed.";
				if (log.isFatalEnabled()) {
					log.fatal(msg);
				}
				throw new IllegalArgumentException(msg);
			}
		}
		forwards.add(
				new ForwardMappingHolder(
						childContext,
						wantedEventClasses
				)
		);
	}
	public synchronized void removeEventForwarding(AbstractRefreshableApplicationContext childContext) {
		Iterator it = forwards.iterator();
		while(it.hasNext()) {
			ForwardMappingHolder holder = (ForwardMappingHolder)it.next();
			if(holder.getContextToForwardedTo() == childContext) {
				it.remove();
				break;
			}
		}
	}
	/**
	 * Hooks child context to parent reload event listenning.
	 *
	 * @param rootContext
	 * @param childContext
	 */
	public static void registerChildForForwardingEvents(
			AbstractRefreshableApplicationContext rootContext,
			AbstractRefreshableApplicationContext childContext,
			Class[] wantedEventClasses) {
		if(rootContext.isActive()) {
			ForwardingListener forwardingListener = (ForwardingListener)rootContext.getBean(FORWARDING_LISTENER_BEAN_NAME);
			forwardingListener.addEventForwarding(childContext, wantedEventClasses);
		}
		else {
			if(log.isErrorEnabled()) {
				log.error("Cannot register " + childContext.toString() + " for forwarning events: root context is closed!");
			}
		}
	}
	/**
	 * Unhooks child context to parent reload event listenning.
	 *
	 * @param rootContext
	 * @param childContext
	 */
	public static void unregisterChildForForwardingEvents(AbstractRefreshableApplicationContext rootContext, AbstractRefreshableApplicationContext childContext) {
		if(rootContext.isActive()) {
			ForwardingListener forwardingListener = (ForwardingListener)rootContext.getBean(FORWARDING_LISTENER_BEAN_NAME);
			forwardingListener.removeEventForwarding(childContext);
		}
		else {
			if(log.isInfoEnabled()) {
				log.info("Cannot unregister " + childContext.toString() + " from forwarning events: root context already closed!");
			}
		}
	}
	/**
	 * Holds information about one forwarding mapping.
	 */
	private class ForwardMappingHolder {
		private AbstractRefreshableApplicationContext contextToForwardedTo;
		private Class[] externalEventClasses;
		public ForwardMappingHolder(AbstractRefreshableApplicationContext contextToForwardedTo, Class[] externalEventClasses) {
			this.contextToForwardedTo = contextToForwardedTo;
			this.externalEventClasses = externalEventClasses;
		}
		public AbstractRefreshableApplicationContext getContextToForwardedTo() {
			return contextToForwardedTo;
		}
		public Class[] getExternalEventClasses() {
			return externalEventClasses;
		}
	}
}

Výše zmíněný BeanPostProcesor s názvem ExternalEventPropagatePostProcessor nám při startu modulu prověří každou beanu, zda implementuje rozhraní ExternalEventListener a pokud ano, zaregistruje do kořenového kontextu aktuálně procesovaný aplikační kontext se seznamem Class aplikačních eventů, které právě procesovaný listener požaduje.


/**
 * This postprocessor registers for forwarding all events, that listeners in current context
 * wants (via contract of ExternalEventListener interface).
 */
public class ExternalEventListenerPostProcessor implements BeanPostProcessor {
	private static Log log = LogFactory.getLog(ExternalEventListenerPostProcessor.class);
	private AbstractRefreshableApplicationContext rootContext;
	private AbstractRefreshableApplicationContext localContext;
	public ExternalEventListenerPostProcessor(AbstractRefreshableApplicationContext rootContext, AbstractRefreshableApplicationContext localContext) {
		this.rootContext = rootContext;
		this.localContext = localContext;
	}
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if(bean instanceof ExternalEventListener) {
			ExternalEventListener listener = (ExternalEventListener)bean;
			Class[] classes = listener.getExternalEventsTypes();
			if (classes != null && classes.length > 0) {
				if(log.isInfoEnabled()) {
					log.info("Registering module for obtaining following external events (" + beanName + "): " + convertClassesToString(classes));
				}
				ForwardingListener.registerChildForForwardingEvents(rootContext, localContext, classes);
			}
		}
		return bean;
	}
	/**
	 * Convert class list to simple class name string.
	 * @param classes
	 * @return
	 */
	private String convertClassesToString(Class[] classes) {
		StringBuffer result = new StringBuffer();
		if(classes != null) {
			for(int i = 0; i < classes.length; i++) {
				Class aClass = classes[i];
				String className = aClass.getName();
				result.append(className.substring(className.lastIndexOf(".") + 1));
				if (i < classes.length) result.append(",");
			}
		}
		return result.toString();
	}
}

Celý výše uvedený proces jsem ještě zakreslil jako sequence diagram, jelikož při takovém množství kódu by mohla být celková představa o fungování celého procesu poněkud zmatená. Pokud tomu tak je, snad se mi podaří nejasnosti rozptýlit následujícím diagramem:

Sekvenční diagram znázorňující princip přeposílání událostí mezi moduly

Sekvenční diagram znázorňující princip přeposílání událostí mezi moduly

Závěr seriálu

Dnešní díl uzavírá seriál o modulárních systémech ve Springu. Popsané řešení dostatečně naplňuje naše požadavky na modulární serverový systém. Je možné že v budoucnosti třebas sáhneme po OSGI, ale v současné době toto jednoduché řešení postačuje.

Sami vidíte, že popsaná implementace, nejde za hranice Springu a že se jedná jen o nenáročné řešení, která vám nezabere víc jak jeden, dva dny implementace a otestování ve vašich podmínkách.

Hlavní přínos vidím v tom, že je jednoduše možné skládat nezávislé knihovny = moduly s přesným vydefinováním jejich rozhraní (nikoliv na úrovni class API, ale na úrovni živých funkčních objektů daného modulu). Naopak můžeme vydefinovat podmnožinu (typicky servisních bean), které budou pro všechny moduly společné, čímž je možné ve výsledku efektivně zjednodušit správu výsledné kompozice.

S čím si zatím nejsem 100% jistý, zda bude možné bez problémů vytvářet transakce zahrnující operace na beanami z různých modulů (tj. aplikačních kontextů) pomocí AOP. Teoreticky by to mělo fungovat bez větších potíží, ale jelikož nejsem schopný věc zpatra domyslet a nenašel jsem zatím čas si na to napsat test (ještě jsem to nepotřeboval), nechci to tu prezentovat jako fakt.

...

Doufám, že vám seriál líbil a že jej považujete za přínosný. Budu vám vděčný, když mi na závěr napíšete nějaký komentář k němu, i kdyby ten komentář měl být pouze ve smyslu, že čas strávený jeho čtením pro vás nebyl čas ztracený. Předem díky za reakce ... statistiky jsou jedna věc a přímá zpětná vazba je věc jiná :-) .