Dopo l’articolo del mese scorso in cui abbiamo affrontato gli argomenti introduttivi della tenologia Java, questo mese proseguiamo il discorso introducendo gli aspetti avanzati di tale piattaforma, analizzando in particolare gli strumenti per la soluzione delle problematiche tipiche della programmazione generica e di quella orientata ad internet.
Verranno affrontati alcuni argomenti di particolare importanza, partendo dall’approfondimento di alcune librerie standard, ed integrando con alcuni esempi pratici la teoria esposta.
Per quanto riguarda le librerie per il networking, verrà descritta la gestione di socket TCP ed UDP e la realizzazione di demoni.
Successivamente si darà spiegazione di come sia possibile interfacciare una applicazione o una applet, con una base di dati per mezzo della libreria JDBC.
Per permettere una graduale acquisizione degli argomenti affrontati, interromperemo questo escursus su Java a questo punto, rimandando al mese successivo la trattazione delle tecnologie di Serializzazione e Remote Method Invocation (RMI) e JavaBeans.
ALCUNE CLASSI DI UTILITÀ
Vediamo prima di tutto un paio di classi di utilità generale, che torneranno utili nel seguito. Esse sono Vector e Hashtable, due modi diversi per raggruppare insieme oggetti diversi.
Vector è una specie di array dinamico di oggetti, nel quale possiamo aggiungerne e rimuoverne a piacere elementi generici (è la classe Vector alloca dinamicamente la memoria richiesta per contenerli).
I metodi principali sono:
public void addElement (Object o);
aggiunge un oggetto in coda
public void insertElementAt (int i, Object o);
inserisce un elemento alla posizione specificata
public void removeElement (Object o);
se o è contenuto nel vettore, lo rimuove
public void removeElementAt (int i);
rimuove l’elemento all’i-ma posizione
public int indexOf (Object o);
restituisce la posizione in cui è contenuto il dato oggetto
public Object elementAt (int i);
restituisce l’elemento all’i-ma posizione
public int size();
restituisce il numero di elementi contenuti nel vettore
Un Hashtable invece, è una sorta di “memoria associativa”, di fatto una tabella a due colonne in cui ogni riga rappresenta un’associazione tra due oggetti. Uno dei due oggetti viene chiamato “chiave”, ed è usato come riferimento per manipolare l’oggetto ad esso associato. I metodi principali di Hashtable sono:
public void put (Object key, Object value);
memorizza un oggetto associato ad una chiave
public Object get (Object key);
restituisce l’oggetto associato alla chiave data
public void remove (Object object);
rimuove il dato oggetto dalla tabella
Vediamo subito un esempio:
import java.util.\*;
public class Containers
{
public static void main (String\[\] args)
{
Vector v = new Vector();
v.addElement("String 1");
v.addElement(new Integer(2));
for (Enumeration e = v.elements(); e.hasMoreElements(); )
{
Object o = (Object)e.nextElement();
System.out.println(o);
}
Hashtable t = new Hashtable();
t.put("one", new Integer(1));
t.put("two", new Integer(2));
t.put("three", new Integer(3));
System.out.println(t);
System.out.println(t.get("two"));
for (Enumeration e = t.elements(); e.hasMoreElements(); )
{
Object o = (Object)e.nextElement();
System.out.println(o);
}
}
}
È particolarmente interessante la classe Enumeration: essa viene usata per accedere in maniera sequenziale agli oggetti contenuti in un contenitore, senza far riferimento alla particolare implementazione (in questo caso senza sapere che Vector implementa un accesso casuale con il metodo elementAt()). Ogni contenitore deve essere in grado di costruire la sua versione di Enumeration, in grado cioè di ispezionarlo, e restituirla con il metodo elements(). Come si vede nell’esempio precedente, lo stesso codice (il blocco for) può essere usato per accedere agli elementi in due contenitori completamente diversi come Vector e Hashtable.
NETWORKING CON JAVA
Essendo stato sviluppato appositamente per operare in rete, Java non poteva non avere una API completa e flessibile dedicata al networking. Come avviene per tutte le API di Java, quello che ci viene messo a disposizione è solo un set minimale di primitive che consentono di effettuare tutte le operazioni fondamentali; al di sopra di questo set è possibile implementare qualcosa di più sofisticato.
Avendo a che fare con macchine in rete, si rendono necessarie due cose fondmaentalmente: da un lato è necessario un formalismo per individuare in maniera esatta ed univoca un host all’interno di una rete, e dall’altro un meccanismo che ci consenta di trasferire dati da un host ad un altro. Visto che Java fa esplicitamente riferimento ad Internet, la terminologia e le interfacce sono prese direttamente dallo standard TCP/IP.
Gli indirizzi
L’indirizzo di un host è rappresentato da un’istanza della classe InetAddress. Essa fornisce una serie di metodi per la manipolazione di indirizzi:
public static byte\[\] getAddress();
restituisce l’indirizzo contenuto nell’istanza corrente in forma numerica
public static InetAddress getByName (String host);
restituisce l’indirizzo data la forma mnemonica (in pratica esegue un’interrogazione DNS)
public String getHostAddress();
restituisce l’indirizzo contenuto nell’istanza corrente nella forma “A.B.C.D.”.
public String getHostName();
restituisce l’indirizzo contenuto nell’istanza corrente in forma mnemonica
public static InetAddress getLocalHost();
restituisce un’instanza di InetAddress che rappresenta l’host locale (“localhost”)
Vediamo con un esempio come utilizzare tali strumenti
import java.io.\*;
import java.net.\*;
public class AddressExample {
public static void main (String\[\] args)
throws Exception {
InetAddress addr = InetAddress.getLocalHost();
System.out.println("LocalHost: " + addr);
System.out.println("LocalHost: " + addr.getHostName());
InetAddress infomedia = InetAddress.getByName("www.infomedia.it");
System.out.println("Infomedia: " + infomedia);
System.out.println("Infomedia: " + infomedia.getHostName());
}
}
I socket
Un socket è un flusso di byte che può essere trasmesso tra due host diversi collegati in rete. Secondo lo standard TCP/IP fondamentalmente possono esistere due tipi di socket:
- 1\. I socket UDP (User Datagram Protocol) implementano un meccanismo semplice ma “non affidabile” per trasmettere “piccoli” pacchetti di dati (datagrammi). Con “non affidabile” si intende che il sistema operativo non garantisce che ogni singolo pacchetto venga consegnato al destinatario e non dà neanche segnalazione di eventuali errori. Inoltre i pacchetti possono arrivare “fuori ordine” (cioè non nello stesso ordine in cui sono arrivati) e possono addirittura arrivare in copie multiple (senza entrare nei dettagli, in generale ciò può essere causato da errori durante il percorso o alla variazione dei percorsi di routing). I socket UDP sembrano inservibili, ma non è così: una buona parte dei protocolli standard di Internet utilizzano l’UDP (tanto per citarne uno, il DNS: Domain Name Service) semplicemente perché sono molto “leggeri” dal punto di vista del sistema operativo e perché tutto sommato sono più che sufficienti per trasmettere semplici meccanismi di “query and reply”. Proprio per queste caratteristiche i socket UDP sono detti “senza connessione” dal momento che ogni pacchetto di fatto vive indipendentemente dagli altri. La loro “inaffidabilità” si supera implementando dei controlli a livello di applicazione. Se poi si deve fare del broadcasting (trasmettere contemporaneamente a tutti gli host di una sottorete) o multicasting (trasmettere contemporaneamente ad un sottoinsieme degli host di una sottorete), i socket UDP sono una scelta obbligata.
2\. I socket TCP (Transport Control Protocol) implementano invece un meccanismo “affidabile” ed “orientato alla connessione”. “Affidabile” perché eventuali errori di comunicazione vengono gestiti dal sistema di trasmissione (recuperati se possibile, segnalati al programma applicativo in caso contrario), “orientato alla connessione” perché un socket TCP rappresenta un collegamento stabile simile ad un file su disco: tutti i byte vengono ricevuti esattamente come sono stati trasmessi. Questo tipo di socket è chiaramente indicato nei casi in cui bisogna trasferire dei dati che non siano limitati ad una manciata di byte (tipici esempi sono l’FTP ed l’HTTP).
Dal momento che un socket collega tra loro due host, dev’essere descritto da una coppia di indirizzi Internet; e dal momento che una macchina può aprire contemporaneamente più socket, esiste un altro parametro, detto porta, che permette di identificare i socket aperti. La porta è un numero a 16 bit ed esistono definizioni standard che associano una data porta ad un dato servizio; per esempio l’FTP usa le porte 20 e 21, il Telnet la porta 23, l’HTTP la porta 80. Quando un server riceve una richiesta di apertura di un socket da parte di un client, esso utilizza il numero di porta per scegliere quale programma di servizio (demone) deve essere attivato (per esempio un Netscape Fastrack piuttosto che un server FTP).
I socket in Java sono implementati con quattro classi distinte:
- 1\. Socket: socket TCP lato client
2. ServerSocket: socket TCP lato server
3. DatagramSocket: socket UDP semplici e broadcast
4. MulticastSocket: socket UDP multicast
Tutti i socket hanno in comune i seguenti metodi:
public InetAddress getLocalAddress();
ritorna l’indirizzo locale di connessione
public int getLocalPort();
ritorna la porta locale di connessione
public void getSoTimeout();
public void setSoTimeout (int timeout);
manipolano l’intervallo di timeout in lettura
Socket di tipo TCP
Un socket di tipo TCP deve essere in generale costruito con almeno tre parametri: la porta locale, l’indirizzo remoto e la porta remota:
- public Socket (InetAddress addr, int port);
public Socket (String addr, int port);
Specificano indirizzo e porta remota; la porta locale viene scelta dinamicamente.
public Socket (String remAddr, int remPort, InetAddress localAddr, int localPort);
Specifica anche la porta locale sulla quale esegue un’operazione di bind.
I parametri dell’host remoto possono essere successivamente letti e modificati con opportuni metodi:
- public void setAddress (InetAddress addr);
public InetAddress getAddress();
manipolano l’indirizzo remoto di connessione
public void setPort (int port);
public int getPort();
manipolano la porta remota di connessione
Quando il socket non serve più deve essere chiuso con il metodo
public void close();
Dal momento che un socket TCP implementa un flusso di dati “orientato alla connessione”, Java permette di associarvi una coppia di stream di i/o, che possono essere ottenuti con i metodi:
public InputStream getInputStream();
public OutputStream getOutputStream();
Da questo punto di vista, i socket TCP si gestiscono in modo molto simile ai file: possiamo associarvi stream di tipo DataInput e DataOutput per effettuare i/o di tipi primitivi.
L’esempio seguente illustra un semplice mini-programma che implementa un servizio simile ad “Echo”, presente in tutte le macchine Unix, che non fa altro che leggere dal client stringhe terminate da un newline e rispedirle indietro.
import java.net.\*;
import java.io.\*;
public class TCPClient
{
public void start()
throws Exception
{
DataInputStream stdIn = new DataInputStream(System.in);
Socket socket = new Socket("localhost", 7777);
DataOutputStream os = new DataOutputStream(socket.getOutputStream());
DataInputStream is = new DataInputStream(socket.getInputStream());
for (;;)
{
System.out.print("input: ");
String userInput = stdIn.readLine();
if (userInput.equals("QUIT"))
break;
os.writeBytes(userInput + '\n');
System.out.println("echo: " + is.readLine());
}
os.close();
is.close();
socket.close();
}
public void main (String\[\] args)
throws Exception
{
TCPClient tcpClient = new TCPClient();
tcpClient.start();
}
}
Se vogliamo creare un socket TCP dal lato server, le cose sono un po’ più complesse. Infatti, in generale, un server può rispondere contemporaneamente a più richieste. Per questo motivo di solito non si usa direttamente un socket server per trasferire dati, ma come “intermediario” per creare dinamicamente un socket che serva una singola richiesta. Questo concetto è chiarito nell’esempio successivo, dove un ruolo fondamentale è giocato dal metodo
public void accept(); che attende una nuova richiesta ed apre “al volo” un nuovo socket per gestirla. mport java.net.\*; import java.io.\*;
public class TCPServer
{
public void start()
throws Exception
{
ServerSocket serverSocket = new ServerSocket(7777);
for (;;)
{
System.out.println("Waiting... ");
Socket socket = serverSocket.accept();
System.out.println("New socket " + socket);
DataInputStream is = new DataInputStream(socket.getInputStream());
DataOutputStream os =
new DataOutputStream(socket.getOutputStream());
for (;;)
{
String userInput = is.readLine();
if (userInput == null \|\| userInput.equals("QUIT"))
break;
os.writeBytes(userInput + '\n');
System.out.println("Replying " + userInput);
}
os.close();
is.close();
System.out.println("Closed socket " + socket);
socket.close();
}
}
public static void main (String\[\] args)
throws Exception
{
TCPServer tcpServer = new TCPServer();
tcpServer.start();
}
}
Il server dimostrativo appena implementato è di nuovo un servizio “Echo”: non fa altro che leggere linee terminate da un newline e rispedirle indietro; termina quando il client chiude la comunicazione mandando la riga “QUIT”. Non è adatto per servire più richieste contemporaneamente: infatti il loop principale ritorna alla accept() solo dopo che la transazione corrente è stata terminata.
Per servire più richieste contemporaneamente si può usare un thread parallelo come nell’esempio seguente:
import java.net.\*; import java.io.\*;
class ServerThread extends Thread
{
private Socket socket;
public ServerThread (Socket socket)
{
this.socket = socket;
}
public void run()
{
try
{
service();
}
catch (Exception e)
{
e.printStackTrace(System.out);
}
}
public void service()
throws Exception
{
DataInputStream is = new DataInputStream(socket.getInputStream());
DataOutputStream os = new DataOutputStream(socket.getOutputStream());
for (;;)
{
String userInput = is.readLine();
if (userInput == null \|\| userInput.equals("QUIT"))
break;
os.writeBytes(userInput + '\n');
System.out.println("Replying " + userInput);
}
os.close();
is.close();
System.out.println("Closed socket" + socket);
socket.close();
}
}
public class TCPParallelServer
{
public void start()
throws Exception
{
ServerSocket serverSocket = new ServerSocket(7777);
for (;;)
{
System.out.println("Waiting... ");
Socket socket = serverSocket.accept();
System.out.println("New socket " + socket);
ServerThread serverThread = new ServerThread(socket);
serverThread.start();
}
}
public static void main (String\[\] args)
throws Exception
{
TCPParallelServer tcpServer = new TCPParallelServer();
tcpServer.start();
}
}
L’esempio appena visto è di fatto l’analogo dei “demoni forkati” di Unix e Windows, ma usando un thread al posto di un processo duplicato è molto più “leggero” dal punto di vista delle risorse di sistema operativo.
L’esempio precedente può essere facilmente generalizzato (dichiarando service() come abstract) per implementare un generico demone di servizio.
Socket di tipo UDP
Come si è detto precedentemente, un socket UDP serve per la trasmissione di “piccoli” pacchetti di dati detti datagrammi (secondo lo standard TCP/IP non più lunghi di 1500 byte). Java definisce una classe apposita, DatagramPacket, per la loro implementazione. Di fatto non è altro che un contenitore di un array di byte con un po’ di metodi per definire e leggere gli indirizzi:
public InetAddress getAddress();
public void setAddress (InetAddress address);
manipolano l’indirizzo remoto
public int getPort();
public void setPort (int port);
manipolano la porta remota
public byte\[\] getData();
public void setData (byte\[\] data);
public int getLenght();
public void setLenght (int lenght);
manipolano il buffer che contiene i dati
Per gestire i datagrammi esiste la classe DatagramSocket. Per crearne un’istanza è sufficiente specificare il numero della porta:
public DatagramSocket (int port);
I datagrammi possono poi essere spediti e ricevuti con i metodi send() e receive(), come nell’esempio seguente:
import java.net.\*;
public class UDPExample
{
public static void main (String\[\] args)
throws Exception
{
byte\[\] msg = {'H', 'e', 'l', 'l', 'o'};
InetAddress addr = InetAddress.getByName("192.5.6.7");
DatagramSocket s = new DatagramSocket(2222);
DatagramPacket hi = new DatagramPacket(msg, msg.length, addr, 2223);
s.send(hi);
byte\[\] buf = new byte\[1000\];
DatagramPacket recv = new DatagramPacket(buf, buf.length);
s.receive(recv);
}
}
Va notato che per spedire un datagramma è necessario specificare ogni volta la porta di destinazione: ciò non deve sorprendere, dal momento che un socket UDP non crea alcuna connessione ed ogni datagramma viene trattato indipendentemente dagli altri.
Per implementare un socket broadcast è sufficiente utilizzare indirizzi IP aventi tutti “1” nel campo host: per esempio 130.251.255.255 se la sottorete è 130.251.\.\. Per quanto riguarda i socket multicast, essi hanno una classe apposita, MultiCastSocket, che si gestisce analogamente ad un DatagramSocket:
import java.net.\*;
public class MulticastExample
{
public static void main (String\[\] args)
throws Exception
{
byte\[\] msg = {'H', 'e', 'l', 'l', 'o'};
InetAddress group = InetAddress.getByName("228.5.6.7");
InetAddress addr = InetAddress.getByName("192.5.6.7");
MulticastSocket s = new MulticastSocket(2222);
s.joinGroup(group);
DatagramPacket hi = new DatagramPacket(msg, msg.length, addr, 2223);
s.send(hi);
byte\[\] buf = new byte\[1000\];
DatagramPacket recv = new DatagramPacket(buf, buf.length);
s.receive(recv);
s.leaveGroup(group);
}
}
L’unica novità è data dai metodi joinGroup() e leaveGroup() che servono, rispettivamente, per “entrare” ed “uscire” dal gruppo di host. Per quanto riguarda gli indirizzi multicast, essi devono essere stati esplicitamente definiti dall’amministratore di sistema.
Uniform Resource Locators (URL)
I socket sono l’oggetto più a basso livello per quanto riguarda il networking. Java mette a disposizione un meccanismo un po’ più evoluto, le “connessioni URL”.
Le URL, nate con il WWW ma ormai usate per ogni tipo di protocollo, consentono di rappresentare in maniera compatta una generica risorsa disponibile in Internet. Una tipica URL ha la forma:
> protocollo://indirizzohost/percorsodellarisorsa/nomedella_risorsa:porta
Le Uniform Resource Locators vengono manipolate in Java per mezzo della classe URL. Tutta una serie di metodi permette di estrarre o modificare i campi che le compongono:
- public String getProtocol();
public String getHost();
public String getFile();
public int getPort();
public String getRef();
Una volta che una URL è stata creata, per leggere il documento corrispondente bisogna aprire una connessione associata:
public URLConnection openConnection();
Questo metodo ci restituisce un oggetto di tipo URLConnection, che di fatto “nasconde” il socket usato per la connessione. Da questo socket possiamo ottenere gli stream di i/o mediante i metodi:
- public InputStream getInputStream();
public OutputStream getOutputStream();
che a questo punto possiamo utilizzare come al solito. Nell’esempio successivo è illustrato un pezzo di codice che legge una pagina WWW:
import java.io.\*; import java.net.\*;
public class URLExample
{
public void start()
throws Exception
{
URL url = new URL("http://www.infomedia.it");
URLConnection conn = url.openConnection();
DataInputStream is = new DataInputStream(conn.getInputStream());
for (;;)
{
String line = is.readLine();
if (line == null)
break;
System.out.println(line);
}
> > > > }
public static void main (String\[\] args)
throws Exception
{
URLExample ue = new URLExample();
ue.start();
}
}
Java prevede che si possano registrare oggetti in grado di gestire protocolli customizzati e oggetti di tipo MIME. La classe UrlConnection definisce il metodo:
public Object getObject();
che è in grado di sfruttarli. Per esempio, è possibile definire un gestore di oggetti di tipo MIME in grado di riconoscere i documenti di tipo MPEG e passare il controllo ad un’istanza di una nostra classe MPEGObject, in grado di leggere il documento. Ad alto livello è sufficiente invocare getObject() per ottenere tale istanza. Il meccanismo di implementazione non è particolamente complesso, ma è lungo da descrivere ed esula dagli scopi di questa trattazione.
LA CONNETTIVITÀ CON I DATABASE: LA JDBC API
A cosa serve collegarsi in rete? Tipicamente a consultare o modificare dati che risiedono su un server. Appare quindi ovvio che Java sia equipaggiato con una API specifica per il collegamento con database (remoti o non).
Una delle API più diffuse per l’accesso a database relazionali è la ODBC (Open DataBase Connectivity), introdotta da Microsoft ed universalmente supportata. Pur essendo diffusamente utilizzata, essa ha un paio di problemi strutturali:
1\. È di difficile apprendimento, in quanto non esiste separazione tra funzionalità di base e funzionalità avanzate; come conseguenza, anche le operazioni più semplici e più frequenti implicano un’eccessiva complessità.
2. Non ha una forte tipizzazione dei dati, ad esempio in C si fa frequente uso dei puntatori void\*, demandando al programma applicativo la responsabilità di determinare per ispezione il tipo corretto delle variabili, aprendo così la porta a sottili bug di programmazione.
Specialmente la seconda caratteristica rende ODBC in pratica improponibile in Java, che invece tipizza fortemente i dati. Per questi motivi, Sun ha creato un’apposita API Java per l’interfacciamento con i database: la JDBC API (Java DataBase Connectivity). Essa è efficiente, si integra consistentemente con l’ambiente Java, è semplice da usare, non pone restrizioni sulle operazioni eseguibili, è facilmente utilizzabile nei sistemi di sviluppo visuale e rapido (RAD).
Per presentare un’interfaccia omogenea pur mantenendo una grande flessibilità d’uso, la JDBC API è organizzata in diversi “strati”: il programma applicativo vede solo lo strato superiore, che è indipendente dal database che interroga, mentre il compito di interfacciarsi effettivamente con il database è delegato ad una serie di driver sottostanti, che possono essere scritti in codice Java o nativo.
La JDBC fa esplicito riferimento al linguaggio SQL, che è il più diffuso strumento per dialogare con un database. Vediamo subito un esempio pratico di interrogazione SQL scritta in Java:
import java.io.\*; import java.sql.\*;
public class JDBCExample1
{
public void start()
throws Exception
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con = DriverManager.getConnection("jdbc:odbc:dbdemo",
"user",
"password");
Statement st = con.createStatement();
ResultSet rs = st.executeQuery("SELECT \* FROM users " +
"ORDER BY last_name,first_name");
while (rs.next())
{
int id = rs.getInt(1);
String first_name = rs.getString(2);
String last_name = rs.getString(3);
String address = rs.getString(4);
String phone = rs.getString(5);
System.out.println("First name: " + first_name + "\n" +
"Last Name: " + last_name + "\n" +
"Address: " + address + "\n" +
"Phone: " + phone);
}
rs.close();
con.close();
}
public static void main (String\[\] args)
throws Exception
{
JDBCExample1 jdbcExample1 = new JDBCExample1();
jdbcExample1.start();
}
}
La prima operazione da eseguire è assicurarsi che il driver prescelto sia stato caricato in memoria; un approccio tipico è fare uso del metodo statico Class.forName() che forza il caricamento di una classe. Subito dopo si usa un oggetto di tipo Connection per aprire la connessione con un database. Come si vede nell’esempio, per individuare una fonte di dati si usa un URL che specifica il protocollo ed il nome della macchina su cui risiede il database. Il codice SQL viene poi passato ad un oggetto di tipo Statement, che lo verifica e lo traduce in formato comprensibile per JDBC. Infine, il metodo executeQuery() interroga il database e produce un oggetto ResultSet che contiene i risultati e che può essere ispezionato campo per campo. L’accesso ai campi è, come al solito, verificato a runtime ed in caso di errori (per esempio se si cerca di leggere una stringa in un intero) viene generata un’eccezione.
L’esempio seguente dimostra come sia possibile aggiornare i dati in un database:
import java.io.\*; import java.sql.\*;
public class JDBCExample2
{
public void start()
throws Exception
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con = DriverManager.getConnection("jdbc:odbc:dbdemo",
"user",
"password");
Statement st = con.createStatement();
st.execute("INSERT INTO users (last_name, first_name, address, phone) "
+ "VALUES('Marco', 'Verdi', 'Via del Fosso', '234234')");
con.close();
}
public static void main (String\[\] args)
throws Exception
{
JDBCExample2 jdbcExample2 = new JDBCExample2();
jdbcExample2.start();
}
}
Negli esempi precedenti si è supposto di conoscere a priori l’esistenza di una tabella (“users”), composta di righe contenenti un intero ed una stringa. È comunque possibile interrogare dinamicamente un database, sia per quanto riguarda il numero di oggetti contenuti che per la loro composizione, consentendo quindi una grande flessibilità d’uso. A questo scopo esistono due classi speciali, DatabaseMetaData e ResultSetMetaData, che consentono di ispezionare la struttura dei dati piuttosto che il loro valore. Vediamo un semplice esempio di interrogazione dinamica:
import java.io.\*; import java.sql.\*;
public class JDBCExample3
{
public void start()
throws Exception
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con = DriverManager.getConnection("jdbc:odbc:dbdemo",
"user",
"password");
DatabaseMetaData dmd = con.getMetaData();
// // QUI ALCUNE RIGHE SONO COMMENTATE PERCHE' SE SI UTILIZZA ACCESS , IL // DRIVER NON SUPPORTA PIENAMENTE METADATA // // ResultSet tables = dmd.getTables(null, "", null, null);
// while (tables.next()){
// String table = tables.getString("TABLE_NAME");
String table = "users";
System.out.println("TABLE: " + table);
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT \* FROM " + table);
ResultSetMetaData rsmd = rs.getMetaData();
int n = rsmd.getColumnCount();
for (int i = 1; i \< n; i++)
{
String col = rsmd.getColumnName(i);
String typ = rsmd.getColumnTypeName(i);
System.out.println(col + ":" + typ);
}
}
con.close();
}
public static void main (String\[\] args)
throws Exception
{
JDBCExample3 jdbcExample3 = new JDBCExample3();
jdbcExample3.start();
}
}
In definitiva, se si conosce già l’SQL sono necessari pochi minuti per scrivere un programma Java che interagisce con un database. Grazie alla perfetta integrabilità di JDBC con gli strumenti per lo sviluppo rapido di applicazioni (RAD), è possibile creare potenti applet per interagire con database già esistenti in un time to market molto breve.
Considerazioni implementative
Il problema, a questo punto, è che i browser, per motivi di sicurezza, consentono ad un applet di collegarsi in rete solo con il server dal quale è stato scaricato (i browser più recenti consentono, o lo faranno a breve, di superare questo vincolo introducendo il concetto di trusted applet per mezzo di firme digitali; ma in questo momento non considereremo questo approccio).
Questo vincolo ci costringe di fatto a mettere il server WWW sulla stessa macchina dove risiede il database da consultare. Spesso questa non è una soluzione accettabile: può capitare di dover accedere contemporaneamente a database installati su macchine diverse, dove magari non è possibile installare un server WWW, tipicamente per motivi di sicurezza (ad esempio si vuole mantenere il database dietro il proprio firewall).
Una possibile soluzione consiste nell’utilizzare la cosiddetta “architettura client/server a tre strati”: tra il client (visualizzazione) ed il server (implementazione del database) si inserisce un livello intermedio che fa da “filtro”, implementando quelle che vengono chiamate “business rules”, cioè gli algoritmi che decidono quali dati visualizzare, in funzione del tipo di applicazione che si sta considerando.
Mentre il database (o i database) rimangono sulle macchine dove sono stati installati, è sufficiente scrivere una piccola applicazione in Java su una terza macchina (quella centrale), che è poi quella dove è stato installato il server WWW. È quest’applicazione, che non essendo un applet non ha limiti di operatività, ad effettuare l’interrogazione del database vera e propria, mentre l’applet si appoggia ad essa per ottenere i dati di cui hanno bisogno. Questa soluzione offre molti vantaggi:
1\. Non richiede nessuna modifica e/o aggiornamento né ai database già esistenti né alle macchine sulle quali risiedono.
2. Permette di utilizzare driver JDBC scritti in codice nativo, che vanno installati solo sulla macchina intermedia e non devono essere così distribuiti sui client (dove, tra l’altro, si avrebbero evidenti problemi di portabilità).
3. Permette di centralizzare sulla macchina intermedia i controlli sulle politiche di accesso, per cui solo la macchina intermedia ha necessità di penetrare l’eventuale firewall che protegge i database.
In conclusione
Per questo mese interrompiamo qui l’analisi delle caratteristiche avanzate di Java, dato che in effetti gli argomenti trattati sono molti. Rimandiamo l’appuntamento al mese prossimo per terminare questa trattazione introduttiva sulle tecnologie avanzate di Java.
