Nell‘articolo precedente si è visto come Spring implementa i concetti del pattern Inversion of Control. In questo articolo invece viene trattato il supporto di Spring per l‘Aspect Oriented Programming, un altro pilastro fondamentale nell‘architettura del framewok. L‘Inversion of Control e l‘Aspect Oriented Programming rappresentano i concetti che stanno alla base del tentativo di Spring di offrire
Introduzione
Esistono caratteristiche di un sistema che sono logicamente indipendenti dal sistema stesso ma lo “invadono” in maniera capillare. Un esempio abusato ma estremamente chiarificatore è rappresentato dal “logging” applicativo. L‘AOP è un paradigma di programmazione che mira a disaccoppiare queste caratteristiche. In poche parole il concetto alla base di AOP è che il codice applicativo originario non deve essere nemmeno consapevole della loro esistenza. Vengono descritti nei prossimi paragrafi i concetti chiave dell‘AOP.
Aspect
Un Aspect rappresenta in senso generale la modularizzazione logica delle caratteristiche indipendenti di un sistema, che normalmente invadono in modo “trasversale” l‘intera applicazione. Il logging e la gestione dei confini transazionali del codice applicativo ne sono un esempio abbastanza chiaro.
Advice
Gli Advices rappresentano l‘implementazione effettiva di un Aspect, e normalmente i vari frameworks li implementano con degli “interceptors” o catene di “interceptors” applicati a punti particolari del codice. Spring implementa questi concetti e fornisce un suo supporto specifico per l‘AOP. Un advice in Spring è implementato come una semplice classe java.
Join Point
Il Join Point è un punto preciso durante la vita del programma, come per esempio l‘esecuzione di un metodo o la gestione di un‘eccezione.
Pointcut
Il Pointcut è un “predicato” che individua un insieme di Join Points, ossia in concreto una regola definita per esempio da una “regular expression”. Il concetto di pointcut è basilare perché le sue implementazioni forniscono i meccanismi di applicazione degli advices stessi al codice applicativo.
Target
D‘ora in poi chiameremo targets le classi alle quali vogliamo applicare dei particolari advices.
Creare e configurare gli advices
Abbiamo visto che un advice rappresenta un‘implementazione di un aspect. Il risultato dell‘applicazione di un‘advice (advising) su una classe in Spring è la generazione a runtime di un proxy che fornisce il comportamento implementato dall‘advice e nello stesso tempo delega le chiamate relative all‘interfaccia della classe target alla classe target stessa. In Spring, contrariamente ad altre tecnologie AOP come AspectJ, gli advices sono applicati sempre a runtime attraverso le informazioni di configurazione. In Spring un advice è sempre implementato da una classe java. Se si utilizza un ApplicationContext gli oggetti proxyies sono creati quando tutti bean configurati sono caricati in memoria. Se gli oggetti target implementano una o più interfacce Spring utilizza la classe java.lang.reflect.Proxy del JDK per generare dinamicamente una nuova classe che implementa tali interfacce, applica gli advices previsti, e delega le chiamate dei metodi delle interfacce del target al target stesso. Se la classe target non implementa nessuna interfaccia Spring utilizza la libreria CGLIB per generare una sottoclasse, applicando gli advices e delegando al classe target le chiamate verso la sottoclasse. Questo secondo approccio è da evitare se non assolutamente necessario, in quanto non favorisce il disaccoppiamento applicativo; sarà normalmente necessario solo se si intende aggiungere del comportamento a oggetti di terze parti che non implementano delle interfacce. Occorre poi notare che nel secondo approccio non è possibile applicare degli advices a metodi definiti final, perché Spring nel creare le sottoclassi suddette sovrascrive i metodi della classe target e vi applica gli advices, e non è possibile sovrascrivere metodi final. In Spring i Joinpoints sono sempre definiti a livello dei metodi, perché un AOP più “profondo” a livello dei campi violerebbe l‘incapsulamento dei dati. In Spring sono previste le seguenti tipologie di advices in corrispondenza delle diverse fasi che caratterizzano la chiamata di un metodo:
- Around: org.aopalliance.intercept.MethodInterceptor intercetta le chiamate al metodo del target
- Before: org.springframework.aop.BeforeAdvice viene eseguito prima che venga chiamato il metodo del target.
- After: org.springframework.aop.AfterReturningAdvice viene eseguito dopo che il metodo esegue il “return”
- Throws: org.springframework.aop.ThrowsAdvice viene eseguito quando il target lancia un‘eccezione
Riprendiamo la classe ArticoloServiceImpl già utilizzata nell‘articolo del mese precedente. Tale classe implementa l‘interfaccia ArticoloService. Ecco di seguito l‘interfaccia e la relativa implementazione:
public interface ArticoloService {
public Articolo getArticolo(String titolo);
public void insertArticolo(Articolo articolo);
}
public class ArticoloServiceImpl {
private ArticoloDAO articoloDAO;
//Costruttore utilizzato dall‘Inversion of Control (o Dipendency //Injection)
public ArticoloServiceImpl(ArticoloDAO articoloDAO) {
this.articoloDAO = articoloDAO;
}
public void insertArticolo(Articolo articolo) {
articoloDAO.insertArticolo(articolo);
}
public Articolo getArticolo(String titolo) {
return articoloDAO.loadArticolo(titolo);
}
}
Immaginiamo di voler stampare nella console il campo “titolo” dell‘oggetto articolo prima che venga chiamato il metodo insertArticolo(Articolo articolo). A questo scopo utilizziamo un BeforeAdvice. E‘ necessario in pratica scrivere un‘implementazione dell‘interfaccia org.springframework.aop.BeforeAdvice:
public interface MethodBeforeAdvice {
void before(Method method, Object[] args, Object target) throws Throwable
}
public class InsertArticoloAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object target) {
Articolo articolo = (Articolo) args[0];
System.out.println("Titolo: " + articolo.getTitolo());
}
}
Allo stesso modo se vogliamo stampare su console gli autori dell‘articolo restituito dal metodo getArticolo(String titolo) possiamo scrivere un‘implementazione dell‘interfaccia org.springframework.aop.AfterReturningAdvice:
public interface AfterReturningAdvice {
void afterReturning(Object returnValue, Method method, Object[] args, Object target)
throws Throwable;
}
Il metodo afterReturnig prende in ingresso il valore di ritorno, che nel nostro caso è un oggetto Articolo al quale possiamo accedere nella nostra implementazione e utilizzarne i metodi per stampare a video quello che ci interessa (non riportiamo l‘implementazione perché non introduce nulla di nuovo rispetto all‘esempio precedente).
Se vogliamo invece intercettare la chiamata del metodo getArticolo(…) dobbiamo fornire un‘implementazione dell‘interfaccia org.aopalliance.intercept.MethodInterceptor (advice di tipo Around):
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
public class GetArticoloInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation)
throws Throwable {
String titolo = (String) invocation.getArguments()[0];
System.out.println("Titolo: " + titolo);
Articolo articolo = invocation.proceed();
return articolo;
}
}
Utilizzando questo approccio siamo in grado di aggiungere del comportamento prima e dopo la chiamata del metodo, che viene eseguita con l‘istruzione proceed(). Possiamo anche far restituire un oggetto diverso da Articolo, anche se questa possibilità è da valutare con cautela perché facilita l‘introduzione di comportamenti anomali e difficilmente individuabili.
Se abbiamo necessità infine di intercettare un‘eccezione dobbiamo fornire un‘implementazione dell‘interfaccia org.springframework.aop.ThrowsAdvice (advice di tipo Throws): Questa è un‘interfaccia marker, non definisce cioè nessun metodo ma serve solamente a caratterizzare logicamente la classe che l‘implementa. Tuttavia per gestire effettivamente le eccezioni l‘implementazione, per “contratto”, dovrà avere almeno un metodo con delle “firme” analoghe alle seguenti:
void afterThrowing(Throwable throwable)
void afterThrowing(Method method, Object[] args, Object target, Throwable throwable)
Il tipo dell‘eccezione gestita dall‘advice dipende dalla “firma” del metodo, ossia dal tipo di eccezione del parametro in ingresso al metodo. E‘ possibile scrivere diversi metodi con diverse specifiche eccezioni analogamente a quanto si fa in un blocco “catch”. Occorre notare che i metodi suddetti non alterano il percorso delle eccezioni, si limitano ad aggiungere del comportamento e quando il metodo ritorna l‘eccezione si propaga nello stack con le regole consuete . Il solo modo per cambiare questo stato di cose è che venga rilanciata una nuova eccezione.
Abbiamo visto come si implementano le diverse tipologie di advices, ma come si applicano effettivamente le implementazioni viste sopra alle classi target? La risposta come di consueto sta nella configurazione di Spring. Spring utilizza la classe ProxyFactoryBean per generare un proxy che fa da involucro al target e fornisce in aggiunta le caratteristiche implementate nell‘advice. Vediamo come si può applicare l‘implementazione InsertArticoloAdvice sopra riportata alla classe ArticoloServiceImpl:
class="com.mypackage.InsertArticoloAdvice "/> class="org.springframework.aop.framework.ProxyFactoryBean">
com.mypackage.ArticoloService
insertArticoloAdvice
In pratica vengono definiti prima i beans relativi alla classe target e all‘advice precedentemente implementato e successivamente viene configurato un proxy attraverso la classe ProxyFactoryBean. Notiamo che per distinguere il proxy dalla classe target si è stato utilizzato il suffisso “Target” per quest‘ultima (l‘importante è che gli id siano diversi). Alla configurazione del proxy viene fornita l‘interfaccia del target, il riferimento al target e il riferimento all‘advice. perché possano essere utilizzate sia le caratteristiche della classe target che quelle dell‘advice il codice client non dovrà utilizzare direttamente la classe target ma richiedere un riferimento alla classe proxy relativa attraverso l‘ApplicationContext.
Definire i Pointcuts
Finora abbiamo visto come creare un advice e come applicarlo a una singola classe target utilizzando la classe ProxyFactoryBean attraverso la configurazione di Spring. Per sfruttare tuttavia al massimo le caratteristiche dell‘AOP ci serve un meccanismo più avanzato di advising. Questo meccanismo è fornito dai pointcuts che ci offrono la possibilità di definire delle regole di “matching” applicate a un insieme di classi. Spring definisce il concetto di pointcut tramite l‘interfaccia:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
L‘interfaccia ClassFilter attraverso il metodo matches(…) serve a stabilire se una classe è interessata o no all‘operazione di advising:
public interface ClassFilter {
boolean matches(Class clazz);
}
L‘interfaccia MethodMatcher invece serve a operare la stessa decisione in base ai metodi dei proxy ottenuti dalle classi target:
public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
public boolean isRuntime();
public boolean matches(Method m, Class target, Object[] args);
}
Il metodo matches(Method m, Class targetClass) serve a stabilire se un metodo di un proxy è candidato all‘advising. Il metodo isRuntime() invece stabilisce se l‘advising è statico o dinamico. L‘advising statico è sempre eseguito mentre l‘advising dinamico è eseguito in base ai valori dei parametri di ingresso delle chiamate al metodo del proxy. Se isRuntime() ritorna true viene eseguito il metodo matches(Method m, Class target, Object[] args) per ogni invocazione del metodo del proxy, che permette di accedere ai valori passati in ingresso attraverso il parametro args, mentre i metodi matches(Method m, Class targetClass) e isRuntime(…) vengono chiamati una sola volta al momento della creazione del proxy stesso. Spring permette di unire in un‘unica entità un pointcut e l‘advice relativo attraverso il concetto di advisor . Un advisor è rappresentato in Spring dall‘interfaccia PointcutAdvisor:
public interface PointcutAdvisor {
Pointcut getPointcut();
Advice getAdvice();
}
Spring utilizza le implementazioni di queste interface in modo dichiarativo attraverso la configurazione del container IoC. In pratica occorre definire un advisor e fornirgli come parametri un advice e un pointcut che definisca le regole di matching verso le classi target. Spring mette a disposizione per i pointcuts statici la classe StaticMethodMatcherPointcut che è possibile derivare con le proprie personalizzazioni. Ma è possibile anche utilizzare direttamente una sua implementazione rappresentata dalla classe NameMatchMethodPointcut che offre un meccanismo di matching basato sulla corrispondenza di uno o più nomi di metodi. Se si vuole invece a disposizione la potenza delle espressioni regolari allora è possibile utilizzare la classe RegexpMethodPointcutAdvisor. Vediamo come si può utilizzare quest‘ultimo advisor nella configurazione di Spring:
class="com.mypackage.MyService"/>
class="org.springframework.aop.support. RegexpMethodPointcutAdvisor ">
.*insert.+ class="org.springframework.aop.framework.ProxyFactoryBean">
com.mypackage.MyService
myAdvisor
Qui viene definito l‘advisor myAdvisor con l‘advice myAdvice e il pointcut rappresentato dall‘espressione regolare “.*insert.+”. Tale regola viene applicata ai nomi “estesi” dei metodi, cioè il package che contiene il metodo più il nome del metodo stesso. Di seguito poi come di consueto viene definito l‘advising della classe myServiceTarget attraverso la classe ProxyFactoryBean, che genera un proxy di nome myService. L‘unica differenza è che qui non viene fornito direttamente l‘advice come nel precedente paragrafo, ma l‘advisor myAdvisor. A questo punto le chiamate al proxy saranno subordinate alle regole definite dal pointcut configurato con myAdvisor, e l‘advice sarà applicato solamente per i metodi il cui nome contiene la porzione “service”.
Per quanto riguarda i pointcuts dinamici infine può essere interessante sapere che Spring mette a disposizione la classe ControlFlowPointcut che permette di controllare lo stack di esecuzione di un metodo per decidere se applicare o no il comportamento definito dall‘advice relativo.
Utilizzare gli Introduction Advices
Abbiamo visto come sia possibile intercettare le chiamate ai metodi con gli advices di tipo around. Esiste anche un‘altra tipologia di interceptor che permette addirittura di aggiungere a una classe target dei nuovi metodi e attributi. Questi intercettori vengono chiamati introductions e offrono delle possibilità senza dubbio potenti. Spring implementa le introductions tramite la sottointerfaccia di MethodInterceptor IntroductionInterceptor che definisce il metodo aggiuntivo:
boolean implementsInterface (Class intf);
Tale metodo restituisce true se l‘interceptor ha la responsabilità di implementare l‘interfaccia passata in ingresso. L‘interceptor cioè utilizza un‘implementazione di tale metodo per capire se un metodo chiamato sull‘oggetto proxy appartiene o no all‘interfaccia che si vuole “introdurre”: se true viene invocata l‘implementazione di tale metodo fornita nella definizione dell‘interceptor stesso, in caso contrario viene chiamato proceed() sulla definizione del metodo in modo che prosegua la chiamata originaria. Vogliamo per esempio “introdurre” un implementazione dell‘interfaccia TitoliPresenti con il metodo stampaTitoli() alla nostra classe ArticoloServiceImpl:
public interface TitoliPresenti {
void stampaTitoli();
}
L‘implementazione del metodo stampaTitoli() dovrebbe accedere agli articoli presenti in archivio e stamparli a video (i dettagli dell‘implementazione non sono importanti e verranno qui tralasciati). Per creare un‘introduction che permetta di arricchire la classe ArticoloServiceImpl dell‘implementazione dell‘interfaccia TitoliPresenti occorre definire una classe che implementi la IntroductionInterceptor e la TitoliPresenti stessa:
public class TitoliPresentiIntroduction implements IntroductionInterceptor, TitoliPresenti {
public boolean implementsInterface(Class intf) {
return intf.isAssignableFrom(TitoliPresenti.class);
}
public Object invoke(MethodInvocation m) throws Throwable {
if (implementsInterface(m.getMethod().getDeclaringClass())) {
return m.getMethod().invoke(this, m.getArguments());
} else {
return m.proceed();
}
}
public void stampaTitoli() {
...
}
}
Come si vede l‘implementazione del metodo stampaTitoli viene chiamata dal metodo invoke solo se implementsInterface ritorna true. Come per gli altri advices è Spring, attraverso la configurazione del container a preoccuparsi di applicare l‘introduction sopra definita all‘oggetto target, in questo caso la classe ArticoloServiceImpl, attraverso la creazione di un proxy. E‘ necessario a questo scopo utilizzare la ProxyFactoryBean in modo analogo a quanto mostrato precedentemente per le altre tipologie di advice.
Esiste una classe DelegatingIntroductionInterceptor che nasconde i dettagli implementativi della IntroductionInterceptor e permette una scrittura più sintetica:
public class TitoliPresentiIntroduction
extends DelegatingIntroductionInterceptor
implements TitoliPresenti {
public void stampaTitoli() {
...
}
}
Come si vede dall‘esempio sopra riportato è necessario in questo caso fornire solamente l‘implementazione dell‘interfaccia TitoliPresenti e nient‘altro.
Spring definisce un advisor specifico per le introduction chiamato IntroductionAdvisor e un‘implementazione specifica DefaultIntroductionAdvisor. E‘ sufficiente definire l‘advisor come bean nella configurazione e passargli in ingresso come argomento del costruttore l‘introduction relativa, nel nostro caso titoliPresentiIntroduction:
.../> >
L‘Auto-Proxying
Per facilitare l‘advising delle classi target in progetti di una certa consistenza Spring offre degli automatismi specifici gestiti dal container. La classe DefaultAdvisorAutoProxyCreator offre il meccanismo più potente in questo senso. E‘ sufficiente definirla come un normale bean nella configurazione di Spring:
...class="org.springframework.aop.framework.
autoproxy.DefaultAdvisorAutoProxyCreator"/>
...
In questo modo, una volta caricati tutti i beans, Spring scorrerà gli advisors configurati e applicherà gli advices ai beans che soddisfino i relativi pointcuts.
Conclusioni
Abbiamo visto in quest‘articolo come Spring implementi i concetti di base dell‘AOP. Tale implementazione fornisce supporto per tutte le caratteristiche più avanzate del framework. Nel prossimo capitolo dedicato all‘accesso ai dati verrà trattato a tale proposito un esempio importante relativo all‘implementazione della transazionalità dichiarativa. Ricordiamo infine che i prossimi capitoli di questa serie tratteranno nell‘ordine: l‘accesso ai dati, l‘MVC orientato alle generiche applicazioni web, l‘MVC orientato alle portlets.
Riferimenti
[1] “Spring Reference” 2.0,
http://www.springframework.org/
[2] Craig Walls – Ryan Breidenbach, “Spring in Action”, Manning, 2005
[3] Rob Harrop – Jan Machacek, “Pro Spring”, Apress, 2005