Historically, programming across
multiple machines has been error-prone, difficult, and complex.
The programmer had to know many details
about the network and sometimes even the hardware. You usually needed to
understand the various “layers” of the networking protocol, and
there were a lot of different functions in each different networking library
concerned with connecting, packing, and unpacking blocks of information;
shipping those blocks back and forth; and handshaking. It was a daunting
task.
However, the concept of distributed
computing is not so difficult. You want to:
Each topic will be given
a light introduction in this chapter. Please note that each subject is
voluminous and by itself the subject of entire books, so this chapter is only
meant to familiarize you with the topics, not make you an expert (however, you
can go a long way with the information presented here on network programming,
servlets and JSPs).
One of Java’s great strengths is
painless networking. The Java network library designers have made it quite
similar to reading and writing files, except that the “file” exists
on a remote machine and the remote machine can decide exactly what it wants to
do about the information you’re requesting or sending. As much as
possible, the underlying details of networking have been abstracted away and
taken care of within the JVM and local machine installation of Java. The
programming model you use is that of a file; in fact, you actually wrap the
network connection (a “socket”) with stream objects, so you end up
using the same method calls as you do with all other streams. In addition,
Java’s built-in multithreading is exceptionally handy when dealing with
another networking issue: handling multiple connections at
once.
Of course, in order to tell one machine
from another and to make sure that you are connected with the machine you want,
there must be some way of uniquely identifying machines
on a network. Early networks were satisfied to provide unique names for machines
within the local network. However, Java works within the Internet, which
requires a way to uniquely identify a machine from all the others in the
world. This is accomplished with the
IP
(Internet Protocol) address that can exist in two forms:
DNS (Domain Name System)
form. My domain name is bruceeckel.com, so suppose I have a computer
called Opus in my domain. Its domain name would be
Opus.bruceeckel.com. This is exactly the kind of name that you use when
you send email to people, and is often incorporated into a World-Wide-Web
address.
In both cases, the IP address is
represented internally as a 32-bit
number[73] (so each
of the quad numbers cannot exceed 255), and you can get a special Java object to
represent this number from either of the forms above by using the static
InetAddress.getByName( ) method that’s in java.net. The
result is an object of type InetAddress that you can use to build a
“socket” as you will see later.
As a simple example of using
InetAddress.getByName( ), consider what happens if you have a
dial-up Internet service provider (ISP). Each time you dial up, you are assigned
a temporary IP address. But while you’re connected, your IP address has
the same validity as any other IP address on the Internet. If someone connects
to your machine using your IP address then they can connect to a Web server or
FTP server that you have running on your machine. Of course, they need to know
your IP address, and since it’s assigned each time you dial up, how can
you find out what it is?
The following program uses
InetAddress.getByName( ) to produce your IP address. To use it, you
must know the name of your computer. It has been tested only on Windows 95, but
there you can go to “Settings,” “Control Panel,”
“Network,” and then select the “Identification” tab.
“Computer name” is the name to put on the command
line.
//: c15:WhoAmI.java // Finds out your network address when // you're connected to the Internet. import java.net.*; public class WhoAmI { public static void main(String[] args) throws Exception { if(args.length != 1) { System.err.println( "Usage: WhoAmI MachineName"); System.exit(1); } InetAddress a = InetAddress.getByName(args[0]); System.out.println(a); } } ///:~
In this case, the machine is called
“peppy.” So, once I’ve connected to my ISP I run the
program:
java WhoAmI peppy
I get back a message like this (of
course, the address is different each time):
peppy/199.190.87.75
If I tell my friend this address, he can
log onto my personal Web server by going to the URL http://199.190.87.75
(only as long as I continue to stay connected during that session). This can
sometimes be a handy way to distribute information to someone else or to test
out a Web site configuration before posting it to a “real”
server.
The whole point of a network is to allow
two machines to connect and talk to each other. Once the two machines have found
each other they can have a nice, two-way conversation. But how do they find each
other? It’s like getting lost in an amusement park: one machine has to
stay in one place and listen while the other machine says, “Hey, where are
you?”
The machine that “stays in one
place” is called the
server, and the one that
seeks is called the
client. This distinction
is important only while the client is trying to connect to the server. Once
they’ve connected, it becomes a two-way communication process and it
doesn’t matter anymore that one happened to take the role of server and
the other happened to take the role of the client.
So the job of the server is to listen for
a connection, and that’s performed by the special server object that you
create. The job of the client is to try to make a connection to a server, and
this is performed by the special client object you create. Once the connection
is made, you’ll see that at both server and client ends, the connection is
just magically turned into an I/O stream object, and from then on you can treat
the connection as if you were reading from and writing to a file. Thus, after
the connection is made you will just use the familiar I/O commands from Chapter
11. This is one of the nice features of Java networking.
For many reasons, you might not have a
client machine, a server machine, and a network available to test your programs.
You might be performing exercises in a classroom situation, or you could be
writing programs that aren’t yet stable enough to put onto the network.
The creators of the Internet Protocol were aware of this issue, and they created
a special address called
localhost to be the
“local loopback” IP
address for testing without a network. The generic way to produce this address
in Java is:
InetAddress addr = InetAddress.getByName(null);
If you hand getByName( ) a
null, it defaults to using the localhost. The InetAddress
is what you use to refer to the particular machine, and you must produce this
before you can go any further. You can’t manipulate the contents of an
InetAddress (but you can print them out, as you’ll see in the next
example). The only way you can create an InetAddress is through one of
that class’s static member methods getByName( ) (which
is what you’ll usually use), getAllByName( ), or
getLocalHost( ).
You can also produce the local loopback
address by handing it the string localhost:
InetAddress.getByName("localhost");
or by using its dotted quad form to name
the reserved IP number for the loopback:
InetAddress.getByName("127.0.0.1");
An IP address isn’t enough to
identify a unique server, since many servers can exist on one machine. Each IP
machine also contains ports, and when you’re setting up a client or
a server you must choose a port
where both client and server agree to connect; if you’re meeting someone,
the IP address is the neighborhood and the port is the bar.
The port is not a physical location in a
machine, but a software abstraction (mainly for bookkeeping purposes). The
client program knows how to connect to the machine via its IP address, but how
does it connect to a desired service (potentially one of many on that machine)?
That’s where the port numbers come in as second level of addressing. The
idea is that if you ask for a particular port, you’re requesting the
service that’s associated with the port number. The time of day is a
simple example of a service. Typically, each service is associated with a unique
port number on a given server machine. It’s up to the client to know ahead
of time which port number the desired service is running on.
The system services reserve the use of
ports 1 through 1024, so you shouldn’t use those or any other port that
you know to be in use. The first choice for examples in this book will be port
8080 (in memory of the venerable old 8-bit Intel 8080 chip in my first computer,
a CP/M
machine).
The socket is the software
abstraction used to represent the “terminals” of a connection
between two machines. For a given connection, there’s a socket on each
machine, and you can imagine a hypothetical “cable” running between
the two machines with each end of the “cable” plugged into a socket.
Of course, the physical hardware and cabling between machines is completely
unknown. The whole point of the abstraction is that we don’t have to know
more than is necessary.
In Java, you create a socket to make the
connection to the other machine, then you get an InputStream and
OutputStream (or, with the appropriate converters, Reader and
Writer) from the socket in order to be able to treat the
connection as an I/O stream object. There are two stream-based socket classes: a
ServerSocket that a server uses to “listen” for incoming
connections and a Socket that a client uses in order to initiate a
connection. Once a client makes a socket connection, the ServerSocket
returns (via the accept( )
method) a corresponding server
side Socket through which direct communications will take place. From
then on, you have a true Socket to Socket connection and you treat
both ends the same way because they are the same. At this point, you use
the methods
getInputStream( )
and
getOutputStream( )
to produce the corresponding InputStream and OutputStream objects
from each Socket. These must be wrapped inside buffers and formatting
classes just like any other stream object described in Chapter
11.
The use of the term ServerSocket
would seem to be another example of a confusing name scheme in the Java
libraries. You might think ServerSocket would be better named
“ServerConnector” or something without the word “Socket”
in it. You might also think that ServerSocket and Socket should
both be inherited from some common base class. Indeed, the two classes do have
several methods in common but not enough to give them a common base class.
Instead, ServerSocket’s job is to wait until some other machine
connects to it, then to return an actual Socket. This is why
ServerSocket seems to be a bit misnamed, since its job isn’t really
to be a socket but instead to make a Socket object when someone else
connects to it.
However, the ServerSocket does
create a physical “server” or listening socket on the host machine.
This socket listens for incoming connections and then returns an
“established” socket (with the local and remote endpoints defined)
via the accept( ) method. The confusing part is that both of these
sockets (listening and established) are associated with the same server socket.
The listening socket can accept only new connection requests and not data
packets. So while ServerSocket doesn’t make much sense
programmatically, it does “physically.”
When you create a ServerSocket,
you give it only a port number. You don’t have to give it an IP address
because it’s already on the machine it represents. When you create a
Socket, however, you must give both the IP address and the port number
where you’re trying to connect. (On the other hand, the Socket that
comes back from ServerSocket.accept( ) already contains all this
information.)
This example makes the simplest use of
servers and clients using sockets. All the server does is wait for a connection,
then uses the Socket produced by that connection to create an
InputStream and OutputStream. After that, everything it reads from
the InputStream it echoes to the OutputStream until it receives
the line END, at which time it closes the connection.
The client makes the connection to the
server, then creates an OutputStream. Lines of text are sent through the
OutputStream. The client also creates an InputStream to hear what
the server is saying (which, in this case, is just the words echoed
back).
Both the server and client use the same
port number and the client uses the local loopback address to connect to the
server on the same machine so you don’t have to test it over a network.
(For some configurations, you might need to be connected to a network for
the programs to work, even if you aren’t communicating over that
network.)
Here is the server:
//: c15:JabberServer.java // Very simple server that just // echoes whatever the client sends. import java.io.*; import java.net.*; public class JabberServer { // Choose a port outside of the range 1-1024: public static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Started: " + s); try { // Blocks until a connection occurs: Socket socket = s.accept(); try { System.out.println( "Connection accepted: "+ socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Output is automatically flushed // by PrintWriter: PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } // Always close the two sockets... } finally { System.out.println("closing..."); socket.close(); } } finally { s.close(); } } } ///:~
You can see that the ServerSocket
just needs a port number, not an IP address (since it’s running on
this machine!). When you call accept( ), the method
blocks until some client tries to connect to it. That is, it’s
there waiting for a connection but other processes can run (see Chapter 14).
When a connection is made, accept( ) returns with a Socket
object representing that connection.
The responsibility for cleaning up the
sockets is crafted carefully here. If the ServerSocket constructor fails,
the program just quits (notice we must assume that the constructor for
ServerSocket doesn’t leave any open network sockets lying around if
it fails). For this case, main( ) throws IOException
so a try block is not necessary. If the ServerSocket constructor
is successful then all other method calls must be guarded in a
try-finally block to ensure that, no matter how the block is left, the
ServerSocket is properly closed.
The same logic is used for the
Socket returned by accept( ). If accept( ) fails,
then we must assume that the Socket doesn’t exist or hold any
resources, so it doesn’t need to be cleaned up. If it’s successful,
however, the following statements must be in a try-finally block so that
if they fail the Socket will still be cleaned up. Care is required here
because sockets use important nonmemory resources, so you must be diligent in
order to clean them up (since there is no destructor in Java to do it for
you).
Both the ServerSocket and the
Socket produced by accept( ) are printed to
System.out. This means that their toString( ) methods are
automatically called. These produce:
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080]
Shortly, you’ll see how these fit
together with what the client is doing.
The next part of the program looks just
like opening files for reading and writing except that the InputStream
and OutputStream are created from the Socket object. Both the
InputStream and OutputStream objects are converted to
Reader and
Writer objects using the
“converter” classes
InputStreamReader and
OutputStreamWriter,
respectively. You could also have used the Java 1.0
InputStream and
OutputStream classes
directly, but with output there’s a distinct advantage to using the
Writer approach. This appears with
PrintWriter, which has an
overloaded constructor that takes a second argument, a boolean flag that
indicates whether to automatically flush the output at the end of each
println( ) (but not print( )) statement. Every
time you write to out, its buffer must be flushed so the information goes
out over the network. Flushing is important for this particular example because
the client and server each wait for a line from the other party before
proceeding. If flushing doesn’t occur, the information will not be put
onto the network until the buffer is full, which causes lots of problems in this
example.
When writing network programs you need to
be careful about using automatic flushing. Every time you flush the buffer a
packet must be created and sent. In this case, that’s exactly what we
want, since if the packet containing the line isn’t sent then the
handshaking back and forth between server and client will stop. Put another way,
the end of a line is the end of a message. But in many cases messages
aren’t delimited by lines so it’s much more efficient to not use
auto flushing and instead let the built-in buffering decide when to build and
send a packet. This way, larger packets can be sent and the process will be
faster.
Note that, like virtually all streams you
open, these are buffered. There’s an exercise at the end of this chapter
to show you what happens if you don’t buffer the streams (things get
slow).
The infinite while loop reads
lines from the BufferedReader in and writes information to
System.out and to the PrintWriter out. Note that these
could be any streams, they just happen to be connected to the network.
When the client sends the line consisting
of “END” the program breaks out of the loop and closes the
Socket.
Here’s the client:
//: c15:JabberClient.java // Very simple client that just sends // lines to the server and reads lines // that the server sends. import java.net.*; import java.io.*; public class JabberClient { public static void main(String[] args) throws IOException { // Passing null to getByName() produces the // special "Local Loopback" IP address, for // testing on one machine w/o a network: InetAddress addr = InetAddress.getByName(null); // Alternatively, you can use // the address or name: // InetAddress addr = // InetAddress.getByName("127.0.0.1"); // InetAddress addr = // InetAddress.getByName("localhost"); System.out.println("addr = " + addr); Socket socket = new Socket(addr, JabberServer.PORT); // Guard everything in a try-finally to make // sure that the socket is closed: try { System.out.println("socket = " + socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Output is automatically flushed // by PrintWriter: PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); for(int i = 0; i < 10; i ++) { out.println("howdy " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } finally { System.out.println("closing..."); socket.close(); } } } ///:~
In main( ) you can see all
three ways to produce the InetAddress of the local loopback IP address:
using null, localhost, or the explicit reserved address
127.0.0.1. Of course, if you want to connect to a machine across a
network you substitute that machine’s IP address. When the InetAddress
addr is printed (via the automatic call to its toString( )
method) the result is:
localhost/127.0.0.1
By handing getByName( ) a
null, it defaulted to finding the localhost, and that produced the
special address 127.0.0.1.
Note that the
Socket called
socket is created with both the InetAddress and the port number.
To understand what it means when you print one of these Socket objects,
remember that an Internet connection is determined uniquely by these four pieces
of data: clientHost, clientPortNumber, serverHost, and
serverPortNumber. When the server comes up, it takes up its assigned port
(8080) on the localhost (127.0.0.1). When the client comes up, it is allocated
to the next available port on its machine, 1077 in this case, which also happens
to be on the same machine (127.0.0.1) as the server. Now, in order for data to
move between the client and server, each side has to know where to send it.
Therefore, during the process of connecting to the “known” server,
the client sends a “return address” so the server knows where to
send its data. This is what you see in the example output for the server
side:
Socket[addr=127.0.0.1,port=1077,localport=8080]
This means that the server just accepted
a connection from 127.0.0.1 on port 1077 while listening on its local port
(8080). On the client side:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
which means that the client made a
connection to 127.0.0.1 on port 8080 using the local port 1077.
You’ll notice that every time you
start up the client anew, the local port number is incremented. It starts at
1025 (one past the reserved block of ports) and keeps going up until you reboot
the machine, at which point it starts at 1025 again. (On UNIX machines, once the
upper limit of the socket range is reached, the numbers will wrap around to the
lowest available number again.)
Once the Socket object has been
created, the process of turning it into a BufferedReader and
PrintWriter is the same as in the server (again, in both cases you start
with a Socket). Here, the client initiates the conversation by sending
the string “howdy” followed by a number. Note that the buffer must
again be flushed (which happens automatically via the second argument to the
PrintWriter constructor). If the buffer isn’t flushed, the whole
conversation will hang because the initial “howdy” will never get
sent (the buffer isn’t full enough to cause the send to happen
automatically). Each line that is sent back from the server is written to
System.out to verify that everything is working correctly. To terminate
the conversation, the agreed-upon “END” is sent. If the client
simply hangs up, then the server throws an exception.
You can see that the same care is taken
here to ensure that the network resources represented by the Socket are
properly cleaned up, using a try-finally block.
Sockets produce a
“dedicated” connection that persists until
it is explicitly disconnected. (The dedicated connection can still be
disconnected un-explicitly if one side, or an intermediary link, of the
connection crashes.) This means the two parties are locked in communication and
the connection is constantly open. This seems like a logical approach to
networking, but it puts an extra load on the network. Later in this chapter
you’ll see a different approach to networking, in which the connections
are only
temporary.
The JabberServer works, but it can
handle only one client at a time. In a typical server, you’ll want to be
able to deal with many clients at once. The answer is
multithreading, and in languages
that don’t directly support multithreading this means all sorts of
complications. In Chapter 14 you saw that multithreading in Java is about as
simple as possible, considering that multithreading is a rather complex topic.
Because threading in Java is reasonably straightforward, making a server that
handles multiple clients is relatively easy.
The basic scheme is to make a single
ServerSocket in the server and call accept( ) to wait for a
new connection. When accept( ) returns, you take the resulting
Socket and use it to create a new thread whose job is to serve that
particular client. Then you call accept( ) again to wait for a new
client.
In the following server code, you can see
that it looks similar to the JabberServer.java example except that all of
the operations to serve a particular client have been moved inside a separate
thread class:
//: c15:MultiJabberServer.java // A server that uses multithreading // to handle any number of clients. import java.io.*; import java.net.*; class ServeOneJabber extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; public ServeOneJabber(Socket s) throws IOException { socket = s; in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Enable auto-flush: out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); // If any of the above calls throw an // exception, the caller is responsible for // closing the socket. Otherwise the thread // will close it. start(); // Calls run() } public void run() { try { while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } System.out.println("closing..."); } catch (IOException e) { } finally { try { socket.close(); } catch(IOException e) {} } } } public class MultiJabberServer { static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Server Started"); try { while(true) { // Blocks until a connection occurs: Socket socket = s.accept(); try { new ServeOneJabber(socket); } catch(IOException e) { // If it fails, close the socket, // otherwise the thread will close it: socket.close(); } } } finally { s.close(); } } } ///:~
The ServeOneJabber thread takes
the Socket object that’s produced by accept( ) in
main( ) every time a new client makes a connection. Then, as before,
it creates a BufferedReader and auto-flushed PrintWriter object
using the Socket. Finally, it calls the special Thread method
start( ), which performs thread initialization and then calls
run( ). This performs the same kind of action as in the previous
example: reading something from the socket and then echoing it back until it
reads the special “END” signal.
The responsibility for cleaning up the
socket must again be carefully designed. In this case, the socket is created
outside of the ServeOneJabber so the responsibility can be shared. If the
ServeOneJabber constructor fails, it will just throw the exception to the
caller, who will then clean up the thread. But if the constructor succeeds, then
the ServeOneJabber object takes over responsibility for cleaning up the
thread, in its run( ).
Notice the simplicity of the
MultiJabberServer. As before, a ServerSocket is created and
accept( ) is called to allow a new connection. But this time, the
return value of accept( ) (a Socket) is passed to the
constructor for ServeOneJabber, which creates a new thread to handle that
connection. When the connection is terminated, the thread simply goes
away.
If the creation of the
ServerSocket fails, the exception is again thrown through
main( ). But if it succeeds, the outer try-finally guarantees
its cleanup. The inner try-catch guards only against the failure of the
ServeOneJabber constructor; if the constructor succeeds, then the
ServeOneJabber thread will close the associated socket.
To test that the server really does
handle multiple clients, the following program creates many clients (using
threads) that connect to the same server. Each thread has a limited lifetime,
and when it goes away, that leaves space for the creation of a new thread. The
maximum number of threads allowed is determined by the final int
maxthreads. You’ll notice that this value is rather critical, since if
you make it too high the threads seem to run out of resources and the program
mysteriously fails.
//: c15:MultiJabberClient.java // Client that tests the MultiJabberServer // by starting up multiple clients. import java.net.*; import java.io.*; class JabberClientThread extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; private static int counter = 0; private int id = counter++; private static int threadcount = 0; public static int threadCount() { return threadcount; } public JabberClientThread(InetAddress addr) { System.out.println("Making client " + id); threadcount++; try { socket = new Socket(addr, MultiJabberServer.PORT); } catch(IOException e) { // If the creation of the socket fails, // nothing needs to be cleaned up. } try { in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Enable auto-flush: out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); start(); } catch(IOException e) { // The socket should be closed on any // failures other than the socket // constructor: try { socket.close(); } catch(IOException e2) {} } // Otherwise the socket will be closed by // the run() method of the thread. } public void run() { try { for(int i = 0; i < 25; i++) { out.println("Client " + id + ": " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } catch(IOException e) { } finally { // Always close it: try { socket.close(); } catch(IOException e) {} threadcount--; // Ending this thread } } } public class MultiJabberClient { static final int MAX_THREADS = 40; public static void main(String[] args) throws IOException, InterruptedException { InetAddress addr = InetAddress.getByName(null); while(true) { if(JabberClientThread.threadCount() < MAX_THREADS) new JabberClientThread(addr); Thread.currentThread().sleep(100); } } } ///:~
The JabberClientThread constructor
takes an InetAddress and uses it to open a Socket. You’re
probably starting to see the pattern: the Socket is always used to create
some kind of Reader and/or Writer (or InputStream and/or
OutputStream) object, which is the only way that the Socket can be
used. (You can, of course, write a class or two to automate this process instead
of doing all the typing if it becomes painful.) Again, start( )
performs thread initialization and calls run( ). Here, messages are
sent to the server and information from the server is echoed to the screen.
However, the thread has a limited lifetime and eventually completes. Note that
the socket is cleaned up if the constructor fails after the socket is created
but before the constructor completes. Otherwise the responsibility for calling
close( ) for the socket is relegated to the run( )
method.
The threadcount keeps track of how
many JabberClientThread objects currently exist. It is incremented as
part of the constructor and decremented as run( ) exits (which means
the thread is terminating). In MultiJabberClient.main( ), you can
see that the number of threads is tested, and if there are too many, no more are
created. Then the method sleeps. This way, some threads will eventually
terminate and more can be created. You can experiment with MAX_THREADS to
see where your particular system begins to have trouble with too many
connections.
The examples you’ve seen so far use
the
Transmission
Control Protocol (TCP, also known as
stream-based
sockets), which is designed for ultimate reliability and guarantees that the
data will get there. It allows retransmission of lost data, it provides multiple
paths through different routers in case one goes down, and bytes are delivered
in the order they are sent. All this control and reliability comes at a cost:
TCP has a high overhead.
There’s a second protocol, called
User
Datagram Protocol (UDP), which doesn’t guarantee that the packets will
be delivered and doesn’t guarantee that they will arrive in the order they
were sent. It’s called an
“unreliable
protocol” (TCP is a
“reliable
protocol”), which sounds bad, but because it’s much faster it can be
useful. There are some applications, such as an audio signal, in which it
isn’t so critical if a few packets are dropped here or there but speed is
vital. Or consider a time-of-day server, where it really doesn’t matter if
one of the messages is lost. Also, some applications might be able to fire off a
UDP message to a server and can then assume, if there is no response in a
reasonable period of time, that the message was lost.
Typically, you’ll do most of your
direct network programming with TCP, and only occasionally will you use UDP.
There’s a more complete treatment of UDP in the first edition of this book
(available on the CD ROM bound into this book, or as a free download from
www.BruceEckel.com).
It’s possible for an applet to
cause the display of any URL through the Web browser the applet is running
within. You can do this with the following line:
getAppletContext().showDocument(u);
in
which u is the URL object. Here’s a simple example that
redirects you to another Web page. The page happens to be the output of a CGI
program, but you can as easily go to an ordinary HTML page, so you could build
on this applet to produce a password-protected gateway to a particular portion
of your Web site:
//: c15:ShowHTML.java // <applet code=ShowHTML width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import com.bruceeckel.swing.*; public class ShowHTML extends JApplet { static final String CGIProgram = "MyCGIProgram"; JButton send = new JButton("Go"); JLabel l = new JLabel(); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); send.addActionListener(new Al()); cp.add(send); cp.add(l); } class Al implements ActionListener { public void actionPerformed(ActionEvent ae) { try { // This could be an HTML page instead of // a CGI program. Notice that this CGI // program doesn't use arguments, but // you can add them in the usual way. URL u = new URL( getDocumentBase(), "cgi-bin/" + CGIProgram); // Display the output of the URL using // the Web browser, as an ordinary page: getAppletContext().showDocument(u); } catch(Exception e) { l.setText(e.toString()); } } } public static void main(String[] args) { Console.run(new ShowHTML(), 100, 50); } } ///:~
The beauty of the
URL class is how much it
shields you from. You can connect to Web servers without knowing much at all
about what’s going on under the covers.
A variation on the above program reads a
file located on the server. The file is specified by the
client:
//: c15:Fetcher.java // <applet code=Fetcher width=500 height=300> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import com.bruceeckel.swing.*; public class Fetcher extends JApplet { JButton fetchIt= new JButton("Fetch the Data"); JTextField f = new JTextField("Fetcher.java", 20); JTextArea t = new JTextArea(10,40); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); fetchIt.addActionListener(new FetchL()); cp.add(new JScrollPane(t)); cp.add(f); cp.add(fetchIt); } public class FetchL implements ActionListener { public void actionPerformed(ActionEvent e) { try { URL url = new URL(getDocumentBase(),f.getText()); t.setText(url + "\n"); InputStream is = url.openStream(); BufferedReader in = new BufferedReader( new InputStreamReader(is)); String line; while ((line = in.readLine()) != null) t.append(line + "\n"); } catch(Exception ex) { t.append(ex.toString()); } } } public static void main(String[] args) { Console.run(new Fetcher(), 500, 300); } } ///:~
There’s actually a lot more to
networking than can be covered in this introductory treatment. Java networking
also provides fairly extensive support for URLs, including protocol handlers for
different types of content that can be discovered at an Internet site. You can
find other Java networking features fully and carefully described in Java
Network Programming by Elliotte Rusty Harold (O’Reilly,
1997).
It has been estimated that half of all
software development involves client/server operations. A great promise of Java
has been the ability to build platform-independent client/server database
applications. In Java 1.1 this has come to fruition with
Java
DataBase Connectivity (JDBC).
One of the major problems with databases
has been the feature wars between the database companies. There is a
“standard” database language,
Structured Query Language
(SQL-92), but usually you must know which database vendor you’re working
with despite the standard. JDBC is designed to be platform-independent, so you
don’t need to worry about the database you’re using while
you’re programming. However, it’s still possible to make
vendor-specific calls from JDBC so you aren’t restricted from doing what
you must.
One place where programmers may need to
use SQL type names is in the SQL TABLE
CREATE statement when they are creating a new
database table and defining the SQL type for each column. Unfortunately there
are significant variations between SQL types supported by different database
products. Different databases that support SQL types with the same semantics and
structure may give those types different names. Most major databases support an
SQL data type for large binary values, in Oracle this type is called a
LONG RAW,
Sybase calls it
IMAGE,
Informix calls it
BYTE, and
DB2 LONG VARCHAR FOR BIT
DATA. Therefore, if database portability is a
goal you should try to use only generic SQL type identifiers.
Portability is an issue when writing for
a book where readers may be testing the examples with all kinds of unknown data
stores. We have tried to write these examples to be as portable as possible. You
should also notice that all the database specific code has been pulled out to a
single class file to centralize any changes that may need to be made to get the
examples operational in your environment.
JDBC, like many of the APIs in Java, is
designed for simplicity. The method calls you make correspond to the logical
operations you’d think of doing when gathering data from a database:
connect to the database, create a statement and execute the query, and look at
the result set.
To allow this platform independence, JDBC
provides a driver manager that dynamically maintains all the driver
objects that your database queries will need. So if you have three different
kinds of vendor databases to connect to, you’ll need three different
driver objects. The driver objects register themselves with the driver manager
at the time of loading, and you can force the loading using
Class.forName( ).
All this
information is combined into one string, the “database URL.” For
example, to connect through the ODBC subprotocol to a database identified as
“people,” the database URL could be:
String dbUrl = "jdbc:odbc:people";
If you’re connecting across a
network, the database URL will also contain the information identifying the
remote machine.
When you’re ready to connect to the
database, you call the static method
DriverManager.getConnection( ), passing it the database URL, the
user name, and a password to get into the database. You get back a
Connection object that you can then use to query and manipulate the
database.
The following example opens a database of
contact information and looks for a person’s last name as given on the
command line. It selects only the names of people that have email addresses,
then prints out all the ones that match the given last name:
//: c15:jdbc:Lookup.java // Looks up email addresses in a // local database using JDBC. import java.sql.*; public class Lookup { public static void main(String[] args) { String dbUrl = "jdbc:odbc:people"; String user = ""; String password = ""; try { // Load the driver (registers itself) Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver"); Connection c = DriverManager.getConnection( dbUrl, user, password); Statement s = c.createStatement(); // SQL code: ResultSet r = s.executeQuery( "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); while(r.next()) { // Capitalization doesn't matter: System.out.println( r.getString("Last") + ", " + r.getString("fIRST") + ": " + r.getString("EMAIL") ); } s.close(); // Also closes ResultSet } catch(Exception e) { e.printStackTrace(); } } } ///:~
You can see the creation of the database
URL as previously described. In this example, there is no password protection on
the database so the user name and password are empty strings.
Once the connection is made with
DriverManager.getConnection( ), you can use the resulting
Connection object to create a Statement object using the
createStatement( )
method. With the resulting
Statement, you can call
executeQuery( ),
passing in a string containing an SQL-92 standard SQL statement. (You’ll
see shortly how you can generate this statement automatically, so you
don’t have to know much about SQL.)
The executeQuery( ) method
returns a ResultSet
object, which is quite a bit like an iterator: the next( ) method
moves the iterator to the next record in the statement, or returns false
if the end of the result set has been reached. You’ll always get a
ResultSet object back from executeQuery( ) even if a query
results in an empty set (that is, an exception is not thrown). Note that you
must call next( ) once before trying to read any record data. If the
result set is empty, this first call to next( ) will return
false. For each record in the result set, you can select the fields using
(among other approaches) the field name as a string. Also note that the
capitalization of the field name is ignored—it doesn’t matter with
an SQL database. You determine the type you’ll get back by calling
getInt( ),
getString( ),
getFloat( ), etc. At
this point, you’ve got your database data in Java native format and can do
whatever you want with it using ordinary Java
code.
With JDBC, understanding the code is
relatively simple. The confusing part is making it work on your particular
system. The reason this is confusing is that it requires you to figure out how
to get your JDBC driver to load properly, and how to set up a database using
your database administration software.
Of course, this process can vary
radically from machine to machine, but the process I used to make it work under
32-bit Windows might give you clues to help you attack your own
situation.
The program above contains the
statement:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
This implies a directory structure, which
is deceiving. With this particular installation of JDK 1.1, there was no file
called JdbcOdbcDriver.class, so if you looked at this example and went
searching for it you’d be frustrated. Other published examples use a
pseudo name, such as “myDriver.ClassName,” which is less than
helpful. In fact, the load statement above for the jdbc-odbc driver (the only
one that actually comes with JDK 1.1) appears in only a few places in the online
documentation (in particular, a page labeled “JDBC-ODBC Bridge
Driver”). If the load statement above doesn’t work, then the name
might have been changed as part of a Java version change, so you should hunt
through the documentation again.
If the load statement is wrong,
you’ll get an exception at this point. To test whether your driver load
statement is working correctly, comment out the code after the statement and up
to the catch clause; if the program throws no exceptions it means that
the driver is loading properly.
Again, this is specific to 32-bit
Windows; you might need to do some research to figure it out for your own
platform.
First, open the control panel. You might
find two icons that say “ODBC.” You must use the one that says
“32bit ODBC,” since the other one is for backward compatibility with
16-bit ODBC software and will produce no results for JDBC. When you open the
“32bit ODBC” icon, you’ll see a tabbed dialog with a number of
tabs, including “User DSN,” “System DSN,” “File
DSN,” etc., in which “DSN” means “Data Source
Name.” It turns out that for the JDBC-ODBC bridge, the only place where
it’s important to set up your database is “System DSN,” but
you’ll also want to test your configuration and create queries, and for
that you’ll also need to set up your database in “File DSN.”
This will allow the Microsoft Query tool (that comes with Microsoft Office) to
find the database. Note that other query tools are also available from other
vendors.
The most interesting database is one that
you’re already using. Standard ODBC supports a number of different file
formats including such venerable workhorses as DBase. However, it also includes
the simple “comma-separated ASCII” format, which virtually every
data tool has the ability to write. In my case, I just took my
“people” database that I’ve been maintaining for years using
various contact-management tools and exported it as a comma-separated ASCII file
(these typically have an extension of .csv). In the “File
DSN” section I chose “Add,” chose the text driver to handle my
comma-separated ASCII file, and then un-checked “use current
directory” to allow me to specify the directory where I exported the data
file.
You’ll notice when you do this that
you don’t actually specify a file, only a directory. That’s because
a database is typically represented as a collection of files under a single
directory (although it could be represented in other forms as well). Each file
usually contains a single table, and the SQL statements can produce results that
are culled from multiple tables in the database (this is called a
join). A database that
contains only a single table (like this one) is usually called a
flat-file
database. Most problems that go beyond the simple storage and retrieval of
data generally require multiple tables that must be related by joins to produce
the desired results, and these are called
relational
databases.
To test the configuration you’ll
need a way to discover whether the database is visible from a program that
queries it. Of course, you can simply run the JDBC program example above up to
and including the statement:
Connection c = DriverManager.getConnection( dbUrl, user, password);
If an exception is thrown, your
configuration was incorrect.
However, it’s useful to get a
query-generation tool involved at this point. I used Microsoft Query that came
with Microsoft Office, but you might prefer something else. The query tool must
know where the database is, and Microsoft Query required that I go to the ODBC
Administrator’s “File DSN” tab and add a new entry there,
again specifying the text driver and the directory where my database lives. You
can name the entry anything you want, but it’s helpful to use the same
name you used in “System DSN.”
Once you’ve done this, you will see
that your database is available when you create a new query using your query
tool.
The query that I created using Microsoft
Query not only showed me that my database was there and in good order, but it
also automatically created the SQL code that I needed to insert into my Java
program. I wanted a query that would search for records that had the last name
that was typed on the command line when starting the Java program. So as a
starting point, I searched for a specific last name, ‘Eckel’. I also
wanted to display only those names that had email addresses associated with
them. The steps I took to create this query were:
The result of this
query will show you whether you’re getting what you want.
Now you can press the SQL button and
without any research on your part, up will pop the correct SQL code, ready for
you to cut and paste. For this query, it looked like this:
SELECT people.FIRST, people.LAST, people.EMAIL FROM people.csv people WHERE (people.LAST='Eckel') AND (people.EMAIL Is Not Null) ORDER BY people.FIRST
With more complicated queries it’s
easy to get things wrong, but with a query tool you can interactively test your
queries and automatically generate the correct code. It’s hard to argue
the case for doing this by hand.
You’ll notice that the code above looks different from what’s used in the program. That’s because the query tool uses full qualification for all of the names, even when there’s only one table involved. (When more than one table is involved, the qualification prevents collisions between columns from different tables that have the same names.) Since this query involves only one table, you can optionally remove the “people” qualifier from most of the names, like this: SELECT FIRST, LAST, EMAIL FROM people.csv people WHERE (LAST='Eckel') AND (EMAIL Is Not Null) ORDER BY FIRST
In addition, you don’t want this
program to be hard coded to look for only one name. Instead, it should hunt for
the name given as the command-line argument. Making these changes and turning
the SQL statement into a dynamically-created String
produces:
"SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST");
SQL has another way to insert names into
a query called
stored
procedures, which is used for speed. But for much of your database
experimentation and for your first cut, building your own query strings in Java
is fine.
You can see from this example that by
using the tools currently available—in particular the query-building
tool—database programming with SQL and JDBC can be quite
straightforward.
It’s more useful to leave the
lookup program running all the time and simply switch to it and type in a name
whenever you want to look someone up. The following program creates the lookup
program as an application/applet, and it also adds name completion so the data
will show up without forcing you to type the entire last name:
//: c15:jdbc:VLookup.java // GUI version of Lookup.java. // <applet code=VLookup // width=500 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import java.sql.*; import com.bruceeckel.swing.*; public class VLookup extends JApplet { String dbUrl = "jdbc:odbc:people"; String user = ""; String password = ""; Statement s; JTextField searchFor = new JTextField(20); JLabel completion = new JLabel(" "); JTextArea results = new JTextArea(40, 20); public void init() { searchFor.getDocument().addDocumentListener( new SearchL()); JPanel p = new JPanel(); p.add(new Label("Last name to search for:")); p.add(searchFor); p.add(completion); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); cp.add(results, BorderLayout.CENTER); try { // Load the driver (registers itself) Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver"); Connection c = DriverManager.getConnection( dbUrl, user, password); s = c.createStatement(); } catch(Exception e) { results.setText(e.getMessage()); } } class SearchL implements DocumentListener { public void changedUpdate(DocumentEvent e){} public void insertUpdate(DocumentEvent e){ textValueChanged(); } public void removeUpdate(DocumentEvent e){ textValueChanged(); } } public void textValueChanged() { ResultSet r; if(searchFor.getText().length() == 0) { completion.setText(""); results.setText(""); return; } try { // Name completion: r = s.executeQuery( "SELECT LAST FROM people.csv people " + "WHERE (LAST Like '" + searchFor.getText() + "%') ORDER BY LAST"); if(r.next()) completion.setText( r.getString("last")); r = s.executeQuery( "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE (LAST='" + completion.getText() + "') AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); } catch(Exception e) { results.setText( searchFor.getText() + "\n"); results.append(e.getMessage()); return; } results.setText(""); try { while(r.next()) { results.append( r.getString("Last") + ", " + r.getString("fIRST") + ": " + r.getString("EMAIL") + "\n"); } } catch(Exception e) { results.setText(e.getMessage()); } } public static void main(String[] args) { Console.run(new VLookup(), 500, 200); } } ///:~
Much of the database logic is the same,
but you can see that a TextListener is added to listen to the
JTextField, so that whenever you type a new character it first tries to do a
name completion by looking up the last name in the database and using the first
one that shows up. (It places it in the completion Label, and uses
that as the lookup text.) This way, as soon as you’ve typed enough
characters for the program to uniquely find the name you’re looking for,
you can stop.
When you browse the online documentation
for JDBC it can seem daunting. In particular, in the
DatabaseMetaData
interface—which is just huge, contrary to most of the interfaces you see
in Java—there are methods such as
dataDefinitionCausesTransactionCommit( ),
getMaxColumnNameLength( ), getMaxStatementLength( ),
storesMixedCaseQuotedIdentifiers( ),
supportsANSI92IntermediateSQL( ),
supportsLimitedOuterJoins( ), and so on. What’s this all
about?
As mentioned earlier, databases have
seemed from their inception to be in a constant state of turmoil, primarily
because the demand for database applications, and thus database tools, is so
great. Only recently has there been any convergence on the common language of
SQL (and there are plenty of other database languages in common use). But even
with an SQL “standard” there are so many variations on that theme
that JDBC must provide the large DatabaseMetaData interface so that your
code can discover the capabilities of the particular “standard” SQL
database that it’s currently connected to. In short, you can write simple,
transportable SQL, but if you want to optimize speed your coding will multiply
tremendously as you investigate the capabilities of a particular vendor’s
database.
This, of course, is not Java’s fault. The discrepancies between database products are just something that JDBC tries to help compensate for. But bear in mind that your life will be easier if you can either write generic queries and not worry too much about performance, or, if you must tune for performance, know the platform you’re writing for so you don’t need to write all that investigation code.
There
is more JDBC information available in the electronic documents that come as part
of the Java distribution from Sun. In addition, you can find more in the book
JDBC Database Access with Java (Hamilton, Cattel, and Fisher,
Addison-Wesley, 1997). Other JDBC books are appearing
regularly.
If you’re connecting across a
network, the database URL will contain the connection information and the dbUrl
can become a bit intimidating. Here is an example from a CloudScape database
being called from a remote client utilizing RMI:
jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db
This database URL is really two jdbc
calls in one. The first part
"jdbc:rmi://192.168.170.27:1099/"
uses RMI to make the connection to the remote database engine listening on port
1099 at IP Address 192.168.170.27. The second part of the URL, "
jdbc:cloudscape:db"
conveys the more typical settings using the subprotocol and database name but
this will only happen after the first section has made the connection via RMI to
the remote machine.
When you’re ready to connect to the
database, you call the static method
DriverManager.getConnection( ), passing it the database URL, the
user name, and a password to get into the database. You get back a
Connection object that you can then use to query and manipulate the
database.
For the example, the database specific
code will reside in the class DBStuff. Our database URL, JDBC driver,
username and password will have access methods, all other SQL statements will be
public strings.
//: c15:jdbc:DBStuff.java // A class to hold all our // database specific code // for the community // interests database import java.sql.*; public class DBStuff { // All the database stuff // Specific for CloudScape. String dbDriver = "COM.cloudscape.core.JDBCDriver"; String dbURL = "jdbc:cloudscape:d:/docs/_work/JSapienDB"; String user = ""; String password = ""; public String dropMemTbl = "drop table MEMBERS"; public String createMemTbl = "create table MEMBERS " + "(MEM_ID INTEGER primary key, " + "MEM_UNAME VARCHAR(12) not null unique, " + "MEM_LNAME VARCHAR(40), " + "MEM_FNAME VARCHAR(20), " + "ADDRESS VARCHAR(40), " + "CITY VARCHAR(20), " + "STATE CHAR(4), " + "ZIP CHAR(5), " + "PHONE CHAR(12), " + "EMAIL VARCHAR(30))"; public String createMemIdx = "create unique index " + "LNAME_IDX on MEMBERS(MEM_LNAME)"; public String dropEvtTbl = "drop table EVENTS"; public String createEvtTbl = "create table EVENTS " + "(EVT_ID INTEGER primary key, " + "EVT_TITLE VARCHAR(30) not null, " + "EVT_TYPE VARCHAR(20), " + "LOC_ID INTEGER, " + "PRICE DECIMAL, " + "DATETIME TIMESTAMP)"; public String createEvtIdx = "create unique index " + "TITLE_IDX on EVENTS(EVT_TITLE)"; public String dropEMTbl = "drop table EVTMEMS"; public String createEMTbl = "create table EVTMEMS " + "(MEM_ID INTEGER not null, " + "EVT_ID INTEGER not null, " + "MEM_ORD INTEGER)"; public String createEMIdx = "create unique index " + "EVTMEM_IDX on EVTMEMS(MEM_ID, EVT_ID)"; public String dropLocTbl = "drop table LOCATIONS"; public String createLocTbl = "create table LOCATIONS " + "(LOC_ID INTEGER primary key, " + "LOC_NAME VARCHAR(30) not null, " + "CONTACT VARCHAR(50), " + "ADDRESS VARCHAR(40), " + "CITY VARCHAR(20), " + "STATE VARCHAR(4), " + "ZIP VARCHAR(5), " + "PHONE CHAR(12), " + "DIRECTIONS VARCHAR(4096))"; public String createLocIdx = "create unique index " + "NAME_IDX on LOCATIONS(LOC_NAME)"; public String getDriver() { return dbDriver; } public String getDbURL() { return dbURL; } public String getUser() { return user; } public String getPassword() { return password; } } ///:~
Our DBStuff class will
generate a set of tables that will have a structure as shown below. Certainly
not elaborate but just right for the level of detail we would like to show.
There are numerous books, seminars and software packages that will help you in
the design and development of a database. We do not want to elaborate on those
areas, we will focus primarily on Java. Our goal is to provide a simple example
to test most of our Enterprise APIs and a structure that could be easily
transferred to the database you use.
The following class, CreateTables,
uses the DBStuff class to load the JDBC driver, make a connection to the
database, then create the table structure outlined above. Once the connection is
made and SQL statements have been written there is not much to do except push
the SQL to the database and or handle errors responsibly.
//: c15:jdbc:CreateTables.java // Creates database tables for // community interests database import java.sql.*; public class CreateTables { public static void main(String[] args) { DBStuff db = new DBStuff(); try { // Load the driver (registers itself) Class.forName(db.getDriver()); } catch(java.lang.ClassNotFoundException e) { System.err.print("ClassNotFoundException: "); e.printStackTrace(); } try { Connection c = DriverManager.getConnection( db.getDbURL(), db.getUser(), db.getPassword()); Statement s = c.createStatement(); // SQL code: // Create the MEMBERS table try { s.executeUpdate( db.dropMemTbl ); } catch(SQLException sqlEx) { String msg; msg = "Table MEMBERS not present. " + "Drop failed."; System.out.println(msg); } s.executeUpdate( db.createMemTbl ); s.executeUpdate( db.createMemIdx ); // Create the EVENTS table try { s.executeUpdate( db.dropEvtTbl ); } catch(SQLException sqlEx) { String msg; msg = "Table EVENTS not present. " + "Drop failed."; System.out.println(msg); } s.executeUpdate( db.createEvtTbl ); s.executeUpdate( db.createEvtIdx ); // Create the EVTMEMS table try { s.executeUpdate( db.dropEMTbl ); } catch(SQLException sqlEx) { String msg; msg = "Table EVTMEMS not present. " + "Drop failed."; System.out.println(msg); } s.executeUpdate( db.createEMTbl ); // Create the LOCATIONS table try { s.executeUpdate( db.dropLocTbl ); } catch(SQLException sqlEx) { String msg; msg = "Table LOCATIONS not present. " + "Drop failed."; System.out.println(msg); } s.executeUpdate( db.createLocTbl ); s.executeUpdate( db.createLocIdx ); s.close(); } catch(Exception e) { e.printStackTrace(); } } } ///:~
You can see the creation of the database
URL as previously described. In this example, there is no password protection on
the database so the user name and password are empty strings.
Once the connection is made with
DriverManager.getConnection( ), you can use the resulting
Connection object to create a Statement object using the
createStatement( )
method. With the resulting
Statement, you can call
executeUpdate( ),
passing in a string containing an SQL-92 standard SQL statement. (You’ll
see shortly how you can generate this statement automatically, so you
don’t have to know much about SQL.) executeUpdate( ) usually
will return the number of rows that were affected by the SQL statement.
executeUpdate( ) is more commonly used to execute
INSERT,
UPDATE, or
DELETE
statements that modify one or more rows. For statements such as
CREATE
TABLE, DROP
TABLE and
CREATE
INDEX, executeUpdate( ) always
returns zero.
The Statement interface provides two
other methods for executing SQL statements: executeQuery( ) and
execute( ). The correct method to use is determined by what the SQL
statement produces.
The executeQuery( ) method
returns a ResultSet
object, which is quite a bit like an iterator: the next( ) method
moves the iterator to the next record in the statement, or returns false
if the end of the result set has been reached. You’ll always get a
ResultSet object back from executeQuery( ) even if a query
results in an empty set (that is, an exception is not thrown). Note that you
must call next( ) once before trying to read any record data. If the
result set is empty, this first call to next( ) will return
false. For each record in the result set, you can select the fields using
(among other approaches) the field name as a string. Also note that the
capitalization of the field name is ignored—it doesn’t matter with
an SQL database. You determine the type you’ll get back by calling
getInt( ),
getString( ),
getFloat( ), etc. At
this point, you’ve got your database data in Java native format and can do
whatever you want with it using ordinary Java code.
The method execute( ) is used
to execute statements that return more than one result set, more than one update
count, or a combination of the two.
Let's now load the table we created above
with data. This will require us to perform a series of
INSERTS
followed by a
SELECT to
see all the data in our table and to let us exercise a result
set.
Now that we understand JDBC and how it
abstracts all those different database backends into a common API set,
let’s turn our attention to another common task - handling HTTP requests
and responses. It is taken for granted in today’s technical environment
that client access from the Internet or corporate intranets is a sure way to
allow many users to access data and resources easily. This type of access is
predicated on the clients utilizing the World Wide Web standards of Hypertext
Markup Language (HTML) and Hypertext Transfer Protocol (HTTP). Wouldn’t it
be nice to have an API set that abstracted out this commonly used area of client
access? Welcome Java Servlets!
Traditionally, the way to handle a
problem such as allowing an Internet client to update their personal data is to
create an HTML page with a text
field and a “submit” button. The user can type whatever he or she
wants into the text field, and it will be submitted to the server without
question. As it submits the data, the Web page also tells the server what to do
with the data by mentioning the
Common
Gateway Interface (CGI) program that the server should run after receiving this
data. This CGI program is typically written in either Perl or C (and sometimes
C++, if the server supports it), and it must handle everything. First it looks
at the data and decides whether it’s in the correct format. If not, the
CGI program must create an HTML page to describe the problem; this page is
handed to the server, which sends it back to the user. The user must then back
up a page and try again. If the data is correct, the CGI program opens the data
file and either adds the email address to the file or discovers that the address
is already in the file. In both cases it must format an appropriate HTML page
for the server to return to the user.
As Java programmers, this seems like an
awkward way for us to solve the problem, and naturally, we’d like to do
the whole thing in Java. First, we’ll use a Java applet to take care of
data validation at the client site, without all that tedious Web traffic and
page formatting. Then let’s skip the Perl CGI script in favor of a Java
application running on the server. In fact, let’s skip the Web server
altogether and simply make our own network connection from the applet to the
Java application on the server!
As you’ll see, there are a number
of issues that make this a more complicated problem than it seems. It would be
ideal to write the applet but applets, while a proven technology with plenty of
support, have been problematic in the Wild World Web where different browsers
handle applet’s differently. In a corporate intranet where there is some
level of standardization this seems possible in the short term but what happens
with the next acquisition or merger? What happens when employees want to start
working from home? That’s easy - things start to look a lot like the
Internet - you can’t depend on anything especially how applets are
implemented in your client’s browsers. So to be on the safe side, what we
really want to do is deal with straight HTML and HTTP within our java server.
The client knows nothing of the implementation, they are only aware that they
can get at their data and perform their work without installing, upgrading or
calling tech support.
Sun has delivered on this need. The
Servlet API wraps up the HTTP protocol so we can put Java on the server side of
our HTTP connection and deal with our client in HTML and HTTP. Servlets are
completely server side java for the Web. The client sees nothing except HTTP and
HTML.
The architecture of the Servlet API is
that of a classic service provider with a service( ) method through
which all client requests will drive and life cycle methods init( )
and destroy( ).
public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); }
getServletConfig( ) sole
purpose is to return a ServletConfig object which contains initialization and
startup parameters for this servlet and getServletInfo( ) returns a
string containing information about the servlet, such as author, version, and
copyright.
The GenericServlet class is a
shell implementation of this interface, nothing more. The HttpServlet
class is an extension of GenericServlet and is designed specifically to
handle the HTTP protocol.
Although you can derive your servlets
from GenericServlet, Sun recommends that all servlets derive from
HttpServlet. This makes sense since your servlet is designed to work with
a servlet engine that is satisfying clients requests from within a Web server.
Why not utilize the built-in parsing capabilities for POST and GET that come
with HttpServlet? We will be getting into this shortly.
The most wonderful attribute of the
Servlet API is the auxiliary objects that come along with the HttpServlet class
to support it. Look at the service( ) method in the Servlet
interface. It has two parameters ServletRequest and
ServletResponse. With the HttpServlet class these two object are
extended for HTTP as well—HttpServletRequest and
HttpServletResponse. Let’s take a closer look.
//: c15:servlets:ServletsRule.java import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class ServletsRule extends HttpServlet { int i = 0; // Servlet "persistence" public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { PrintWriter out = res.getWriter(); out.print("<HEAD><TITLE>"); out.print("A server-side strategy"); out.print("</TITLE></HEAD><BODY>"); out.print("<h1>Servlets Rule! " + i++); out.print("</h1></BODY>"); out.close(); } } ///:~
ServletsRule is about as simple as
a servlet can get. But that is the beauty of it all - just think how much stuff
is being handled for us! Once the servlet is initialized - its init( )
method has run to completion - can clients enter the service( )
method. In the service method our main responsibility is to interact with the
HTTP request the client sent us and build a HTTP response based upon the
attributes contained within the request. In ServletsRule we only manipulate the
response object without looking at what the client may has sent us. We call the
getWriter( ) method of the response object to get a PrintWriter
object. The PrintWriter is used for writing character-based response
data.
As we dive more deeply into the
HttpRequest and HttpResponse objects you should notice that a
greater understanding of HTTP and HTML would be helpful. Servlets are designed
for Web server-side development and it shows. Now let’s scratch a little
deeper by getting some HTML form data that was passed to the servlet in the
request object.
//: c15:servlets:EchoForm.java // Dumps the name-value pairs of any HTML form import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.util.*; public class EchoForm extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.print("<h1>Your form contained:</h1>"); Enumeration flds = req.getParameterNames(); while(flds.hasMoreElements()) { String field = (String)flds.nextElement(); String value = req.getParameter(field); out.print(field + " = " + value + "<br>"); } out.close(); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { doGet(req, res); } } ///:~
The EchoForm servlet parrots back
to our client the request fields that have been sent to the servlet as part of
the request object. Since the request fields will be placed into a response we
must set up the response object with calls to setContentType( ) and
getWriter( ). In an HttpResponse object the
setContentType( ) methods sets the Content-Type HTTP header. This is
most commonly "text/html".
Notice that in EchoForm the
service( ) method has been replaced by doGet( ). This
automatic HTTP method name parsing is a feature of HttpServlet. HTTP was
designed for the Web and has been made more general than necessary. The first
word on the full request line is simply the name of the method (command) to be
executed on the Web page. The built-in methods are GET, POST, HEAD, PUT, DELETE,
LINK and UNLINK. The HttpServlet class is written to parse these methods and let
the programmer react differently for a GET than a POST or even a HEAD request
method. If you don't care you can just override service( ). Since
most HTTP methods are POST or GET many times you just implement one and direct
the other to it.
In The HTTP request object has the
potential to come with request parameters and generally does. The parameters are
typically name-value pairs sent as part of its query string (for GET requests)
or as encoded post data (for POST requests). EchoForm uses the request object's
getParameterNames( ) method to loop through the parameter list. The
getParameter( ) method is used to pull the value. The pair is then
written to the PrintWriter of the response object with the appropriate HTML
tags. The response object is sent to the client when the servlet is
finished.
Now that you understand the basics you
should be realizing that servlets are excellent for server-side Web development.
Elegant and straight forward, they do just about everything for you - right?
Well, almost. Remember early we said all client requests drive through the
service method? This is the well-used, high traffic corridor of the servlet and
more than one client request may come through at the same time. The servlet
engine has a pool of threads that it will dispatch to handle client requests. It
is quite likely that two clients arriving at the same time could be processing
through your service( ) or doGet( ) or doPost( ) methods at the
same time. Therefore the service( ) methods and other methods called by
HttpServlet.service( ) (e.g., doGet( ), doPost( ),
doHead( ),etc.) need to be written in a thread-safe manner. Any common
resources (files, databases) that will be used by your client requests will need
to be synchronized.
ThreadServlet is a simple example that
simply synchronizes around the threads sleep( ) method. This will hold up
all threads until the allotted time (5000 ms) is all used up. When testing this
you should start several browsers instances and hit this servlet as quickly as
possible.
//: c15:servlets:ThreadServlet.java import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class ThreadServlet extends HttpServlet { int i; public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); synchronized(this) { try { Thread.currentThread().sleep(5000); } catch(InterruptedException e) {} } out.print("<h1>Finished " + i++ + "</h1>"); out.close(); } } ///:~
The servlet API comes with more than just
the classes that implement the servlet interface, GenericServlet and HttpServlet
and the Request and Response objects. The design of HTTP is such that it is a
'sessionless' protocol. A great deal of effort has gone into mechanisms that
will allow Web developers to track sessions. How could companies do e-commerce
if you couldn't keep track of client and the items they have put into their
shopping cart? You couldn't! This may be great for privacy advocates but it does
little to help create robust, commerce driven Web sites.
There are several methods of session
tracking but the most common method is with persistent 'cookies'. The term
cookie sounds cute and could be perceived as a session tracking solution that
was baked up in someone's garage. The fact is that cookies are an integral part
of the Internet standards. The HTTP Working Group of the Internet Engineering
Task Force has written cookies in the official standard in RFC 2109
(http://ds.internic.net/rfc/rfc2109.txt or check
www.cookiecentral.com).
A cookie is nothing more than a small
piece of information sent by a Web server to a browser. The browser stores the
cookie locally and all calls to the server from that browser will contain the
cookie as an identifier. The cookie therefore acts to uniquely identify the
client with each hit of this Web server. It should be noted that clients can
turn off the browsers ability to accept cookies. If your site still need to be
able to session track this type of client then another method of session
tracking (URL rewriting or hidden form fields) will have to be incorporated. The
session tracking capabilities built into the Servlet API are designed around
cookies.
The Servlet API (version 2.0 and up)
provides the javax.servlet.http.Cookie class. This class incorporates all the
HTTP header details and allows the setting of various cookie attributes. Using
the cookie is simply a matter of creating it using the constructor and adding it
to the response object. The constructor takes a cookie name as the first
argument and a value as the second. Cookies are added to the response object
before you send any content.
Cookie oreo = new Cookie("TIJava", "2000"); res.addCookie(cookie);
Cookies are then received by calling the
getCookies( ) method of the HttpServletRequest object which returns an
array of cookie objects.
Cookie[] cookies = req.getCookies();
A session in the world of HTTP and the
Internet is one or more page requests by a client to a Web site during a defined
period of time. If I am buying my groceries on-line, I want a session to be
confined to the period from when I first add an item to my shopping cart to the
point where I checkout. Each item I add to the shopping cart will be a new
connection in the HTTP world, they have no knowledge of previous connections or
items in the shopping cart. The mechanics supplied by the Cookie specification
allows us to perform 'session tracking'.
You should understand that a cookie is an
object that encapsulates that small bit of information that will be stored on
the client side. A Servlet Session object lives on the server side of the
communication channel and its goal is to capture data about this client that
would be useful as the client moves through and interacts with your Web site.
This data may be pertinent for the present session, such as items in the
shopping cart or it may be information you asked the client to enter such as
authentication information entered when the client first entered your Web site
and which should not have to be re-enter before a set time of
inactivity.
The Session class of the Servlet API uses
the Cookie class but really all the session object needs is a unique identifier
stored on the client and passed to the server. Usually this is a cookie and that
is the mechanism we will cover here. Web sites may also use the other types of
session tracking but these mechanisms will be more difficult to implement as
they are not encapsulated into the Servlet API.
Let's take a look at implementing session
tracking with the Servlet API:
//: c15:servlets:SessionPeek.java // Using the HttpSession class. import java.io.*; import java.util.Date; import javax.servlet.*; import javax.servlet.http.*; public class SessionPeek extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Retrieve Seesion Object before any // output is sent to the client. HttpSession session = req.getSession(); // Get the output stream ServletOutputStream out = res.getOutputStream(); res.setContentType("text/html"); out.println("<HEAD><TITLE> SessionPeek "); out.println(" </TITLE></HEAD><BODY>"); out.println("<h1> SessionPeek </h1>"); // A simple hit counter for this session. Integer ival = (Integer) session.getValue("sesspeek.cntr"); if(ival==null) ival = new Integer(1); else ival = new Integer(ival.intValue() + 1); session.putValue("sesspeek.cntr", ival); out.println("You have hit this page <b>" + ival + "</b> times.<p>"); // Session Data out.println("<h2>"); out.println(" Saved Session Data </h2>"); // loop through all data in the session // and spit is out. String[] sesNames = session.getValueNames(); for(int i = 0; i < sesNames.length; i++) { String name = sesNames[i]; String value = session.getValue(name).toString(); out.println(name + " = " + value + "<br>"); } // Session Statistics out.println("<h3> Session Statistics </h3>"); out.println("Session ID: " + session.getId() + "<br>"); out.println("New Session: " + session.isNew() + "<br>"); out.println("Creation Time: " + session.getCreationTime()); out.println("<I>(" + new Date(session.getCreationTime()) + ")</I><br>"); out.println("Last Accessed Time: " + session.getLastAccessedTime()); out.println("<I>(" + new Date(session.getLastAccessedTime()) + ")</I><br>"); out.println("Session Inactive Interval: " + session.getMaxInactiveInterval()); out.println("Session ID in Request: " + req.getRequestedSessionId() + "<br>"); out.println("Is session id from Cookie: " + req.isRequestedSessionIdFromCookie() + "<br>"); out.println("Is session id from URL: " + req.isRequestedSessionIdFromURL() + "<br>"); out.println("Is session id valid: " + req.isRequestedSessionIdValid() + "<br>"); out.println("</BODY>"); out.close(); } public String getServletInfo() { return "A session tracking servlet"; } } ///:~
The very first thing we do when we enter
the doGet( ) method is to call getSession( ) on the request
object. getSession will return the session object associated with this request.
Do not be misled into thinking that the session object is returned with
the request. The session object does not travel across the network it lives on
the server and is associated with a client and its requests.
getSession( ) now comes in two
versions—no parameter as used here and getSession(boolean).
getSession(true) is equivalent to getSession( ). The only reason for the
boolean is to state whether you want to the session object created if it is not
found. getSession(true) is the most likely call hence
getSession( ).
The session object, if it is not new,
will give us details about our client on their previous visits. If the session
object is new then we will start with this visit to gather information about
this client’s activities. Capturing this client information is done
through the setAttribute( ) and getAttribute( ) methods of the session
object.
java.lang.Object getAttribute(java.lang.String) void setAttribute(java.lang.String name, java.lang.Object value)
The session object uses a simple
name-value pairing for loading information. The values must be derived from
java.lang.Object and the name is a string. In ServletPeek we are keeping track
of how many times the client has been back here during this session. This is
done with an Integer object that is named sesspeek.cntr. You should notice in
the if-else statement that if the name is not found we create a new Integer with
value of 1, otherwise we create a new Integer with a value equal to the
incremented value of the previously held Integer. We stick the new Integer into
the session object and let the garbage collector handle the old Integer. You
should realize that if you are using the same key the new object would overwrite
the old one. Lastly, we use our incremented counter to display how many times
the client has visited during this session.
Related to getAttribute( ) and
setAttribute( ) is getAttributeNames( ). getAttributeNames( )
returns an enumeration of all the names of objects that are bound to the session
object. This is quite handy and a small while loop has been added to SessionPeek
to show this method in action.
This brings us to the question
“Just how long does a session object hang around for?” The answer
depends upon the servlet engine you are using although I think they usually
default to 30 minutes (1800 seconds), which is what you should see from the
ServletPeek call to getMaxInactiveInterval( ). We have tested this
and found mixed results between servlet engines. Sometimes the session object
can hang around overnight. I have never seen a case where the session object
disappears before the Inactive Interval. You can try this by setting the
Inactive Interval with setMaxInactiveInterval( ) to 5 seconds and
see if your session object hangs around or if it is cleaned up at the
appropriate time. This may be an attribute you will want to investigate while
choosing a servlet engine.
If you are not already working with an
application server that handles Sun's Servlet and JSP technologies for you, then
you may want to reevaluate your choice of application server or you may want to
download the Tomcat implementation of Java Servlets and JSPs. This can be found
at jakarta.apache.org.
First, you should follow the instructions
for decompressing the version specific to your environment. This will install
the Tomcat implementation into a directory structure under where you unzipped
it. Second, edit the server.xml so that you have a new Web application. Lastly,
you will then want to load the servlet examples into that new Web application
directory where the Tomcat server can find
them.
Servlets are found by the servlet engine
looking for the .class file along a Web application path defined by the
configuration of the Web server. That makes sense, no problem there. But what if
we would like the Web server to compile the servlet at the time it is
invoked, thereby insuring that the latest and greatest code is delivered to the
client. This is the nature of Java Server Pages or JSPs.
You can think of JSPs as special Java
tags inside the HTML page that will result in a servlet being generated, then
compiled by the Web server at the time the client invokes that page. This allows
the separation of the dynamic content and the static content in the HTML page.
You get support for scripting and tags plus the reuse associated with those tags
and the JSP components that provide the functionality. Essentially, your
previously static HTML pages have become dynamic in some of it
parts.
The structure of a JSP page is a cross
between a servlet and an HTML page. The JSP page is a test-based document that
describes how to process a request and create a response. The text based
description of the page intermixes template data with some dynamic actions and
leverages the Java platform. Java Server Pages is a standard extension that is
defined on top of the Servlet Standard Extension. JSP 1.1 uses class from
Servlet 2.2 which relies on JRE 1.1.
Here’s an extremely simple JSP
example that uses a standard Java library call to get the current time in
milliseconds, which is then divided by 1000 to produce the time in seconds.
Since a JSP expression (the <%= ) is used, the result of the
calculation is coerced into a String so it can be printed to the
out object (which puts it in the Web page):
//:! c15:jsp:ShowSeconds.jsp <html><body> <H1>The time in seconds is: <%= System.currentTimeMillis()/1000 %></H1> </body></html> ///:~
There is a great deal more going on here
than meets the eye. If we follow the route of the request and response we can
get an idea how much many layers the request and response are moving through.
The client creates the request for the JSP page and sends it off to the Web
Server. The Web Server must be able to find the JSP page and forward the request
to the page. The JSP page has associated with it a compiled component that is
created from the Java code embedded within. This JSP component is the ultimate
destination of the request and the creator of the response. The response then
bubbles up the same path that the request followed picking up pieces to pass
back to the client along the way.
This is important as Sun describes a JSP
page as “a text-based document that describes how to process a
request and create a response.” That about a broad as you
can get so let’s dig deeper.
We know that the server automatically
creates, compiles, loads and runs a special servlet to generate the page’s
content. The static portions of the HTML page are generated by the servlet using
the equivalent of out.println( ) calls within the servlet. The
dynamic portions are included directly into the servlet.
There is good and bad in everything and
JSPs are no different. The downside to all this dynamism is poor performance for
first time access. Try it—it is obvious. The first access is slow and
subsequent accesses are excellent.
When we looked at servlets there were
several objects already built into the API—response, request, session,
etc. These objects are very conveniently built into the JSP specification and
they provide the same robust foundation for manipulating HTTP and HTML in a Web
application.
JSP writers have access to these implicit
objects within the JSP page just as you would within a servlet. The implicit
objects in a JSP are detailed in the table below. Each of the variables has a
class or interface that is defined in the core Java technology or the Java
Servlet API. Scope of each object can vary significantly. For example, a Session
object would have a scope exceeding that of a page as it many span several
client requests and pages and an application object would provide service to a
group of JSP pages that together would represent a Web
application.
Implicit variable |
Of Type (javax.servlet) |
Description |
Scope |
request |
protocol dependent subtype of :
HttpServletRequest. |
The request that triggers the service
invocation. |
request |
response |
protocol dependent subtype of :
HttpServletResponse. |
The response to the
request. |
page |
pageContext |
jsp.PageContext |
The page context encapsulates
implementation-dependent features and provides convenience methods and namespace
access for this JSP. |
page |
session |
Protocol dependent subtype of:
http.HttpSession |
The session object created for the
requesting client. See Servlet Session object. |
session |
application |
ServletContext |
The servlet context obtained from the
servlet configuration object (e.g.,
getServletConfig( ).getContext( ) |
app |
out |
jsp.JspWriter |
The object that writes into the output
stream. |
page |
config |
ServletConfig |
The ServletConfig for this
JSP. |
page |
page |
java.lang.Object |
The instance of this page’s
implementation class processing the current request. |
page |
The implicit objects and the power of
Java are all brought together with JSP actions. Actions affect the current out
stream and use, modify or create objects. The actions to be performed will be
determined by the details of the request object received by the JSP page. The
JSP specification includes actions types that are standard and must be
implemented by conforming engines. The syntax for action elements is based on
XML.
The actions all start with
Directives. Directives are messages to the JSP engine and the syntax
is:
<%@ directive {attr=”value”}*%>
Directives do not produce any output into
the current out stream but they are important in setting up your JSP pages
attributes and dependencies with the JSP engine. As an example the
line:
<%@ page language=”java” %>
says that the scripting language being
used within the JSP page is Java. In fact the specification only describes the
semantics of scripts for the language attribute equal to Java. This should give
you an idea of the flexibility that is being built into the JSP technology. In
the future, if you were to choose another language, say Python (a good scripting
choice), then that language would have to support the Java Run-time Environment
by exposing the Java technology object model to the script environment,
especially the implicit variables defined above, JavaBeans properties, and
public methods.
The most important directive is the page
directive. It defines a number of page dependent attributes and communications
these attributes to the JSP engine. These attributes include: language, extends,
import, session, buffer, autoFlush, isThreadSafe, info and errorPage. For
example:
<%@ page session=”true” import=”java.util.*” %>
This line indicates that the page
requires participation in an (HTTP) session. Since we have not set the language
directive the JSP engine defaults to java and the implicit script language
variable named “session” is of type javax.servlet.http.HttpSession.
If the directive had been false then the implicit variable “session”
would be unavailable, the default is true.
The import attribute describes the types
that are available to the scripting environment. This attribute is used just as
it would be in the Java programming language i.e. a (comma separated) list of
either a fully qualified Java type name denoting that type, or of a package
named followed by the “.*” string denoting all the public types
declared in that package. The import list is imported by the translated JSP page
implementation and is available to the scripting environment. Again, this is
currently only defined for when the value of the language directive is
“java”.
Once the directives have been used to set
the scripting environment we can utilize the scripting language elements. JSP
1.1 has three scripting language elements—declarations, scriptlets, and
expressions. A declaration will declare elements, a scriptlet is a
statement fragment, and an expression is a complete language expression.
In JSP each scripting element begins with a “<%”. The
exact syntax for each is:
<%! declaration %> <% scriptlet %> <%= expression %>
White space is optional after
“<%!”, “<%”, “<%=”, and before
“%>”.
As mentioned earlier, all these tags are
based upon XML. More accurately you could state that a JSP page could be mapped
to a XML document and although this is a little touted section of the
specification, I suspect you will be hearing more and more about this aspect of
JSP as Java, XML, and server-side Java become more intertwined. I will not go
into these mapping details here but you should be aware of them and if you need
more details you should refer to the JSP specification. Therefore, you should
realize that the XML equivalent syntax for the scripting elements above would
be:
<jsp:declaration> declaration </jsp:declaration> <jsp:scriptlet> scriptlet </jsp:scriptlet> <jsp:expression> expression </jsp:expression>
Declarations are used to declare
variables and methods in the scripting language used in a JSP page—Java at
this time. The declaration should be a complete Java statement and should not
produce any output in the current out stream. In the Hello.jsp example below the
variables loadTime, loadDate and hitCount are all complete Java statements
declares new variables and initializes them.
//:! c15:jsp:Hello.jsp <%-- This JSP comment will not appear in the generated html --%> <%-- This is a JSP directive: --%> <%@ page import="java.util.*" %> <%-- These are declarations: --%> <%! long loadTime= System.currentTimeMillis(); Date loadDate = new Date(); int hitCount = 0; %> <html><body> <%-- The next several lines are the result of a JSP expression inserted in the generated html; the '=' indicates a JSP expression --%> <H1>This page was loaded at <%= loadDate %> </H1> <H1>Hello, world! It's <%= new Date() %></H1> <H2>Here's an object: <%= new Object() %></H2> <H2>This page has been up <%= (System.currentTimeMillis()-loadTime)/1000 %> seconds</H2> <H3>Page has been accessed <%= ++hitCount %> times since <%= loadDate %></H3> <%-- A "scriptlet" which writes to the server console. Note that a ';' is required: --%> <% System.out.println("Goodbye"); out.println(“Cheerio”); %> </body></html> ///:~
At the tail end of Hello.jsp is a
scriptlet that writes “Goodbye” to the Web server console and
“Cheerio” to the implicit out JspWriter object. Scriptlets
can contain any code fragments that are valid Java statements. Scriptlets are
executed at request-processing time. When all the scriptlets fragments in a
given JSP are combined in the order they appear in the JSP page, they should
yield a valid statement as defined by the Java programming language. Whether or
not they produce any output into the out stream depends upon the actual code in
the scriptlet. You should be careful as scriptlets can have side effects through
their modification of the objects visible within them.
JSP expressions can found intermingled
with the HTML in the middle section of Hello.jsp. Expressions are interesting
because they must be complete Java statements, which are then evaluated. The
result of the JSP expression is coerced to a java.lang.String which is emitted
into the current implicit
out
JspWriter object. If the result of the expression cannot be coerced to a
java.lang.String then a ClassCastException is
thrown.
//:! c15:jsp:DisplayFormData.jsp <%-- Fetching the data from an HTML form. --%> <%-- This JSP also generates the form. --%> <%@ page import="java.util.*" %> <html><body> <H1>DisplayFormData</H1><H3> <% Enumeration f = request.getParameterNames(); if(!f.hasMoreElements()) { // No fields %> <form method="POST" action="DisplayFormData.jsp"> <% for(int i = 0; i < 10; i++) { %> Field<%=i%>: <input type="text" size="20" name="Field<%=i%>" value="Value<%=i%>"><br> <% } %> <INPUT TYPE=submit name=submit Value="Submit"> </form> <% } %> <% Enumeration flds = request.getParameterNames(); while(flds.hasMoreElements()) { String field = (String)flds.nextElement(); String value = request.getParameter(field); %> <li><%= field %> = <%= value %></li> <% } %></H3></body></html> ///:~
I have spent some time trying to get my
code editor to view a .jsp page with syntax highlighting. This is bit more
difficult than it would seem. Is a .jsp page Java or is it HTML? That's easy -
it's both. So setting up my color coding was like melding the HTML section with
the Java section. The real pint is that a .jsp page provides a new set of tags
that allows you to separate the passive display code (HTML) from the dynamic
programming code (Java).
There is no reason you can't have a whole
block of code that performs some action that will provide content for you HTML.
This action could be a database call or a call to some other resource.
PageContext.jsp below calls getAttributeNamesInScope( ) method of
pageContext to get all the attributes in the scope passed in (1 refers to
page).
//:! c15:jsp:PageContext.jsp <%--Viewing the attributes in the pageContext--%> <%-- Note that you can include any amount of code inside the scriptlet tags --%> <%@ page import="java.util.*" %> <html><body> <% session.setAttribute("My dog", "Ralph"); for(int scope = 1; scope <= 4; scope++) { out.println("<H3>Scope: " + scope + "</H3><BR>"); Enumeration e = pageContext.getAttributeNamesInScope(scope); while(e.hasMoreElements()) { out.println("\t<li>" + e.nextElement() + "</li>"); } } %> <H4>End of list</H4> </body></html> ///:~
The output looks like
this:
•
javax.servlet.jsp.jspOut
•
javax.servlet.jsp.jspPage
•
javax.servlet.jsp.jspSession
•
javax.servlet.jsp.jspApplication
•
javax.servlet.jsp.jspPageContext
•
javax.servlet.jsp.jspConfig
•
javax.servlet.jsp.jspResponse
•
javax.servlet.jsp.jspRequest
•
org.apache.tomcat.servlet.resolved
• My dog
•
sun.servlet.workdir
•
javax.servlet.context.tempdir
Scope 1 is the page scope and all objects
reference available in this scope will be discarded upon completion of the
current request by the page body. Scope 2 refers to the request scope and will
be discarded upon completion of the current client request. As you can see I am
using the Apache Tomcat implementation of the Servlets and JSP. (I am not sure
what org.apache.tomcat.servlet.resolved is I will try to find out.) Scope 3 will
be our session scope and the only object we have with session scope is the one
that we added right before the for loop - "My dog". Scope 4 is the scope of our
application and is based upon the ServletContext object. There is one
ServletContext per "Web application" per Java Virtual Machine. (A "Web
application" is a collection of servlets and content installed under a specific
subset of the server's URL namespace such as /catalog. In the Tomcat release
this information is set via server.xml file.) At the application scope level we
have to objects that represent paths for working directory and temporary
directory.
Let's take a closer look at sessions
within the JSP model. The next example will exercise the session object a little
bit and allow you to manipulate the amount of time before your session becomes
invalid. First we must capture some information about this session object. I
make a call to getID( ), getCreationTime( ) and
getMaxInactiveInterval( ) and display these attributes about our
session. When I first bring this session up in the Tomcat implementation the
MaxInactiveInterval is 1800 seconds or 30 minutes. Now I know a bit about my
session, so lets change its behavior by shortening the MaxInactiveInterval to 5
seconds. Now we should see some action. Next, I check to see if the object "My
dog" is attached to the session object giving it session scope. The first time
through this should be null but right afterwards we do create a String object
"Ralph" and attach it to the session object by call setAttribute( ).
Now Ralph should hang around for at least 5 seconds. The invalidate button at
the bottom calls a second .jsp page SessionObject2.jsp that simply asks the
session if it has the object tagged "My dog" then kills the session by calling
invalidate( ) on the session object. "Ralph" is gone. The other
button on the bottom of SessionObject.jsp is "Keep Around". This calls a third
page, SessionObject3.jsp, that does NOT invalidate the session and you can see
that "Ralph" in fact does hang around as long as your 5 second time interval
does not expire. Try the refresh button on SessionObject.jsp or move back and
forth between SessionObject and SessionObject3.jsp (Keep Around button) a couple
of times using different intervals to get a feel for how long "My dog" stays
around. (For those of you who have kids this is like the Tomagotchi pets - as
long as you play with "Ralph" he will stick around otherwise he packs it up :-)
//:! c15:jsp:SessionObject.jsp <%--Setting and getting session object values--%> <html><body> <H1>Session id: <%= session.getId() %></H1> <H3><li>This session was created at <%= session.getCreationTime() %></li></H1> <H3><li>MaxInactiveInterval= <%= session.getMaxInactiveInterval() %></li></H3> <% session.setMaxInactiveInterval(5); %> <H3><li>Reset MaxInactiveInterval= <%= session.getMaxInactiveInterval() %></li></H3> <H2>If this session object "My dog" is still around <H2> <H3><li>Session value for "My dog" = <%= session.getAttribute("My dog") %></li></H3> <%-- Now add the session object "My dog" --%> <% session.setAttribute("My dog", new String("Ralph")); %> <H1>My dog's name is <%= session.getAttribute("My dog") %></H1> <%-- See if "My dog" wanders to another form --%> <FORM TYPE=POST ACTION=SessionObject2.jsp> <INPUT TYPE=submit name=submit Value="Invalidate"> </FORM> <FORM TYPE=POST ACTION=SessionObject3.jsp> <INPUT TYPE=submit name=submit Value="Keep Around"> </FORM> </body></html> ///:~
//:! c15:jsp:SessionObject2.jsp <%--The session object carries through--%> <html><body> <H1>Session id: <%= session.getId() %></H1> <H1>Session value for "My dog" <%= session.getValue("My dog") %></H1> <% session.invalidate(); %> </body></html> ///:~
//:! c15:jsp:SessionObject3.jsp <%--The session object carries through--%> <html><body> <H1>Session id: <%= session.getId() %></H1> <H1>Session value for "My dog" <%= session.getValue("My dog") %></H1> <FORM TYPE=POST ACTION=SessionObject.jsp> <INPUT TYPE=submit name=submit Value="Return"> </FORM> </body></html> ///:~
//:! c15:jsp:Cookies.jsp <%--This program has different behaviors under different browsers! --%> <html><body> <H1>Session id: <%= session.getId() %></H1> <% Cookie[] cookies = request.getCookies(); for(int i = 0; i < cookies.length; i++) { %> Cookie name: <%= cookies[i].getName() %> <br> value: <%= cookies[i].getValue() %><br> Max age in seconds: <%= cookies[i].getMaxAge() %><br> <% cookies[i].setMaxAge(3); %> Max age in seconds: <%= cookies[i].getMaxAge() %><br> <% response.addCookie(cookies[i]); %> <% } %> <%-- <% response.addCookie( new Cookie("Bob", "Car salesman")); %> --%> </body></html> ///:~
Traditional approaches to executing code
on other machines across a network have been confusing as well as tedious and
error-prone to implement. The nicest way to think about this problem is that
some object happens to live on another machine, and you can send a message to
that object and get a result as if the object lived on your local machine. This
simplification is exactly what Java
Remote Method Invocation
(RMI) allows you to do. This section walks you through the steps necessary to
create your own RMI objects.
RMI makes heavy use of interfaces. When
you want to create a remote object, you mask the underlying implementation by
passing around an interface. Thus, when the client gets a reference to a remote
object, what they really get is an interface reference, which happens to
connect to some local stub code that talks across the network. But you
don’t think about this, you just send messages via your interface
reference.
Here’s a simple
remote interface that represents an accurate time service:
//: c15:rmi:PerfectTimeI.java // The PerfectTime remote interface. package c15.rmi; import java.rmi.*; interface PerfectTimeI extends Remote { long getPerfectTime() throws RemoteException; } ///:~
It looks like any other interface except
that it extends Remote and all of its methods throw
RemoteException. Remember that an interface and all of its methods
are automatically public.
The server must contain a class that
extends
UnicastRemoteObject and
implements the remote interface. This class can also have additional methods,
but only the methods in the remote interface will be available to the client, of
course, since the client will get only a reference to the interface, not the
class that implements it.
You must explicitly define the
constructor for the remote object even if you’re only defining a default
constructor that calls the base-class constructor. You must write it out since
it must throw RemoteException.
Here’s the implementation of the
remote interface PerfectTimeI:
//: c15:rmi:PerfectTime.java // The implementation of // the PerfectTime remote object. package c15.rmi; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.net.*; public class PerfectTime extends UnicastRemoteObject implements PerfectTimeI { // Implementation of the interface: public long getPerfectTime() throws RemoteException { return System.currentTimeMillis(); } // Must implement constructor // to throw RemoteException: public PerfectTime() throws RemoteException { // super(); // Called automatically } // Registration for RMI serving: public static void main(String[] args) { System.setSecurityManager( new RMISecurityManager()); try { PerfectTime pt = new PerfectTime(); Naming.bind( "//peppy:2005/PerfectTime", pt); System.out.println("Ready to do time"); } catch(Exception e) { e.printStackTrace(); } } } ///:~
Here, main( ) handles all the
details of setting up the server. When you’re serving RMI objects, at some
point in your program you must:
remote object registry for
bootstrapping purposes. One remote object can have methods that produce
references to other remote objects. This allows you to set it up so the client
must go to the registry only once, to get the first remote
object.
Here, you see a call to the static
method
Naming.bind( ).
However, this call requires that the registry be running as a separate process
on the computer. The name of the registry server is
rmiregistry, and under
32-bit Windows you say:
start rmiregistry
to start it in the background. On Unix,
it is:
rmiregistry &
Like many network programs, the
rmiregistry is located at the IP address of whatever machine started it
up, but it must also be listening at a port. If you invoke the
rmiregistry as above, with no argument, the registry’s port will
default to 1099. If you want it to be at some other port, you add an argument on
the command line to specify the port. For this example, the port will be located
at 2005, so the rmiregistry should be started like this under 32-bit
Windows:
start rmiregistry 2005
or for Unix:
rmiregistry 2005 &
The information about the port must also
be given to the bind( ) command, as well as the IP address of the
machine where the registry is located. But this brings up what can be a
frustrating problem if you’re expecting to test RMI programs locally the
way the network programs have been tested so far in this chapter. In the JDK
1.1.1 release, there are a couple of
problems:[74]
localhost
does not work with RMI. Thus, to experiment with RMI on a single machine,
you must provide the name of the machine. To find out the name of your machine
under 32-bit Windows, go to the control panel and select “Network.”
Select the “Identification” tab, and you’ll see your computer
name. In my case, I called my computer “Peppy.” It appears that
capitalization is ignored.
TCP/IP connection, even if all
your components are just talking to each other on the local machine. This means
that you must connect to your Internet service provider before trying to run the
program or you’ll get some obscure exception messages.
With all this in mind, the
bind( ) command becomes:
Naming.bind("//peppy:2005/PerfectTime", pt);
If you are using the default port 1099,
you don’t need to specify a port, so you could say:
Naming.bind("//peppy/PerfectTime", pt);
You should be able to perform local
testing by leaving off the IP address and using only the
identifier:
Naming.bind("PerfectTime", pt);
The name for the service is arbitrary; it
happens to be PerfectTime here, just like the name of the class, but you could
call it anything you want. The important thing is that it’s a unique name
in the registry that the client knows to look for to procure the remote object.
If the name is already in the registry, you’ll get an
AlreadyBoundException. To
prevent this, you can always use
rebind( )
instead of bind( ), since rebind( ) either adds a new
entry or replaces the one that’s already there.
Even though main( ) exits,
your object has been created and registered so it’s kept alive by the
registry, waiting for a client to come along and request it. As long as the
rmiregistry is running and you don’t call
Naming.unbind( )
on your
name, the object will be there. For this reason, when you’re developing
your code you need to shut down the rmiregistry and restart it when you
compile a new version of your remote object.
You aren’t forced to start up
rmiregistry as an external process. If you know that your application is
the only one that’s going to use the registry, you can start it up inside
your program with the line:
LocateRegistry.createRegistry(2005);
Like before, 2005 is the port number we
happen to be using in this example. This is the equivalent of running
rmiregistry 2005 from a command line, but it can often be more convenient
when you’re developing RMI code since it eliminates the extra steps of
starting and stopping the registry. Once you’ve executed this code, you
can bind( ) using Naming as
before.
If you compile and run
PerfectTime.java, it won’t work even if you have the
rmiregistry running correctly. That’s because the framework for RMI
isn’t all there yet. You must first create the
stubs and
skeletons that provide the
network connection operations and allow you to pretend that the remote object is
just another local object on your machine.
What’s going on behind the scenes
is complex. Any objects that you pass into or return from a remote object must
implement Serializable
(if you want to pass remote references instead of the entire objects, the object
arguments can implement Remote), so you can imagine that the stubs and
skeletons are automatically performing serialization and deserialization as they
“marshal” all of the arguments across the network and return the
result. Fortunately, you don’t have to know any of this, but you do
have to create the stubs and skeletons. This is a simple process: you invoke the
rmic tool on your
compiled code, and it creates the necessary files. So the only requirement is
that another step be added to your compilation process.
The rmic tool is particular about
packages and classpaths,
however. PerfectTime.java is in the package c15.rmi, and even if
you invoke rmic in the same directory in which PerfectTime.class
is located, rmic won’t find the file, since it searches the
classpath. So you must specify the location off the class path, like
so:
rmic c15.rmi.PerfectTime
You don’t have to be in the
directory containing PerfectTime.class when you execute this command, but
the results will be placed in the current directory.
When rmic runs successfully,
you’ll have two new classes in the directory:
PerfectTime_Stub.class PerfectTime_Skel.class
corresponding to the stub and skeleton.
Now you’re ready to get the server and client to talk to each
other.
The whole point of RMI is to make the use
of remote objects simple. The only extra thing that you must do in your client
program is to look up and fetch the remote interface from the server. From then
on, it’s just regular Java programming: sending messages to objects.
Here’s the program that uses PerfectTime:
//: c15:rmi:DisplayPerfectTime.java // Uses remote object PerfectTime. package c15.rmi; import java.rmi.*; import java.rmi.registry.*; public class DisplayPerfectTime { public static void main(String[] args) { System.setSecurityManager( new RMISecurityManager()); try { PerfectTimeI t = (PerfectTimeI)Naming.lookup( "//peppy:2005/PerfectTime"); for(int i = 0; i < 10; i++) System.out.println("Perfect time = " + t.getPerfectTime()); } catch(Exception e) { e.printStackTrace(); } } } ///:~
The ID string is the same as the one used
to register the object with Naming, and the first part represents the URL
and port number. Since you’re using a URL, you can also specify a machine
on the Internet.
What comes back from
Naming.lookup( ) must be cast to the remote interface, not to
the class. If you use the class instead, you’ll get an
exception.
You can see in the method
call
t.getPerfectTime( )
that once you have a reference to the
remote object, programming with it is indistinguishable from programming with a
local object (with one difference: remote methods throw
RemoteException).
In large, distributed applications, your
needs might not be satisfied by the preceding approaches. For example, you might
want to interface with legacy data stores, or you might need services from a
server object regardless of its physical location. These situations require some
form of Remote Procedure Call (RPC), and possibly language independence. This is
where CORBA can help.
CORBA is not a
language feature; it’s an integration technology. It’s a
specification that vendors can follow to implement CORBA-compliant integration
products. CORBA is part of the OMG’s effort to define a standard framework
for distributed, language-independent object interoperability.
CORBA supplies the ability to make remote
procedure calls into Java objects and non-Java objects, and to interface with
legacy systems in a location-transparent way. Java adds networking support and a
nice object-oriented language for building graphical and non-graphical
applications. The Java and OMG object model map nicely
to each other; for example, both Java and CORBA implement the interface concept
and a reference object model.
The object interoperability specification
developed by the OMG is commonly referred to as the Object Management
Architecture (OMA). The OMA defines two components: the Core Object Model and
the OMA Reference Architecture. The Core Object Model states the basic concepts
of object, interface, operation, and so on. (CORBA is a refinement of the Core
Object Model.) The OMA Reference Architecture defines an underlying
infrastructure of services and mechanisms that allow objects to interoperate.
The OMA Reference Architecture includes the Object Request Broker (ORB), Object
Services (also known as CORBA services), and common facilities.
The ORB is the communication bus by which
objects can request services from other objects, regardless of their physical
location. This means that what looks like a method call in the client code is
actually a complex operation. First, a connection with the server object must
exist, and to create a connection the ORB must know where the server
implementation code resides. Once the connection is established, the method
arguments must be marshaled, i.e. converted in a binary stream to be sent across
a network. Other information that must be sent are the server machine name, the
server process, and the identity of the server object inside that process.
Finally, this information is sent through a low-level wire protocol, the
information is decoded on the server side, and the call is executed. The ORB
hides all of this complexity from the programmer and makes the operation almost
as simple as calling a method on local object.
There is no specification for how an ORB
Core should be implemented, but to provide a basic compatibility among different
vendors’ ORBs, the OMG defines a set of services that are accessible
through standard interfaces.
CORBA is designed for language transparency: a client object can call methods on a server object of different class, regardless of the language they are implemented with. Of course, the client object must know the names and signatures of methods that the server object exposes. This is where IDL comes in. The CORBA IDL is a language-neutral way to specify data types, attributes, operations, interfaces, and more. The IDL syntax is similar to the C++ or Java syntax. The following table shows the correspondence between some of the concepts common to three languages that can be specified through CORBA IDL:
CORBA IDL |
Java |
C++ |
Module |
Package |
Namespace |
Interface |
Interface |
Pure abstract class |
Method |
Method |
Member function |
The inheritance concept is supported as
well, using the colon operator as in C++. The programmer writes an IDL
description of the attributes, methods, and interfaces that will be implemented
and used by the server and clients. The IDL is then compiled by a
vendor-provided IDL/Java compiler, which reads the IDL source and generates Java
code.
The IDL compiler is an extremely useful
tool: it doesn’t just generate a Java source equivalent of the IDL, it
also generates the code that will be used to marshal method arguments and to
make remote calls. This code, called the stub and skeleton code, is organized in
multiple Java source files and is usually part of the same Java package.
The naming service is one of the
fundamental CORBA services. A CORBA object is accessed through a reference, a
piece of information that’s not meaningful for the human reader. But
references can be assigned programmer-defined, string names. This operation is
known as stringifying the reference, and one of the OMA components, the
Naming Service, is devoted to performing string-to-object and object-to-string
conversion and mapping. Since the Naming Service acts as a telephone directory
that both servers and clients can consult and manipulate, it runs as a separate
process. Creating an object-to-string mapping is called binding an
object, and removing the mapping is called unbinding. Getting an
object reference passing a string is called resolving the
name.
For example, on startup, a server
application could create a server object, bind the object into the name service,
and then wait for clients to make requests. A client first obtains a server
object reference, resolving the string name, and then can make calls into the
server using the reference.
Again, the Naming Service specification
is part of CORBA, but the application that implements it is provided by the ORB
vendor. The way you get access to the Naming Service functionality can vary from
vendor to vendor.
The code shown here will not be elaborate
because different ORBs have different ways to access CORBA services, so examples
are vendor specific. (The example below uses JavaIDL, a free product from Sun
that comes with a light-weight ORB, a naming service, and an IDL-to-Java
compiler.) In addition, since Java is young and still evolving, not all CORBA
features are present in the various Java/CORBA products.
We want to implement a server, running on
some machine, that can be queried for the exact time. We also want to implement
a client that asks for the exact time. In this case we’ll be implementing
both programs in Java, but we could also use two different languages (which
often happens in real situations).
The first step is to write an IDL
description of the services provided. This is usually done by the server
programmer, who is then free to implement the server in any language in which a
CORBA IDL compiler exists. The IDL file is distributed to the client side
programmer and becomes the bridge between languages.
The example below shows the IDL
description of our ExactTime server:
//: c15:corba:ExactTime.idl //# You must install idltojava.exe from //# java.sun.com and adjust the settings to use //# your local C preprocessor in order to compile //# This file. See docs at java.sun.com. module remotetime { interface ExactTime { string getTime(); }; }; ///:~
This is a declaration of the
ExactTime interface inside the remotetime namespace. The interface
is made up of one single method that gives back the current time in
string format.
The second step is to compile the IDL to
create the Java stub and skeleton code that we’ll use for implementing the
client and the server. The tool that comes with the JavaIDL product is
idltojava:
idltojava remotetime.idl
This will automatically generate code for
both the stub and the skeleton. Idltojava generates a Java package
named after the IDL module, remotetime, and the generated Java files are
put in the remotetime subdirectory. _ExactTimeImplBase.java is the
skeleton that we’ll use to implement the server object, and
_ExactTimeStub.java will be used for the client. There are Java
representations of the IDL interface in ExactTime.java and a couple of
other support files used, for example, to facilitate access to the naming
service operations.
Below you can see the code for the server
side. The server object implementation is in the ExactTimeServer class.
The RemoteTimeServer is the application that creates a server object,
registers it with the ORB, gives a name to the object reference, and then sits
quietly waiting for client requests.
//: c15:corba:RemoteTimeServer.java import remotetime.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; import java.util.*; import java.text.*; // Server object implementation class ExactTimeServer extends _ExactTimeImplBase { public String getTime(){ return DateFormat. getTimeInstance(DateFormat.FULL). format(new Date( System.currentTimeMillis())); } } // Remote application implementation public class RemoteTimeServer { public static void main(String[] args) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Create the server object and register it: ExactTimeServer timeServerObjRef = new ExactTimeServer(); orb.connect(timeServerObjRef); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Assign a string name to the // object reference (binding): NameComponent nc = new NameComponent("ExactTime", ""); NameComponent[] path = { nc }; ncRef.rebind(path, timeServerObjRef); // Wait for client requests: java.lang.Object sync = new java.lang.Object(); synchronized(sync){ sync.wait(); } } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } } ///:~
As you can see, implementing the server
object is simple; it’s a regular Java class that inherits from the
skeleton code generated by the IDL compiler. Things get a bit more complicated
when it comes to interacting with the ORB and other CORBA
services.
This is a short description of what the
JavaIDL-related code is doing (primarily ignoring the part of the CORBA code
that is vendor dependent). The first line in main( ) starts up the
ORB, and of course, this is because our server object will need to interact with
it. Right after the ORB initialization, a server object is created. Actually,
the right term would be a transient servant object: an object that
receives requests from clients, and whose lifetime is the same as the process
that creates it. Once the transient servant object is created, it is registered
with the ORB, which means that the ORB knows of its existence and can now
forward requests to it.
Up to this point, all we have is
timeServerObjRef, an object reference that is known only inside the
current server process. The next step will be to assign a stringified name to
this servant object; clients will use that name to locate the servant object. We
accomplish this operation using the Naming Service. First, we need an object
reference to the Naming Service; the call to
resolve_initial_references( ) takes the stringified object reference
of the Naming Service that is “NameService,” in JavaIDL, and returns
an object reference. This is cast to a specific NamingContext reference
using the narrow( ) method. We can use now the naming
services.
To bind the servant object with a
stringified object reference, we first create a NameComponent object,
initialized with “ExactTime,” the name string we want to bind to the
servant object. Then, using the rebind( ) method, the stringified
reference is bound to the object reference. We use rebind( ) to
assign a reference, even if it already exists, whereas bind( )
raises an exception if the reference already exists. A name is made up in CORBA
by a sequence of NameContexts—that’s why we use an array to bind the
name to the object reference.
The servant object is finally ready for
use by clients. At this point, the server process enters a wait state. Again,
this is because it is a transient servant, so its lifetime is confined to the
server process. JavaIDL does not currently support persistent
objects—objects that survive the execution of the process that creates
them.
Now that we have an idea of what the
server code is doing, let’s look at the client code:
//: c15:corba:RemoteTimeClient.java import remotetime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*; public class RemoteTimeClient { public static void main(String[] args) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Get (resolve) the stringified object // reference for the time server: NameComponent nc = new NameComponent("ExactTime", ""); NameComponent[] path = { nc }; ExactTime timeObjRef = ExactTimeHelper.narrow( ncRef.resolve(path)); // Make requests to the server object: String exactTime = timeObjRef.getTime(); System.out.println(exactTime); } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } } ///:~
The first few lines do the same as they
do in the server process: the ORB is initialized and a reference to the naming
service server is resolved. Next, we need an object reference for the servant
object, so we pass the stringified object reference to the
resolve( ) method, and we cast the result into an ExactTime
interface reference using the narrow( ) method. Finally, we call
getTime( ).
Finally we have a server and a client
application ready to interoperate. You’ve seen that both need the naming
service to bind and resolve stringified object references. You must start the
naming service process before running either the server or the client. In
JavaIDL, the naming service is a Java application that comes with the product
package, but it can be different with other products. The JavaIDL naming service
runs inside an instance of the JVM and listens by default to network port
900.
Now you are ready to start your server
and client application (in this order, since our server is transient). If
everything is set up correctly, what you’ll get is a single output line on
the client console window, giving you the current time. Of course, this might be
not very exciting by itself, but you should take one thing into account: even if
they are on the same physical machine, the client and the server application are
running inside different virtual machines and they can communicate via an
underlying integration layer, the ORB and the Naming Service.
This is a simple example, designed to
work without a network, but an ORB is usually configured for location
transparency. When the server and the client are on different machines, the ORB
can resolve remote stringified references using a component known as the
Implementation Repository. Although the Implementation Repository is part
of CORBA, there is almost no specification, so it differs from vendor to
vendor.
As you can see, there is much more to
CORBA than what has been covered here, but you should get the basic idea. If you
want more information about CORBA, the place to start is the OMG Web site, at
www.omg.org. There you’ll find documentation, white papers,
proceedings, and references to other CORBA sources and
products.
Java applets can act as CORBA clients.
This way, an applet can access remote information and services exposed as CORBA
objects. But an applet can connect only with the server from which it was
downloaded, so all the CORBA objects the applet interacts with must be on that
server. This is the opposite of what CORBA tries to do: give you complete
location transparency.
This is an issue of network security. If
you’re on an intranet, one solution is to loosen the security restrictions
on the browser. Or, set up a firewall policy for connecting with external
servers.
Some Java ORB products offer proprietary
solutions to this problem. For example, some implement what is called HTTP
Tunneling, while others have their special firewall features.
This is too complex a topic to be covered
in an appendix, but it is definitely something you should be aware
of.
You saw that one of the main CORBA
features is RPC support, which allows your local objects to call methods in
remote objects. Of course, there already is a native Java feature that does
exactly the same thing: RMI (see Chapter 15). While RMI makes RPC possible
between Java objects, CORBA makes RPC possible between objects implemented in
any language. It’s a big difference.
However, RMI can
be used to call services on remote, non-Java code. All you need is some kind of
wrapper Java object around the non-Java code on the server side. The wrapper
object connects externally to Java clients via RMI, and internally connects to
the non-Java code using one of the techniques shown above, such as JNI or
J/Direct.
This approach requires you to write a
kind of integration layer, which is exactly what CORBA does for you, but then
you don’t need a third-party
ORB.
By
now[75], you've
been introduced to CORBA and RMI. But could you imagine trying to develop a
large-scale application using CORBA and/or RMI? Suppose you need to develop a
multi-tiered application to view and update records in a database through a Web
interface. You can write a database application using JDBC, a Web interface
using JSP/Servlets, and a distributed system using CORBA/RMI. But what extra
considerations must you make when developing a distributed object based system
rather than just knowing API's? Here are the issues:
Performance: The new
distributed objects that you are creating are going to have to perform, as they
potentially could service many clients at a time. So you'll want to think of
optimization techniques such as caching, pooling of resources (e.g., JDBC
database connections). You'll also have to manage the lifecycle of your
distributed object.
Scalability: The
distributed objects must also be scalable. Scalability in a distributed
application means that the number of instances of your distributed object can be
increased and moved over to a different machine without the modification of any
code. Take for example a system that you develop internally as a small lookup of
clients inside your organization from a database. The application works well
when you use it, but your manager has seen it and has said "Robert, that is an
excellent system, get it on our public Web-site now!!!"-Will my distributed
object be able to handle the load of a potentially limitless
demand?
Security: Does my
distributed object manage the authorization of the clients that accesses it? Can
I add new users and roles to it without recompilation?
Distributed Transactions:
Can my distributed object reference distributed-transactions transparently? Can
I update my Oracle and Sybase databases simultaneously within the same
transaction and roll them both back if a certain criteria is not
met?
Reusability: Have I created
my distributed object so that I can move it into another vendors' application
server? Can I resell my distributed object (component) to somebody else? Can I
buy somebody else's component and use it without having to recompile and 'hack
it into shape'?
Availability: If one of the
machines in my system was to go down, are my clients able to automatically
fail-over to back up copies of my objects running on other
machines?
As you can see from the above, the
considerations that a developer must make when developing a distributed system
and we haven't even mentioned solving the problem that we were originally trying
to solve!
So you now have your list of extra
problems that you must solve. So how do you go about doing it? Surely somebody
must have done this before? Could I use some well-known design patterns to help
me solve these problems? Then an idea flashes in your head... "I could create a
framework that handles all of these issues and write my components on top of the
framework!".... This is where Enterprise JavaBeans comes into the
picture.
Sun, along with other leading distributed
object vendors realized that sooner or later every development team would be
reinventing the wheel. So they created the Enterprise JavaBeans specification
(EJB). EJB is a specification for a server-side component model that tackles all
of the considerations mentioned above using a defined, standard approach that
allows developers to create components-which are actually called Enterprise
JavaBeans (EJB's)-that are isolated from low-level 'plumbing' code and focus
solely on providing business logic. Because EJB's are defined as a standard they
can be used without being vendor
dependent.
The Enterprise JavaBeans specification,
currently at version 1.1 - public release 2 - defines a server side component
model. It defines 6 roles that are used to perform the tasks in development and
deployment as well as defining the components of the
system.
The EJB specification defines roles that are used during in the development, deployment and running of a distributed system. Vendors, administrators and developers play the various roles. They allow the partitioning of technical and domain knowledge. This allows the vendor to provide a technically sound framework and the developers to create domain specific components e.g., an Accounting component. The same party can perform one or many roles. The roles defined in the EJB specification have been summarized in the following table:
Role |
Responsibility |
Enterprise Bean Provider |
The developer who is responsible for
creating reusable EJB components. These components are packaged into a special
jar file (ejb-jar file). |
Application Assembler |
Create and assemble applications from a
collection of ejb-jar files. This includes writing applications that utilize the
collection of EJB’s (e.g., Servlets, JSP, Swing etc.
etc.) |
Deployer |
The Deployer’s role of the to take
the collection of ejb-jar files from the Assembler and/or Bean Provider and
deploy them into a run-time environment - one or many EJB Container(s).
|
EJB Container/Server Provider
|
Provide a run-time environment and tools
that are used to deploy, administer and “run” EJB
components. |
System Administrator |
Over see the most important goal of the
entire system - That it is up and running. Management of a distributed
application can consist of many different components and services all configured
and interacting together correctly. |
EJB components are reusable business
logic. EJB components adhere to strict standards and design patterns as defined
in the EJB specification. This allows the components to be portable and also
allow other services—such as security, caching and distributed
transactions - to be performed on the components’ behalf. An Enterprise
Bean Provider is responsible for developing EJB components. The internals of an
EJB component are covered in - “What makes up an EJB
component?”
The EJB Container is a run-time
environment that contains -or runs—EJB components and provides a set of
standard services to the components. The EJB Containers responsibilities are
tightly defined by the specification to allow for vendor neutrality. The EJB
container provides the low-level “plumbing” of EJB, including
distributed transactions, security, life cycle management of beans, caching,
threading and session management. The EJB Container Provider is responsible for
providing an EJB Container.
An EJB Server is defined as an
Application Server that contains and runs 1 or more EJB Containers. that both
the Container and Server are the same vendor. The EJB Server Provider is
responsible for providing an EJB Server. The specification suggests and you can
assume for this introduction, that the EJB Container and EJB Server are the
same.
Java Naming and Directory Interface
(JNDI) is used in Enterprise JavaBeans as the naming service for EJB Components
on the network and other container services such as transactions. JNDI maps very
closely to other naming and directory standards such as CORBA CosNaming and can
actually be implemeted as a wrapper on top of it.
JTA/JTS is used in Enterprise JavaBeans
as the transactional API. An Enterprise Bean Provider can use the JTS to create
transactional code although the EJB Container commonly implements transactions
in EJB on the EJB components’ behalf. The Deployer can define the
transactional attributes of an EJB component at deployment time. The EJB
Container is responsible for handling the transaction whether it is local or
distributed. The JTS specification is the Java mapping to the CORBA OTS (Object
Transaction Service)
The EJB specification defines
interoperability with CORBA. The 1.1 specification quotes “The
Enterprise JavaBeans architecture will be compatible with the CORBA
protocols.” CORBA interoperability is achieved through the mapping of
EJB services such as JTS and JNDI to corresponding CORBA services and the
implementation of RMI on top of the CORBA protocol IIOP.
Use of CORBA and RMI/IIOP in Enterprise
JavaBeans is implemented in the EJB Container and is the responsibility of the
EJB Container provider. Use of CORBA and RMI/IIOP in the EJB Container is hidden
from the EJB Component itself. This means that the Enterprise Bean Provider can
write their EJB Component and deploy it into any EJB Container without any
regard of which communication protocol is being used.
The components and available services of
EJB.
The Enterprise Bean is a Java class that
the Enterprise Bean Provider develops. It implements an Enterprise Bean
interface (more detail in a later section) and provides the implementation of
the business methods that the component is to perform. The class does not
implement any authorization/authentication code, multithreading, transactional
code.
Every Enterprise Bean that is created
must have an associated Home interface. The Home interface is used as a factory
for your EJB. Clients use the Home interface to find an instance of your EJB or
create a new instance of your EJB.
The Remote interface is a Java Interface
that reflects the methods of your Enterprise Bean that you wish to expose to the
outside world. The Remote interface plays a similar role to a CORBA IDL
interface.
The Deployment Descriptor is an XML file
that contains information about your EJB. Using XML allows the Deployer to
easily change attributes about your EJB. The configurable attributes defined in
the Deployment Descriptor include:
The EJB-Jar file is a normal java jar
file that contains your EJB, Home and Remote interfaces, as well as the
Deployment Descriptor.
Now that we have our EJB-Jar file
containing our Bean, Home and Remote interfaces and Deployment Descriptor,
Let’s take a look at how all of these pieces fit together and why Home and
Remote interfaces are needed and how the EJB Container uses
them.
The EJB Container implements the Home and
Remote interfaces that are in our EJB-Jar file. Because the EJB Container
implements the Home interface that - as mentioned earlier - provides methods to
create and find your EJB. This means that the EJB Container is responsible for
the lifecycle management of your EJB. This level of indirection allows for
optimizations to occur. For example 5 clients simultaneously request to create
an EJB through a Home Interface, the EJB Container could create only one and
share that EJB between all 5 clients. This is achieved through the Remote
Interface, which is again implemented by the EJB Container. The implemented
Remote object plays the role of a proxy object to the EJB.
The following diagram show the level of
indirection achieved by this approach and that all calls to the
EJB are ‘proxied’ through the EJB Container via the Home and Remote
interfaces. This level of indirection is also the reason why the EJB Container
can control security and transactional behavior.
There should be one question in your head
from the previous section, “Surely sharing the same EJB between clients
can improve performance, but what If I want to maintain state on my
server?”
The Enterprise JavaBeans specification
defines different types of EJB’s that have different characteristics and
exhibit different behavior. Two categories of EJB’s have been defined in
the specification. Session Beans and Entity Beans, and each of these categories
has variations. A hierarchy of the various types of EJB components is shown in
the following figure.
Session Beans are used to represent
Use-Cases or Workflow on behalf of a client. They represent operations on
persistent data, not persistent data itself. There are two types of Session
Beans, Stateless and Stateful. All Session Beans must implement the
javax.ejb.SessionBean interface. The EJB Container governs the life of a Session
Bean. If the EJB Container crashes, data for all Stateful Session Beans could be
lost. Some high-end EJB Containers provide recovery for Stateful Session
Beans.
Stateless Session Beans are the simplest
type of EJB component to implement. They do not maintain any conversational
state with clients between method invocations so they are easily reusable on the
server side and because they can be cached, they scale well on demand. When
using Stateless Session Beans, all state must be stored outside of the
EJB.
Stateful Session Beans - as you could
probably guess - maintain state between invocations. They have a 1 to 1 logical
mapping to a client and can maintain state within themselves. The EJB Container
is responsible for pooling and caching of Stateful Session Beans, which is
achieved through Passivation and Activation.
Entity Beans are components that
represent persistent data and behavior of this data. Entity Beans can be shared
amongst multiple clients, the same as data in a database. The EJB Container is
responsible for caching Entity Beans and for maintaining the integrity of the
Entity Beans. The life of an Entity Bean outlives the EJB Container, so if an
EJB Container crashes, the Entity Bean is expected to still be available when
the EJB Container becomes available.
There are two types of Entity Beans,
those that have Bean-Managed persistence and Container Managed
persistence.
A CMP Entity Bean has its’
persistence implemented on its behalf by the EJB Container. Through attributes
specified in the Deployment Descriptor, the EJB Container will map the Entity
Bean’s attributes to some persistent store (usually -but not
always—a database). CMP reduces the time to develop and dramatically
reduces the amount of code required for the EJB.
A BMP Entity Bean has its’
persistence implemented by the Enterprise Bean Provider. The Enterprise Bean
Provider is responsible for implementing the logic required to create a new EJB,
update some attributes of the EJBS, delete an EJB and find an EJB from
persistent store. This usually involves writing JDBC code to interact with a
database or other persistent store. With BMP, the developer is in full control
of how the Entity Bean persistence is managed.
BMP also gives flexibility where a CMP
implementation may not be available e.g., if you wanted to create an EJB that
wrapped some code on an existing mainframe system, you could write your
persistence using CORBA.
There is much confusion about the
relationship between the JavaBeans component model and the Enterprise JavaBeans
specification. Whilst both the JavaBeans and Enterprise JavaBeans specifications
share the same objectives in promoting reuse and portability of Java code
between development and deployment tools with the use of standard design
patterns, the motives behind each specification are geared to solve different
problems.
The standards defined in the JavaBeans
component model are designed for creating reusable components that are typically
used in IDE development tools and are commonly, although not exclusively visual
components.
The Enterprise JavaBeans specification
defines a component model for developing server side java code. Because
EJB’s can potentially run on many different server-side platforms
-including mainframes that do not have visual displays - An EJB cannot make use
of the java.awt package.
We will now implement the “Perfect
Time” example from the previous RMI section as an Enterprise JavaBean
component. The example will be a simple Stateless Session Bean. Enterprise
JavaBean components will consist of at least one class and two interfaces.
The first interface defined is the Remote
Interface to our Enterprise JavaBean component. When you create a Remote
interface for an EJB , you must follow these guidelines:
Here is the simple
remote interface for our PerfectTime EJB:
//: c15:ejb:PerfectTime.java //# You must install the J2EE Java Enterprise //# Edition from java.sun.com and add j2ee.jar //# To your CLASSPATH in order to compile //# This file. See details at java.sun.com. // Remote Interface of PerfectTimeBean import java.rmi.*; import javax.ejb.*; public interface PerfectTime extends EJBObject { public long getPerfectTime() throws RemoteException; } ///:~
The second interface defined is the Home
Interface to our Enterprise JavaBean component. The Home interface is the
factory where our component will be created. The Home interface can define
create or finder methods. Create methods create instances of EJB’s, finder
methods locate existing EJB’s and are used for Entity Beans only. When you
create a Home interface for an EJB , you must follow these
guidelines:
The standard naming convention for Home interfaces is to take the Remote interface name and append “Home” to the end. Following is the Home interface for our PerfectTime EJB: //: c15:ejb:PerfectTimeHome.java // Home Interface of PerfectTimeBean. import java.rmi.*; import javax.ejb.*; public interface PerfectTimeHome extends EJBHome { public PerfectTime create() throws CreateException, RemoteException; } ///:~
Now that we have defined the interfaces
of our component, we can now implement the business logic behind it. When you
create your EJB implementation class, you must follow these guidelines, (note
that you should consult the EJB specification for a complete list of guidelines
when developing Enterprise JavaBeans):
//: c15:ejb:PerfectTimeBean.java // Simple Stateless Session Bean // that returns current system time. import java.rmi.*; import javax.ejb.*; public class PerfectTimeBean implements SessionBean { private SessionContext sessionContext; //return current time public long getPerfectTime() { return System.currentTimeMillis(); } // EJB methods public void ejbCreate() throws CreateException {} public void ejbRemove() {} public void ejbActivate() {} public void ejbPassivate() {} public void setSessionContext(SessionContext ctx) { sessionContext = ctx; } }///:~
Notice that the EJB methods
(ejbCreate( ), ejbRemove( ), ejbActivate( ),
ejbPassivate( ) ) are all empty. These methods are invoked by the
EJB Container and are used to control the state of our component. As this is a
simple example, we can leave these empty. The setSessionContext( )
method passes a javax.ejb.SessionContext object which contains information about
context that the component is in, such as the current transaction and security
information.
After we have created our Enterprise
JavaBean, we then need to create a Deployment Descriptor. In EJB 1.1, the
Deployment Descriptor is an XML file that describes the EJB component. The
Deployment Descriptor should be stored in a file called
ejb-jar.xml.
<?xml version="1.0" encoding="Cp1252"?> <!DOCTYPE ejb-jar PUBLIC '-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN' 'http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd'> <ejb-jar> <description>example for chapter 15</description> <display-name></display-name> <small-icon></small-icon> <large-icon></large-icon> <enterprise-beans> <session> <ejb-name>PerfectTime</ejb-name> <home>PerfectTimeHome</home> <remote>PerfectTime</remote> <ejb-class>PerfectTimeBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> </session> </enterprise-beans> <ejb-client-jar></ejb-client-jar> </ejb-jar>
Inside the
<session> tag of our deployment descriptor we can see that our
Component, the Remote interface and Home interfaces are begin defined.
Deployment Descriptors can easily be automatically generated inside tools such
as JBuilder.
Along with the standard ejb-jar.xml
deployment descriptor the EJB 1.1 specification states that any vendor
specific tags should be stored in a separate file, this is to achieve high
portability between components and different brands EJB
containers.
Now that we have created our component,
and defined it’s composition in the Deployment Descriptor we then need to
archive the files inside a standard Java Archive (JAR) file. The Deployment
Descriptors should be placed inside the /META-INF sub-directory of the
Jar file.
Once we have defined our EJB component in
the Deployment Descriptor, the Deployer should then deploy the EJB component
into the EJB Container. At the time of writing, this process was quite "GUI
intensive" and specific to each individual EJB Container, so we decided not to
document the entire deployment process in this overview. Every EJB Container,
however will have a documented process for deploying an EJB.
Because an EJB component is a distributed
object, the deployment process should also create some client stubs for calling
the EJB component. These classes should be placed on the classpath of the client
application. Because EJB components can be implemented on top of RMI-IIOP
(CORBA) or RMI-JRMP, the stubs generated could vary between EJB Containers,
nevertheless they are generated classes.
When a client program wishes to invoke an
EJB, it must lookup the EJB component inside JNDI and obtain a reference to the
Home interface of the EJB component. The Home Interface can then be invoked to
create an instance of the EJB, which can then be invoked.
In this example the Client program is a
simple Java program, but you should remember that it could just as easily be a
Servlet, a JSP even a CORBA or RMI distributed object.
The PerfectTimeClient code is as
follows.
//: c15:ejb:PerfectTimeClient.java // Client program for PerfectTimeBean public class PerfectTimeClient { public static void main(String[] args) throws Exception { // Get a JNDI context using the // JNDI Naming service: javax.naming.Context context = new javax.naming.InitialContext(); // Look up the home interface in the // JNDI Naming service: Object ref = context.lookup("perfectTime"); // Cast the remote object to the home interface: PerfectTimeHome home = (PerfectTimeHome) javax.rmi.PortableRemoteObject.narrow( ref, PerfectTimeHome.class); // Create a remote object from the home interface: PerfectTime pt = home.create(); // Invoke getPerfectTime() System.out.println( "Perfect Time EJB invoked, time is: " + pt.getPerfectTime() ); } } ///:~
The Enterprise JavaBeans specification -
although initially seems very daunting - is a dramatic step forward in the
standardization and simplification or distributed object computing. It is a
major piece of the Java 2, Enterprise Edition Platform and is receiving much
support from the Distributed Object community. Many tools are currently
available or will be in the near future to help accelerate the development of
EJB components.
This overview was aimed at giving you a
brief tour as to what EJB is all about. For more information about the
Enterprise JavaBeans specification you should see the Official Enterprise
JavaBeans Home Page at http://java.sun.com/products/ejb/. Here you can
download the latest specification as well as the Java 2, Enterprise Edition
Reference Implementation, which you can use to develop and deploy your own EJB
components.
This
section[76] gives
an overview of Sun Microsystems's Jini technology. It describes some Jini nuts
and bolts and shows how Jini's architecture helps to raise the level of
abstraction in distributed systems programming, effectively turning network
programming into object-oriented programming.
Traditionally, operating systems have
been designed with the assumption that a computer will have a processor, some
memory, and a disk. When you boot a computer, the first thing it does is look
for a disk. If it doesn't find a disk, it can't function as a computer.
Increasingly, however, computers are appearing in a different guise: as embedded
devices that have a processor, some memory, and a network connection—but
no disk. The first thing a cell phone does when you boot it up, for example, is
look for the telephone network. If it doesn't find the network, it can't
function as a cell phone. This trend in the hardware environment, from
disk-centric to network-centric, will affect how we organize our
software—and that's where Jini comes in.
Jini is an attempt to rethink computer
architecture, given the rising importance of the network and the proliferation
of processors in devices that have no disk drive. These devices, which will come
from many different vendors, will need to interact over a network. The network
itself will be very dynamic—devices and services will be added and removed
regularly. Jini provides mechanisms to enable smooth adding, removal, and
finding of devices and services on the network. In addition, Jini provides a
programming model that makes it easier for programmers to get their devices
talking to each other.
Building on top of Java, object
serialization, and RMI (which together enable objects to move around the network
from virtual machine to virtual machine) Jini attempts to extend the benefits of
object-oriented programming to the network. Instead of requiring device vendors
to agree on the network protocols through which their devices can interact, Jini
enables the devices to talk to each other through interfaces to
objects.
Jini is a set of APIs and network
protocols that can help you build and deploy distributed systems that are
organized as federations of services. A service can be anything
that sits on the network and is ready to perform a useful function. Hardware
devices, software, communications channels—even human users
themselves—can be services. A Jini-enabled disk drive, for example, could
offer a “storage” service. A Jini-enabled printer could offer a
“printing” service. A federation of services, then, is a set of
services, currently available on the network, that a client (meaning a program,
service, or user) can bring together to help it accomplish some goal.
To perform a task, a client enlists the
help of services. For example, a client program might upload pictures from the
image storage service in a digital camera, download the pictures to a persistent
storage service offered by a disk drive, and send a page of thumbnail-sized
versions of the images to the printing service of a color printer. In this
example, the client program builds a distributed system consisting of itself,
the image storage service, the persistent storage service, and the
color-printing service. The client and services of this distributed system work
together to perform the task: to offload and store images from a digital camera
and print a page of thumbnails.
The idea behind the word federation
is that the Jini view of the network doesn't involve a central controlling
authority. Because no one service is in charge, the set of all services
available on the network form a federation—a group composed of equal
peers. Instead of a central authority, Jini's run-time infrastructure merely
provides a way for clients and services to find each other (via a lookup
service, which stores a directory of currently available services). After
services locate each other, they are on their own. The client and its enlisted
services perform their task independently of the Jini run-time infrastructure.
If the Jini lookup service crashes, any distributed systems brought together via
the lookup service before it crashed can continue their work. Jini even includes
a network protocol that clients can use to find services in the absence of a
lookup service.
Jini defines a run-time infrastructure
that resides on the network and provides mechanisms that enable you to add,
remove, locate, and access services. The run-time infrastructure resides in
three places: in lookup services that sit on the network, in the service
providers (such as Jini-enabled devices), and in clients. Lookup services
are the central organizing mechanism for Jini-based systems. When new
services become available on the network, they register themselves with a lookup
service. When clients wish to locate a service to assist with some task, they
consult a lookup service.
The run-time infrastructure uses one
network-level protocol, called discovery, and two object-level protocols,
called join and lookup. Discovery enables clients and services to
locate lookup services. Join enables a service to register itself in a lookup
service. Lookup enables a client to query for services that can help accomplish
its goals.
Discovery works like this: Imagine you
have a Jini-enabled disk drive that offers a persistent storage service. As soon
as you connect the drive to the network, it broadcasts a presence
announcement by dropping a multicast packet onto a well-known port. Included
in the presence announcement is an IP address and port number where the disk
drive can be contacted by a lookup service.
Lookup services monitor the well-known
port for presence announcement packets. When a lookup service receives a
presence announcement, it opens and inspects the packet. The packet contains
information that enables the lookup service to determine whether or not it
should contact the sender of the packet. If so, it contacts the sender directly
by making a TCP connection to the IP address and port number extracted from the
packet. Using RMI, the lookup service sends an object, called a service
registrar, across the network to the originator of the packet. The purpose
of the service registrar object is to facilitate further communication with the
lookup service. By invoking methods on this object, the sender of the
announcement packet can perform join and lookup on the lookup service. In the
case of the disk drive, the lookup service would make a TCP connection to the
disk drive and would send it a service registrar object, through which the disk
drive would then register its persistent storage service via the join
process.
Once a service provider has a service
registrar object, the end product of discovery, it is ready to do a
join—to become part of the federation of services that are registered in
the lookup service. To do a join, the service provider invokes the
register( ) method on the service registrar object, passing as a
parameter an object called a service item, a bundle of objects that describe the
service. The register( ) method sends a copy of the service item up
to the lookup service, where the service item is stored. Once this has
completed, the service provider has finished the join process: its service has
become registered in the lookup service.
The service item is a container for
several objects, including an object called a service object, which
clients can use to interact with the service. The service item can also include
any number of attributes, which can be any object. Some potential
attributes are icons, classes that provide GUIs for the service, and objects
that give more information about the service.
Service objects usually implement one or
more interfaces through which clients interact with the service. For example, a
lookup service is a Jini service, and its service object is the service
registrar. The register( ) method invoked by service providers
during join is declared in the ServiceRegistrar interface (a member of
the net.jini.core.lookup package), which all service registrar objects
implement. Clients and service providers talk to the lookup service through the
service registrar object by invoking methods declared in the
ServiceRegistrar interface. Likewise, a disk drive would provide a
service object that implemented some well-known storage service interface.
Clients would look up and interact with the disk drive by this storage service
interface.
Once a service has registered with a
lookup service via the join process, that service is available for use by
clients who query that lookup service. To build a distributed system of services
that will work together to perform some task, a client must locate and enlist
the help of the individual services. To find a service, clients query lookup
services via a process called lookup.
To perform a lookup, a client invokes the
lookup( ) method on a service registrar object. (A client, like a
service provider, gets a service registrar through the previously-described
process of discovery.) The client passes as an argument to lookup( )
a service template, an object that serves as search criteria for the
query. The service template can include a reference to an array of Class
objects. These Class objects indicate to the lookup service the Java type
(or types) of the service object desired by the client. The service template can
also include a service ID, which uniquely identifies a service, and
attributes, which must exactly match the attributes uploaded by the service
provider in the service item. The service template can also contain wildcards
for any of these fields. A wildcard in the service ID field, for example, will
match any service ID. The lookup( ) method sends the service
template to the lookup service, which performs the query and sends back zero to
any matching service objects. The client gets a reference to the matching
service objects as the return value of the lookup( ) method.
In the general case, a client looks up a
service by Java type, usually an interface. For example, if a client needed to
use a printer, it would compose a service template that included a Class
object for a well-known interface to printer services. All printer services
would implement this well-known interface. The lookup service would return a
service object (or objects) that implemented this interface. Attributes can be
included in the service template to narrow the number of matches for such a
type-based search. The client would use the printer service by invoking methods
from the well-known printer service interface on the service object.
Jini's architecture brings
object-oriented programming to the network by enabling network services to take
advantage of one of the fundamentals of objects: the separation of interface and
implementation. For example, a service object can grant clients access to the
service in many ways. The object can actually represent the entire service,
which is downloaded to the client during lookup and then executed locally.
Alternatively, the service object can serve merely as a proxy to a remote
server. Then when the client invokes methods on the service object, it sends the
requests across the network to the server, which does the real work. A third
option is for the local service object and a remote server to each do part of
the work.
One important consequence of Jini's
architecture is that the network protocol used to communicate between a proxy
service object and a remote server does not need to be known to the client. As
illustrated in the figure below, the network protocol is part of the service's
implementation. This protocol is a private matter decided upon by the developer
of the service. The client can communicate with the service via this private
protocol because the service injects some of its own code (the service object)
into the client's address space. The injected service object could communicate
with the service via RMI, CORBA, DCOM, some home-brewed protocol built on top of
sockets and streams, or anything else. The client simply doesn't need to care
about network protocols, because it can talk to the well-known interface that
the service object implements. The service object takes care of any necessary
communication on the network.
Different implementations of the same
service interface can use completely different approaches and network protocols.
A service can use specialized hardware to fulfill client requests, or it can do
all its work in software. In fact, the implementation approach taken by a single
service can evolve over time. The client can be sure it has a service object
that understands the current implementation of the service, because the client
receives the service object (by way of the lookup service) from the service
provider itself. To the client, a service looks like the well-known interface,
regardless of how the service is implemented.
Jini attempts to raise the level of
abstraction for distributed systems programming, from the network protocol level
to the object interface level. In the emerging proliferation of embedded devices
connected to networks, many pieces of a distributed system may come from
different vendors. Jini makes it unnecessary for vendors of devices to agree on
network level protocols that allow their devices to interact. Instead, vendors
must agree on Java interfaces through which their devices can interact. The
processes of discovery, join, and lookup, provided by the Jini run-time
infrastructure, will enable devices to locate each other on the network. Once
they locate each other, devices will be able to communicate with each other
through Java interfaces.
[73]
This means a maximum of just over four billion numbers, which is rapidly running
out. The new standard for IP addresses will use a 128-bit number, which should
produce enough unique IP addresses for the foreseeable future.
[74]
Many brain cells died in agony to discover this information.
[75]
This section was contributed by Robert Castaneda, with help from Dave
Bartlett.
[76]
This section was contributed by Bill Venners (www.artima.com).