Perchè
la connessione può essere un problema?
Nelle più classiche applicazioni client-server, scritte
in modo accurato, la connessione con il database non dovrebbe
quasi mai generare problemi. Normalmente una volta autenticato
l'utente, viene aperta una connessione che rimane viva sino
a quando l'applicazione viene chiusa. Sono veramente pochissime
le possibilità che queste connessioni rimangano aperte,
provocando complicazioni sul database. Ma nel caso di applicazioni
che viaggiano attraverso il web, gestire la chiusura di una
connessione è una operazione molto più complessa.
A differenza delle applicazioni classiche, infatti, la chiusura
della connessione viene lasciata all'utente che, al termine
della sua sessione di lavoro, dovrebbe esplicitamente effettuare
una operazione di logoff dal database.
Ma non è solo questo l'unico problema legato a questo
argomento. Il rischio di timeouts, dovuti ad attese prolungate,
che possono anche derivare da momenti di lentezza della rete
o da una più bassa velocità di trasferimento,
possono rivelarsi pericolose, perchè non permetterebbero
la corretta gestione delle risorse che sono state aperte.
Ed ancora, la possibilità che un elevatissimo numero
di utenti possano accedere alla stessa applicazione web in
contemporanea può comunque rivelarsi problematico,
soprattutto se il database ha un numero limitato di connessioni
contemporanee possibili.
Il primo istinto sarebbe quello di risolvere questi problemi
semplicemente ricreando la connessione al database prima di
ogni operazione e chiuderla una volta terminata la singola
operazione. Un po' di codice ben scritto permetterebbe anche
di gestire tutti i possibili errori derivanti dalla rete (timeouts,
problemi di comunicazione) e di fornire quindi una buona garanzia
di non lasciare nessuna connessione o risorsa aperta. Sebbene
questo possa sembrare un ottimo escamotage, si genererebbe
un differente tipo di problema, legato principalmente al fatto
che la connessione è una operazione piuttosto lenta
e quindi ricrearne una nuova ogni volta può portare
ad un forte degrado delle prestazioni dell'applicazione stessa.
La soluzione ottimale si trova, come spesso succede, nel mezzo...
ossia tra la possibilità di avere una connessione unica
e la necessità di aprire una nuova connessione ogni
qualvolta si necessiti dell'accesso al database, attraverso
l'uso di un "Connection Pool", come quello offerto
da DBCP uno dei più utilizzati componenti del progetto
Commons del gruppo Apache Jakarta.
Figura 1 - La homepage ufficiale del componente DBCP,
all'indirizzo http://jakarta.apache.org/commons/dbcp/
Cos'è
un "Connection Pool"?
Il termine inglese "Pool" non significa solo "piscina",
ma viene spesso utilizzato per indicare il concetto di "insieme".
Il "Car Pool", infatti, non è una piscina
contenuta all'interno di una automobile, ma l'utilizzo da
parte di più persone dello stesso mezzo per, ad esempio,
recarsi al lavoro. Il "Connection Pool" è,
quindi, da considerare come un oggetto che gestisce un insieme
di connessioni, la cui logica di funzionamento è piuttosto
semplice.
Quando viene istanziato, questo gestore si occupa di aprire
una serie di connessioni verso il database. Ogni qualvolta
altri oggetti necessitano di accedere al database, richiedono
la connessione al gestore che ne fornisce una libera. Quando
l'operazione sarà terminata l'oggetto restituirà
la connessione al gestore che potrà quindi metterla
a disposizione di altri oggetti che la richiederanno.
In questo modo non vi sarà alcun degrado nelle prestazioni,
in quanto le connessioni non saranno ricreate ogni volta,
ma solamente in fase di attivazione dell'applicazione, così
come non vi saranno rischi di connessioni inattive bloccate
per lungo termine, in quanto, una volta completata l'operazione,
questa viene rilasciata ritornando nuovamente a disposizione.
Scrivere un connection pool elementare è, a dire il
vero, una operazione piuttosto semplice, se ci si limita alla
gestione delle connessioni con il database (facilmente attuabile
anche con una qualsiasi Collection) ed ai metodi di base che
si occupano di fornire una connessione e di rilasciarla una
volta completato il suo compito. Ma non è certo questo
uno strumento professionale che possa essere usato in ambienti
produttivi, dove diventano fondamentali molti aspetti che
vanno dalla sicurezza che eventuali timeouts siano gestiti,
che le risorse vengano chiuse correttamente e che sia possibile
definire la dimensione del pool di connessione e fornire anche
la possibilità di scegliere se il gestore possa aprire
nuove connessioni nel caso in cui quelle iniziali non siano
più sufficienti.
Dal punto di vista dello sviluppatore diventa anche estremamente
importante che questo oggetto sia non solo facile e facilmente
configurabile, ma che sia assolutamente trasparente, ossia
che si comporti esattamente come un normalissimo database
driver, in modo da non dover modificare o intervenire troppo
a livello di codice, soprattutto nelle operazioni di richiesta
e rilascio della connessione.
Jakarta
Commons DataBase Connection Pool
All'interno del gruppo Jakarta sono numerosi i progetti che
devono interagire con i database e l'assenza di una soluzione
interna ha spinto il gruppo a ideare e sviluppare una soluzione
che potesse sopperire a questa mancanza. E così, all'interno
del progetto Commons, sono nati due componenti: "Pool"
e "DBCP (DataBase Connection Pool)". Il primo fornisce
una serie di librerie e interfacce per la costruzione di pool
di oggetti generici, mentre il secondo è una vera e
propria implementazione del primo e propone una soluzione
valida e professionale per la gestione di un pool di connessioni
a database. Entrambi i componenti sono oramai giunti alla
versione 1.1 e sono già stati utilizzati all'interno
di diversi progetti e prodotti, non solo all'interno del gruppo
Jakarta e non solo in ambito Open Source.
Figura 2 - La homepage ufficiale del componente Pool,
su cui si appoggia DBCP, all'indirizzo http://jakarta.apache.org/commons/pool/
La
prima operazione da eseguire, come sempre, consiste nell'acquisire
le librerie necssarie per il funzionamento del componente.
Anche per DBCP vi sono due possibili forme, quella binaria
(attraverso l'indirizzo http://jakarta.apache.org/site/binindex.cgi),
oppure quella di sorgente (all'indirizzo http://jakarta.apache.org/site/sourceindex.cgi).
DBCP necessita di altri due componenti. Il primo, accennato
in precedenza, è "Pool", mentre il secondo
è il componente "Collections". Entrambi possono
essere acquisiti agli stessi indirizzi e devono essere presenti
nel classpath.
Un
primo esempio
DBCP consente di accedere ad un database come implementazione
delle due interfacce standard JDBC, ossia java.sql.Driver
e javax.sql.DataSource. In entrambi i casi, comunque, è
necessario partire da un oggetto di tipo org.apache.commons.pool.ObjectPool,
la cui interfaccia viene definita all'interno del componente
"Pool" e dove è possibile anche trovare una
implementazione generica che può essere più
che sufficiente.
ObjectPool
pool = new GenericObjectPool(null);
Un
altro oggetto necssario per la costruzione del nostro pool
di connessione è una connection factory, il cui scopo
è quello di creare e, quindi, fornire le connessioni
al database al sistema vero e proprio di pooling. Vi sono
differenti implementazioni di questa factory, a seconda della
tipologia di oggetto che si intende utilizzare per creare
le connessioni. Esiste il DriverManagerConnectionFactory,
il DriverConnectionFactory ed il DataSourceConnectionFactory,
rispettivamente se si intende utilizzare un DriverManager,
un Driver oppure un DataSource per creare la connessione.
Nel nostro esempio utilizzeremo la soluzione più semplice,
ossia la prima:
DriverManagerConnectionFactory
connFactory;
connFactory = new DriverManagerConnectionFactory("jdbc:connessione");
Le
connessioni che vengono costruite da questa factory sono le
normali connessioni al database ed è necessario che
queste vengano "avvolte" all'interno di un oggetto
che sia in grado di fornirgli i comportamenti necessari per
essere gestite come un pool. Per questo è necessario
creare un oggetto di tipo PoolableConnectionFactory a cui
verranno passati gli oggetti sino ad ora creati, oltre ad
una serie di informazioni addizionali:
PoolableConnectionFactory
poolConnFactory;
poolConnFactory = new PoolableConnectionFactory(connFactory,connPool,...);
Avendo
scelto di utilizzare la classica interfaccia java.sql.Driver,
si deve costruire e registrare il PoolingDriver, in questo
modo:
PoolingDriver
driver = new PoolingDriver();
driver.registerPool("mioPool", connPool);
A
questo punto tutto sarà pronto e, ogni qualvolta si
renderà necessaria una connessione al database, sarà
sufficiente richiederla in modo standard al DriverManager:
Connection
conn;
conn = DriverManager.getConnection("jdbc:apache:commons:dbcp:mioPool");
La
connessione ottenutà sarà una connessione al
database a tutti gli effetti (implementazione di java.sql.Connection)
e quindi non richiederà comportamenti differenti dal
solito. Quando la connessione non sarà più necessaria,
al termine delle operazioni di accesso o modifica dei dati,
basterà semplicemente chiudere la connessione utilizzando
il metodo close:
conn.close();
Come
accennato in precedenza, questa connessione sarà a
conoscenza del corretto comportamento da adottare in situazioni
come questa. Il metodo close non chiuderà la connessione,
ma la rilascerà, in modo che sia a disposizione per
la prossima richiesta.
Per
l'esempio che segue, viene utilizzato il database mySql. Se
volete utilizzare questo esempio con un differente database,
basterà sostituire il nome del driver, la stringa di
connessione, compresi username e password) e il comando SQL
di test.
import
java.sql.*;
import org.apache.commons.dbcp.*;
import org.apache.commons.pool.*;
import org.apache.commons.pool.impl.*;
public
class Esempio1 {
public static final void main(String[] args) {
// registra il driver
try {
Class.forName("com.mysql.jdbc.Driver");
}
catch (Exception e) {
e.printStackTrace();
}
// inizializza e registra il pooling
driver
ObjectPool connPool = new GenericObjectPool(null);
DriverManagerConnectionFactory connFactory;
connFactory = new DriverManagerConnectionFactory(
"jdbc:mysql://localhost/mysql",
"test", "test");
PoolableConnectionFactory poolConnFactory;
poolConnFactory = new PoolableConnectionFactory(connFactory,
connPool,
null,
null, false, true);
PoolingDriver driver = new PoolingDriver();
driver.registerPool("test",
connPool);
// esegui una connessione ed una operazione
di test
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection("jdbc:apache:commons:dbcp:test");
st = conn.createStatement();
rs = st.executeQuery("SELECT
* FROM user");
while (rs.next()) {
String user
= rs.getString("User");
String host
= rs.getString("Host");
System.out.println("Utente:
" + user + ", Host: " + host);
}
}
catch (SQLException sqlE) {
sqlE.printStackTrace();
// gestione errore
}
finally {
try {
rs.close();
}
catch (Exception e) {
/* errore...
*/
}
try {
st.close();
}
catch (Exception e) {
/* errore...
*/
}
try {
conn.close();
}
catch (Exception e) {
/* errore...
*/
}
}
}
}
Maggior
personalizzazione
Questo primo esempio ha mostrato quali siano gli step necessari
per la creazione di un pool di connessione semplice e che
utilizza i valori standard previsti dal componente. E' ovvio
che questi valori non saranno sempre sufficienti per tutte
le evenienze e diventa quindi importante che il sistema possa
essere configurato per ogni necessità senza, ovviamente,
dover intervenire sul codice ogni volta. Vi sono differenti
soluzioni per raggiungere questo scopo più o meno complessi,
anche perchè sono differenti gli elementi che possono
essere configurati.
Figura 3 - Le opzioni di configurazioni più comuni
sono disponibili all'indirizzo http://jakarta.apache.org/commons/dbcp/configuration.html.
Peccato che non siano fornite maggiori informazioni su come
e quando utilizzarle...
Nel
nostro esempio è stata utilizzata l'implementazione
generica di base dell'interfaccia ObjectPool che può
essere configurata utilizzando un oggetto di tipo GenericObjectPool.Config.
Tramite i getters ed i setters di questo oggetto è
possibile definire alcune delle caratteristiche principali
del pool tra cui:
Un
altro oggetto che può essere configurato è il
PoolableConnectionFactory. Al suo costruttore, oltre alla
ConnectionFactory ed all'ObjectPool, si possono fornire una
serie di informazioni aggiuntive, tra cui:
Se
poi al fine di rendere il nostro gestore più o meno
potente, vengono utilizzati per l'inizializzazione del sistema
di pooling ulteriori oggetti oppure differenti implementazioni
di alcune interfacce, è possibile che vi siano altre
possibili configurazioni che possono personalizzare e definire
ancora meglio il sistema stesso.
Per effettuare queste personalizzazioni è possibile
creare un piccolo oggetto che si occupa della configurazione
del sistema, leggendo un file di properties oppure un documento
XML e definendo tutti i valori nel modo e nella posizione
opportuna. Un'altra possibile soluzione consiste nel rendere
questa configurazione automatica, creando delle implementazioni
degli oggetti, in grado di leggere la propria configurazione
in modo indipendente. Ma entrambe queste soluzioni comunque,
richiedono la scrittura di un po' di codice.
Esiste
però una soluzione differente e indubbiamente più
semplice che consiste nell'utilizzare un documento JOCL (Java
Object Configuration Language), che ha un formato basato sull'XML,
tipo questo:
<object
class="org.apache.commons.dbcp.PoolableConnectionFactory"
xmlns="http://apache.org/xml/xmlns/jakarta/commons/jocl">
<!--
Il primo argomento è il ConnectionFactory -->
<object class="org.apache.commons.dbcp.DriverManagerConnectionFactory">
<string value="jdbc:mysql://localhost/mysql"/>
<string value="test"/>
<string value="test"/>
</object>
<!--
Il secondo argomento è l'ObjectPool -->
<object class="org.apache.commons.pool.impl.GenericObjectPool">
<object class="org.apache.commons.pool.PoolableObjectFactory"
null="true"/>
<!--
massimo numero connessioni attive -->
<int value="10"/>
<!-- quando non ci sono piu' connessioni libere 0 = errore,
1 = blocca, 2 = cresci --> <byte
value="1"/>
<!--
massima attesa -->
<long
value="2000"/>
<!--
massimo numero di connessioni in attesa -->
<int
value="10"/>
<!--
esegui il test sulla richiesta -->
<boolean value="true"/>
<!--
esegui il test dopo il ritorno -->
<boolean value="false"/>
<!--
millisecondi di attesa tra i cicli del processo di controllo
-->
<long value="10000"/>
<!--
numero di connessioni da verificare perchè si esegua
il processo di controllo -->
<int
value="5"/>
<!--
tempo minimo di controllo -->
<long
value="5000"/>
<!--
test quando in attesa -->
<boolean value="true"/>
</object>
<!--
Il terzo argomento è il KeyedObjectPoolFactory -->
<object class="org.apache.commons.pool.KeyedObjectPoolFactory"
null="true"/>
<!--
Il quarto argomento è la stringa di validazione -->
<string value="SELECT COUNT(*) FROM DUAL"/>
<!--
Il quinto argomento è il flag per il read-only -->
<boolean value="false"/>
<!--
Il sesto argomento è il flag di auto commit -->
<boolean value="true"/>
</object>
Questo
documento deve essere poi salvato in una qualsiasi posizione
all'interno del classpath.
Figura 4 - Il Javadoc del package org.apache.commons.jocl,
utilizzato dal sistema per poter
leggere ed applicare il documento JOCL. La pagina principale
delle API è accessibile
all'indirizzo http://jakarta.apache.org/commons/dbcp/apidocs/index.html
All'interno
del nuovo esempio non sarà più necessario definire
nulla, ma esclusivamente registrare il driver, così
come è stato registrato quello specifico per il database:
Class.forName("org.apache.commons.dbcp.PoolingDriver");
Quindi,
per ottenere una connessione, basterà richiedere una
connessione all'oggetto DriverManager fornendo, così
come nell'esempio precedente, la stringa di connessione assumendo
che al file JOCL sia stato dato il nome "Esempio2.jocl"
e sia stato inserito nella root del classpath:
Connection
conn;
conn = DriverManager.getConnection("jdbc:apache:commons:dbcp:/Esempio2");
Ecco
l'esempio trasformato:
import
java.sql.*;
public
class Esempio2 {
public static final void main(String[] args) {
System.setProperty("org.xml.sax.driver","org.apache.crimson.parser.XMLReaderImpl");
// registra i driver
try {
Class.forName("com.mysql.jdbc.Driver");
Class.forName("org.apache.commons.dbcp.PoolingDriver");
}
catch (Exception e) {
e.printStackTrace();
}
// esegui una connessione ed una operazione
di test
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
String connStr = "jdbc:apache:commons:dbcp:/Esempio2";
conn = DriverManager.getConnection(connStr);
st = conn.createStatement();
rs = st.executeQuery("SELECT
* FROM user");
while (rs.next()) {
String user
= rs.getString("User");
String host
= rs.getString("Host");
System.out.println("Utente:
" + user + ", Host: " + host);
}
}
catch (SQLException sqlE) {
sqlE.printStackTrace();
}
finally {
try {
rs.close();
}
catch (Exception e) {
/* errore...
*/
}
try {
st.close();
}
catch (Exception e) {
/* errore...
*/
}
try {
conn.close();
}
catch (Exception e) {
/* errore...
*/
}
}
}
}
L'esecuzione
di questo codice fornirà lo stesso identico risultato
dell'esempio precedente.
Figura 5 - All'indirizzo http://cvs.apache.org/viewcvs/jakarta-commons/dbcp/doc/
sono
disponibili alcuni esempi, semplici ed immediati, di utilizzo
del componente
anche per ottenere dei DataSource.
Conclusioni
Tempo fa all'interno del Servlet Container Tomcat (implementazione
di riferimento delle specifiche Sun su Servlet e JSP), veniva
fornito un sistema di connection pool open source che, però,
non era molto ben supportato e, soprattutto, poco performante.
All'epoca lo sviluppo di DBCP non sembrava essere partito
sotto i migliori auspici. Lo stato del progetto era piuttosto
indietro e lo sviluppo procedeva molto lentamente. Nonostante
il mio grandissimo rispetto per i progetti del gruppo, non
ho avuto interesse a continuare a seguire lo stato di avanzamento
del componente.
Verso la fine del 2003, quando è stata rilasciata la
versione 1.1 di DBCP, mi è capitata l'occasione di
provare questo componente e, con grandissima gioia, mi sono
reso conto che da allora il componente era diventato un prodotto
assolutamente valido e professionale, pronto per essere utilizzato
all'interno di un ambiente di produzione, non solo di test
e di sviluppo. Molti progetti, non solo appartenenti al gruppo
Jakarta, e non solo Open Source, utilizzano questo componente
al loro interno oppure ne prevedono il suo uso.
Il primo approccio con DBCP non è immediato e, soprattutto
nella fase iniziale, non è molto semplice capire come
devono essere costruiti e configurati i vari oggetti che sono
necessari per la creazione del gestore del pool di connessioni.
Una volta superato questo ostacolo, magari utilizzando la
soluzione offerta dal documento JOCL, ecco che DBCP si rivela
uno strumento molto potente e totalmente trasparente, semplificando
il lavoro di chi deve "aggiungere" questo strumento
in un secondo tempo.
Come già successo in altre occasioni, anche in questo
caso il più grande difetto di questo componente è
la limitatissima documentazione operativa. L'esempio lampante
è la pagina che presenta i parametri di configurazione
che sono ben presentati, ma che non forniscono sufficienti
informazioni su come e dove questi parametri possono essere
utilizzati.
Ma se questi sono i difetti... ben vengano.
Risorse
Scarica gli esempi descritti
nell'articolo.
|