MokaByte 86 - Giugno 2004 
Jakarta Commons
VII parte: DBCP

di
Alessandro "Kazuma"
Garbagnati

Chiunque sviluppi in Java è certo a conoscenza che utilizzare un database attraverso questo linguaggio non è assolutamente un problema, grazie a JDBC che è parte integrante della J2SE. Questo framework mette a disposizione tutti gli strumenti necessari per poter accedere al database, completare ogni tipologia di operazione non solo all'interno degli archivi, ma anche all'interno della stessa struttura dei dati.
Qualche volta però, si incontrano piccoli ostacoli, piuttosto comuni, legati alla gestione della connessione al database nel caso di applicazioni che utilizzano la rete internet.

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.


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it