Gli Enterprise Java Beans (EJB) sono componenti scritti in Java che realizzano la logica di business (application layer) di un’applicazione. Vivono all’interno di un EJB Container (un runtime environment nell’Application Server) che ne gestisce il ciclo di vita e le sue invocazioni. Il container fornisce anche meccanismi ausiliari, come la transazionalità e la sicurezza, in modo trasparente ai client. Gli EJB realizzati secondo le specifiche J2EE possono essere dispiegati (deploy) in diversi application server J2EE compliant. Sono quindi portabili e possono essere aggregati per realizzare nuovi EJB.
Gli EJB possono essere classificati in tre categorie:
- Session Bean
- Entity Bean
- Message Drive Bean
Session Bean
Sono classi che implementano la logica di business dell’applicazione e costituiscono il punto di accesso con cui i client possono interagire con tale logica, realizzando delle sessioni di comunicazione. Dal punto di vista del client, ogni Session Bean è associato ad uno ed uno solo Client. Lo stato dell’interazione con un client (lo stato Session Bean) è transiente, ovvero quando termina la sessione a cui è associato, il bean viene distrutto, insieme con lo stato. Una sessione termina quando il client esplicitamente la rimuove oppure per un periodo di inattività superiore ad un tempo prefissato.
Esistono due tipi di Session Bean: stateless e stateful.
Stateless | Non mantengono lo stato della sessione con il client. Per tale motivo il container può assegnare un qualsiasi session EJB stateless già esistente a qualsiasi client e, per risparmiare memoria, può anche associare lo stesso bean a più client. Questa politica di allocazione dipende dall’implementazione dell’EJB Container ed è totalmente trasparente ai client che “credono” di avere uno stateless session bean dedicato. |
Stateful | Mantengono lo stato conversazionale nella sessione con il client per cui ogni Stateful Session Bean è associato sempre ad un solo client. |
Entity Bean
Rappresentano logicamente gli oggetti di business di una applicazione distribuita. Sono oggetti persistiti opportunamente mappati nelle tabelle di un database relazionale, e molto spesso l’istanza di un entity bean corrisponde ad una tupla della corrispondente tabella. La loro definizione prevedono l’esistenza di proprietà definite chiavi primarie per essere identificati univocamente e possono essere legati ad altri entity bean da relazioni di tipo: one-to-one, one-to-many, many-to-one e many-to-many.
Di norma tale bean possono essere condivisi tra diversi client perché questi possono voler accedere agli stessi dati. E’ importante quindi che il container fornisca un servizio per la gestione delle transazioni. La persistenza di tali oggetti può essere gestita i due modalità distinte:
- Bean Managed Persistent Bean (MDB): il bean implementa anche il codice necessario alla interazione con il database;
- Container Managed Persistent Bean (CMB) è il ocntainer, che in base alla configurazione del bean, si occupa di interagire con database.
Message Drive Bean
Di fatto sono molto simili ai Session Bean, quindi usufruiscono di tutti i servizi del container, con la differenza che non hanno metodi che possono essere invocati da un applicazione client, ma gestiscono la comunicazione con altri sistemi o all’interno dello stesso container tramite lo scambio di messaggi asincroni e l’utilizzo del protocollo JMS.
EJB3
Le principali differenza tra la versione 3 della specifica EJB e le precedenti versioni sono:
- l’utilizzo delle annotation che si sostituiscono alle interfacce;
- l’eliminazione della interfaccia home;
- l’introduzione delle Java persistence Api (JPA) per l’accesso al database;
- miglioramento delle performance dovuto all’utilizzo di oggetti POJO annotati piuttosto che descrittori xml;
- utilizzo della dependency injection per l’iniezione delle dipendenze.
Il Progetto
Consideriamo una applicazione J2EE che implementa un ATM (Automated Teller Machine). Semplificando notevolmente l’applicativo considerando tre semplici componenti:
- un DAO per l’accesso ai conti correnti che per semplicità saranno interamente gestiti in memoria;
- un service per l’implementazione della logica di deposito e prelevamento del denaro;
- un bean di sessione per l’iinterfacciamento con l’utente.
La relazione tra questi tre componenti è evidenziata nel diagramma UML mostrato nell’immagine seguente:
Generiamo quindi una applicazione denominata ejb-bank
di tipo Maven ed inseriamo le dipendenze necessarie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<dependencies> <!-- Dependency for EJB --> <dependency> <groupId>javax.ejb</groupId> <artifactId>javax.ejb-api</artifactId> <version>3.2</version> </dependency> <!-- Dependency for JBoss Weld reference CDI implementation --> <dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>1.2</version> <scope>provided</scope> </dependency> </dependencies> |
La prima dipendenza contiene classi ed interfacce per l’implementazione degli EJB, ovvero definiscono il contratto tra EJB e client, e tra EJB e container. La secondo contiene le interfacce che definiscono la dependency injection (CDI).
Cominciamo l’implementazione dell’ATM descrivendo il componente BankAccountDao
. Trattandosi di un componente che non ha logica applicativa, ma che semplicemente accede ai dati, si è scelto di non implementarlo come EJB, ma come semplice bean iniettato utilizzando il framework CDI e quindi annotato come @Dependant
(per dettagli si rimanda all’articolo Utilizzo di CDI in JSF 2.0). Inoltre non si è voluto inserire nell’esempio la complessità necessaria alla gestione di un database, quindi la persistenza è realizzata in memoria attraverso la gestione di una semplice mappa.
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 |
public interface BankAccountDao { public BankAccount find( String iban ); public void save( BankAccount account ); public void save( Movement movement ); } @Dependent public class BankAccountDaoImpl implements BankAccountDao { private static HashMap<String, BankAccount> accounts = new HashMap<String, BankAccount>(); @Override public BankAccount find(String iban) { if ( iban != null ) { BankAccount account = accounts.get( iban ); if ( account == null ) { account = new BankAccount( iban ); accounts.put( iban, account ); } return account; } return null; } @Override public void save(BankAccount account) { accounts.put( account.getIban(), account ); } @Override public void save(Movement movement) { accounts.get( movement.getIban() ).addMovement( movement ); } } |
Il componente BankAccountService
implementa invece la logica di gestione del conto corrente e quindi è implementato come EJB ma di tipo stateless. Per farlo è necessario definire l’interfaccia del servizio annotandola con @Remote
, mentre la sua implementazione dovrà essere annotata con @Stateless
. Si noti l’utilizzo dell’iniezione per recuperare il DAO.
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 |
@Remote public interface BankAccountService { public BankAccount find(String iban); public BankAccount withdraw( BankAccount account, BigDecimal amount ); public BankAccount deposit( BankAccount account, BigDecimal amount ); } @Stateless public class BankAccountServiceImpl implements BankAccountService { @Inject BankAccountDao dao; @Override public BankAccount find(String iban) { return dao.find(iban); } @Override public BankAccount withdraw(BankAccount account, BigDecimal amount) { Movement movement = new Movement( account.getIban(), amount.negate() ); account.addMovement(movement); account.setBalance( account.getBalance().subtract( amount ) ); dao.save( account ); return account; } @Override public BankAccount deposit(BankAccount account, BigDecimal amount) { Movement movement = new Movement( account.getIban(), amount ); account.addMovement(movement); account.setBalance( account.getBalance().add( amount ) ); dao.save( account ); return account; } } |
Implementiamo infine il componente AtmService
. Si tratta del componente con cui interagirà il client per i servizi di accesso al conto, deposito e prelevamento. Poiché si è prevista una fase si autenticazione dell’utente, realizzata banalmente recuperandone il conto corrente su cui poi operare, tale componente sarà un EJB si tipo stateful. Come fatto precedentemente annotiamo l’interfaccia con @Remote
e la sua implementazione con @SessionScoped
e @Stateful
.
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 |
@Remote public interface AtmService { public Boolean autenticate( String iban ); public void withdraw( BigDecimal amount ); public void deposit( BigDecimal amount ); public BigDecimal balance(); } @SessionScoped @Stateful public class AtmServiceImpl implements Serializable, AtmService { @EJB private BankAccountService bas; private BankAccount account; @Override public Boolean autenticate(String iban) { account = bas.find(iban); return account != null; } @Override public void withdraw( BigDecimal amount ) { account = bas.withdraw( account, amount ); } @Override public void deposit( BigDecimal amount) { account = bas.deposit( account, amount ); } @Override public BigDecimal balance() { return account.getBalance(); } } |
Si noti che l’iniezione del service BankAccountService
avviene questa volta con l’annotazione @EJB
che è specifica per l’implementazione della dependency injection tra EBJ. Sono intercambiabili si gli EJB sono ospitati nello stesso container, ma nei casi più complessi l’annotazione @EJB
consente di specificare parametri aggiuntivi per l’individuazione del bean target.
1 2 3 4 5 6 7 8 |
@Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Retention(value = RetentionPolicy.RUNTIME) public @interface EJB { public String name() default ""; public String beanName() default ""; public Class beanInterface() default Object.class; public String mappedName() default ""; } |
Avvio di WildFly
Una volta avviata l’applicazione sull’application server WildFly, notate la sezione di log seguente in cui è mostrato il JNDI name assegnato a ciascun EJB.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
14:39:48,043 INFO [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-4) JNDI bindings for session bean named BankAccountServiceImpl in deployment unit deployment "ejb-bank.jar" are as follows: java:global/ejb-bank/BankAccountServiceImpl!it.javaboss.services.BankAccountService java:app/ejb-bank/BankAccountServiceImpl!it.javaboss.services.BankAccountService java:module/BankAccountServiceImpl!it.javaboss.services.BankAccountService java:jboss/exported/ejb-bank/BankAccountServiceImpl!it.javaboss.services.BankAccountService java:global/ejb-bank/BankAccountServiceImpl java:app/ejb-bank/BankAccountServiceImpl java:module/BankAccountServiceImpl 14:39:48,044 INFO [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-4) JNDI bindings for session bean named AtmServiceImpl in deployment unit deployment "ejb-bank.jar" are as follows: java:global/ejb-bank/AtmServiceImpl!it.javaboss.services.AtmService java:app/ejb-bank/AtmServiceImpl!it.javaboss.services.AtmService java:module/AtmServiceImpl!it.javaboss.services.AtmService java:jboss/exported/ejb-bank/AtmServiceImpl!it.javaboss.services.AtmService java:global/ejb-bank/AtmServiceImpl java:app/ejb-bank/AtmServiceImpl java:module/AtmServiceImpl |
Il Client
Per l’utilizzo del servizio ATM remoto implementiamo un client java di tipo maven che utilizza la stringa JNDI per la connessione all’EJB. Il client innanzitutto crea il contesto remoto (Context
) quindi recupera l’EJB su cui eseguirà le operazioni di: autenticazione, saldo e deposito.
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 |
public static void main(String[] args) { // Connection to Wildfly Server instance String host = "127.0.0.1"; String port = "8080"; // Wildfly HTTP port Context remotingContext; AtmService service; try { remotingContext = ClientUtility.createRemoteEjbContext(host, port); service = ClientUtility.createEjbProxy( remotingContext, ClientUtility.getLookupName(true), AtmService.class ); if (service.autenticate("1111")) { // Stampa saldo iniziale System.out.println("Amount: " + service.balance()); // Deposito service.deposit(BigDecimal.TEN); // Stampa saldo finale System.out.println("Amount: " + service.balance()); } } catch (NamingException e) { e.printStackTrace(); } catch (ClassCastException e) { e.printStackTrace(); } } |
Il codice seguente crea il contesto iniziale:
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 |
public static Context createRemoteEjbContext(String host, String port) throws NamingException { if (initialContext == null) { Hashtable<Object, Object> props = new Hashtable<>(); props.put(INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory"); props.put(URL_PKG_PREFIXES, "org.jboss.ejb.client.naming"); props.put("jboss.naming.client.ejb.context", false); props.put("org.jboss.ejb.client.scoped.context", true); props.put("endpoint.name", "client-endpoint"); props.put("remote.connections", "default"); props.put("remote.connection.default.host", host); props.put("remote.connection.default.port", port); props.put("remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS",false); props.put("remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED", false); props.put(PROVIDER_URL, "http-remoting://" + host + ":" + port); initialContext = new InitialContext(props); } return initialContext; } |
Per la generazione della stringa JNDI si rimanda al seguente link: EJB invocations from a remote client using JNDI. Per completezza ne riportiamo il codice:
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 |
public static String getLookupName(boolean stateful) { /* * The app name is the EAR name of the deployed EJB without .ear suffix. * Since we haven't deployed the application as a .ear, the app name for * us will be an empty string */ String appName = ""; /* * The module name is the JAR name of the deployed EJB without the .jar * suffix. */ String moduleName = "ejb-bank"; /* * AS7 allows each deployment to have an (optional) distinct name. This * can be an empty string if distinct name is not specified. */ String distinctName = ""; // The EJB bean implementation class name String beanName = AtmServiceImpl.class.getSimpleName(); // Fully qualified remote interface name final String interfaceName = AtmService.class.getName(); // Create a look up string name String name = "ejb:" + appName + "/" + moduleName + "/" + distinctName + "/" + beanName + "!" + interfaceName + ( stateful ? "?stateful" : "" ); return name; } |
Codice Sorgente
Il codice sorgente sia del client che del server è scaricabile qui: ejb-bank e ejb-bank-client.