Eventi di dominio in Spring framework

Eventi di dominio in Spring framework

Qualche giorno fa un collega mi ha chiesto consigli su come disaccoppiare il codice che gestisce la logica di business dal codice che produce eventuali “reazioni” del sistema all’operazione eseguita. La sua necessità sorgeva dal fatto che l’applicazione su cui sta lavorando, realizzata con Spring framework, sarebbe stata utilizzata da diversi clienti, ciascuno dei quali avrebbe potenzialmente richiesto, per la medesima operazione principale, l’esecuzione di differenti operazioni secondarie. Una soluzione a questo problema è l’introduzione nel sistema degli eventi di dominio.

Eventi di dominio

Il concetto di evento di dominio appartiene al Domain-driven design (DDD), ma a ben vedere non viene esplicitamente citato nel testo di Eric Evans del 2003. L’autore del DDD infatti introdusse gli eventi di dominio solo in lavori successivi e forse è per questo che la loro adozione è meno diffusa rispetto ai concetti di Entity e Value Object.

In estrema sintesi, un evento di dominio rappresenta un accadimento che ha modificato lo stato del sistema, del modello, avvenuto in un preciso istante di tempo e scatenato da una precisa causa, ad esempio da una richiesta dell’utente. Ad un evento sono associate anche altre caratteristiche, come la tipologia dell’evento (“prodotto acquistato”, “utente iscritto”, ecc.) ed ulteriori informazioni specifiche per ciascuna tipologia (identificativo del prodotto acquistato, identificativo dell’utente registrato, ecc.).

Gli eventi di dominio possono essere utilizzati per molteplici scopi. Ad esempio, memorizzandoli su file o database si realizza un log dettagliato delle operazioni effettuate sul sistema. Oppure possono essere impiegati per disaccoppiare cause ed effetti all’interno di uno stesso dominio, o ancora per notificare le modifiche subite da un dominio a domini esterni.

Eventi di Spring framework

Spring framework, fin dalle sue prime versioni, possiede un sistema di gestione degli eventi. Inizialmente era destinato principalmente alla gestione degli eventi specifici del framework, come il “refresh del context”, ma di recente (versioni 4.2+) tale sistema è stato reso più flessibile per semplificare la gestione di eventi definiti dallo sviluppatore, come ad esempio gli eventi di dominio.

Per modellare un evento, come ad esempio la creazione di un nuovo post di un blog, basta creare una generica classe, con gli attributi che riteniamo sufficienti a descrivere i dettagli dell’evento. E’ consigliabile assegnare alla classe un nome che sia identificativo della tipologia di evento. Inoltre è opportuno renderla immutabile: una volta istanziato un evento non deve poter essere modificato, non c’è motivo per farlo.

    public final class PostCreatedEvent {

	private final int postId;
	
	private final int userId;
	
	private final boolean isPublished;
	
	private final LocalDateTime creationDateTime;
	
	public PostCreatedEvent(int postId, int userId, boolean isPublished) {
		this.postId = postId;
		this.userId = userId;
		this.isPublished = isPublished;
		this.creationDateTime = LocalDateTime.now();
		
	}

	public int getPostId() {
		return postId;
	}

	public int getUserId() {
		return userId;
	}

	public boolean isPublished() {
		return isPublished;
	}

	public LocalDateTime getCreationDateTime() {
		return creationDateTime;
	}	
}

Pubblicare un evento

Per trasmettere un evento ai potenziali fruitori è sufficiente utilizzare il metodo publishEvent della classe ApplicationEventPublisher di Spring.

public class PostService {

	@Autowired
	ApplicationEventPublisher publisher;

	public void create() {
		...
		publisher.publishEvent(new PostCreatedEvent(123, 456, true));
	}
}

Ricevere un evento

Per processare un evento basta sviluppare un metodo che lo preveda come parametro di input, in una qualunque classe, e annotare il metodo con @EventListener:

@Component
public class PostEventsListener {

	private static final Logger logger = LoggerFactory.getLogger(PostEventsListener.class);

	@EventListener
	public void onCreate(PostCreatedEvent event) {
		logger.info("PostEventsListener.onCreate for post " + event.getPostId());
	}
}

In caso di presenza di più listener per lo stesso tipo di evento, per specificare un ordine è possibile utilizzare l’annotazione @Order.

Nel precedente esempio, abbiamo definito il metodo onCreate come void. Interessante notare che se invece avessimo restituito un oggetto, questo sarebbe stato interpretato da Spring come un nuovo evento. In questo modo è possibile, ad esempio, realizzare un layer di traduzione degli eventi di un dominio in eventi di un dominio esterno.

Ricezione condizionata di un evento

E’ anche possibile ricevere l’evento in maniera condizionata, ovvero soltanto se uno specifico attributo dell’evento vale true, utilizzando una SpEl:

@EventListener(condition = "#event.isPublished")

E’ inoltre possibile condizionare la ricezione di un evento legandolo ad una fase della transazione corrente. Ad esempio, se vogliamo ricevere un evento solo se la transazione si è conclusa correttamente, al posto dell’annotazione @EventListener dovremo usare l’annotazione @TransactionalEventListener, che di default attiva il metodo listener solo in caso di commit. Potremo configurare questa annotazione per ricevere l’evento anche in altre fasi della transazione, ad esempio BEFORE_COMMIT, AFTER_ROLLBACK e AFTER_COMPLETION.

Ricezione sincrona e asincrona

I metodi annotati con @EventListener ricevono l’evento in maniera sincrona con il metodo che ha generato l’evento. Anche i metodi annotati con @TransactionalEventListener vengono eseguiti in maniera sincrona, con l’unica differenza che non vengono invocati nell’esatto momento in cui l’evento viene generato ma la loro esecuzione è posticipata all’esecuzione della fase della transazione a cui sono associati, ad esempio al commit della transazione. Sono quindi eseguiti dallo stesso thread che ha generato l’evento e pertanto, in una applicazione web, la response non viene restituita al chiamante fintanto che sono in esecuzione i metodi listener.

E’ possibile però utilizzare un altra caratteristica dello Spring framework per rendere asincrona l’esecuzione dei listener, quindi affidare la loro esecuzione ad un thread differente da quello nel quale è stato generato l’evento. Per farlo è sufficiente configurare l’applicazione con l’annotazione @EnableAsync e applicare sui metodi listener l’annotazione @Async.

@Component
public class PostEventsListener {

	private static final Logger logger = LoggerFactory.getLogger(PostEventsListener.class);
	
	@EventListener
	public void onCreate(PostCreatedEvent event) {
		logger.info("PostEventsListener.onCreate for post " + event.getPostId()); 
	}
	
	@TransactionalEventListener
	public void onCreateTrans(PostCreatedEvent event) {
		logger.info("PostEventsListener.onCreateTrans for post " + event.getPostId()); 
	}
	
	@Async
	@TransactionalEventListener
	public void onCreateAsyncTrans(PostCreatedEvent event) {
		logger.info("PostEventsListener.onCreateAsyncTrans for post " + event.getPostId());
	}
}

Spring in questo caso, nel momento in cui viene generato un evento, avvia un nuovo thread in cui eseguire il metodo del listener.

Se questa modalità di esecuzione è molto utilizzata nella nostra applicazione allora, per evitare l’overhead dovuto alla creazione di sempre nuovi thread, è possibile indicare a Spring di adottare un gestore dei thread asincroni che supporti meccanismi di cache (pool) dei thread, come il ThreadPoolTaskExecutor, istanziabile nel seguente modo:

@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
	ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
	pool.setCorePoolSize(10);
	return pool;
}

Eventi di dominio in un sistema distribuito

L’approccio descritto è utile a disaccoppiare il codice che genera un evento di dominio da quello che lo gestisce ma richiede che produttore e consumatore di eventi siano sviluppati all’interno della stessa applicazione Spring. Spesso però è necessario notificare un evento di dominio ad un altro applicativo, ma purtroppo le cose si complicano poiché significa che ci troviamo di fronte ad un sistema distribuito. In tal caso è opportuno utilizzare strumenti di integrazione, come Apache Kafka o RabbitMQ.

In questo caso l’approccio descritto tornerebbe comunque utile, ad esempio, per disaccoppiare il codice che genera l’evento dal codice che inoltra l’evento ad uno di questi sistemi di integrazione.