MokaByte
Numero 16 - Febbraio1998
|
|||
|
|
||
Giudici e Puliti |
|
||
Eccoci infine alla terza
puntata di questa miniserie introduttiva a Java: ricordo che nelle puntate predenti
abbiamo prima effettuato una panoramica della tecnologia (),
per poi affrontare alcune delle caratteristiche avanzate del linguaggio ().
Questo mese
proseguiremo in questa direzione: ricordo che avevamo parlato la volta
scorsa di networking e di interfacciamento con database per mezzo delle
JDBC API, mentre in questa puntata parleremo di programmazione distribuita
(RMI API), di sicurezza, e di JavaBeans.
SERIALIZZAZIONE
E INVOCAZIONE REMOTA DI METODI (RMI API)
Iniziamo a parlare
di programmazione distribuita: una delle architetture standard utilizzate
in tale ambito, prevede l’utilizzazione di un server e di un client (ad
esempio una applet inserita in una pagina html), i quali devono comunicare
fra loro in maniera più o meno complicata.
Scrivere un
protocollo di comunicazione per far “parlare” l’applet con il server ha
un suo costo ed inoltre aumenta il codice da sottoporre a debugging. Con
la serializzazione automatica e l’invocazione remota di metodi, Java ci
consente di implementare tutti i moduli del progetto senza dover uscire
dall’ambiente Java, permettendo minori investimenti di formazione per i
programmatori ed un time to market ridotto.
Serializzazione
Siccome l’utilità
di un database è rendere disponibili i dati che contiene, si pone
il problema di tradurre una struttura dati complessa in una sequenza di
byte che possa essere scritta in un file o spedita su una connessione di
rete, per poterlo ricostruire in un secondo momento (oggetto persistente)
o per ottenerne una copia su una macchina diversa. Queste operazioni si
chiamano, rispettivamente, serializzazione e deserializzazione, anche se,
in pratica, si usa il primo termine per riferirsi ad entrambe. Attraverso
la (de)serializzazione possiamo implementare la persistenza di un oggetto:
nel tempo, se vogliamo che l’oggetto “sopravviva” tra diverse istanze di
esecuzione di un programma, o nello spazio, se vogliamo trasferirlo così
com’è da una macchina ad un’altra in una rete.
Java è
in grado di serializzare automaticamente una struttura dati arbitrariamente
complessa: è sufficiente che le classi coinvolte nell’operazione
asseriscano di implementare l’interfaccia Serializable (che peraltro non
contiene alcun metodo, e quindi non impone nessun lavoro aggiuntivo al
programmatore). Quasi tutti gli oggetti standard di Java soddisfano questa
proprietà, così è necessario occuparsi esplicitamente
solo di poche eccezioni.
Vediamo, per
esempio, come è possibile implementare una rappresentazione degli
oggetti contenuti nel database degli esempi precedenti in forma serializzabile
automaticamente:
import java.io.*;Gli oggetti della classe Record possono essere costruiti e stampati (per debugging). Un brevissimo pezzo di codice dimostra come sia facile creare una struttura dati basata su Record e memorizzarla su hard disk:
public class Record implements Serializable
{
private String firstName;
private String lastName;
private int phone;
public Record (String firstName, String lastName, int phone)
{
this.firstName = firstName;
this.lastName = lastName;
this.phone = phone;
}
public String toString()
{
return "[" + firstName + ", " + lastName + ", " + phone + "]";
}
}
import java.io.*;Lanciando il programma precedente viene creato il file “data.ser”, che contiene tutta la struttura dati. Con la stessa semplicità è possibile leggerla e ricrearla in memoria:import java.util.*;
public class SerializationWriteExample{
public static void main (String[] args)
throws Exception
{
Hashtable table = new Hashtable();
table.put("user1", new Record("Mario", "Rossi", 21));
table.put("user2", new Record("Roberta", "Verdi", 19));
table.put("user3", new Record("Giorgio", "Bianchi", 24));
System.out.println(table);
FileOutputStream fos = new FileOutputStream("data.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(table);
oos.close();
}
}
import java.io.*;Come si vede, i dati sono “sopravvissuti” alla terminazione del primo programma, pertanto possiamo dire di avere implementato la loro persistenza nel tempo.import java.util.*;
public class SerializationReadExample{
public static void main (String[] args)
throws Exception
{
FileInputStream fis = new FileInputStream("data.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Hashtable table = (Hashtable)ois.readObject();
System.out.println(table);
ois.close();
}
}
Invocazione
di Metodi Remoti (RMI API)
Se si vuole
progettare un’applicazione distribuita con un linguaggio a basso livello,
bisogna occuparsi direttamente almeno delle seguenti operazioni fondamentali:
Primo
step: definizione ed implementazione degli oggetti remoti
Nel gergo di
RMI, si definisce oggetto remoto un oggetto i cui metodi possono essere
richiamati da una macchina virtuale diversa da quella in cui l’oggetto
risiede. Un’interfaccia remota è invece un’interfaccia che dichiara
i metodi usati da un oggetto remoto.
Per utilizzare
un oggetto con la RMI API, per prima cosa è necessario scrivere
la sua interfaccia remota definendo i metodi che esso utilizza. Negli esempi
successivi verrà illustrato un semplice database server, basato
su JDBC, in grado di distribuire i dati attraverso RMI. Per semplicità
supponiamo che il server possa essere interrogato semplicemente attraverso
un metodo query(), a cui passiamo una query SQL e che restituisce il vettore
di Record corrispondente:
import java.io.*;Ogni interfaccia remota deve estendere java.rmi.Remote ed ogni metodo deve dichiarare di poter lanciare l’eccezione java.rmi.RemoteException, utilizzata per segnalare un eventuale errore sul canale di comunicazione.
import java.util.*;
import java.rmi.*;public interface DataBaseConnection extends Remote {
public Vector query (String sql) throws RemoteException;
}
import java.io.*;Oltre a dichiarare di implementare l’interfaccia remota, bisogna anche estendere java.rmi.UnicastRemoteServer, una classe predefinita che fornisce il supporto per referenziare l’oggetto.import java.util.*;
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
public class DataBaseConnectionImpl extends UnicastRemoteObject implements DataBaseConnection{
private Connection con;
private Statement stat;
public DataBaseConnectionImpl (String url, String user, String password) throws SQLException, RemoteException {
con = DriverManager.getConnection(url, user, password);
System.out.println("Opened DB Connection " + url);
}
public Vector query (String sql) throws RemoteException {
System.out.println("Received query " + sql);
Vector v = new Vector();
try {
stat = con.createStatement();
ResultSet rs = stat.executeQuery(sql);
ResultSetMetaData rsmd = rs.getMetaData();
int n = rsmd.getColumnCount();
while (rs.next()) {
Hashtable record = new Hashtable();
for (int i = 1; i <= n; i++) {
String field = rsmd.getColumnName(i);
String value = rs.getString(i);
record.put(field, value);
}
v.addElement(record);
}
}
catch (SQLException e){
e.printStackTrace(System.err);
}
System.out.println("Replying to query " + sql);
return v;
}
}
Secondo
step: generazione di stub e skeleton
È evidente
che l’oggetto DataBaseConnectionImpl sarà istanziato ed andrà
in esecuzione sul lato server della nostra connessione. Sul lato client,
invece, non ha senso istanziare direttamente un oggetto di tipo DataBaseConnectionImpl:
infatti si otterrebbe semplicemente una seconda copia di DataBaseConnectionImpl,
completamente indipendente e scollegata da quella che già gira sul
server.
Tuttavia, sul
lato client deve pur esistere un oggetto sul quale sia possibile invocare
il metodo query() e quest’oggetto deve anche occuparsi di generare un messaggio
sul canale di comunicazione per “avvisare” il server che è stata
richiesta un’operazione, attendere la risposta con i risultati e restituirli
al chiamante. Questo tipo di oggetto è chiamato stub (termine in
quest’accezione traducibile in italiano con la parola “surrogato”).
Il programma
sul client, di fatto, “crede” di avere a disposizione il vero oggetto DataBaseConnectionImpl,
mentre in realtà opera con un suo “surrogato”.
Parimenti, sul
server deve esistere un oggetto in grado di ricevere il messaggio, interpretarlo,
eseguire l’operazione sull’oggetto di tipo DataBaseConnectionImpl, raccogliere
il risultato e rispedirlo indietro. Questo tipo di oggetto viene chiamato
skeleton (scheletro). Sul server, quindi, l’oggetto DataBaseConnectionImpl
“crede” di interagire con un altro oggetto locale, mentre in realtà
ha a che fare con lo skeleton che fa la “controfigura” dell’oggetto remoto.
Riassumendo:
ogni oggetto remoto deve avere, oltre alla sua implementazione, un’interfaccia,
uno stub e uno skeleton.
Questi ultimi
due vengono automaticamente generati da un apposito compilatore, rmic,
che è incluso nel JDK a partire dalla versione 1.1. Se compiliamo
DataBaseConnectionImpl e lanciamo il comando
rmic –d . DataBaseConnectionImplTerzo step: registrazione e referenziazione di un oggetto remotootteniamo automaticamente DataBaseConnectionImpl_stub.class e DataBaseConnectionImpl_skel.class.
import java.rmi.*;Le registrazioni sono gestite da un apposito programma, rmiregistry, che deve essere stato preventivamente lanciato sul lato server.import java.sql.*;
public class DBRMIServer
{
public static void main (String[] args) throws Exception {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
DataBaseConnectionImpl dbc =
new DataBaseConnectionImpl("jdbc:odbc:dbdemo", "user", "password");
System.out.println("Registering dbserver...");
Naming.bind("dbserver", dbc);
}
}
import java.rmi.*;import java.util.*;
public class DBRMIClient
{
private DataBaseConnection dbc;
public void start()
throws Exception
{
dbc = (DataBaseConnection)
Naming.lookup("rmi://hornet.esng.dibe.unige.it/dbserver");
Vector v = dbc.query("SELECT * FROM users WHERE last_name='Rossi'");
for (Enumeration e = v.elements(); e.hasMoreElements(); )
{
Hashtable record = (Hashtable)e.nextElement();
System.out.println(record);
}
}
public static void main (String[] args)
throws Exception
{
System.setSecurityManager(new RMISecurityManager());
DBRMIClient client = new DBRMIClient();
client.start();
}
}
L’argomento
da passare al metodo statico Naming.lookup() è una URL contenente
il nome della macchina che ospita l’oggetto remoto ed il nome con cui l’oggetto
è stato registrato. Si noti che l’oggetto server non è dichiarato
come DataBaseConnectionImpl, ma come DataBaseConnection.
Dal momento
che stub e skeleton risiedono sulla macchina server, essi devono essere
scaricati dalla rete: di quest’operazione si occupa lo speciale class loader
di RMI, java.rmi.RMIClassLoader. Anche il codice che implementa la classe
Record viene caricato remotamente.
Avendo a che
fare con classi scaricate da rete, Java si pone il problema della sicurezza.
A questo scopo, la RMI API definisce un apposito security manager, chiamato
java.rmi.SecurityManager, che, quando si scrive un’applicazione, va esplicitamente
installato prima di tentare una connessione con un oggetto remoto. Per
gli applet ciò non è necessario, in quanto è il browser,
nel caso supporti la RMI, ad aver già fornito un opportuno security
manager che include automaticamente le funzionalità richieste. D’altronde
questa è l’unica soluzione attuabile, perché in un browser
è impossibile cambiare il security manager di default, per evidenti
motivi di sicurezza.
Infine, la RMI
API implementa un meccanismo di garbage collection distribuita: ogni oggetto
è eliminato solo quando non è più referenziato né
localmente, né remotamente.
JAVA
BEANS
Uno degli scopi
principali della programmazione ad oggetti (OOP) è creare un nuovo
modo di organizzare il software per renderlo il più modulare possibile,
in modo da permettere, tra le altre cose, la massima riutilizzazione del
codice già scritto e funzionante. La OOP si basa sul concetto di
incapsulamento, che consiste nell’isolare il più possibile l’interfaccia
dall’implementazione di un oggetto per isolarlo dal sistema circostante,
favorendone il debugging e permettendone il riutilizzo in ambienti differenti.
Ma quando si
passa dalla teoria alla pratica ci si trova a dover gestire insiemi complessi
di classi tra di loro interdipendenti, con strutture che rendono praticamente
impossibile il riuso di una singola classe senza dover “tirare in ballo”
tutte le altre.
La progettazione
“a componenti” è stata pensata per risolvere questo problema. Un
componente è un “mattoncino” di codice (che può essere molto
semplice o arbitrariamente sofisticato), dotato di proprietà ben
definite e capace di eseguire delle operazioni (tipicamente rispondere
e generare eventi) secondo un’interfaccia ben precisa.
I JavaBeans
rappresentano l’approccio Java-oriented ai componenti. Essi definiscono
una serie di metodi standard per la manipolazione delle proprietà
e la gestione degli eventi; per esempio, una proprietà XXX deve
essere manipolata con i metodi:
void setXXX (type
x);
type getXXX();
Grazie al meccanismo della introspezione (in pratica l’ispezione dinamica di codice Java compilato per accedere ai nomi dei membri e dei metodi), un programma di sviluppo visuale può scoprire automaticamente quali sono le proprietà e gli eventi tipici di un componente, arrivando così a generare dei “property inspector” e “interaction wizard” che consentono di modificare le proprietà e gestire gli eventi con pochi colpi di mouse (per esempio collegando visualmente due componenti tra loro). In questo modo il programmatore si risparmia la scrittura manuale di parecchie righe di programma.
Volendo costruire un componente visuale e riutilizzabile per visualizzare i dati provenienti da un database, si può costruire una sottoclasse di Panel che viene associata ad una query di una tabella. Vedremo in un secondo momento la definizione di componenti per visualizzare i risultati della query. La classe DBPanel deve essere in grado di eseguire la query, memorizzarne internamente i risultati e definire un cursore che punta al “record corrente” da visualizzare. Una possibile implementazione è la seguente:DBPanel usa la classe DataBaseConnection già analizzata in precedenza. Essa viene collegata alla sorgente di dati nel costruttore. Il testo della query è una stringa SQL che viene considerata come una proprietà del Bean: si possono infatti vedere i due metodi accessori setQuery() e getQuery(), che consentono di impostare il suo valore attraverso un property inspector di un qualsiasi sistema di sviluppo visuale. Il metodo execute() esegue la query e forza un repaint(), ipotizzando che il DBPanel contenga dei componenti interni in grado di visualizzare i dati. I metodi prevRecord() e nextRecord() muovono indietro ed avanti il cursore che punta al record corrente. Da notare infine la coppia di metodi addActionListener() e removeActionListener(): DBPanel è in grado di generare un ActionEvent ogni volta che il cursore viene spostato.
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.rmi.*;
import java.beans.*;
public class DBPanel extends Panel{
private String sql;
private DataBaseConnection dbClient;
private Vector v = new Vector();
private Vector listenerList = new Vector();
private int currentRecord;
public DBPanel(){
try{
dbClient = (DataBaseConnection)
Naming.lookup("rmi://hornet.esng.dibe.unige.it/dbserver");
}
catch (Exception e){
e.printStackTrace(System.err);
}
}
public void setQuery (String sql){
this.sql = sql;
}
public void getQuery (String sql){
return sql;
}
public void execute(){
try{
v = dbClient.query(sql);
currentRecord = 0;
repaint(0);
}
catch (Exception e){
e.printStackTrace(System.err);
}
}
public Hashtable currentRecord(){
if (v.isEmpty())
return null;
return (Hashtable)v.elementAt(currentRecord);
}
public void nextRecord(){
if (currentRecord < v.size() - 1){
currentRecord++;
repaint(0);
fireAction();
}
}
public void prevRecord(){
if (currentRecord > 0){
currentRecord--;
repaint(0);
fireAction();
}
}
public synchronized void addActionListener (ActionListener l){
listenerList.addElement(l);
}
public synchronized void removeActionListener (ActionListener l){
listenerList.removeElement(l);
}
private void fireAction(){
Vector targets;
synchronized (this){
targets = (Vector)listenerList.clone();
}
ActionEvent event = new ActionEvent(this, currentRecord,
v.elementAt(currentRecord).toString());
for (Enumeration e = targets.elements(); e.hasMoreElements();){
ActionListener target = (ActionListener)e.nextElement();
target.actionPerformed(event);
}
}
}
import java.util.*;Il metodo paint() verifica se il componente è parte di un DBPanel, ed in tal caso interroga quest’ultimo per ottenere il record corrente. Il valore corrispondente al campo associato viene letto e visualizzato.import java.awt.*;
public class DBTextArea extends TextField {
private String fieldName;
public void setFieldName (String name){
fieldName = name;
}
public String getFieldName(){
return fieldName;
}
public void paint (Graphics g){
Component parent = getParent();
if (parent instanceof DBPanel){
DBPanel p = (DBPanel)parent;
Hashtable record = p.currentRecord();
if (record != null){
String value = (String)record.get(fieldName);
if (value != null)
setText(value);
}
}
super.paint(g);
}
}
import java.util.*;Come si può notare il bean che si è creato è stato inserito all’interno di una applicazione in maniera relativamente semplice: la cosa importante è che tale processo è del tutto indipendente dal tipo di applicazione che si deve realizzare, essendo il bean a tutti gli effetti un oggetto a se stante riutilizzabile in ogni situazione in maniera standard.import java.awt.*;
import java.applet.*;
import java.awt.event.*;
public class RMIAppletClient extends Applet implements ActionListener
{
private DBPanel dBPanel;
private DBTextArea taFirstName;
private DBTextArea taLastName;
private Label lbFirstName;
private Label lbLastName;
private Button btPrev;
private Button btNext;
private TextArea txArea;
public void init()
{
super.init();
setLayout(null);
addNotify();
resize(420,270);
dBPanel = new DBPanel();
dBPanel.setLayout(null);
dBPanel.reshape(6,18,234,84);
add(dBPanel);
dBPanel.setQuery("SELECT * FROM users ORDER BY last_name, first_name");
taFirstName = new DBTextArea();
taFirstName.reshape(108,18,120,24);
dBPanel.add(taFirstName);
taLastName = new DBTextArea();
taLastName.reshape(108,48,120,24);
dBPanel.add(taLastName);
lbFirstName = new java.awt.Label("First Name:");
lbFirstName.reshape(6,18,79,21);
dBPanel.add(lbFirstName);
lbLastName = new java.awt.Label("Last Name:");
lbLastName.reshape(6,48,70,20);
dBPanel.add(lbLastName);
btPrev = new java.awt.Button("Prev");
btPrev.reshape(336,30,66,26);
add(btPrev);
btNext = new java.awt.Button("Next");
btNext.reshape(336,66,66,26);
add(btNext);
txArea = new java.awt.TextArea();
txArea.reshape(12,102,390,147);
add(txArea);
taFirstName.setFieldName("first_name");
taLastName.setFieldName("last_name");
btPrev.addActionListener(this);
btNext.addActionListener(this);
dBPanel.addActionListener(this);
dBPanel.execute();
}
public void actionPerformed (ActionEvent event)
{
if (event.getSource() == btPrev)
dBPanel.prevRecord();
else if (event.getSource() == btNext)
dBPanel.nextRecord();
else if (event.getSource() == dBPanel)
{
Hashtable record = dBPanel.currentRecord();
txArea.append(record.toString() + "\n");
}
}
}
Conclusione
Con l’analisi
dei beans termina qui la terza parte di questa miniserie su Java. Spero
di essere riuscito a dare un’idea di ciò che è possibile
fare con questo linguaggio e con la tecnologia ad esso collegata; queste
tre puntate ovviamente non hanno la pretesa di insegnare a programmare
in maniera approfondita, ma piuttosto di presentare la filosofia particolare
che stà alla base di Java, e la differente concezione di sviluppo
del software che deve essere utilizzata. Vorrei che questo punto fosse
chiaro: prima di imparare ad utilizzare le vari librerie di sistema, è
importante per prima cosa capire come un determminato problema deve essere
affrontato, per poi passare alla fase implementativa.
Più di
ogni altra cosa in Java è necessaria una analisi client server del
problema ed una pasua scomposizione nelle parti fondamentali, per poter
realizzare un progetto realmente in Java, e una semplice traduzione da
un linguaggio ad un altro, cosa che non mette in luce le reali potenzialità
di Java, ma anzi ne può a volte esaltare i difetti.
Invito
tutti quanti ad approfondire i vari argomenti qui accennati, sia
utilizzati gli ottimi manuali disponibili in libreria, sia seguendo gli
articoli che di mese in mese vi proponiamo.
|
||
MokaByte ricerca
nuovi collaboratori.
|
||
|