Aspect Oriented Programming
La programmazione orientata agli aspetti è un paradigma di programmazione che ha come obiettivo la modularizzazione del software attraverso la creazione di entità software, denominate aspetti, che intervengono nell’interazione tra oggetti al fine di eseguire compiti comuni.
Vedremo che con questo paradigma sarà possibile aggiungere comportamento ad un codice esistente senza andare a modificare il codice stesso. E lo faremo specificando separatamente quale codice viene modificato attraverso la definizione di un pointcut. Questo permette ai comportamenti che non rientrano nella logica di business (come il logging) di essere aggiunti ad un programma senza incasinare il codice.
Formalmente un Aspect è una classe java che espone metodi chiamati advice. Un pointcut invece è una espressione che indica quale metodo della logica di business è interessato dallo specifico aspetto. Tuttavia un punto da notare è che esso non indica mai quale metodo dell’aspect (advice) deve essere applicato. Ma semplicemente, attraverso l’interpretazione dell’espressione, indica i metodi della logica di business in cui applicare un advice. In breve, è una sorta di selettore di metodi della logica di business nel suo complesso.
La tabella seguente mostra le tipologie di advice messe a disposizione dal framework Spring AOP.
Before Advice | Viene eseguito prima dell’esecuzione del metodo di business e una volta completata l’esecuzione dell’advice, l’elaborazione del metodo avverrà automaticamente. |
After Advice | Viene eseguito dopo l’esecuzione del metodo di business e una volta che l’esecuzione del metodo è terminata, l’esecuzione dell’advice avrà luogo automaticamente. |
Around Advice | Viene eseguito in due momenti: prima dell’esecuzione del metodo di business e dopo. |
After Returning Advice | Viene eseguito dopo l’esecuzione del metodo di business ma solamente quando questo ritorna con successo. |
After Throwing Advice | Viene eseguito dopo l’esecuzione del metodo di business ma solo se questo fallisce. |
Implementazione in Spring Boot
Generalmente esistono due modi per implementare AOP in un’applicazione Spring. Il primo, più classico, consiste nell’utilizzare un file di configurazione XML in cui vengono dichiarati i vari concetti dell’AOP. Il secondo, più comune, fa uso delle annotazioni della libreria AspectJ. Si tratta di una libreria di programmazione orientata agli aspetti (AOP) specifica per il linguaggio di programmazione Java, ampiamente utilizzate grazie alla sua semplicità e l’usabilità.
Vediamo quindi come utilizzare tale libreria in un progetto Spring Boot. Procediamo creano un progetto base attraverso il tool Spring Initializr, senza aggiungere alcuna dipendenza. Quindi aggiungiamo al pom.xml generato, la dipendenza a spring-boot-starter-aop
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.1</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>it.javaboss</groupId> <artifactId>invoice-aop</artifactId> <version>0.0.1-SNAPSHOT</version> <name>invoice-aop</name> <description>Demo project for Spring Boot to show AOP</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
Per utilizzare l’AOP è necessario annotare la classe di configurazione del progetto con @EnableAspectJAutoProxy
, la quale abiliterà Spring a riconoscere e gestire l’annotaziona @Aspect
, che vedremo nel prossimo paragrafo.
1 2 3 4 5 6 7 |
@SpringBootApplication @EnableAspectJAutoProxy public class InvoiceApplication { public static void main(String[] args) { SpringApplication.run(InvoiceApplication.class, args); } } |
Aspect, Pointcut e Advice
L’annotazione @Aspect
, inserita nella dichiarazione di una classe, serve ad identificarla come classe aspect. Al suo interno sono poi definiti i pointcut e gli advice attraverso l’annotazione dei suoi metodi rispettivamente con @Pointcut
o una qualsiasi delle annotazioni advice (@Before
, @After
, @AfterReturning
, @AfterThrowing
e @Around
).
Prima però di vedere tali annotazioni all’opera consideriamo un caso abbastanza comune di classe di business InvoiceService
che implementa un metodo save()
, in cui si accede ad una base dati per inserire un nuovo record.
1 2 3 4 5 6 7 |
@Component public class InvoiceService { public void save() { System.out.println("Executing save()"); ... } } |
Trattandosi di un accesso al DB in scrittura, si ha la necessità di operare in un contesto transazionale, che può essere gestito interamente all’interno del metodo save()
, oppure, attraverso AOP, creando e distruggendo tale contesto ogni volta che il metodo è invocato. Per farlo introduciamo la classe InvoiceAspect
ed il pointcut relativo al metodo che vogliamo intercettare, nel modo seguente:
1 2 3 4 5 6 |
@Aspect @Component public class InvoiceAspect { @Pointcut("execution(public void it.javaboss.invoiceaop.service.InvoiceService.save())") public void pountcut1() {} } |
Come ci aspettavamo la classe è un componente Spring annotata con @Aspect
. Il metodo pointcut1()
è invece annotato con @Pointcut
, annotazione che richiede l’utilizzo di una pointcut expression. Non aprofondiamo in questo articolo la sintassi delle espressioni pointcut, ci basti sapere per ora che execution(...)
identifica esattamente l’esecuzione del metodo specificato. Nel nostro esempio, quindi, il pointcut identifica esattamente l’esecuzione del metodo save()
. Esiste però la possibilità di utilizzare delle wildcard, in modo da rendere il pointcut meno specifico. Ad esempio, volendo intercettare l’esecuzione di tutti i metodi della classe InvoiceService
, si può utilizzare l’espressione: "execution(* it.javaboss.invoiceaop.service.InvoiceService.*(..))")
.
Gestione delle Transazioni
Per gestire il contesto transazionale dobbiamo implementare tre metodi nella classe InvoiceAspect
che dovranno rispettivamente: aprire una transazione, eseguire il commit nel caso in cui l’esecuzione del metodo save()
vada a buon fine ed infine chiudere la transazione. La classe quindi si presenterà nel seguente modo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Aspect @Component public class InvoiceAspect { @Pointcut("execution(public void it.javaboss.invoiceaop.service.InvoiceService.save())") public void pountcut1() {} @Before("pountcut1()") public void beginTransaction() { System.out.println("Transaction begins !"); } @After("pountcut1()") public void completeTransaction() { System.out.println("Transaction completes !"); } @AfterReturning("pountcut1()") public void commitTransaction() { System.out.println("Transaction committed !"); } } |
Si noti che le tre annotazioni advice @Before
, @After
e @AfterReturning
richiedono anch’esse la definizione di una poitcut expression, che nel caso specifico identifica esattamente l’espressione definita in pointcut1()
. Nulla ci vieta però di utilizzare direttamente l’espressione definita sul metodo pointcut1()
nel’annotazione advice. Quindi ad esempio potremmo avere:
1 2 3 4 |
@Before("execution(public void it.javaboss.invoiceaop.service.InvoiceService.save())") public void beginTransaction() { System.out.println("Transaction begins !"); } |
Per eseguire il test di quanto implementato utilizziamo un componente che implementa l’interfaccia Spring CommandLineRunner, e che verrà quindi eseguito immediatamente all’avvia dell’applicazione:
1 2 3 4 5 6 7 8 9 10 |
@Component public class InvoiceRunner implements CommandLineRunner { @Autowired private InvoiceService service; @Override public void run(String... args) throws Exception { service.save(); } } |
Una volta eseguita l’applicazione troveremo nell standard output i messaggi mostrati di seguito, in cui è evidente che l’esecuzione del metodo di business è preceduta dall’esecuzione del metodo che si occupa di aprire la transazione e seguita da quelli che, rispettivamente, eseguono il commit e chiudono la transazione.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.1) 2021-06-17 13:59:14.288 INFO 24657 --- [ main] i.j.invoiceaop.InvoiceApplication : Starting InvoiceApplication using Java 12.0.1 on MrMacBookPro.lan with PID 24657 (/Users/massimo/Workspace/javaboss9/invoce-aop/target/classes started by massimo in /Users/massimo/Workspace/javaboss9/invoce-aop) 2021-06-17 13:59:14.290 INFO 24657 --- [ main] i.j.invoiceaop.InvoiceApplication : No active profile set, falling back to default profiles: default 2021-06-17 13:59:14.878 INFO 24657 --- [ main] i.j.invoiceaop.InvoiceApplication : Started InvoiceApplication in 0.915 seconds (JVM running for 1.237) Transaction begins ! Executing save() Transaction committed ! Transaction completes ! |
Gestione delle Eccezioni
Naturalmente immaginare che un metodo di business non vada mai in eccezione non è pensabile, e la gestione dei casi eccezionali è molto importante, soprattutto se abbiamo a che fare con accessi al DB. Dobbiamo infatti prevedere la possibilità di eseguire il rollback della transazione nel caso in cui il metodo non termini correttamente. Per farlo ci viene in aiuto l’annotazione @AfterThrowing
, che può essere utilizzata per identificare l’operazione da eseguire in caso di eccezione. Implementiamo quindi il metodo rollbackTransaction()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@Aspect @Component public class InvoiceAspect { @Pointcut("execution(public void it.javaboss.invoiceaop.service.InvoiceService.save())") public void pountcut1() {} @Before("pountcut1()") public void beginTransaction() { System.out.println("Transaction begins !"); } @After("pountcut1()") public void completeTransaction() { System.out.println("Transaction completes !"); } @AfterReturning("pountcut1()") public void commitTransaction() { System.out.println("Transaction committed !"); } @AfterThrowing("pountcut1()") public void rollbackTransaction() { System.out.println("Transaction rolled back !"); } } |
Si noti che il metodo rollbackTransaction()
sarà eseguito esclusivamente nel caso in cui sia sollevata una eccezione durante l’esecuzione del metodo save(), e che inoltre il metodo commitTransaction()
non sarà eseguito in tale eventualità. Quello che ci aspettiamo di vedere sulla console in caso di eccezione saranno i seguenti messaggi:
1 2 3 4 |
Transaction begins ! Executing save() Transaction rolled back ! Transaction completes ! |
@AfterReturning e @AfterThrowing
Le annotazioni @AfterReturning
e @AfterThrowing
hanno la particolarità di poter intercettare rispettivamente l’oggetto restituito dal metodo o l’eccezione sollevata. Ad esempio consideriamo il seguente metodo advice:
1 2 3 4 |
@AfterReturning(value="execution(public * it.javaboss.invoiceaop.service.InvoiceService.find*(..))", returning = "obj") public void getAdviceReturnValue(Object obj) { System.out.println("Returning Value is : " + obj); } |
Come è facile comprendere l’annotazione ha una poitcut expression che intercetta l’esecuzione di tutti i metodi find
della classe di business. Ma la peculiarità è che, attraverso il parametro returning
è possibile indicare quale, tra i parametri del metodo getAdviceReturnValue
, dovrà contenere il risultato dell’elaborazione del metodo find()
. Quindi, ad esempio, se si immagina di invocare findById( 10L )
, sulla console troveremo il messaggio:
Returning Value is : Invoice Id 10
In modo del tutto analogo possiamo procedere con l’annotazione @AfterThrowing
con l’eccezione che questa volta andremo ad indicare il parametro destinato a contenere l’oggetto Throwable
rappresentativo dell’eccezione sollevata.
1 2 3 4 |
@AfterThrowing(value="pountcut1()", throwing = "th") public void rollbackTransaction(Throwable th) { System.out.println("Transaction rolled back ! Message from method : "+th); } |
ProceedingJoinPoint
L’oggetto ProceedingJoinPoint
può essere utilizzato come argomento di un metodo advice per accedere ad informazioni relativi al junction point ed al metodo intercettato. La classe dispone di diversi metodi (ereditati dalla classe JoinPoint
) tra i quali:
- getTarget() – restituisce l’oggetto/componente intercettato;
- getSignature() – restituisce la firma del metodo invocato sull’oggetto target;
- getArgs() – restituisce gli argomenti del metodo invocato.
Supponiamo ad esempio di avere nella classe InvoiceService
un metodo update()
così definito:
1 2 3 |
public void update(Long id) { System.out.println("Update method is getting Executed!"); } |
Aggiungiamo quindi alla classe InvoiceAspect
il seguente pointcut e metodo advice annotato con @Around
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Pointcut("execution(public void it.javaboss.invoiceaop.service.InvoiceService.update(..))") public void pountcut2() {} @Around("pountcut2()") public void updateAdvice(ProceedingJoinPoint pj) throws Throwable { System.out.println("Executing Before part of business method"); System.out.println(pj.getTarget()); System.out.println(pj.getSignature().getName()); Object[] intA = pj.getArgs(); for (int i=0;i<intA.length;i++){ System.out.println(intA[i]); } pj.proceed(); System.out.println("Executing After part of business method"); } |
Si noti che il metodo proceed()
svolge un ruolo importante in questa tipologia di advice; esso infatti consente di procedere nell’esecuzione del metodo intercettato. Eseguendo il codice verranno stampati nella console i seguenti messaggi:
1 2 3 4 5 6 |
Executing Before part of business method it.javaboss.invoiceaop.service.InvoiceService@130a0f66 update 20 Update method is getting Executed! Executing After part of business method |
Il Codice Sorgente
Il codice sorgente del progetto è scaricabile qui invoce-aop.