Il
problema delle connessioni nelle applicazioni web è uno di quelli
che richiedono soluzioni di alto livello, sia dal punto di vista della
progettazione che della realizzazione.
Supponiamo
di avere una servlet che interroga un database, e vediamo dove e come si
può affrontare il problema della connessione. Ci sono sicuramente
due possibilità:
-
l’oggetto
java.sql.Connection viene dichiarato come attributo e poi “istanziato”
nell’init(), oppure la prima volta che si entra nel service(), e poi chiuso
nel destroy().
-
L’oggetto
java.sql.Connection viene dichiarato e istanziato nel service() prima di
effettuare le operazioni sql sul db, e poi chiuso, sempre nel service(),
alla fine di queste.
Ambedue
queste possibilità hanno dei pro e dei contro. Nel primo caso, la
connessione viene aperta e chiusa una volta sola, e questo comporta senz’altro
un’ottimizzazione delle prestazioni, poichè l’apertura di una connessione
è una cosa ‘pesante’, soprattutto su web. D’altro canto, però,
la nostra servlet dovrà implementare SingleThreadModel, oppure il
service() dovrà essere dichiarato synchronized. Questo perchè
Connection, se dichiarata come attributo della classe, è una risorsa
condivisa da tutti i thread che vengono lanciati, e quindi bisogna impedire
che più thread tentino di usarla contemporaneamente (appunto impedendo
che vengano lanciati più thread). Quindi saremo in grado di evadere
una richiesta per volta, e questo provocherà un peggioramento delle
prestazioni, in quanto le richieste verranno serializzate (accodate). Nel
secondo caso, invece, non abbiamo alcun problema di accesso concorrente,
perchè la dichiarazione di Connection sta dentro il metodo service(),
per cui vengono create tante Connection quanti sono i thread lanciati.
Tuttavia, la connessione viene aperta e chiusa, da parte di ogni utente,
tutte le volte che si effettua una richiesta, e anche questo è un
carico di lavoro molto pesante per il server.
L’uso
di un pool di connessioni consente di sfruttare la parte migliore di ambedue
queste tecniche senza doverne sopportare anche gli svantaggi: istanziamo
l’ oggetto che ci serve una volta sola, e lo usiamo tutte le volte che
ci serve. La differenza rispetto al primo caso è che non facciamo
l’istanza di java.sql.Connection, ma di un oggetto (il pool appunto) che
gestisce le nostre richieste di connessione. La connessione già
pronta viene ottenuta nel service() (come nel secondo caso), e alla fine
di esso viene restituita (non chiusa!). Per fare questo, abbiamo bisogno
di scrivere una classe che ‘avvolga’ un oggetto Connection e alcuni altri
attributi che ci serviranno per avere ragguagli sullo stato dell’oggetto.
Usando
la tecnica che abbiamo descritto il mese scorso, possiamo scrivere una
classe simile a questa:
import
java.sql.*;
public
class PoolableConnection {
private Connection con = null;
private boolean locked = false;
private long calledTime = 0;
Il
cotruttore, che sarà usato dal pool per fare le istanze di questa
classe, valorizza un oggetto Connection:
public
PoolableConnection(Connection aConnection){
if(aConnection != null){
con = aConnection;
}
}
public Connection getConnection() {
return con;
}
Il
pool, inoltre, chiama metodo che segue per impostare lo stato di ‘occupato’
di questo oggetto, prima di darlo all’applicazione. Inoltre, l'oggetto
memorizza il momento in cui è stato ‘prelevato’. Questo tornerà
utile nel caso in cui il pool fallisca nel rilasciare una connessione,
caso che affrontiamo più avanti.
public
void setLocked(boolean lock){
locked = lock;
calledTime = System.currentTimeMillis();
}
quando
l’intero pool verrà chiuso, ogni oggetto PoolableConnection provvederà
a chiudere la sua connessione con il DB.
public
void close() throws SQLException{
try{
con.close();
}
catch(SQLException sqle){
System.err.println(sqle.getMessage());
}
}
Il
sorgente completo (di questa come delle altre classi dell’esempio) è
disponibile in questo file zip.
PoolableConnection
è scritto appositamente per essere usato da un pool. La classe ConnectionPool,
che è il pool vero e proprio, dovrebbe in primo luogo predisporre
le connessioni, quindi una lista (un Vector, oppure una LinkedList, oppure
una HashTable) in cui inserire in maniera ordinata e indicizzata una collezione
di oggetti PoolableConnection. Io ho scelto di usare un vettore, che si
presta benissimo ai nostri scopi. Se volessimo subordinare il rilascio
delle connessioni a criteri come LIFO o FIFO, allora probabilmente dovremmo
anche pensare ad una coda di richieste.
Vediamo
le caratteristiche salienti del pool.
In
primo luogo definisce una serie di variabili necessarie alla connessione
con un database via jdbc.
public
class ConnectionPool{
private String driver;
private String url;
private int size;
private int pctIncrease;
private String user;
private String password;
private Vector pool;
private java.sql..Driver drv;
private long maxTimeInUse = 10000;
La
prima, intuitiva funzione, di cui questa classe ha bisogno è un
metodo per inizializzare il pool:
public
synchronized void initializePool() throws Exception{
//
test di tutte le variabili necessarie
//
alla connessione: se ne manca anche una sola,
//
lanciamo un’eccezione
if(driver == null){
throw new Exception("Nome driverjdbc non specificato");
}
.....
//creazione delle connessioni
try{
drv = (Driver)Class.forName(driver).newInstance();
DriverManager.registerDriver(drv);
for(int i = 0;i < size; i++){
Connection con = createConnection();
if(con != null){
PoolableConnection pcon = new PoolableConnection(con);
addConnection(pcon);
}
}
}
catch(Exception e){
throw new Exception("eccezione in initializeConnection: "
+e.getMessage());
}
}
Quindi,
dopo aver fatto l’istanza del pool, la nostra applicazione dovrà
valorizzare le variabili del pool e poi chiamare initializePool(). A questo
punto il pool è pronto per essere usato. Per ottenere un oggetto
connection dal pool, è sufficiente invocare il metodo getConnection().
Vediamo come funziona:
public
synchronized Connection getConnection() throws Exception {
PoolableConnection pcon = null;
// cerca una connessione disponibile
for(int i = 0; i < pool.size(); i++){
pcon = (PoolableConnection)pool.elementAt(i);
if(!pcon.isLocked()){
pcon.setLocked(true);
return pcon.getConnection();
}
}
// Se non trova una connessione libera ne
// aggiunge una nuova se ha ancora spazio per farlo
boolean expand = (size+(size/100*pctIncrease) < pool.size());
if(expand){
expandPool();
}
}
se
le connessioni sono tutte impegnate, getConnection si chiede se gli elementi
del vettore sono in numero maggiore rispetto a size più la percentuale
massima di incremento. Se non è così, invoca il metodo expand
che crea una nuova connessione e la aggiunge al vettore.
La
parte più delicata sta nel rilascio della connessione. La volta
scorsa parlavamo appunto dei rischi che si celano dietro ad eventi come
una non corretta gestione delle eccezioni ed in altre disattenzioni. Qualunque
ipotesi di impossibilità di rilasciare la connessione si verifichi,
noi dobbiamo sapere come e quando correggerla. Vediamo cosa succede nel
metodo releaseConnection():
public
synchronized void releaseConnection(Connection con) {
boolean foundAndKilled = false;
for(int i = 0;i < pool.size(); i++){
PoolableConnection pcon;
pcon=(PoolableConnection)pool.elementAt(i);
if(pcon.getConnection() == con){
System.out.println("sto rilasciando la connessione "+i);
pcon.setLocked(false);
foundAndKilled = true;
break;
}
}
if(!foundAndKilled){
new ControlThread(maxTimeInUse,pool);
}
}
Notate
che se dopo aver letto tutto il vettore questo metodo non riesce a rilasciare
il lock sull’oggetto che gli è stato passato, invoca un thread che
legge di nuovo tutto il vettore, alla ricerca di quelle connessioni per
le quali il tempo di impiego si sia prolungato oltre quanto ci si aspetta:
questa condizione provoca una forzatura di setLocked(false), in modo che
l’oggetto ritorna ad essere disponibile e libero.
Nella
servlet, gestiamo il pool come attributo, istanziandolo ell’init():
Nota:
nell’esempio ho utilizzato una url al database di default di Oracle, nell’ipotesi
che il motore di servlet (esempio Tomcat) e il server Oracle si trovino
sulla stessa macchina.
ConnectionPool
pool = null;
public
void init(ServletConfig config) throws ServletException{
super.init(config);
try{
pool = new ConnectionPool();
ool.setDriver("oracle.jdbc.driver.OracleDriver");
pool.setURL("jdbc:oracle:thin:@localhost:1521:orcl");
pool.setUser("scott");
pool.setPassword("tiger");
pool.setSize(10);
pool.setPctIncrease(30);
pool.initializePool();
}
catch(ClassNotFoundException e){
throw new ServletException(e.toString());
}
catch(Exception e){
throw new ServletException(e.toString());
}
}
Nel
service(), istanziamo un oggetto Connection:
Connection
con = pool.getConnection();
e in
una clausola finally, alla fine di tutte le operazioni che implicano la
connessione al database, lo rilasciamo:
pool.releaseConnection(con);
L’esempio
che segue esegue una select dalla tabella EMP del famoso utente Scott/tiger:
public
void service(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException{
res.setContentType("text/html");
Connection con = null;;
try{
//il pool restituisce la prima connessione libera
con = pool.getConnection();
PrintWriter out = res.getWriter();
String html = "<html><body><h1>";
html+= con.toString();
html+="<table border=\"1\">";
Statement stat = con.createStatement();
String sql = "SELECT * FROM emp";
ResultSet rs = stat.executeQuery(sql);
while (rs.next()){
html += "<tr><td>"+rs.getString(1)+"</td>";
html += "<td>"+rs.getString(2)+"</td>";
html += "<td>"+rs.getString(3)+"</td>";
html += "<td>"+rs.getString(4)+"</td>";
html += "<td>"+rs.getString(5)+"</td>";
html += "<td>"+rs.getString(6)+"</td>";
html += "<td>"+rs.getString(7)+"</td>";
html += "<td>"+rs.getString(8)+"</td></tr>";
}
html += "</table></body></html>";
out.print(html);
rs.close();
stat.close();
out.close();
}
catch(Exception e){
throw new ServletException(e.getMessage(),e);
}
finally{
pool.releaseConnection(con);
}
}
Se
avete accesso ad un database oracle potete provare questo esempio così
com’è, altrimenti dovete sostituire sia il nome del driver che la
url! Ad esempio, se disponete di MS Access, configurate un driver ODBC
da ‘Pannello di Controllo’, ad esempio ‘mio_database’ associato al
sample db di access ‘Noorthern Wind’. Quindi il driver diventa:
“jdbc.odbc.JdbcOdbcDriver”
e la
url:
“jdbc:odbc:mio_database”
Se,
infine, non disponete di un servlet engine, potete provare il pool come
un’applicazione, anche se chiaramente perde molto del suo significato.
Se
invece avete un servle engine, provate ad aprire due finestre del browser
e a lanciare contemporaneamente la servlet, e vedrete che la pagina conterrà
la stampa di due diversi oggetti connection!
Rimane
da esaminare il contenuto del metodo destroy() della servlet:
public
void destroy(){
try{
pool.emptyPool();
}
catch(Exception e){
e.printStackTrace();
}
}
Quando
la servlet viene chiusa dalla VM, anche il pool che essa aveva istanziato
viene rilasciato.
Se
lanciate questa applicazione su un server Oracle, avete la possibilità
di controllare il comportamento della vostra applicazione, monitorando
ciò che avviene sul db.
Quando
lanciate l’applicazione, un numero size di connessioni vengono aperte.
Se conoscete la password dell’utente system o sys, provate la query:
SELECT username FROM v$session;
e vedrete
che size sessioni (connessioni) sono state aperte a nome dell’utente
scott!
Se
chiudete l’applicazione, ed eseguite la stessa select, noterete che anche
le sessioni di Scott (o dell’utente che avete usato) non esistono più.
Ci
si può domandare ancora una cosa: se il pool viene incrementato
in base alla percentuale pctIncrease, è desiderabile che noi manteniamo
tutte le connessioni che sono state aperte in più rispetto a size?
Bisognerebbe
creare un processo che monitorizzi costantemente il pool e controlli quanto
(in termini di tempo e di numero di volte) le connessioni eccedenti la
dimensione originale vengono usate! Una procedura del genere sarebbe di
per se più complessa del pool (pure migliorabile sotto diversi aspetti)
che abbiamo appena descritto.
Tuttavia,
si può pensare ad una cosa meno impegnativa, e stabilire ad esempio
che nelle connessioni create dopo l’inizializzazione del pool venga valorizzato
anche un altro parametro, che memorizzi il tempo per il quale la connessione
NON è stata utilizzata! Comunque, la cosa essenziale quando si progetta
e si realizza un’applicazione che interagisce con una base dati (in generale
quando si creano dei processi asincroni), è impedire che si allochino
risorse senza controllo da parte dell’applicazione stessa.
La
gestione degli imprevisti è un altro punto fondamentale: anche se,
di regola, il pool che abbiamo scritto non dovrebbe aver bisogno di interventi
eccezionali, non possiamo fare affidamento sulla buona sorte, e dobbiamo
gestire i casi, pur remoti, in cui risorse inutilizzate rimangano appese
sul server
Infatti,
ControlThread scatta subito, ogni volta che il metodo releaseConnection()
non riesce a rilasciare un oggetto!
Il
mese prossimo affronteremo l’implementazione di un rudimentale server usando
un pool di thread che si occuperà di gestire una coda di connessioni. |