In questa serie di articoli dedicati allo standard Java EE descriveremo le tecnologie che tale specifica racchiude in sè, cosa si propone di innovare e le motivazioni che hanno spinto la Sun a inizare a delinearlo già nel lontano 1998. In questo articolo parliamo di Enterprise Application Technologies.
Introduzione
La parte più importante delle specifiche Java EE [1] è senza dubbio quella che riguarda le tecnologie enterprise, prima fra tutte la tecnologia EJB.
In questo articolo tratteremo prima di tutto il meccanismo standard di dependency injection, successivamente tratteremo di CDI [2] (che è un ottimo framework di collegamento tra le tecnologie enterprise e quelle web) e poi tratteremo delle novità introdotte con la versione 3.1 degli EJB.
Enterprise Application Technologies
Per comodità riportiamo l’elenco di tutte le tecnologie enterprise (in grassetto le tecnologie nuove più rilevanti):
- EJB 3.1 -> JSR-318
- JPA 2.0 -> JSR-317
- Contexts and Dependency Injection, CDI -> JSR-299
- Dependency Injection, DI -> JSR-330
- Bean Validation -> JSR-303
- Managed Beans -> JSR-316
- Interceptors -> JSR-318
- Java EE Connector Architecture -> JSR-322
- Common Annotations -> JSR-250
- JMS -> JSR-914
- JTA -> JSR-907
- Java Mail -> JSR-919
Dal punto di vista delle tecnologie enterprise, l’architettura di una applicazione Java EE ha subìto una vera e propria rivoluzione: per la prima volta è stato introdotto il concetto di Managed Bean e, più in generale, di Dependency Injection (DI) [3].
Anticipazioni in Java EE 5
In realtà, nello standard Java EE 5, era già stato anticipato un rudimentale concetto di “dependency injection” chiamato resource injection. Infatti era possibile iniettare nel contesto di un container Java EE 5 delle risorse come: connessioni JMS, data source, JPA entity manager ed EJBs tramite le annotazioni: @Resource, @PersistenceContext, @PersistenceUnit e @EJB.
Queste risorse potevano essere iniettate all’interno di Servlets, JSF backing beans ed EJB. Questo modello era evidentemente adeguato solo per EJB, nel senso che le risorse che si potevano iniettare erano, di fatto, solo EJB o domini JPA. Quindi questo modello di DI era abbastanza limitato rispetto al concetto più generale offerto da Spring o Google Guice.
Per esempio non si potevano iniettare EJB all’interno di una classe di test fatta con JUnit, semplicemente perchè una classe di test non è un EJB. Inoltre non sempre era necessario scrivere un servizio @EJB, perchè magari non era necessario che il servizio fosse transazionale, nè tanto meno thread-safe: un EJB è, per definizione, sia transaction-safe che thread-safe. Per inciso, oggi lo sono anche gli EJB @Singleton; tuttavia è possibile cambiare questo comportamento tramite @ConcurrencyAttribute(NO_LOCK) introdotto nelle nuove specifiche: ne parleremo più avanti.
Quindi era complicato integrare software di terze parti o magari software “fatto in casa” con lo standard Java EE; a meno che non fosse software interamente basato su JPA.
Le soluzioni Java EE 6
Questo problema è esattamente ciò che è stato risolto con le nuove specifiche CDI e DI (JSR-299 e 330) e tramite l’introduzione del concetto di Managed Bean (JSR-316).
Un Managed Bean è un qualsiasi oggetto Java che ha, a differenza di altri oggetti, un ben definito ciclo di vita create/destroy che un contenitore Java EE può richiamare tramite @PostConstruct e @PreDestroy; inoltre il bean deve essere annotato con @ManagedBean (vedremo più avanti che in realtà, grazie alle specifiche CDI basate sui managed bean, non sarà più obbligatorio annotare gli oggetti con @ManagedBean).
A fronte di questo, ogni oggetto può essere gestito da un contenitore, e quindi possono essere iniettati qualsiasi tipo di POJO all’interno di altri POJO, e non siamo più limitati a dover per forza iniettare risorse all’interno di EJB (o EJB all’interno di EJB) come succedeva nelle specifiche Java EE passate.
Inoltre gli EJB adesso possono essere visti come @ManagedBean con servizi aggiuntivi, quali thread-safety e transactions-safety. Le specifiche di DI aggiungono la possibilità di iniettare un managed bean all’interno di un altro managed bean tramite l’annotazione @Inject, @Qualifier e @ScopeType (per darne lo scope all’interno del contenitore).
Lo standard CDI aggiunge alle DI le definizioni di scope quali:
- Dependent
- ApplicationScoped
- SessionScoped
- ConversationScoped
- RequestScoped
La figura 1 mostra il modo in cui vengono usate le nuove specifiche, integrandole tra di loro, per disegnare una buona architettura enterprise Java EE:
Figura 1 – CDI e Java EE.
Managed Beans (JSR-316)
La specifica dei Managed Bean è contenuta direttamente dentro la JSR-316 e definisce i requisiti che deve avere una classe java per poter diventare un managed bean.
Annotazione
Un Managed Bean può essere definito tramite annotazione javax.annotation.ManagedBean (o anche tramite XML).
No classe final, astratta o non static
Un Managed Bean non deve essere una classe final, astratta oppure una non-static inner class.
No serializzazione
Un Managed Bean non dovrebbe essere serializzabile, a differenza di un classico JavaBean.
Nome univoco
Un Managed Bean deve avere un nome univoco all’interno del container. In maniera facoltativa è possibile espicitare tale nome, altrimenti viene preso il nome della classe con la prima lettera in minuscolo. Per ogni Managed Bean il contenitore rende disponibile i seguenti nomi JNDI.
Nel namespace dell’applicazione
java:app//
Nel namespace del modulo:
java:module/
Costruttore vuoto
Un Managed Bean deve avere almeno il costruttore vuoto.
Annotazione di callback
Inoltre ogni Managed Bean può usare le annotazioni javax.annotation.PostConstruct e javax.annotation.PreDestroy per identificare le callback del proprio costruttore e distruttore.
Ed ecco un esempio di Managed Bean:
package org.mokabyte;
import javax.annotation.ManagedBean;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ManagedBean
public class MyBean {
private String message;
public MyBean() {
//empty.
}
@PostConstruct
public void init() {
message = "Hello World!";
}
@PreDestroy
public void destroy() {
message = "";
}
public String sayHello() {
return message;
}
}
Dependency Injection (JSR-330)
Questo standard fa da base per CDI, e definisce il modo in cui i bean vengono iniettati all’interno di altri bean. Le annotazioni che vedremo sono @Named, @Injection e @Produces.
@Named
@Named permette di dare un nome al Managed Bean: il nome potrà essere utilizzato all’interno del container, per esempio da JSF via Unified EL (Expression Language). Il parametro da dare a Named è una stringa ed è appunto il nome da dare all’istanza; questo parametro è opzionale: se non è presente allora il nome dell’istanza è il nome della classe con la prima lettera in minuscolo (lower-case). @Named può essere usato al posto di @ManagedBean.
@Injection
L’annotazione che permette di iniettare un bean all’interno di un altro è @Inject, e può essere applicato a campi, metodi o costruttori, sia statici che di istanza. Continuando l’esempio di prima ecco come iniettare MyBean all’interno di un altro bean:
package org.mokabyte;
import javax.inject.Inject;
import javax.inject.Named;
@Named("anotherBean")
public class AnotherBean {
private @Inject
MyBean myBean;
public String sayHello() {
return myBean.sayHello();
}
//@Inject
public String setMyBean(MyBean bean) {
return myBean = bean;
}
}
Come si vede, myBean viene iniettato all’atto della costruzione del bean AnotherBean, inoltre l’annotazione @Inject può essere anche messa nel metodo setter (invece che nel campo privato) e l’effetto è lo stesso: DI chiamerà il metodo setter iniettandogli una nuova istanza di MyBean. All’interno il metodo setter valorizza myBean e quindi avremo una nuova istanza di MyBean disponibile.
Lo standard definisce che @Inject crei una nuova istanza della classe da iniettare e poi se ne dimentichi, cioè che non gestisca lo scope dell’oggetto iniettato (questo è propriamente un compito di CDI).
Qualifiers
Supponiamo di voler qualificare due tipi diversi di istanze di una stessa interfaccia: questo ci può essere comodo se un servizio può essere fornito con algoritmi diversi, e quindi possiamo dare un nome specifico a ogni implementazione. Definiamo ora una interfaccia che fornisce un certo servizio, e definiamo due classi diverse che implementano questo servizio in modo differente:
package org.mokabyte;
public interface Greetings {
public String sayHello(String name);
}
package org.mokabyte;
public class InformalGreetings implements Greetings{
@Override
public String sayHello(String name) {
return "Hello " + name;
}
}
package org.mokabyte;
public class FormalGreetings implements Greetings {
@Override
public String sayHello(String name) {
return "Good morning " + name;
}
}
A questo punto i due bean non sono stati qualificati. Questo permette al contenitore di considerare i bean automaticamente qualificati con javax.enterprise.inject.Default, e quindi qualificati come default. È chiaro che è necessario qualificare almeno uno dei due in modo esplicito per poterli distinguere, e lasciare l’altro come @Default. Nel nostro esempio qualifichiamo entrambi:
package org.mokabyte;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface Informal { }
E analogamente definiamo public @interface Formal.
A questo punto qualifichiamo InformalGreetings con @Informal e FormalGreetings con @Formal:
package org.mokabyte;
@Informal
public class InformalGreetings implements Greetings{
@Override
public String sayHello(String name) {
return "Hello " + name;
}
}
package org.mokabyte;
@Formal
public class FormalGreetings implements Greetings {
@Override
public String sayHello(String name) {
return "Good morning " + name;
}
}
Bene, adesso utilizziamo questi nuovi qualificatori all’interno di un bean che utilizza l’interfaccia Greetings:
package org.mokabyte;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class Person {
@Inject @Formal Greetings greetings;
public String meet(String name) {
return greetings.sayHello(name);
}
}
Tramite l’annotazione @Inject il contenitore inietta ogni volta una nuova istanza dell’oggetto; se invece volessimo iniettare sempre la stessa istanza, allora dovremmo annotare la classe con @Singleton: il contenitore lo creerà una volta sola e utilizzerà sempre la stessa istanza ad ogni richiesta:
package org.mokabyte;
import javax.inject.Singleton;
@Singleton
public class Logger {
public String log(String message) {
return "Log: " + message;
}
}
In questo modo useremo sempre un solo logger. Da notare che abbiamo utilizzato javax.inject.Singleton e non javax.ejb.Singleton.
La differenza è notevole: se usiamo l’interfaccia DI, allora otteniamo solo la funzionalità prevista da un Depenency Injector: iniettiamo una istanza di una classe senza aggiungere altro; se invece usiamo l’interfaccia EJB, allora otteniamo anche le proprietà di transaction-safety e thread-safety. Inoltre l’istanza creata non è un semplice ManagedBean ma è un EJB, e quindi, per definizione, di EJB ha le due proprietà esposte sopra; in sostanza non dobbiamo usare @Lock e/o @Transaction all’interno di un @Named: per ottenere la thread-safety e/o la transaction-safety basta usare direttamente un EJB al posto di un Managed Bean; ne guadagnamo di leggibilità.
Producer
Ci sono dei casi in cui la creazione di un bean è complessa, quindi occorre usare un factory bean per poter isolare tutto il codice di creazione e configurazione dell’istanza. Per poter fare questo, lo standard introduce @Produces che permette di implementare il pattern Factory in maniera semplice.
package org.mokabyte;
import javax.enterprise.inject.Produces;
public class PersonFactory {
@Produces Person makePerson() {
System.out.println("Making Person via Factory");
return new Person();
}
}
La cosa interessante è che il metodo di factory può prendere in input dei parametri che permettono al factory di decidere come comportarsi nella creazione dell’istanza.
Per esempio, torniamo agli oggetti di Greetings: abbiamo definito due Qualifiers, uno che crea un @Formal ed un altro che crea un @Informal. Questa logica possiamo “nasconderla” all’interno di un Factory bean nel seguente nodo:
package org.mokabyte;
import javax.enterprise.inject.Produces;
public class GreetingsFactory {
private boolean isFormal = false;
@Produces
Greetings makeGreetings(@Formal Greetings formal,@Informal Greetings informal) {
System.out.println("Creating greetings via factory bean");
if (isFormal)
return formal;
else
return informal;
}
}
In questo modo abbiamo delegato al factory bean anche la scelta di quale Greetings creare. I due parametri di ingresso vengono valorizzati e iniettati automaticamente dal container: prima di tutto vengono create due istanze di Greetings: una formale ed una informale, poi viene chiamato il metodo makeGreetings() e viene ritornato al chiamante(@Inject) il Greetings giusto.
Negli altri bean non sarà più necessario decidere quale Greetings usare, ma basterà solamente usare @Inject e ci penserà il GreetingsFactory a creare l’oggetto giusto per noi.
Un ulteriore esempio
Facciamo adesso un altro esempio un pò più avanzato. Vogliamo sempre scrivere un Factory che sappia creare un Greetings formale o informale, ma questa volta vogliamo che sia il client a decidere direttamente quale prendere, tramite dei parametri di configurazione e senza qualificatori (e inoltre non vogliamo che vengano create due nuove istanze ogni volta). Quindi iniziamo con il definire un tipo di configurazione tramite annotazioni:
package org.mokabyte;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ TYPE, FIELD, METHOD, PARAMETER })
public @interface GreetingsConfigure {
String Type() default "Formal";
}
A Questo punto ridefiniamo il Factory Bean nel seguente modo:
package org.mokabyte;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
public class GreetingsFactory {
@Produces
Greetings makeGreetings(InjectionPoint injectionPoint) {
GreetingsConfigure conf = injectionPoint.getAnnotated().getAnnotation(
GreetingsConfigure.class);
if (conf != null) {
if (conf.Type().equalsIgnoreCase(("Informal"))) {
return new InformalGreetings();
}
}
return new FormalGreetings();
}
}
Lato client avremo adesso:
package org.mokabyte;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class Person {
private @Inject @GreetingsConfigure(Type="Formal") Greetings greetings;
public String meet(String name) {
return greetings.sayHello(name);
}
}
Il comportamento adesso è lo stesso di quello che avevamo prima con i Qualifiers, ma il vantaggio è quello di poter avere piena libertà di configurazione del Factory lato client (possiamo mettere anche molti parametri dentro l’annotazione di configurazione GreetingsConfigure) senza dover creare di volta in volta nuovi qualificatori; infine adesso il controllo è sul client: il factory viene solo istruito su quale istanza creare.
Questi esempi dimostrano la flessibilità del meccanismo di Factory.
Patterns
Ricapitolando ciò che abbiamo visto, ecco l’elenco dei patterns implementati da DI:
- Dependency Injection: @Inject (assieme a @Qualifier);
- Factory Bean: @Produces (assieme a @Qualifier);
- Singleton Factory Bean: @Singleton.
Come vedremo, CDI implementa altri patterns estendendo ciò che abbiamo già visto in DI.
Un insieme di tecnologie
Sebbene non abbiamo ancora descritto CDI, possiamo già da ora pensare a queste tecnologie come un tutt’uno sebbene facciano parte di JSR differenti: ManagedBeans (JSR-316), DI (JSR-330) e CDI (JSR-299). Allora possiamo ridisegnare lo schema presentato a inizio articolo nel modo illustrato in figura 2.
Figura 2 – Architettura basata su CDI.
Da qui vediamo come CDI (e DI) è adesso il fondamento di qualsiasi architettura basata su Java EE: qualsiasi estensione a Java EE deve naturalmente basarsi su CDI. E quindi ora gli EJB sono diventati superflui? Effettivamente, con l’introduzione di CDI (e DI), gli EJB possono sembrare superflui. In realtà non è propriamente cosi: gli EJB vengono rivisti oggi come una tecnologia che permette di implementare alcuni pattern. Vediamoli di seguito.
Bean thread-safe e transaction-safe
In particolare possiamo creare uno shopping trolley con @Stateful, in questa maniera abbiamo creato dei managed bean che automaticamente sono legati solo a una sessione web in maniera sicura (thread e transaction-safe).
Bean Singleton
Possiamo creare dei bean @Singleton e ancora thread e transaction-safe (nello standard EJB3.1 è stata aggiunta l’annotazione @Singleton per gli EJB, come abbiamo già visto in un esempio precendente).
Esportazione di servizi
Possiamo esportare dei servizi in maniera remota verso clienti esterni (per esempio applet) o anche ad altri container distribuiti: in questo modo abbiamo gratis tutta la logica di serializzazione/deserializzazione di metodi e dati; inoltre da questa versione dello standard i nomi JNDI sono stati standardizzati con delle regole ben precise, questo semplifica l’interoperabilità tra contenitori differenti. È anche vero, però, che questo meccanismo può essere implementato alternativamente con SOAP e/o RESTful (JAX-WS, RS).
L’idea è quindi quella di basare l’architettura di un’applicazione Java EE tutta su CDI, ed usare gli EJB solo in casi specifici: in tutti i punti laddove non possiamo usufruire dei servizi resi dagli EJB in maniera semplice ed elegante via CDI. Per esempio nessuno ci vieta di creare un managed bean in cui tutti i metodi siano annotati con @Lock e @Transaction, ma allora a questo punto si può benissimo annotare direttamente la classe con @Stateful (o @Stateless) e otteniamo così sempre un Managed Bean (un EJB è sempre un managed bean per definizione) transazionale e thread-safe.
Lo standard non vieta affato di poter annotare una classe sia come EJB che come Managed Bean in maniera esplicita: se vogliamo creare un servizio transazionale che viene utilizzato da una pagina JSF, allora possiamo annotare il servizio con @Stateful @Named(“shoppingTrolley”) in modo tale da poter richiamare il servizio da una pagina XHTML tramite la seguente sintassi: “#shoppingTrolley”.
Test
Implementiamo adesso un semplice test che permetta di verificare gli esempi esposti sopra. È necessario installarsi un Application Server e lanciarlo ogni volta che vogliamo eseguire un test? La risposta è no, altrimenti CDI sarebbe veramente poco produttivo!
Ecco i passi che dobbiamo seguire per provare quanto appena spiegato. Prima di tutto occorre una distribuzione di Eclipse che supporti almeno maven. Vi consiglio di scaricarvi il JBoss Developer Tool e di provarlo:
Figura 3 – JBoss Developer Tool.
Adesso create un progetto Maven con le seguenti coordinate:
4.0.0
mokabyte
cdi
0.0.1-SNAPSHOT
Mokabyte CDI Tutorial
Come si nota è sufficiente un progetto che generi un semplice JAR: un modulo basato su CDI è un semplice JAR con un file beans.xml presente nella directory di informazioni (META-INF) del JAR.
Per provare dei semplici esempi come quelli che abbiamo fatto è sufficiente usare CDI Advocacy.
CDI Advocacy
CDI Advocacy [4] è un progetto che serve proprio per semplificare l’uso di CDI nel test e quindi è l’ideale per far funzionare dei programmi di esempio. CDI Advocacy permette inoltre di astrarci dal tipo di implementazione di CDI (che sia Weld di JBoss o altre) e di utilizzare le dipendenze maven per scegliere il proprio container di prova.
Tramite questa libreria inoltre non è più necessario provare CDI scaricandosi, installandosi ed eseguendo un intero application server (sia esso Glassfish e/o JBoss), ma è un piccolo “adapter” che permette di eseguire codice CDI con il minimo supporto Java EE possibile.
Come dicono gli autori di questo progetto: “CDI è per Spring e Guice ciò che JPA è stato per Hibernate e TopLink”, cioè è una standardizzazione del meccanismo di dependency injection introdotto da questi due framework.
Quindi ora iniziamo ad aggiungere le dipendenze da questa libreria:
org.cdisource.beancontainer
beancontainer-api
1.0-SNAPSHOT
javax.validation
validation-api
1.0.0.GA
org.cdiadvocate
cdiadvocate-weld-impl
1.0-SNAPSHOT
Abbiamo scelto l’implementazione di Weld, quindi abbiamo aggiunto cdiadvocate-weld-impl.
Adesso aggiungiamo i due repository che ci servono: quello di Google che ospita questo progetto e quello di JBoss che, ovviamente, ospita Weld.
cdi.advocate
CDI Advocacy
http://jee6-cdi.googlecode.com/svn/m2/repository/
jboss.maven.repo
JBoss Repository
http://repository.jboss.org/nexus/content/groups/public-jboss/
Aggiungiamo le classi che abbiamo spiegato nell’articolo. CDI si attiva automaticamente mettendo in META-INF il file beans.xml.
Questo file XML è praticamente un XML vuoto, poichè al suo interno contiene le definizioni dei bean, ma siccome usiamo le annotazioni, il file risulta vuoto (in realtà vengono definite anche le alternative, gli interceptors, ma di questo parleremo nel prossimo articolo). Aggiungiamo quindi il file: src/resources/META-INF/beans.xml nel progetto. Adesso scriviamo la parte più importante, cioè il programma di test:
package org.mokabyte;
import org.cdiadvocate.beancontainer.BeanContainer;
import org.cdiadvocate.beancontainer.BeanContainerManager;
import org.testng.Assert;
import org.testng.annotations.Test;
public class PersonTest {
@Test
public void meet() {
System.out.println("Start test CDI");
BeanContainer beanContainer = BeanContainerManager.getInstance();
beanContainer.start();
Person person = (Person) beanContainer.getBeanByName("person");
String message = person.meet("MokaByte!");
System.out.println("Message : " + message);
Assert.assertEquals(message, "Good morning MokaByte!");
}
}
Stiamo testando solamente la classe Person, e verificando che scriva correttamente il saluto “formale”: Good morning MokaByte!
Come si vede grazie a CDI Advocate possiamo accedere al contenitore tramite una factory in modo del tutto indipentende rispetto il contenitore sottostante: la factory è BeanContainerManager che crea un wrapper al contenitore, poi con il metodo start() lo fa partire (e quindi crea tutti i bean necessari); dopo di ciò possiamo richiamare un bean mediante il suo nome (e non il nome JNDI, ma proprio quello dichiarato con @Named) e chiamare il metodo meet() che è il metodo da testare.
Questo meccanismo è esattamente lo stesso quando usiamo Spring invece di CDI: con Spring useremmo l’interfaccia ApplicationContext per poi richiamare il metodo getBean() per prendere l’istanza della classe. Con google Guice si fa la stessa cosa tramite Guice e Injector.
In questo senso, dunque, è proprio vero quanto già accennato: per Spring e Guice, CDI è ciò che JPA è stato per Hibernate e TopLink, ossia una standardizzazione di un contenitore generico basato sulla dependency injection.
Conclusioni
Abbiamo visto nel dettaglio le funzionalità offerte dallo standard JSR-299, i pattern implementati e come utilizzarli al meglio. Nel prossimo articolo vedremo allo stesso modo CDI: i pattern implementati e in particolare come si gestisce un contesto web per utilizzare in modo semplice le pagine JSF. Allegato a questo articolo (menu in alto a destra) trovate il progetto Maven: non dovete far altro che importarlo dentro Eclipse e fare delle prove.
Riferimenti
[1] Sun/Oracle, JSR 316: Java Platform, Enterprise Edition 6 (Java EE 6) Specification
http://jcp.org/en/jsr/detail?id=316
[2] Sun/Oracle, JSR 299: Contexts and Dependency Injection for the Java EE platform
http://jcp.org/en/jsr/detail?id=318
[3] Sun/Oracle, JSR 330: Dependency Injection for Java
http://jcp.org/en/jsr/detail?id=330
[4] CDI Advocate
Gli esempi da scaricare:
https://github.com/CDISource/examples
Motivazioni del perchè è stata scritta questa libreria:
http://cdi4jadvocate.blogspot.it/
Sito di riferimento:
http://sites.google.com/site/cdipojo/