Il Pattern Decorator e Java EE 6: quando il vecchio si fa nuovo con CDI

Per chi è cresciuto a pane e GoF non sarà certo la prima volta che sente parlare del Pattern Decorator. In due parole, questo pattern permette di aggiungere in modo modulare funzionalità, a compile-time, ai metodi di una classe esistente, mantenendone l’interfaccia intatta. L’aggiunta o la rimozione di queste funzionalità risulta quindi totalmente trasparente all’utilizzatore. Nel contesto attuale dello sviluppo Java, sempre più governato da “container” che risolvono automaticamente le dipendenze, implementare questo pattern poteva non essere banale fino a ieri. La nuova specifica Context and Dependancy Injection (CDI) introdotta in Java EE 6 invece supporta nativamente i decoratori: vediamo come.

Interceptor vs Decorators

A prima vista uno potrebbe chiedersi: “ma che differenza c’è dalla normale AOP implementata con Interceptors“? Il concetto di interceptor lo ritroviamo sia in Spring sia nella specifica EJB che nella stessa CDI. A che serve? In pratica è uno strumento potente che permette di catturare e separare aspetti ortogonali alla logica applicativa. E’ perfetto quindi per risolvere questioni tecniche come la gestione delle transazioni, dei log o delle sicurezze. Per natura quindi gli intercettori (passate l’orrendo italianismo) non sono a conoscenza del contesto che intercettano: semplicemente permettono di eseguire operazioni prima e/o dopo la chiamata ad un metodo di business, senza entrare in merito alla logica del metodo chiamato.

E se invece avessimo bisogno di entrare in merito al contesto chiamato? Ecco, i decoratori servono proprio a questo! In CDI, un decoratore intercetta le chiamate relative a specifiche interfacce Java della nostra applicazione, entrando quindi in merito alla logica di business: possiamo pensarli come wrapper del nostro strato applicativo nel quale possiamo modellare e delegare specifiche funzionalità. In sostanza sono complementari agli interceptors, dai quali si differenziano per specificità d’impiego e conoscenza semantica del contesto in cui agiscono.

Ovviamente è possibile applicare contemporaneamente alla stessa classe sia un interceptor che un decorator. In che ordine verranno chiamati? Come ci si può attendere, l’interceptor avrà la precedenza sul decoratore. Cose più strane come intercettare un decoratore invece non si può fare… anche per fortuna direi! 😉

Un esempio di decoratore con CDI

Immaginiamo di aver inventato l’applicazione del momento che tutti stanno scaricando, e di aver la necessità di mostrare il contatore dei download che si aggiorna una volta al giorno. Ci sarà quindi una classe delegata a gestire i download che, tra l’altro, esporrà anche il metodo per il calcolo del numero di download:

public interface DownloadManager extends Serializable {

   ...
   Long getCount();
   ...
}

Come evitare di far eseguire i conti ogni volta che viene chiamato il metodo getCount() senza impattare il codice già sviluppato? Un decoratore è la soluzione ideale. La teoria ci dice che deve implementare anch’esso la stessa interfaccia della classe che si vuole decorare, incapsulando una precisa responsabilità: quella cioè di “cachare” il risultato del metodo per un giorno. Vediamo subito il codice:

@Decorator
public abstract class DownloadManagerCache implements DownloadManager {

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

   @Inject @Delegate @Any
   private DownloadManager downloadManager;
   
   @Resource(name="config/cache/timeout")
   private long timeout;
   
   private Long count;
   
   private long lastUpdate;
   
   @Override
   public Long getCount() {
      if (count == null || System.currentTimeMillis() > lastUpdate + timeout) {
         count = downloadManager.getCount();
         lastUpdate = System.currentTimeMillis();
      } else {         
         logger.info("Loading counter from cache...");
      }
      return count;
   }
}

Partiamo intanto col notare che la classe è astratta: questo ci dà la libertà di implementare solo i metodi che effettivamente vogliamo decorare! Definiamo poi la classe come decoratore tramite l’apposita annotazione @Decorator. Eclipse ci informerà immediatamente che ogni decoratore deve avere uno ed un solo attributo annotato con @Delegate: questo sarà il riferimento alla classe che stiamo decorando. Accompagnando poi questa annotazione con @Inject e @Any, stiamo dicendo al BeanManager CDI che il decoratore è valido per ogni implementazione dell’interfaccia DownloadManager. L’attributo count farà da cache al contatore mentre timeout e lastUpdate serviranno per valutare la validità della cache stessa.

Tralasciando per il momento l’annotazione su timeout e dedichiamoci al metodo getCount. Esso semplicemente controllerà che la cache sia stata inizializzata o che sia ancora valida: in caso affermativo, restituirà il valore di count, altrimenti lo popolerà chiamando il metodo del delegato (cioè quello decorato!) e aggiornando l’attributo lastUpdate.

Prima di partire

Bene ma il decoratore che ciclo di vita ha? Ne ha uno proprio? Ebbene no! Il decoratore assume il ciclo di vita della singola istanza della classe che decora. Che significa? Che se ho un bean in @ApplicationScoped, anche il decoratore avrà lo stesso scope (e così via). Una paio di curiosità: se ho due bean @ApplicationScoped che implementano la stessa interfaccia, le istanze del decoratore saranno due, una per ogni implementazione. Le eventuali variabili di stato del decoratore faranno quindi riferimento alla specifica implementazione che decorano.
Inoltre, i bean da decorare devono essere gestiti dal BeanManager di CDI, purtroppo non funziona sugli EJB 🙁

Cosa manca quindi per far funzionare l’esempio?

  • l’implementazione di DownloadManager deve essere @ApplicationScoped
  • è necessario attivare il decoratore registrandolo nel file META-INF/beans.xml (o WEB-INF/beans.xml nei progetti web)
     
      it.cosenonjaviste.DownloadManagerCache
     
    
  • definiamo il valore di timeout. Poteva essere inserito tranquillamente qui nel codice, ma per mettere ancora un po’ di carne al fuoco rendiamolo configurabile! In realtà questo banale esempio ci mostra come i bean CDI siano perfettamente integrati con tutte le funzionalità dell’Application Server… Basta inserire per esempio una env-entry nel web.xml ed il gioco è fatto:

    
      timeout cache
      config/cache/timeout
      java.lang.Long
      86400000
    
    

    Il valore corrisponde ad un giorno in millisecondi.

Conclusioni

Abituati a sentire di AOP e interceptors, Java EE 6 porta una ventata di “novità”, se così si può dire, integrando nativamente i decoratori dotandoli di una veste più moderna. Sta a noi utilizzatori riuscire a capire se effettivamente era una cosa che ci mancava o se potevamo farne a meno: come si dice… ai posteri l’ardua sentenza!

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio . - -

  • Andrea Ligios

    Non mi è chiaro il ciclo di vita del @Decorator e delle sue variabili (ad esempio “count”); è @ApplicationScoped ? Possiamo quindi considerarlo un Singleton a tutti gli effetti ? Se non ci fosse il timeout di mezzo, il dato messo in cache la prima volta verrebbe quindi mantenuto per tutta la durata dell’applicazione, e fornito a tutti i client ?

  • Andrea Ligios

    Ad ogni modo non funziona, ho fatto diverse prove e le variabili del @Decorator sono sempre vuote. Nel mio caso DownloadManager è un @Singleton o uno @Stateless… è forse dovuto a questo, e quindi il CDI container non comunica bene con l’EJB container ? P.S: nell’articolo manca anche il riferimento al beans.xml

  • Ciao, ho aggiornato il post con il paragrafo “Prima di partire”, spero di aver risposto alle tue domande. Effettivamente ho dato per scontato un po’ troppe cose, grazie della precisazione.

  • Andrea Ligios

    Grazie, ora è tutto chiarissimo (niente EJB ed @ApplicationScoped ereditato), e complimenti per l’articolo