MokaByte Numero 16 - Febbraio1998
Foto
Java  Start Up - 3
di
Giudici 
e
Puliti
 Per chi si affaccia alla tecnologia Java per la prima volta  ecco una miniserie introduttiva sulla tecnologia




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.*;
 
 

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 + "]";

      }

  }
 
 

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:
import java.io.*;

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();

      }

  }

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.io.*;

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();

      }

  }

Come si vede, i dati sono “sopravvissuti” alla terminazione del primo programma, pertanto possiamo dire di avere implementato la loro persistenza nel tempo.
 

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:
 

La RMI API è un insieme di classi che permettono di sviluppare applicazioni Java distribuite in rete senza preoccuparsi di questi dettagli tecnici di “basso livello”, che vengono gestiti automaticamente secondo metodologie standard. La RMI API è consistente con il resto dell’architettura Java per quanto riguarda la portabilità, la sicurezza, la gestione degli errori con le eccezioni, la gestione della memoria con la garbage collection, il caricamento dinamico delle classi.
Vediamo ora con un semplice esempio come sia possibile implementare con RMI una semplice applicazione distribuita.
 

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.*;
import java.util.*;
import java.rmi.*;

public  interface DataBaseConnection extends Remote {
  public   Vector query (String sql) throws RemoteException;
}
 

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.
Una volta scritta l’interfaccia, bisogna modificare la classe originale:
import java.io.*;

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;

      }

  }

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.
 

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 . DataBaseConnectionImpl

otteniamo automaticamente DataBaseConnectionImpl_stub.class e DataBaseConnectionImpl_skel.class.
 

Terzo step: registrazione e referenziazione di un oggetto remoto
A questo punto abbiamo tutto il necessario per far funzionare in remoto l’oggetto DataBaseConnectionImpl, ma bisogna definire le modalità di collegamento tra client e server.
Sul lato server, il programma che gestisce DataBaseConnectionImpl deve “comunicare al mondo” che possiede un oggetto disponibile per l’invocazione remota. Per questo scopo è sufficiente richiamare un metodo statico di una classe di RMI, Naming.bind(), che associa l’istanza di DataBaseConnectionImpl ad un nome che verrà usato per identificarlo:
import java.rmi.*;

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);

      }

  }

Le registrazioni sono gestite da un apposito programma, rmiregistry, che deve essere stato preventivamente lanciato sul lato server.
Sul lato client si può ottenere un reference ad un oggetto remoto con il seguente codice:
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:
 

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);

        }

    }

  }
 
 

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.
A questo punto va implementato un controllo in grado di visualizzare i dati, per esempio una sottoclasse di TextField a cui va associato il nome di un campo del record corrente: essa deve essere in grado di accedere automaticamente al valore associato e visualizzarlo. Se il nome del campo è implementato come proprietà del Bean, allora potrà essere modificato attraverso un property inspector, permettendo la realizzazione di una maschera video in tempi molto ridotti. Una possibile implementazione è la seguente:
import java.util.*;

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);

      }

  }
 
 

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.
A questo punto bisogna costruire un applet dimostrativo che usi i componenti per l’accesso al database. L’esempio seguente visualizza i campi “first_name” e “last_name”, definisce due pulsanti per cambiare il record corrente ed una text area per dimostrare l’avvenuta ricezione degli ActionEvent da DBPanel:
import java.util.*;

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");

          }

      }

  }
 
 
 

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.
facilmente utilizzato.

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 Web  1998 - www.mokabyte.it

MokaByte ricerca nuovi collaboratori. 
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it