MokaByte
Numero 15 - Gennaio 1998
|
|||
|
|
||
Fabrizio Giudici Giovanni Puliti |
|
||
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 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
public
Object get (Object key);
restituisce l’oggetto associato alla chiave data
public
void remove (Object object);
rimuove il dato oggetto dalla tabella
import java.util.*;È 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.
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);
}
}
}
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 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”)
import java.io.*;I socket
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());
}
}
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).
I socket in Java sono implementati con quattro classi distinte:
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 (String remAddr, int remPort, InetAddress localAddr, int localPort);
Specifica anche la porta locale sulla quale esegue un’operazione di bind.
public
void setPort (int port);
public
int getPort();
manipolano la porta remota di connessione
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();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.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();
}
}
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
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
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://indirizzo_host/percorso_della_risorsa/nome_della_risorsa:portaLe 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 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:
import java.io.*;Java prevede che si possano registrare oggetti in grado di gestire protocolli customizzati e oggetti di tipo MIME. La classe UrlConnection definisce il metodo: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();
}
}
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.
|
||
MokaByte ricerca
nuovi collaboratori.
|
||
|