Skip to content

2. Architettura a livelli di un'applicazione Java

Un'applicazione Java è spesso suddivisa in livelli, ciascuno con un ruolo ben definito. Consideriamo un'architettura comune, l'architettura a tre livelli:

  • Il livello [1], qui indicato come [ui] (User Interface), è il livello che interagisce con l'utente tramite una GUI Swing, un'interfaccia console o un'interfaccia web. Il suo ruolo è quello di fornire i dati provenienti dall'utente al livello [2] o di presentare all'utente i dati forniti dal livello [2].
  • Il livello [2], qui indicato come [business], è il livello che applica le cosiddette regole di business — ovvero la logica specifica dell’applicazione — senza preoccuparsi della provenienza dei dati che riceve o della destinazione dei risultati che produce.
  • Il livello [3], qui indicato come [DAO] (Data Access Object), è il livello che fornisce al livello [2] i dati pre-memorizzati (file, database, ecc.) e memorizza alcuni dei risultati forniti dal livello [2].

Esistono vari modi per implementare il livello [DAO]. Esaminiamone alcuni:

Il livello [JDBC] sopra indicato è il livello standard utilizzato in Java per accedere ai database. Esso isola il livello [DAO] dal DBMS che gestisce il database. In teoria, è possibile cambiare DBMS senza modificare il codice del livello [DAO]. Nonostante questo vantaggio, l'API JDBC presenta alcuni svantaggi:

  • Tutte le operazioni sul DBMS possono generare l'eccezione SQLException. Ciò costringe il codice chiamante (in questo caso il livello [DAO]) a racchiuderle in blocchi try/catch, rendendo così il codice piuttosto verboso.
  • Il livello [DAO] non è completamente indipendente dal DBMS. Ad esempio, i DBMS dispongono di metodi proprietari per la generazione automatica dei valori delle chiavi primarie che il livello [DAO] non può ignorare. Pertanto, quando si inserisce un record:
    • Con Oracle, il livello [DAO] deve prima ottenere un valore per la chiave primaria del record e poi inserire il record.
    • Con SQL Server, il livello [DAO] inserisce il record, al quale il DBMS assegna automaticamente un valore di chiave primaria, valore che viene restituito al livello [DAO].

Queste differenze possono essere eliminate utilizzando le procedure memorizzate. Nell'esempio precedente, il livello [DAO] chiamerà una procedura memorizzata in Oracle o SQL Server che tiene conto delle caratteristiche specifiche del DBMS. Queste saranno nascoste al livello [DAO]. Tuttavia, mentre il cambio di DBMS non richiederà la riscrittura del livello [DAO], richiederà comunque la riscrittura delle procedure memorizzate. Questo potrebbe non essere considerato un ostacolo insormontabile.

Sono stati compiuti numerosi sforzi per isolare il livello [DAO] dagli aspetti proprietari dei DBMS. Una soluzione che ha riscosso grande successo in questo ambito negli ultimi anni è Hibernate:

Il livello [Hibernate] si colloca tra il livello [DAO] scritto dallo sviluppatore e il livello [JDBC]. Hibernate è un ORM (Object-Relational Mapper), uno strumento che fa da ponte tra il mondo relazionale dei database e quello degli oggetti manipolati da Java. Lo sviluppatore del livello [DAO] non vede più il livello [JDBC] né le tabelle del database di cui desidera utilizzare il contenuto. Vede solo la rappresentazione a oggetti del database, fornita dal livello [Hibernate]. Il collegamento tra le tabelle del database e gli oggetti gestiti dal livello [DAO] viene stabilito principalmente in due modi:

  • tramite file di configurazione in stile XML
  • tramite annotazioni Java nel codice, una tecnica disponibile solo a partire da JDK 1.5

Il livello [Hibernate] è un livello di astrazione progettato per essere il più trasparente possibile. Lo scenario ideale è che lo sviluppatore del livello [DAO] non si renda assolutamente conto di lavorare con un database. Ciò è fattibile se non è lui a scrivere la configurazione che fa da ponte tra il mondo relazionale e quello orientato agli oggetti. La configurazione di questo ponte è piuttosto delicata e richiede una certa esperienza.

Il livello [4] degli oggetti, che rispecchia il database, è chiamato "contesto di persistenza". Un livello [DAO] basato su Hibernate esegue operazioni di persistenza (CRUD: create, read, update, delete) sugli oggetti nel contesto di persistenza; queste operazioni vengono tradotte da Hibernate in istruzioni SQL eseguite dal livello JDBC. Per le operazioni di interrogazione del database (SQL SELECT), Hibernate fornisce agli sviluppatori un HQL (Hibernate Query Language) per interrogare il contesto di persistenza [4] piuttosto che il database stesso.

Hibernate è popolare ma complesso da padroneggiare. La curva di apprendimento, spesso presentata come facile, è in realtà piuttosto ripida. Non appena si dispone di un database con tabelle che presentano relazioni uno-a-molti o molti-a-molti, la configurazione del ponte relazionale/oggetto va oltre le capacità di un principiante medio. Errori di configurazione possono portare ad applicazioni con prestazioni scadenti.

Alla luce del successo dei prodotti ORM, Sun, il creatore di Java, ha deciso di standardizzare un livello ORM tramite una specifica chiamata JPA (Java Persistence API), che è stata rilasciata insieme a Java 5. La specifica JPA è stata implementata da vari prodotti: Hibernate, TopLink, EclipseLink, OpenJPA, ecc. Con JPA, l'architettura precedente diventa la seguente:

Il livello [DAO] ora interagisce con la specifica JPA, un insieme di interfacce. Gli sviluppatori hanno guadagnato in termini di standardizzazione. In precedenza, se uno sviluppatore cambiava il proprio livello ORM, doveva anche cambiare il proprio livello [DAO], che era stato scritto per interagire con un ORM specifico. Ora, scriverà un livello [DAO] che interagisce con un livello JPA. Indipendentemente dal prodotto che implementa il livello JPA, l'interfaccia presentata al livello [DAO] rimane la stessa.

In questo documento, useremo un livello [DAO] costruito sopra un livello JPA/Hibernate o JPA/EclipseLink. Useremo anche il framework Spring 2.8 per collegare questi livelli tra loro.

Il vantaggio principale di Spring è che consente di collegare i livelli tramite la configurazione anziché nel codice. Pertanto, se l’implementazione JPA/Hibernate deve essere sostituita con un’implementazione Hibernate senza JPA — ad esempio, perché l’applicazione è in esecuzione in un ambiente JDK 1.4 che non supporta JPA — questa modifica nell’implementazione del livello [DAO] non ha alcun impatto sul codice del livello [business]. È necessario modificare solo il file di configurazione di Spring che collega i livelli tra loro.

Con Java EE 5 esiste un'altra soluzione: implementare i livelli [business] e [DAO] utilizzando EJB3 (Enterprise JavaBeans versione 3):

Vedremo che questa soluzione non è molto diversa da quella che utilizza Spring. L'ambiente Java EE 5 è disponibile all'interno dei cosiddetti server applicativi come Sun Application Server 9.x (Glassfish), JBoss Application Server, Oracle Container for Java (OC4J), ... Un server applicativo è essenzialmente un server di applicazioni web. Esistono anche i cosiddetti ambienti EE 5 "stand-alone", ovvero quelli che possono essere utilizzati al di fuori di un server applicativo. È il caso di JBoss EJB3 o OpenEJB.

In un ambiente EE5, i livelli sono implementati da oggetti chiamati EJB (Enterprise Java Beans). Nelle versioni precedenti di EE, gli EJB (EJB 2.x) erano noti per essere difficili da implementare e testare, e talvolta presentavano prestazioni insufficienti. Si distingue tra bean "entity" EJB 2.x e bean "session" EJB 2.x. In breve, un EJB 2.x "entity" corrisponde a una riga di una tabella di database, mentre un EJB 2.x "session" è un oggetto utilizzato per implementare i livelli [logica di business] e [DAO] di un'architettura multistrato. Una delle critiche principali mosse ai livelli implementati con gli EJB è che possono essere utilizzati solo all'interno di contenitori EJB, un servizio fornito dall'ambiente EE (Enterprise Edition). Questo ambiente, più complesso da configurare rispetto a un ambiente SE (Standard Edition), può scoraggiare gli sviluppatori dall'effettuare test frequenti. Tuttavia, esistono ambienti di sviluppo Java che facilitano l'uso di un server applicativo automatizzando la distribuzione degli EJB sul server: Eclipse, NetBeans, JDeveloper, IntelliJ IDEA. In questa sede utilizzeremo NetBeans 6.8 e il server applicativo GlassFish v3.

Il framework Spring è stato creato in risposta alla complessità di EJB2. In un ambiente SE, Spring fornisce un numero significativo di servizi tipicamente offerti dagli ambienti EE. Ad esempio, nella sezione "Persistenza dei dati", Spring fornisce i pool di connessioni e i gestori di transazioni richiesti dalle applicazioni. L'emergere di Spring ha favorito una cultura dei test unitari, che è diventata più facile da implementare nel contesto SE rispetto a quello EE. Spring consente l'implementazione di livelli applicativi utilizzando oggetti Java standard (POJO, Plain Old/Ordinary Java Object), consentendone il riutilizzo in altri contesti. Infine, integra numerosi strumenti di terze parti in modo abbastanza trasparente, in particolare strumenti di persistenza come Hibernate, EclipseLink, iBatis, ...

Java EE 5 è stato progettato per ovviare alle carenze della specifica EJB 2. EJB 2.x si è evoluto in EJB 3. Si tratta di POJO annotati con tag che li rendono oggetti speciali quando si trovano all'interno di un contenitore EJB 3. All'interno del contenitore, l'EJB 3 può sfruttare i servizi del contenitore (pool di connessioni, gestore delle transazioni, ecc.). Al di fuori del contenitore EJB3, l'EJB3 diventa un oggetto Java standard. Le sue annotazioni EJB vengono ignorate.

Sopra, abbiamo rappresentato Spring e un contenitore EJB3 come una possibile infrastruttura (framework) per la nostra architettura multistrato. È questa infrastruttura che fornirà i servizi di cui abbiamo bisogno: un pool di connessioni e un gestore delle transazioni.

  • Con Spring, i livelli saranno implementati utilizzando POJO. Questi avranno accesso ai servizi di Spring (pool di connessioni, gestore delle transazioni) tramite l'iniezione di dipendenze in questi POJO: durante la loro creazione, Spring inietta i riferimenti ai servizi di cui avranno bisogno.
  • Con il contenitore EJB3, i livelli saranno implementati utilizzando EJB. Un'architettura a livelli implementata con EJB3 non è molto diversa da quella implementata con POJO istanziati da Spring. Troveremo molte somiglianze.
  • Infine, presenteremo un esempio di applicazione web a più livelli: