ZeePedia

Distributed Computing:Network programming, Servlets, CORBA, Enterprise JavaBeans

<< Multiple Threads:Responsive user interfaces, Sharing limited resources, Runnable revisited
A: Passing & Returning Objects:Aliasing, Making local copies, Cloning objects >>
img
public void run() {
while (true) {
try {
sleep(100);
synchronized(this) {
while(suspended)
wait();
}
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
t.setText(Integer.toString(count++));
}
}
}
public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(t);
suspend.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
ss.fauxSuspend();
}
});
cp.add(suspend);
resume.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
ss.fauxResume();
}
});
cp.add(resume);
}
public static void main(String[] args) {
Console.run(new Suspend(), 300, 100);
}
} ///:~
876
Thinking in Java
img
The flag suspended inside Suspendable is used to turn suspension on
and off. To suspend, the flag is set to true by calling fauxSuspend( )
and this is detected inside run( ). The wait( ), as described earlier in this
chapter, must be synchronized so that it has the object lock. In
fauxResume( ), the suspended flag is set to false and notify( ) is
called--since this wakes up wait( ) inside a synchronized clause the
fauxResume( ) method must also be synchronized so that it acquires
the lock before calling notify( ) (thus the lock is available for the wait( )
to wake up with). If you follow the style shown in this program you can
avoid using suspend( ) and resume( ).
The destroy( ) method of Thread has never been implemented; it's like
a suspend( ) that cannot resume, so it has the same deadlock issues as
suspend( ). However, this is not a deprecated method and it might be
implemented in a future version of Java (after 2) for special situations in
which the risk of a deadlock is acceptable.
You might wonder why these methods, now deprecated, were included in
Java in the first place. It seems an admission of a rather significant
mistake to simply remove them outright (and pokes yet another hole in
the arguments for Java's exceptional design and infallibility touted by Sun
marketing people). The heartening part about the change is that it clearly
indicates that the technical people and not the marketing people are
running the show--they discovered a problem and they are fixing it. I find
this much more promising and hopeful than leaving the problem in
because "fixing it would admit an error." It means that Java will continue
to improve, even if it means a little discomfort on the part of Java
programmers. I'd rather deal with the discomfort than watch the language
stagnate.
Priorities
The priority of a thread tells the scheduler how important this thread is.
If there are a number of threads blocked and waiting to be run, the
scheduler will run the one with the highest priority first. However, this
doesn't mean that threads with lower priority don't get run (that is, you
can't get deadlocked because of priorities). Lower priority threads just
tend to run less often.
Chapter 14: Multiple Threads
877
img
Although priorities are interesting to know about and to play with, in
practice you almost never need to set priorities yourself. So feel free to
skip the rest of this section if priorities aren't interesting to you.
Reading and setting priorities
You can read the priority of a thread with getPriority( ) and change it
with setPriority( ). The form of the prior "counter" examples can be
used to show the effect of changing the priorities. In this applet you'll see
that the counters slow down as the associated threads have their priorities
lowered:
//: c14:Counter5.java
// Adjusting the priorities of threads.
// <applet code=Counter5 width=450 height=600>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
class Ticker2 extends Thread {
private JButton
b = new JButton("Toggle"),
incPriority = new JButton("up"),
decPriority = new JButton("down");
private JTextField
t = new JTextField(10),
pr = new JTextField(3); // Display priority
private int count = 0;
private boolean runFlag = true;
public Ticker2(Container c) {
b.addActionListener(new ToggleL());
incPriority.addActionListener(new UpL());
decPriority.addActionListener(new DownL());
JPanel p = new JPanel();
p.add(t);
p.add(pr);
p.add(b);
p.add(incPriority);
p.add(decPriority);
878
Thinking in Java
img
c.add(p);
}
class ToggleL implements ActionListener {
public void actionPerformed(ActionEvent e) {
runFlag = !runFlag;
}
}
class UpL implements ActionListener {
public void actionPerformed(ActionEvent e) {
int newPriority = getPriority() + 1;
if(newPriority > Thread.MAX_PRIORITY)
newPriority = Thread.MAX_PRIORITY;
setPriority(newPriority);
}
}
class DownL implements ActionListener {
public void actionPerformed(ActionEvent e) {
int newPriority = getPriority() - 1;
if(newPriority < Thread.MIN_PRIORITY)
newPriority = Thread.MIN_PRIORITY;
setPriority(newPriority);
}
}
public void run() {
while (true) {
if(runFlag) {
t.setText(Integer.toString(count++));
pr.setText(
Integer.toString(getPriority()));
}
yield();
}
}
}
public class Counter5 extends JApplet {
private JButton
start = new JButton("Start"),
upMax = new JButton("Inc Max Priority"),
downMax = new JButton("Dec Max Priority");
private boolean started = false;
Chapter 14: Multiple Threads
879
img
private static final int SIZE = 10;
private Ticker2[] s = new Ticker2[SIZE];
private JTextField mp = new JTextField(3);
public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
for(int i = 0; i < s.length; i++)
s[i] = new Ticker2(cp);
cp.add(new JLabel(
"MAX_PRIORITY = " + Thread.MAX_PRIORITY));
cp.add(new JLabel("MIN_PRIORITY = "
+ Thread.MIN_PRIORITY));
cp.add(new JLabel("Group Max Priority = "));
cp.add(mp);
cp.add(start);
cp.add(upMax);
cp.add(downMax);
start.addActionListener(new StartL());
upMax.addActionListener(new UpMaxL());
downMax.addActionListener(new DownMaxL());
showMaxPriority();
// Recursively display parent thread groups:
ThreadGroup parent =
s[0].getThreadGroup().getParent();
while(parent != null) {
cp.add(new Label(
"Parent threadgroup max priority = "
+ parent.getMaxPriority()));
parent = parent.getParent();
}
}
public void showMaxPriority() {
mp.setText(Integer.toString(
s[0].getThreadGroup().getMaxPriority()));
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
if(!started) {
started = true;
for(int i = 0; i < s.length; i++)
s[i].start();
880
Thinking in Java
img
}
}
}
class UpMaxL implements ActionListener {
public void actionPerformed(ActionEvent e) {
int maxp =
s[0].getThreadGroup().getMaxPriority();
if(++maxp > Thread.MAX_PRIORITY)
maxp = Thread.MAX_PRIORITY;
s[0].getThreadGroup().setMaxPriority(maxp);
showMaxPriority();
}
}
class DownMaxL implements ActionListener {
public void actionPerformed(ActionEvent e) {
int maxp =
s[0].getThreadGroup().getMaxPriority();
if(--maxp < Thread.MIN_PRIORITY)
maxp = Thread.MIN_PRIORITY;
s[0].getThreadGroup().setMaxPriority(maxp);
showMaxPriority();
}
}
public static void main(String[] args) {
Console.run(new Counter5(), 450, 600);
}
} ///:~
Ticker2 follows the form established earlier in this chapter, but there's
an extra JTextField for displaying the priority of the thread and two
more buttons for incrementing and decrementing the priority.
Also notice the use of yield( ), which voluntarily hands control back to
the scheduler. Without this the multithreading mechanism still works, but
you'll notice it runs slowly (try removing the call to yield( ) to see this).
You could also call sleep( ), but then the rate of counting would be
controlled by the sleep( ) duration instead of the priority.
The init( ) in Counter5 creates an array of ten Ticker2s; their buttons
and fields are placed on the form by the Ticker2 constructor. Counter5
adds buttons to start everything up as well as increment and decrement
Chapter 14: Multiple Threads
881
img
the maximum priority of the thread group. In addition, there are labels
that display the maximum and minimum priorities possible for a thread
and a JTextField to show the thread group's maximum priority. (The
next section will describe thread groups.) Finally, the priorities of the
parent thread groups are also displayed as labels.
When you press an "up" or "down" button, that Ticker2's priority is
fetched and incremented or decremented accordingly.
When you run this program, you'll notice several things. First of all, the
thread group's default priority is five. Even if you decrement the
maximum priority below five before starting the threads (or before
creating the threads, which requires a code change), each thread will have
a default priority of five.
The simple test is to take one counter and decrement its priority to one,
and observe that it counts much slower. But now try to increment it again.
You can get it back up to the thread group's priority, but no higher. Now
decrement the thread group's priority a couple of times. The thread
priorities are unchanged, but if you try to modify them either up or down
you'll see that they'll automatically pop to the priority of the thread group.
Also, new threads will still be given a default priority, even if that's higher
than the group priority. (Thus the group priority is not a way to prevent
new threads from having higher priorities than existing ones.)
Finally, try to increment the group maximum priority. It can't be done.
You can only reduce thread group maximum priorities, not increase them.
Thread groups
All threads belong to a thread group. This can be either the default thread
group or a group you explicitly specify when you create the thread. At
creation, the thread is bound to a group and cannot change to a different
group. Each application has at least one thread that belongs to the system
thread group. If you create more threads without specifying a group, they
will also belong to the system thread group.
Thread groups must also belong to other thread groups. The thread group
that a new one belongs to must be specified in the constructor. If you
create a thread group without specifying a thread group for it to belong to,
882
Thinking in Java
img
it will be placed under the system thread group. Thus, all thread groups in
your application will ultimately have the system thread group as the
parent.
The reason for the existence of thread groups is hard to determine from
the literature, which tends to be confusing on this subject. It's often cited
as "security reasons." According to Arnold & Gosling,2 "Threads within a
thread group can modify the other threads in the group, including any
farther down the hierarchy. A thread cannot modify threads outside of its
own group or contained groups." It's hard to know what "modify" is
supposed to mean here. The following example shows a thread in a "leaf"
subgroup modifying the priorities of all the threads in its tree of thread
groups as well as calling a method for all the threads in its tree.
//: c14:TestAccess.java
// How threads can access other threads
// in a parent thread group.
public class TestAccess {
public static void main(String[] args) {
ThreadGroup
x = new ThreadGroup("x"),
y = new ThreadGroup(x, "y"),
z = new ThreadGroup(y, "z");
Thread
one = new TestThread1(x, "one"),
two = new TestThread2(z, "two");
}
}
class TestThread1 extends Thread {
private int i;
TestThread1(ThreadGroup g, String name) {
super(g, name);
}
void f() {
2 The Java Programming Language, by Ken Arnold and James Gosling, Addison-Wesley
1996 pp 179.
Chapter 14: Multiple Threads
883
img
i++; // modify this thread
System.out.println(getName() + " f()");
}
}
class TestThread2 extends TestThread1 {
TestThread2(ThreadGroup g, String name) {
super(g, name);
start();
}
public void run() {
ThreadGroup g =
getThreadGroup().getParent().getParent();
g.list();
Thread[] gAll = new Thread[g.activeCount()];
g.enumerate(gAll);
for(int i = 0; i < gAll.length; i++) {
gAll[i].setPriority(Thread.MIN_PRIORITY);
((TestThread1)gAll[i]).f();
}
g.list();
}
} ///:~
In main( ), several ThreadGroups are created, leafing off from each
other: x has no argument but its name (a String), so it is automatically
placed in the "system" thread group, while y is under x and z is under y.
Note that initialization happens in textual order so this code is legal.
Two threads are created and placed in different thread groups.
TestThread1 doesn't have a run( ) method but it does have an f( ) that
modifies the thread and prints something so you can see it was called.
TestThread2 is a subclass of TestThread1 and its run( ) is fairly
elaborate. It first gets the thread group of the current thread, then moves
up the heritage tree by two levels using getParent( ). (This is contrived
since I purposely place the TestThread2 object two levels down in the
hierarchy.) At this point, an array of references to Threads is created
using the method activeCount( ) to ask how many threads are in this
thread group and all the child thread groups. The enumerate( ) method
places references to all of these threads in the array gAll, then I simply
884
Thinking in Java
img
move through the entire array calling the f( ) method for each thread, as
well as modifying the priority. Thus, a thread in a "leaf" thread group
modifies threads in parent thread groups.
The debugging method list( ) prints all the information about a thread
group to standard output and is helpful when investigating thread group
behavior. Here's the output of the program:
java.lang.ThreadGroup[name=x,maxpri=10]
Thread[one,5,x]
java.lang.ThreadGroup[name=y,maxpri=10]
java.lang.ThreadGroup[name=z,maxpri=10]
Thread[two,5,z]
one f()
two f()
java.lang.ThreadGroup[name=x,maxpri=10]
Thread[one,1,x]
java.lang.ThreadGroup[name=y,maxpri=10]
java.lang.ThreadGroup[name=z,maxpri=10]
Thread[two,1,z]
Not only does list( ) print the class name of ThreadGroup or Thread,
but it also prints the thread group name and its maximum priority. For
threads, the thread name is printed, followed by the thread priority and
the group that it belongs to. Note that list( ) indents the threads and
thread groups to indicate that they are children of the unindented thread
group.
You can see that f( ) is called by the TestThread2 run( ) method, so it's
obvious that all threads in a group are vulnerable. However, you can
access only the threads that branch off from your own system thread
group tree, and perhaps this is what is meant by "safety." You cannot
access anyone else's system thread group tree.
Controlling thread groups
Putting aside the safety issue, one thing thread groups seem to be useful
for is control: you can perform certain operations on an entire thread
group with a single command. The following example demonstrates this,
and the restrictions on priorities within thread groups. The commented
numbers in parentheses provide a reference to compare to the output.
Chapter 14: Multiple Threads
885
img
//: c14:ThreadGroup1.java
// How thread groups control priorities
// of the threads inside them.
public class ThreadGroup1 {
public static void main(String[] args) {
// Get the system thread & print its Info:
ThreadGroup sys =
Thread.currentThread().getThreadGroup();
sys.list(); // (1)
// Reduce the system thread group priority:
sys.setMaxPriority(Thread.MAX_PRIORITY - 1);
// Increase the main thread priority:
Thread curr = Thread.currentThread();
curr.setPriority(curr.getPriority() + 1);
sys.list(); // (2)
// Attempt to set a new group to the max:
ThreadGroup g1 = new ThreadGroup("g1");
g1.setMaxPriority(Thread.MAX_PRIORITY);
// Attempt to set a new thread to the max:
Thread t = new Thread(g1, "A");
t.setPriority(Thread.MAX_PRIORITY);
g1.list(); // (3)
// Reduce g1's max priority, then attempt
// to increase it:
g1.setMaxPriority(Thread.MAX_PRIORITY - 2);
g1.setMaxPriority(Thread.MAX_PRIORITY);
g1.list(); // (4)
// Attempt to set a new thread to the max:
t = new Thread(g1, "B");
t.setPriority(Thread.MAX_PRIORITY);
g1.list(); // (5)
// Lower the max priority below the default
// thread priority:
g1.setMaxPriority(Thread.MIN_PRIORITY + 2);
// Look at a new thread's priority before
// and after changing it:
t = new Thread(g1, "C");
g1.list(); // (6)
t.setPriority(t.getPriority() -1);
g1.list(); // (7)
886
Thinking in Java
img
// Make g2 a child Threadgroup of g1 and
// try to increase its priority:
ThreadGroup g2 = new ThreadGroup(g1, "g2");
g2.list(); // (8)
g2.setMaxPriority(Thread.MAX_PRIORITY);
g2.list(); // (9)
// Add a bunch of new threads to g2:
for (int i = 0; i < 5; i++)
new Thread(g2, Integer.toString(i));
// Show information about all threadgroups
// and threads:
sys.list(); // (10)
System.out.println("Starting all threads:");
Thread[] all = new Thread[sys.activeCount()];
sys.enumerate(all);
for(int i = 0; i < all.length; i++)
if(!all[i].isAlive())
all[i].start();
// Suspends & Stops all threads in
// this group and its subgroups:
System.out.println("All threads started");
sys.suspend(); // Deprecated in Java 2
// Never gets here...
System.out.println("All threads suspended");
sys.stop(); // Deprecated in Java 2
System.out.println("All threads stopped");
}
} ///:~
The output that follows has been edited to allow it to fit on the page (the
java.lang. has been removed) and to add numbers to correspond to the
commented numbers in the listing above.
(1) ThreadGroup[name=system,maxpri=10]
Thread[main,5,system]
(2) ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
(3) ThreadGroup[name=g1,maxpri=9]
Thread[A,9,g1]
(4) ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
Chapter 14: Multiple Threads
887
img
(5) ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
Thread[B,8,g1]
(6) ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,6,g1]
(7) ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
(8) ThreadGroup[name=g2,maxpri=3]
(9) ThreadGroup[name=g2,maxpri=3]
(10)ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
ThreadGroup[name=g2,maxpri=3]
Thread[0,6,g2]
Thread[1,6,g2]
Thread[2,6,g2]
Thread[3,6,g2]
Thread[4,6,g2]
Starting all threads:
All threads started
All programs have at least one thread running, and the first action in
main( ) is to call the static method of Thread called
currentThread( ). From this thread, the thread group is produced and
list( ) is called for the result. The output is:
(1) ThreadGroup[name=system,maxpri=10]
Thread[main,5,system]
You can see that the name of the main thread group is system, and the
name of the main thread is main, and it belongs to the system thread
group.
The second exercise shows that the system group's maximum priority
can be reduced and the main thread can have its priority increased:
888
Thinking in Java
img
(2) ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
The third exercise creates a new thread group, g1, which automatically
belongs to the system thread group since it isn't otherwise specified. A
new thread A is placed in g1. After attempting to set this group's
maximum priority to the highest level and A's priority to the highest level,
the result is:
(3) ThreadGroup[name=g1,maxpri=9]
Thread[A,9,g1]
Thus, it's not possible to change the thread group's maximum priority to
be higher than its parent thread group.
The fourth exercise reduces g1's maximum priority by two and then tries
to increase it up to Thread.MAX_PRIORITY. The result is:
(4) ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
You can see that the increase in maximum priority didn't work. You can
only decrease a thread group's maximum priority, not increase it. Also,
notice that thread A's priority didn't change, and now it is higher than the
thread group's maximum priority. Changing a thread group's maximum
priority doesn't affect existing threads.
The fifth exercise attempts to set a new thread to maximum priority:
(5) ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
Thread[B,8,g1]
The new thread cannot be changed to anything higher than the maximum
thread group priority.
The default thread priority for this program is six; that's the priority a new
thread will be created at and where it will stay if you don't manipulate the
priority. Exercise 6 lowers the maximum thread group priority below the
default thread priority to see what happens when you create a new thread
under this condition:
(6) ThreadGroup[name=g1,maxpri=3]
Chapter 14: Multiple Threads
889
img
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,6,g1]
Even though the maximum priority of the thread group is three, the new
thread is still created using the default priority of six. Thus, maximum
thread group priority does not affect default priority. (In fact, there
appears to be no way to set the default priority for new threads.)
After changing the priority, attempting to decrement it by one, the result
is:
(7) ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
Only when you attempt to change the priority is the thread group's
maximum priority enforced.
A similar experiment is performed in (8) and (9), in which a new thread
group g2 is created as a child of g1 and its maximum priority is changed.
You can see that it's impossible for g2's maximum to go higher than g1's:
(8) ThreadGroup[name=g2,maxpri=3]
(9) ThreadGroup[name=g2,maxpri=3]
Also notice that g2 is automatically set to the thread group maximum
priority of g1 as g2 is created.
After all of these experiments, the entire system of thread groups and
threads is printed:
(10)ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
ThreadGroup[name=g2,maxpri=3]
Thread[0,6,g2]
Thread[1,6,g2]
Thread[2,6,g2]
890
Thinking in Java
img
Thread[3,6,g2]
Thread[4,6,g2]
So because of the rules of thread groups, a child group must always have a
maximum priority that's less than or equal to its parent's maximum
priority.
The last part of this program demonstrates methods for an entire group of
threads. First the program moves through the entire tree of threads and
starts each one that hasn't been started. For drama, the system group is
then suspended and finally stopped. (Although it's interesting to see that
suspend( ) and stop( ) work on entire thread groups, you should keep
in mind that these methods are deprecated in Java 2.) But when you
suspend the system group you also suspend the main thread and the
whole program shuts down, so it never gets to the point where the threads
are stopped. Actually, if you do stop the main thread it throws a
ThreadDeath exception, so this is not a typical thing to do. Since
ThreadGroup is inherited from Object, which contains the wait( )
method, you can also choose to suspend the program for any number of
seconds by calling wait(seconds * 1000). This must acquire the lock
inside a synchronized block, of course.
The ThreadGroup class also has suspend( ) and resume( ) methods
so you can stop and start an entire thread group and all of its threads and
subgroups with a single command. (Again, suspend( ) and resume( )
are deprecated in Java 2.)
Thread groups can seem a bit mysterious at first, but keep in mind that
you probably won't be using them directly very often.
Runnable revisited
Earlier in this chapter, I suggested that you think carefully before making
an applet or main Frame as an implementation of Runnable. Of course,
if you must inherit from a class and you want to add threading behavior to
the class, Runnable is the correct solution. The final example in this
chapter exploits this by making a Runnable JPanel class that paints
different colors on itself. This application is set up to take values from the
command line to determine how big the grid of colors is and how long to
Chapter 14: Multiple Threads
891
img
sleep( ) between color changes. By playing with these values you'll
discover some interesting and possibly inexplicable features of threads:
//: c14:ColorBoxes.java
// Using the Runnable interface.
// <applet code=ColorBoxes width=500 height=400>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
class CBox extends JPanel implements Runnable {
private Thread t;
private int pause;
private static final Color[] colors = {
Color.black, Color.blue, Color.cyan,
Color.darkGray, Color.gray, Color.green,
Color.lightGray, Color.magenta,
Color.orange, Color.pink, Color.red,
Color.white, Color.yellow
};
private Color cColor = newColor();
private static final Color newColor() {
return colors[
(int)(Math.random() * colors.length)
];
}
public void paintComponent(Graphics  g) {
super.paintComponent(g);
g.setColor(cColor);
Dimension s = getSize();
g.fillRect(0, 0, s.width, s.height);
}
public CBox(int pause) {
this.pause = pause;
t = new Thread(this);
t.start();
}
892
Thinking in Java
img
public void run() {
while(true) {
cColor = newColor();
repaint();
try {
t.sleep(pause);
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
}
public class ColorBoxes extends JApplet {
private boolean isApplet = true;
private int grid = 12;
private int pause = 50;
public void init() {
// Get parameters from Web page:
if (isApplet) {
String gsize = getParameter("grid");
if(gsize != null)
grid = Integer.parseInt(gsize);
String pse = getParameter("pause");
if(pse != null)
pause = Integer.parseInt(pse);
}
Container cp = getContentPane();
cp.setLayout(new GridLayout(grid, grid));
for (int i = 0; i < grid * grid; i++)
cp.add(new CBox(pause));
}
public static void main(String[] args) {
ColorBoxes applet = new ColorBoxes();
applet.isApplet = false;
if(args.length > 0)
applet.grid = Integer.parseInt(args[0]);
if(args.length > 1)
applet.pause = Integer.parseInt(args[1]);
Console.run(applet, 500, 400);
}
Chapter 14: Multiple Threads
893
img
} ///:~
ColorBoxes is the usual applet/application with an init( ) that sets up
the GUI. This sets up the GridLayout so that it has grid cells in each
dimension. Then it adds the appropriate number of CBox objects to fill
the grid, passing the pause value to each one. In main( ) you can see
how pause and grid have default values that can be changed if you pass
in command-line arguments, or by using applet parameters.
CBox is where all the work takes place. This is inherited from JPanel
and it implements the Runnable interface so each JPanel can also be a
Thread. Remember that when you implement Runnable, you don't
make a Thread object, just a class that has a run( ) method. Thus, you
must explicitly create a Thread object and hand the Runnable object to
the constructor, then call start( ) (this happens in the constructor). In
CBox this thread is called t.
Notice the array colors, which is an enumeration of all the colors in class
Color. This is used in newColor( ) to produce a randomly selected
color. The current cell color is cColor.
paintComponent( ) is quite simple--it just sets the color to cColor and
fills the entire JPanel with that color.
In run( ), you see the infinite loop that sets the cColor to a new random
color and then calls repaint( ) to show it. Then the thread goes to
sleep( ) for the amount of time specified on the command line.
Precisely because this design is flexible and threading is tied to each
JPanel element, you can experiment by making as many threads as you
want. (In reality, there is a restriction imposed by the number of threads
your JVM can comfortably handle.)
This program also makes an interesting benchmark, since it can show
dramatic performance differences between one JVM threading
implementation and another.
Too many threads
At some point, you'll find that ColorBoxes bogs down. On my machine,
this occurred somewhere after a 10 x 10 grid. Why does this happen?
894
Thinking in Java
img
You're naturally suspicious that Swing might have something to do with
it, so here's an example that tests that premise by making fewer threads.
The following code is reorganized so that an ArrayList implements
Runnable and that ArrayList holds a number of color blocks and
randomly chooses ones to update. Then a number of these ArrayList
objects are created, depending roughly on the grid dimension you choose.
As a result, you have far fewer threads than color blocks, so if there's a
speedup we'll know it was because there were too many threads in the
previous example:
//: c14:ColorBoxes2.java
// Balancing thread use.
// <applet code=ColorBoxes2 width=600 height=500>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
class CBox2 extends JPanel {
private static final Color[] colors = {
Color.black, Color.blue, Color.cyan,
Color.darkGray, Color.gray, Color.green,
Color.lightGray, Color.magenta,
Color.orange, Color.pink, Color.red,
Color.white, Color.yellow
};
private Color cColor = newColor();
private static final Color newColor() {
return colors[
(int)(Math.random() * colors.length)
];
}
void nextColor() {
cColor = newColor();
repaint();
}
public void paintComponent(Graphics g) {
Chapter 14: Multiple Threads
895
img
super.paintComponent(g);
g.setColor(cColor);
Dimension s = getSize();
g.fillRect(0, 0, s.width, s.height);
}
}
class CBoxList
extends ArrayList implements Runnable {
private Thread t;
private int pause;
public CBoxList(int pause) {
this.pause = pause;
t = new Thread(this);
}
public void go() { t.start(); }
public void run() {
while(true) {
int i = (int)(Math.random() * size());
((CBox2)get(i)).nextColor();
try {
t.sleep(pause);
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
public Object last() { return get(size() - 1);}
}
public class ColorBoxes2 extends JApplet {
private boolean isApplet = true;
private int grid = 12;
// Shorter default pause than ColorBoxes:
private int pause = 50;
private CBoxList[] v;
public void init() {
// Get parameters from Web page:
if (isApplet) {
String gsize = getParameter("grid");
if(gsize != null)
896
Thinking in Java
img
grid = Integer.parseInt(gsize);
String pse = getParameter("pause");
if(pse != null)
pause = Integer.parseInt(pse);
}
Container cp = getContentPane();
cp.setLayout(new GridLayout(grid, grid));
v = new CBoxList[grid];
for(int i = 0; i < grid; i++)
v[i] = new CBoxList(pause);
for (int i = 0; i < grid * grid; i++) {
v[i % grid].add(new CBox2());
cp.add((CBox2)v[i % grid].last());
}
for(int i = 0; i < grid; i++)
v[i].go();
}
public static void main(String[] args) {
ColorBoxes2 applet = new ColorBoxes2();
applet.isApplet = false;
if(args.length > 0)
applet.grid = Integer.parseInt(args[0]);
if(args.length > 1)
applet.pause = Integer.parseInt(args[1]);
Console.run(applet, 500, 400);
}
} ///:~
In ColorBoxes2 an array of CBoxList is created and initialized to hold
grid CBoxLists, each of which knows how long to sleep. An equal
number of CBox2 objects is then added to each CBoxList, and each list
is told to go( ), which starts its thread.
CBox2 is similar to CBox: it paints itself with a randomly chosen color.
But that's all a CBox2 does. All of the threading has been moved into
CBoxList.
The CBoxList could also have inherited Thread and had a member
object of type ArrayList. That design has the advantage that the add( )
and get( ) methods could then be given specific argument and return
value types instead of generic Objects. (Their names could also be
Chapter 14: Multiple Threads
897
img
changed to something shorter.) However, the design used here seemed at
first glance to require less code. In addition, it automatically retains all
the other behaviors of an ArrayList. With all the casting and parentheses
necessary for get( ), this might not be the case as your body of code
grows.
As before, when you implement Runnable you don't get all of the
equipment that comes with Thread, so you have to create a new Thread
and hand yourself to its constructor in order to have something to
start( ), as you can see in the CBoxList constructor and in go( ). The
run( ) method simply chooses a random element number within the list
and calls nextColor( ) for that element to cause it to choose a new
randomly selected color.
Upon running this program, you see that it does indeed run faster and
respond more quickly (for instance, when you interrupt it, it stops more
quickly), and it doesn't seem to bog down as much at higher grid sizes.
Thus, a new factor is added into the threading equation: you must watch
to see that you don't have "too many threads" (whatever that turns out to
mean for your particular program and platform--here, the slowdown in
ColorBoxes appears to be caused by the fact that there's only one thread
that is responsible for all painting, and it gets bogged down by too many
requests). If you have too many threads, you must try to use techniques
like the one above to "balance" the number of threads in your program. If
you see performance problems in a multithreaded program you now have
a number of issues to examine:
1.
Do you have enough calls to sleep( ), yield( ), and/or wait( )?
2.
Are calls to sleep( ) long enough?
3.
Are you running too many threads?
4.
Have you tried different platforms and JVMs?
Issues like this are one reason that multithreaded programming is often
considered an art.
898
Thinking in Java
img
Summary
It is vital to learn when to use multithreading and when to avoid it. The
main reason to use it is to manage a number of tasks whose intermingling
will make more efficient use of the computer (including the ability to
transparently distribute the tasks across multiple CPUs) or be more
convenient for the user. The classic example of resource balancing is using
the CPU during I/O waits. The classic example of user convenience is
monitoring a "stop" button during long downloads.
The main drawbacks to multithreading are:
1.
Slowdown while waiting for shared resources
2.
Additional CPU overhead required to manage threads
3.
Unrewarded complexity, such as the silly idea of having a separate
thread to update each element of an array
4.
Pathologies including starving, racing, and deadlock
An additional advantage to threads is that they substitute "light"
execution context switches (of the order of 100 instructions) for "heavy"
process context switches (of the order of 1000s of instructions). Since all
threads in a given process share the same memory space, a light context
switch changes only program execution and local variables. On the other
hand, a process change--the heavy context switch--must exchange the full
memory space.
Threading is like stepping into an entirely new world and learning a whole
new programming language, or at least a new set of language concepts.
With the appearance of thread support in most microcomputer operating
systems, extensions for threads have also been appearing in programming
languages or libraries. In all cases, thread programming (1) seems
mysterious and requires a shift in the way you think about programming;
and (2) looks similar to thread support in other languages, so when you
understand threads, you understand a common tongue. And although
support for threads can make Java seem like a more complicated
language, don't blame Java. Threads are tricky.
Chapter 14: Multiple Threads
899
img
One of the biggest difficulties with threads occurs because more than one
thread might be sharing a resource--such as the memory in an object--
and you must make sure that multiple threads don't try to read and
change that resource at the same time. This requires judicious use of the
synchronized keyword, which is a helpful tool but must be understood
thoroughly because it can quietly introduce deadlock situations.
In addition, there's a certain art to the application of threads. Java is
designed to allow you to create as many objects as you need to solve your
problem--at least in theory. (Creating millions of objects for an
engineering finite-element analysis, for example, might not be practical in
Java.) However, it seems that there is an upper bound to the number of
threads you'll want to create, because at some point a large number of
threads seems to become unwieldy. This critical point is not in the many
thousands as it might be with objects, but rather in the low hundreds,
sometimes less than 100. As you often create only a handful of threads to
solve a problem, this is typically not much of a limit, yet in a more general
design it becomes a constraint.
A significant nonintuitive issue in threading is that, because of thread
scheduling, you can typically make your applications run faster by
inserting calls to sleep( ) inside run( )'s main loop. This definitely
makes it feel like an art, in particular when the longer delays seem to
speed up performance. Of course, the reason this happens is that shorter
delays can cause the end-of-sleep( ) scheduler interrupt to happen before
the running thread is ready to go to sleep, forcing the scheduler to stop it
and restart it later so it can finish what it was doing and then go to sleep.
It takes extra thought to realize how messy things can get.
One thing you might notice missing in this chapter is an animation
example, which is one of the most popular things to do with applets.
However, a complete solution (with sound) to this problem comes with
the Java JDK (available at java.sun.com) in the demo section. In addition,
we can expect better animation support to become part of future versions
of Java, while completely different non-Java, non-programming solutions
to animation for the Web are appearing that will probably be superior to
traditional approaches. For explanations about how Java animation
works, see Core Java 2 by Horstmann & Cornell, Prentice-Hall, 1997. For
more advanced discussions of threading, see Concurrent Programming
900
Thinking in Java
img
in Java by Doug Lea, Addison-Wesley, 1997, or Java Threads by Oaks &
Wong, O'Reilly, 1997.
Exercises
Solutions to selected exercises can be found in the electronic document The Thinking in Java
Annotated Solution Guide, available for a small fee from .
1.
Inherit a class from Thread and override the run( ) method.
Inside run( ), print a message, and then call sleep( ). Repeat this
three times, then return from run( ). Put a start-up message in
the constructor and override finalize( ) to print a shut-down
message. Make a separate thread class that calls System.gc( )
and System.runFinalization( ) inside run( ), printing a
message as it does so. Make several thread objects of both types
and run them to see what happens.
2.
Modify Sharing2.java to add a synchronized block inside the
run( ) method of TwoCounter instead of synchronizing the
entire run( ) method.
3.
Create two Thread subclasses, one with a run( ) that starts up,
captures the reference of the second Thread object and then calls
wait( ). The other class' run( ) should call notifyAll( ) for the
first thread after some number of seconds have passed, so the first
thread can print a message.
4.
In Counter5.java inside Ticker2, remove the yield( ) and
explain the results. Replace the yield( ) with a sleep( ) and
explain the results.
5.
In ThreadGroup1.java, replace the call to sys.suspend( ) with
a call to wait( ) for the thread group, causing it to wait for two
seconds. For this to work correctly you must acquire the lock for
sys inside a synchronized block.
6.
Change Daemons.java so that main( ) has a sleep( ) instead of
a readLine( ). Experiment with different sleep times to see what
happens.
Chapter 14: Multiple Threads
901
img
7.
In Chapter 8, locate the GreenhouseControls.java example,
which consists of three files. In Event.java, the class Event is
based on watching the time. Change Event so that it is a Thread,
and change the rest of the design so that it works with this new
Thread-based Event.
8.
Modify Exercise 7 so that the java.util.Timer class found in JDK
1.3 is used to run the system.
9.
Starting with SineWave.java from Chapter 13, create a program
(an applet/application using the Console class) that draws an
animated sine wave that appears to scrolls past the viewing
window like an oscilloscope, driving the animation with a
Thread. The speed of the animation should be controlled with a
java.swing.JSlider control.
10.
Modify Exercise 9 so that multiple sine wave panels are created
within the application. The number of sine wave panels should be
controlled by HTML tags or command-line parameters.
11.
Modify Exercise 9 so that the java.swing.Timer class is used to
drive the animation. Note the difference between this and
java.util.Timer.
902
Thinking in Java
img
15: Distributed
Computing
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 basic idea of distributed computing is not so difficult, and is
abstracted very nicely in the Java libraries. You want to:
! Get some information from that machine over there and move it to
this machine here, or vice versa. This is accomplished with basic
network programming.
! Connect to a database, which may live across a network. This is
accomplished with Java DataBase Connectivity (JDBC), which is
an abstraction away from the messy, platform-specific details of
SQL (the structured query language used for most database
transactions).
! Provide services via a Web server. This is accomplished with Java's
servlets and Java Server Pages (JSPs).
! Execute methods on Java objects that live on remote machines
transparently, as if those objects were resident on local machines.
This is accomplished with Java's Remote Method Invocation
(RMI).
903
img
! Use code written in other languages, running on other
architectures. This is accomplished using the Common Object
Request Broker Architecture (CORBA), which is directly
supported by Java.
! Isolate business logic from connectivity issues, especially
connections with databases including transaction management
and security. This is accomplished using Enterprise JavaBeans
(EJBs). EJBs are not actually a distributed architecture, but the
resulting applications are usually used in a networked client-
server system.
! Easily, dynamically, add and remove devices from a network
representing a local system. This is accomplished with Java's Jini.
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).
Network programming
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.
This section introduces Java's networking support using easy-to-
understand examples.
904
Thinking in Java
img
Identifying a machine
Of course, in order to tell one machine from another and to make sure
that you are connected with a particular machine, 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 which can exist in
two forms:
1.
The familiar DNS (Domain Name System) form. My domain name
is bruceeckel.com, and if 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.
2.
Alternatively, you can use the "dotted quad" form, which is four
numbers separated by dots, such as 123.255.28.120.
In both cases, the IP address is represented internally as a 32-bit number1
(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
1 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.
Chapter 15: Distributed Computing
905
img
address, and since a new one is 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. On
Windows 95/98, 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 and I have a Web server running on my
computer, he can connect to it by going to the URL http://199.190.87.75
(only as long as I continue to stay connected during that session). This can
906
Thinking in Java
img
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.
Servers and clients
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 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.
Testing programs without a network
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);
Chapter 15: Distributed Computing
907
img
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 overloaded 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");
(assuming "localhost" is configured in your machine's "hosts" table), or by
using its dotted quad form to name the reserved IP number for the
loopback:
InetAddress.getByName("127.0.0.1");
All three forms produce the same result.
Port: a unique place
within the machine
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 a 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.
908
Thinking in Java
img
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).
Sockets
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
Socket through which communications will take place on the server side.
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 naming 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
Chapter 15: Distributed Computing
909
img
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.
(However, the Socket that comes back from ServerSocket.accept( )
already contains all this information.)
A simple server and client
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. These are converted to a Reader and a Writer, then
wrapped in a BufferedReader and a PrintWriter. After that,
everything it reads from the BufferedReader it echoes to the
PrintWriter 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 and performs the same wrapping as in the server. Lines
of text are sent through the resulting PrintWriter. The client also
creates an InputStream (again, with appropriate conversions and
wrapping) to hear what the server is saying (which, in this case, is just the
words echoed back).
910
Thinking in Java
img
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;
Chapter 15: Distributed Computing
911
img
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).
912
Thinking in Java
img
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).
Chapter 15: Distributed Computing
913
img
The infinite while loop reads lines from the BufferedReader in and
writes information to System.out and to the PrintWriter out. Note
that in and out 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(
914
Thinking in Java
img
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,
Chapter 15: Distributed Computing
915
img
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.
916
Thinking in Java
img
Sockets produce a "dedicated" connection that persists until it is explicitly
disconnected. (The dedicated connection can still be disconnected
unexplicitly 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.
Serving multiple clients
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;
Chapter 15: Distributed Computing
917
img
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) {
System.err.println("IO Exception");
} finally {
try {
socket.close();
} catch(IOException e) {
System.err.println("Socket not closed");
}
}
}
}
public class MultiJabberServer {
918
Thinking in Java
img
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( ).
Chapter 15: Distributed Computing
919
img
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 the creation 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. The maximum number of threads allowed is determined by the
final int MAX_THREADS.
//: 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) {
920
Thinking in Java
img
System.err.println("Socket failed");
// 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) {
System.err.println("Socket not closed");
}
}
// 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) {
System.err.println("IO Exception");
} finally {
// Always close it:
Chapter 15: Distributed Computing
921
img
try {
socket.close();
} catch(IOException e) {
System.err.println("Socket not closed");
}
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
922
Thinking in Java
img
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.
Datagrams
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, including an example, in the first edition of this book
(available on the CD ROM bound into this book, or as a free download
from ).
Using URLs from within an applet
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:
Chapter 15: Distributed Computing
923
img
getAppletContext().showDocument(u);
in which u is the URL object. Here's a simple example that redirects you
to another Web page. Although you're just redirected to an HTML page,
you could also redirect to the output of a CGI program.
//: 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 {
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 a CGI program instead of
// an HTML page.
URL u = new URL(getDocumentBase(),
"FetcherFrame.html");
// Display the output of the URL using
// the Web browser, as an ordinary page:
getAppletContext().showDocument(u);
} catch(Exception e) {
l.setText(e.toString());
}
}
}
924
Thinking in Java
img
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.
Reading a file from the server
A variation on the above program reads a file located on the server. In this
case, 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");
Chapter 15: Distributed Computing
925
img
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);
}
} ///:~
The creation of the URL object is similar to the previous example--
getDocumentBase( ) is the starting point as before, but this time the
name of the file is read from the JTextField. Once the URL object is
created, its String version is placed in the JTextArea so we can see what
it looks like. Then an InputStream is procured from the URL, which in
this case will simply produce a stream of the characters in the file. After
converting to a Reader and buffering, each line is read and appended to
the JTextArea. Note that the JTextArea has been placed inside a
JScrollPane so that scrolling is handled automatically.
More to networking
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).
926
Thinking in Java
img
Java Database
Connectivity (JDBC)
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. 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 you must usually
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 calls it 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. I have tried to
write these examples to be as portable as possible. You should also notice
that the database-specific code has been isolated in order to centralize any
changes that you may need to perform to get the examples operational in
your environment.
Chapter 15: Distributed Computing
927
img
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( ).
To open a database, you must create a "database URL" that specifies:
1.
That you're using JDBC with "jdbc."
2.
The "subprotocol": the name of the driver or the name of a
database connectivity mechanism. Since the design of JDBC was
inspired by ODBC, the first subprotocol available is the "jdbc-odbc
bridge," specified by "odbc."
3.
The database identifier. This varies with the database driver used,
but it generally provides a logical name that is mapped by the
database administration software to a physical directory where the
database tables are located. For your database identifier to have
any meaning, you must register the name using your database
administration software. (The process of registration varies from
platform to platform.)
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 contain the
connection information identifying the remote machine and can become a
bit intimidating. Here is an example of a CloudScape database being
called from a remote client utilizing RMI:
jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db
928
Thinking in Java
img
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, call the static method
DriverManager.getConnection( ) and pass 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)
throws SQLException, ClassNotFoundException {
String dbUrl = "jdbc:odbc:people";
String user = "";
String password = "";
// 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 " +
Chapter 15: Distributed Computing
929
img
"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
}
} ///:~
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 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
930
Thinking in Java
img
native format and can do whatever you want with it using ordinary Java
code.
Getting the example to work
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.
Step 1: Find the JDBC Driver
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 the JDK) 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.
Chapter 15: Distributed Computing
931
img
Step 2: Configure the database
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 "System 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 my "people" database) is usually called a flat-file database. Most
problems that go beyond the simple storage and retrieval of data generally
932
Thinking in Java
img
require multiple tables that must be related by joins to produce the
desired results, and these are called relational databases.
Step 3: Test the configuration
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.
Step 4: Generate your SQL query
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:
1.
Start a new query and use the Query Wizard. Select the "people"
database. (This is the equivalent of opening the database
connection using the appropriate database URL.)
Chapter 15: Distributed Computing
933
img
2.
Select the "people" table within the database. From within the
table, choose the columns FIRST, LAST, and EMAIL.
3.
Under "Filter Data," choose LAST and select "equals" with an
argument of "Eckel." Click the "And" radio button.
4.
Choose EMAIL and select "Is not Null."
5.
Under "Sort By," choose FIRST.
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
Especially with more complicated queries it's easy to get things wrong, but
by using 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.
Step 5: Modify and paste in your query
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)
934
Thinking in Java
img
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.
A GUI version of the lookup
program
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.*;
Chapter 15: Distributed Computing
935
img
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.toString());
}
}
class SearchL implements DocumentListener {
public void changedUpdate(DocumentEvent e){}
public void insertUpdate(DocumentEvent e){
textValueChanged();
}
public void removeUpdate(DocumentEvent e){
textValueChanged();
}
}
936
Thinking in Java
img
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.toString());
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.toString());
}
Chapter 15: Distributed Computing
937
img
}
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
DocumentListener is added to listen to the JTextField (see the
javax.swing.JTextField entry in the Java HTML documentation from
java.sun.com for details), 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 JLabel, 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.
Why the JDBC API
seems so complex
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
938
Thinking in Java
img
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 quite as 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.
A more sophisticated example
A more interesting example2 involves a multitable database that resides
on a server. Here, the database is meant to provide a repository for
community activities and to allow people to sign up for these events, so it
is called the Community Interests Database (CID). This example will only
provide an overview of the database and its implementation, and is not
intended to be an in-depth tutorial on database development. There are
numerous books, seminars, and software packages that will help you in
the design and development of a database.
In addition, this example presumes the prior installation of an SQL
database on a server (although it could also be run on a local machine),
and the interrogation and discovery of an appropriate JDBC driver for
that database. Several free SQL databases are available, and some are
even automatically installed with various flavors of Linux. You are
responsible for making the choice of database and locating the JDBC
driver; the example here is based on an SQL database system called
"Cloudscape."
To keep changes in the connection information simple, the database
driver, database URL, user name, and password are placed in a separate
class:
//: c15:jdbc:CIDConnect.java
// Database connection information for
// the community interests database (CID).
2 Created by Dave Bartlett.
Chapter 15: Distributed Computing
939
img
public class CIDConnect {
// All the information specific to CloudScape:
public static String dbDriver =
"COM.cloudscape.core.JDBCDriver";
public static String dbURL =
"jdbc:cloudscape:d:/docs/_work/JSapienDB";
public static String user = "";
public static String password = "";
} ///:~
In this example, there is no password protection on the database so the
user name and password are empty strings.
The database consists of a set of tables that have a structure as shown
here:
EVTMEMS
EVENTS
MEM_ID
EVT_ID
EVT_ID
TITLE (IE)
MEM_ORD
TYPE
LOC_ID
PRICE
DATETIME
MEMBERS
MEM_ID
LOCATIONS
MEM_UNAME (AK)
LOC_ID
MEM_LNAME (IE)
NAME (IE)
MEM_FNAME (IE)
CONTACT
ADDRESS
ADDRESS
CITY
CITY
STATE
STATE
ZIP
ZIP
PHONE
PHONE
EMAIL
DIRECTIONS
"Members" contains community member information, "Events" and
"Locations" contain information about the activities and where they take
place, and "Evtmems" connects events and members that would like to
attend that event. You can see that a data member in one table produces a
key in another table.
940
Thinking in Java
img
The following class contains the SQL strings that will create these
database tables (refer to an SQL guide for an explanation of the SQL
code):
//: c15:jdbc:CIDSQL.java
// SQL strings to create the tables for the CID.
public class CIDSQL {
public static String[] sql = {
// Create the MEMBERS table:
"drop table MEMBERS",
"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))",
"create unique index " +
"LNAME_IDX on MEMBERS(MEM_LNAME)",
// Create the EVENTS table
"drop table EVENTS",
"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)",
"create unique index " +
"TITLE_IDX on EVENTS(EVT_TITLE)",
// Create the EVTMEMS table
"drop table EVTMEMS",
"create table EVTMEMS " +
"(MEM_ID INTEGER not null, " +
"EVT_ID INTEGER not null, " +
"MEM_ORD INTEGER)",
Chapter 15: Distributed Computing
941
img
"create unique index " +
"EVTMEM_IDX on EVTMEMS(MEM_ID, EVT_ID)",
// Create the LOCATIONS table
"drop table LOCATIONS",
"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))",
"create unique index " +
"NAME_IDX on LOCATIONS(LOC_NAME)",
};
} ///:~
The following program uses the CIDConnect and CIDSQL information
to load the JDBC driver, make a connection to the database, and then
create the table structure diagrammed above. To connect with 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 use to query and manipulate the
database. Once the connection is made you can simply push the SQL to
the database, in this case by marching through the CIDSQL array.
However, the first time this program is run, the "drop table" command
will fail, causing an exception, which is caught, reported, and then
ignored. The reason for the "drop table" command is to allow easy
experimentation: you can modify the SQL that defines the tables and then
rerun the program, causing the old tables to be replaced by the new.
In this example, it makes sense to let the exceptions be thrown out to the
console:
//: c15:jdbc:CIDCreateTables.java
// Creates database tables for the
// community interests database.
import java.sql.*;
942
Thinking in Java
img
public class CIDCreateTables {
public static void main(String[] args)
throws SQLException, ClassNotFoundException,
IllegalAccessException {
// Load the driver (registers itself)
Class.forName(CIDConnect.dbDriver);
Connection c = DriverManager.getConnection(
CIDConnect.dbURL, CIDConnect.user,
CIDConnect.password);
Statement s = c.createStatement();
for(int i = 0; i < CIDSQL.sql.length; i++) {
System.out.println(CIDSQL.sql[i]);
try {
s.executeUpdate(CIDSQL.sql[i]);
} catch(SQLException sqlEx) {
System.err.println(
"Probably a 'drop table' failed");
}
}
s.close();
c.close();
}
} ///:~
Note that all changes in the database can be controlled by changing
Strings in the CIDSQL table, without modifying CIDCreateTables.
executeUpdate( ) will usually 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.
To test the database, it is loaded with some sample data. This requires a
series of INSERTs followed by a SELECT to produce result set. To make
additions and changes to the test data easy, the test data is set up as a
two-dimensional array of Objects, and the executeInsert( ) method
can then use the information in one row of the table to create the
appropriate SQL command.
Chapter 15: Distributed Computing
943
img
//: c15:jdbc:LoadDB.java
// Loads and tests the database.
import java.sql.*;
class TestSet {
Object[][] data = {
{ "MEMBERS", new Integer(1),
"dbartlett", "Bartlett", "David",
"123 Mockingbird Lane",
"Gettysburg", "PA", "19312",
"123.456.7890",  "bart@you.net" },
{ "MEMBERS", new Integer(2),
"beckel", "Eckel", "Bruce",
"123 Over Rainbow Lane",
"Crested Butte", "CO", "81224",
"123.456.7890", "beckel@you.net" },
{ "MEMBERS", new Integer(3),
"rcastaneda", "Castaneda", "Robert",
"123 Downunder Lane",
"Sydney", "NSW", "12345",
"123.456.7890", "rcastaneda@you.net" },
{ "LOCATIONS", new Integer(1),
"Center for Arts",
"Betty Wright", "123 Elk Ave.",
"Crested Butte", "CO", "81224",
"123.456.7890",
"Go this way then that." },
{ "LOCATIONS", new Integer(2),
"Witts End Conference Center",
"John Wittig", "123 Music Drive",
"Zoneville", "PA", "19123",
"123.456.7890",
"Go that way then this." },
{ "EVENTS", new Integer(1),
"Project Management Myths",
"Software Development",
new Integer(1), new Float(2.50),
"2000-07-17 19:30:00" },
{ "EVENTS", new Integer(2),
"Life of the Crested Dog",
"Archeology",
944
Thinking in Java
img
new Integer(2), new Float(0.00),
"2000-07-19 19:00:00" },
// Match some people with events
{  "EVTMEMS",
new Integer(1),  // Dave is going to
new Integer(1),  // the Software event.
new Integer(0) },
{ "EVTMEMS",
new Integer(2),  // Bruce is going to
new Integer(2),  // the Archeology event.
new Integer(0) },
{ "EVTMEMS",
new Integer(3),  // Robert is going to
new Integer(1),  // the Software event.
new Integer(1) },
{ "EVTMEMS",
new Integer(3), // ... and
new Integer(2), // the Archeology event.
new Integer(1) },
};
// Use the default data set:
public TestSet() {}
// Use a different data set:
public TestSet(Object[][] dat) { data = dat; }
}
public class LoadDB {
Statement statement;
Connection connection;
TestSet tset;
public LoadDB(TestSet t) throws SQLException {
tset = t;
try {
// Load the driver (registers itself)
Class.forName(CIDConnect.dbDriver);
} catch(java.lang.ClassNotFoundException e) {
e.printStackTrace(System.err);
}
connection = DriverManager.getConnection(
CIDConnect.dbURL, CIDConnect.user,
CIDConnect.password);
Chapter 15: Distributed Computing
945
img
statement = connection.createStatement();
}
public void cleanup() throws SQLException {
statement.close();
connection.close();
}
public void executeInsert(Object[] data) {
String sql = "insert into "
+ data[0] + " values(";
for(int i = 1; i < data.length; i++) {
if(data[i] instanceof String)
sql += "'" + data[i] + "'";
else
sql += data[i];
if(i < data.length - 1)
sql += ", ";
}
sql += ')';
System.out.println(sql);
try {
statement.executeUpdate(sql);
} catch(SQLException sqlEx) {
System.err.println("Insert failed.");
while (sqlEx != null) {
System.err.println(sqlEx.toString());
sqlEx = sqlEx.getNextException();
}
}
}
public void load() {
for(int i = 0; i< tset.data.length; i++)
executeInsert(tset.data[i]);
}
// Throw exceptions out to console:
public static void main(String[] args)
throws SQLException {
LoadDB db = new LoadDB(new TestSet());
db.load();
try {
// Get a ResultSet from the loaded database:
ResultSet rs = db.statement.executeQuery(
946
Thinking in Java
img
"select " +
"e.EVT_TITLE, m.MEM_LNAME, m.MEM_FNAME "+
"from EVENTS e, MEMBERS m, EVTMEMS em " +
"where em.EVT_ID = 2 " +
"and e.EVT_ID = em.EVT_ID " +
"and m.MEM_ID = em.MEM_ID");
while (rs.next())
System.out.println(
rs.getString(1) + "  " +
rs.getString(2) + ", " +
rs.getString(3));
} finally {
db.cleanup();
}
}
} ///:~
The TestSet class contains a default set of data that is produced if you
use the default constructor; however, you can also create a TestSet object
using an alternate data set with the second constructor. The set of data is
held in a two-dimensional array of Object because it can be any type,
including String or numerical types. The executeInsert( ) method uses
RTTI to distinguish between String data (which must be quoted) and
non-String data as it builds the SQL command from the data. After
printing this command to the console, executeUpdate( ) is used to send
it to the database.
The constructor for LoadDB makes the connection, and load( ) steps
through the data and calls executeInsert( ) for each record. cleanup( )
closes the statement and the connection; to guarantee that this is called, it
is placed inside a finally clause.
Once the database is loaded, an executeQuery( ) statement produces a
sample result set. Since the query combines several tables, it is an
example of a join.
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 appear
regularly.
Chapter 15: Distributed Computing
947
img
Servlets
Client access from the Internet or corporate intranets is a sure way to
allow many users to access data and resources easily3. This type of access
is based on clients using the World Wide Web standards of Hypertext
Markup Language (HTML) and Hypertext Transfer Protocol (HTTP). The
Servlet API set abstracts a common solution framework for responding to
HTTP requests.
Traditionally, the way to handle a problem such as allowing an Internet
client to update a database is to create an HTML page with text fields and
a "submit" button. The user types the appropriate information into the
text fields and presses the "submit" button. The data is submitted along
with a URL that tells the server what to do with the data by specifying the
location of a Common Gateway Interface (CGI) program that the server
runs, providing the program with the data as it is invoked. The CGI
program is typically written in Perl, Python, C, C++, or any language that
can read from standard input and write to standard output. That's all that
is provided by the Web server: the CGI program is invoked, and standard
streams (or, optionally for input, an environment variable) are used for
input and output. The CGI program is responsible for everything else.
First it looks at the data and decides whether the format is correct. If not,
the CGI program must produce HTML to describe the problem; this page
is handed to the Web server (via standard output from the CGI program),
which sends it back to the user. The user must usually back up a page and
try again. If the data is correct, the CGI program processes the data in an
appropriate way, perhaps adding it to a database. It must then produce an
appropriate HTML page for the Web server to return to the user.
It would be ideal to go to a completely Java-based solution to this
problem--an applet on the client side to validate and send the data, and a
servlet on the server side to receive and process the data. Unfortunately,
although applets are a proven technology with plenty of support, they
have been problematic to use on the Web because you cannot rely on a
3 Dave Bartlett was instrumental in the development of this material, and also the JSP
section.
948
Thinking in Java
img
particular version of Java being available on a client's Web browser; in
fact, you can't rely on a Web browser supporting Java at all! In an
intranet, you can require that certain support be available, which allows a
lot more flexibility in what you can do, but on the Web the safest approach
is to handle all the processing on the server side and deliver plain HTML
to the client. That way, no client will be denied the use of your site
because they do not have the proper software installed.
Because servlets provide an excellent solution for server-side
programming support, they are one of the most popular reasons for
moving to Java. Not only do they provide a framework that replaces CGI
programming (and eliminates a number of thorny CGI problems), but all
your code has the platform portability gained from using Java, and you
have access to all the Java APIs (except, of course, the ones that produce
GUIs, like Swing).
The basic servlet
The architecture of the servlet API is that of a classic service provider with
a service( ) method through which all client requests will be sent by the
servlet container software, and life cycle methods init( ) and destroy( ),
which are called only when the servlet is loaded and unloaded (this
happens rarely).
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( )'s sole purpose is to return a ServletConfig object
that contains initialization and startup parameters for this servlet.
getServletInfo( ) returns a string containing information about the
servlet, such as author, version, and copyright.
Chapter 15: Distributed Computing
949
img
The GenericServlet class is a shell implementation of this interface and
is typically not used. The HttpServlet class is an extension of
GenericServlet and is designed specifically to handle the HTTP
protocol-- HttpServlet is the one that you'll use most of the time.
The most convenient attribute of the servlet API is the auxiliary objects
that come along with the HttpServlet class to support it. If you look at the
service( ) method in the Servlet interface, you'll see it has two
parameters: ServletRequest and ServletResponse. With the
HttpServlet class these two object are extended for HTTP:
HttpServletRequest and HttpServletResponse. Here's a simple
example that shows the use of HttpServletResponse:
//: 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 {
res.setContentType("text/html");
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. The servlet is
initialized only once by calling its init( ) method, on loading the servlet
after the servlet container is first booted up. When a client makes a
request to a URL that happens to represent a servlet, the servlet container
intercepts this request and makes a call to the service( ) method, after
setting up the HttpServletRequest and HttpServletResponse
objects.
950
Thinking in Java
img
The main responsibility of the service( ) method is to interact with the
HTTP request that the client has sent, and to build an HTTP response
based on the attributes contained within the request. ServletsRule only
manipulates the response object without looking at what the client may
have sent.
After setting the content type of the response (which must always be done
before the Writer or OutputStream is procured), the getWriter( )
method of the response object produces a PrintWriter object, which is
used for writing character-based response data (alternatively,
getOutputStream( ) produces an OutputStream, used for binary
response, which is only utilized in more specialized solutions).
The rest of the program simply sends HTML back to the client (it's
assumed you understand HTML, so that part is not explained) as a
sequence of Strings. However, notice the inclusion of the "hit counter"
represented by the variable i. This is automatically converted to a String
in the print( ) statement.
When you run the program, you'll notice that the value of i is retained
between requests to the servlet. This is an essential property of servlets:
since only one servlet of a particular class is loaded into the container, and
it is never unloaded (unless the servlet container is terminated, which is
something that only normally happens if you reboot the server computer),
any fields of that servlet class effectively become persistent objects! This
means that you can effortlessly maintain values between servlet requests,
whereas with CGI you had to write values to disk in order to preserve
them, which required a fair amount of fooling around to get it right, and
resulted in a non-cross-platform solution.
Of course, sometimes the Web server, and thus the servlet container,
must be rebooted as part of maintenance or during a power failure. To
avoid losing any persistent information, the servlet's init( ) and
destroy( ) methods are automatically called whenever the servlet is
loaded or unloaded, giving you the opportunity to save data during
shutdown, and restore it after rebooting. The servlet container calls the
destroy( ) method as it is terminating itself, so you always get an
opportunity to save valuable data as long as the server machine is
configured in an intelligent way.
Chapter 15: Distributed Computing
951
img
There's one other issue when using HttpServlet. This class provides
doGet( ) and doPost( ) methods that differentiate between a CGI "GET"
submission from the client, and a CGI "POST." GET and POST vary only
in the details of the way that they submit the data, which is something
that I personally would prefer to ignore. However, most published
information that I've seen seems to favor the creation of separate
doGet( ) and doPost( ) methods instead of a single generic service( )
method, which handles both cases. This favoritism seems quite common,
but I've never seen it explained in a fashion that leads me to believe that
it's anything more than inertia from CGI programmers who are used to
paying attention to whether a GET or POST is being used. So in the spirit
of "doing the simplest thing that could possibly work,"4 I will just use the
service( ) method in these examples, and let it care about GETs vs.
POSTs. However, keep in mind that I might have missed something and
so there may in fact be a good reason to use doGet( ) and doPost( )
instead.
Whenever a form is submitted to a servlet, the HttpServletRequest
comes preloaded with all the form data, stored as key-value pairs. If you
know the names of the fields, you can just use them directly with the
getParameter( ) method to look up the values. You can also get an
Enumeration (the old form of the Iterator) to the field names, as is
shown in the following example. This example also demonstrates how a
single servlet can be used to produce the page that contains the form, and
to respond to the page (a better solution will be seen later, with JSPs). If
the Enumeration is empty, there are no fields; this means no form was
submitted. In this case, the form is produced, and the submit button will
re-call the same servlet. If fields do exist, however, they are displayed.
//: 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.*;
4 A primary tenet of Extreme Programming (XP). See www.xprogramming.com.
952
Thinking in Java
img
public class EchoForm extends HttpServlet {
public void service(HttpServletRequest req,
HttpServletResponse res) throws IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
Enumeration flds = req.getParameterNames();
if(!flds.hasMoreElements()) {
// No form submitted -- create one:
out.print("<html>");
out.print("<form method=\"POST\"" +
" action=\"EchoForm\">");
for(int i = 0; i < 10; i++)
out.print("<b>Field" + i + "</b> " +
"<input type=\"text\""+
" size=\"20\" name=\"Field" + i +
"\" value=\"Value" + i + "\"><br>");
out.print("<INPUT TYPE=submit name=submit"+
" Value=\"Submit\"></form></html>");
} else {
out.print("<h1>Your form contained:</h1>");
while(flds.hasMoreElements()) {
String field= (String)flds.nextElement();
String value= req.getParameter(field);
out.print(field + " = " + value+ "<br>");
}
}
out.close();
}
} ///:~
One drawback you'll notice here is that Java does not seem to be designed
with string processing in mind--the formatting of the return page is
painful because of line breaks, escaping quote marks, and the "+" signs
necessary to build String objects. With a larger HTML page it becomes
unreasonable to code it directly into Java. One solution is to keep the page
as a separate text file, then open it and hand it to the Web server. If you
have to perform any kind of substitution to the contents of the page, it's
not much better since Java has treated string processing so poorly. In
these cases you're probably better off using a more appropriate solution
(Python would be my choice; there's a version that embeds itself in Java
called JPython) to generate the response page.
Chapter 15: Distributed Computing
953
img
Servlets and multithreading
The servlet container 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( ) at the same time. Therefore
the service( ) method must written in a thread-safe manner. Any access
to common resources (files, databases) will need to be guarded by using
the synchronized keyword.
The following simple example puts a synchronized clause around the
thread's sleep( ) method. This will block all other threads until the
allotted time (five seconds) is all used up. When testing this you should
start several browser instances and hit this servlet as quickly as possible
in each one--you'll see that each one has to wait until its turn comes up.
//: 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) {
System.err.println("Interrupted");
}
}
out.print("<h1>Finished " + i++ + "</h1>");
out.close();
}
} ///:~
It is also possible to synchronize the entire servlet by putting the
synchronized keyword in front of the service( ) method. In fact, the
only reason to use the synchronized clause instead is if the critical
954
Thinking in Java
img
section is in an execution path that might not get executed. In that case,
you might as well avoid the overhead of synchronizing every time by using
a synchronized clause. Otherwise, all the threads will have to wait
anyway so you might as well synchronize the whole method.
Handling sessions with servlets
HTTP is a "sessionless" protocol, so you cannot tell from one server hit to
another if you've got the same person repeatedly querying your site, or if
it is a completely different person. A great deal of effort has gone into
mechanisms that will allow Web developers to track sessions. Companies
could not do e-commerce without keeping track of a client and the items
they have put into their shopping cart, for example.
There are several methods of session tracking, but the most common
method is with persistent "cookies," which are an integral part of the
Internet standards. The HTTP Working Group of the Internet
Engineering Task Force has written cookies into the official standard in
RFC 2109 (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 on the local disk, and
whenever another call is made to the URL that the cookie is associated
with, the cookie is quietly sent along with the call, thus providing the
desired information back to that server (generally, providing some way
that the server can be told that it's you calling). Clients can, however, turn
off the browser's ability to accept cookies. If your site must track a client
who has turned off cookies, then another method of session tracking
(URL rewriting or hidden form fields) must be incorporated by hand,
since the session tracking capabilities built into the servlet API are
designed around cookies.
The Cookie class
The servlet API (version 2.0 and up) provides the 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 adding it to the
response object. The constructor takes a cookie name as the first
Chapter 15: Distributed Computing
955
img
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 recovered by calling the getCookies( ) method of the
HttpServletRequest object, which returns an array of cookie objects.
Cookie[] cookies = req.getCookies();
You can then call getValue( ) for each cookie, to produce a String
containing the cookie contents. In the above example,
getValue("TIJava") will produce a String containing "2000."
The Session class
A session is one or more page requests by a client to a Web site during a
defined period of time. If you buy groceries online, for example, you want
a session to be confined to the period from when you first add an item to
"my shopping cart" to the point where you check out. Each item you add
to the shopping cart will result in a new HTTP connection, which has no
knowledge of previous connections or items in the shopping cart. To
compensate for this lack of information, the mechanics supplied by the
cookie specification allow your servlet to perform session tracking.
A servlet Session object lives on the server side of the communication
channel; its goal is to capture useful data about this client 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 data such as authentication information that was entered when
the client first entered your Web site, and which should not have to be
reentered during a particular set of transactions.
The Session class of the servlet API uses the Cookie class to do its work.
However, all the Session object needs is some kind of unique identifier
stored on the client and passed to the server. 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
(that is, you must write them by hand to deal with the situation when the
client has disabled cookies).
956
Thinking in Java
img
Here's an example that implements session tracking with the servlet API:
//: c15:servlets:SessionPeek.java
// Using the HttpSession class.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SessionPeek extends HttpServlet {
public void service(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException {
// Retrieve Session Object before any
// output is sent to the client.
HttpSession session = req.getSession();
res.setContentType("text/html");
PrintWriter out = res.getWriter();
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.getAttribute("sesspeek.cntr");
if(ival==null)
ival = new Integer(1);
else
ival = new Integer(ival.intValue() + 1);
session.setAttribute("sesspeek.cntr", ival);
out.println("You have hit this page <b>"
+ ival + "</b> times.<p>");
out.println("<h2>");
out.println("Saved Session Data </h2>");
// Loop through all data in the session:
Enumeration sesNames =
session.getAttributeNames();
while(sesNames.hasMoreElements()) {
String name =
sesNames.nextElement().toString();
Object value = session.getAttribute(name);
out.println(name + " = " + value + "<br>");
Chapter 15: Distributed Computing
957
img
}
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";
}
} ///:~
Inside the service( ) method, getSession( ) is called for the request
object, which returns the Session object associated with this request. The
Session object does not travel across the network, but instead it lives on
the server and is associated with a client and its requests.
958
Thinking in Java
img
getSession( ) 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 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 the client
from previous visits. If the Session object is new then the program will
start to gather information about this client's activities on this visit.
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 name is a String, and the value can be any object
derived from java.lang.Object. SessionPeek keeps track of how many
times the client has been back during this session. This is done with an
Integer object named sesspeek.cntr. If the name is not found an
Integer is created with value of one, otherwise an Integer is created
with the incremented value of the previously held Integer. The new
Integer is placed into the Session object. If you use same key in a
setAttribute( ) call, then the new object overwrites the old one. The
incremented counter is used to display the number of times that the client
has visited during this session.
getAttributeNames( ) is related to getAttribute( ) and
setAttribute( ); it returns an enumeration of the names of the objects
that are bound to the Session object. A while loop in SessionPeek
shows this method in action.
You may wonder how long a Session object hangs around. The answer
depends on the servlet container you are using; they usually default to 30
minutes (1800 seconds), which is what you should see from the
ServletPeek call to getMaxInactiveInterval( ). Tests seem to
produce mixed results between servlet containers. Sometimes the
Session object can hang around overnight, but I have never seen a case
where the Session object disappears in less than the time specified by the
Chapter 15: Distributed Computing
959
img
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 container.
Running the servlet examples
If you are not already working with an application server that handles
Sun's servlet and JSP technologies for you, you may download the Tomcat
implementation of Java servlets and JSPs, which is a free, open-source
implementation of servlets, and is the official reference implementation
sanctioned by Sun. It can be found at jakarta.apache.org.
Follow the instructions for installing the Tomcat implementation, then
edit the server.xml file to point to the location in your directory tree
where your servlets will be placed. Once you start up the Tomcat program
you can test your servlet programs.
This has only been a brief introduction to servlets; there are entire books
on the subject. However, this introduction should give you enough ideas
to get you started. In addition, many of the ideas in the next section are
backward compatible with servlets.
Java Server Pages
Java Server Pages (JSP) is a standard Java extension that is defined on
top of the servlet Extensions. The goal of JSPs is the simplified creation
and management of dynamic Web pages.
The previously mentioned, freely available Tomcat reference
implementation from jakarta.apache.org automatically supports JSPs.
JSPs allow you to combine the HTML of a Web page with pieces of Java
code in the same document. The Java code is surrounded by special tags
that tell the JSP container that it should use the code to generate a servlet,
or part of one. The benefit of JSPs is that you can maintain a single
document that represents both the page and the Java code that enables it.
The downside is that the maintainer of the JSP page must be skilled in
both HTML and Java (however, GUI builder environments for JSPs
should be forthcoming).
960
Thinking in Java
img
The first time a JSP is loaded by the JSP container (which is typically
associated with, or even part of, a Web server), the servlet code necessary
to fulfill the JSP tags is automatically generated, compiled, and loaded
into the servlet container. The static portions of the HTML page are
produced by sending static String objects to write( ). The dynamic
portions are included directly into the servlet.
From then on, as long as the JSP source for the page is not modified, it
behaves as if it were a static HTML page with associated servlets (all the
HTML code is actually generated by the servlet, however). If you modify
the source code for the JSP, it is automatically recompiled and reloaded
the next time that page is requested. Of course, because of all this
dynamism you'll see a slow response for the first-time access to a JSP.
However, since a JSP is usually used much more than it is changed, you
will normally not be affected by this delay.
The structure of a JSP page is a cross between a servlet and an HTML
page. The JSP tags begin and end with angle brackets, just like HTML
tags, but the tags also include percent signs, so all JSP tags are denoted by
<% JSP code here %>
The leading percent sign may be followed by other characters that
determine the precise type of JSP code in the tag.
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 and placed on
the generated Web page:
//:! c15:jsp:ShowSeconds.jsp
<html><body>
<H1>The time in seconds is:
<%= System.currentTimeMillis()/1000 %></H1>
</body></html>
///:~
In the JSP examples in this book, the first and last lines are not included
in the actual code file that is extracted and placed in the book's source-
code tree.
Chapter 15: Distributed Computing
961
img
When the client creates a request for the JSP page, the Web server must
have been configured to relay the request to the JSP container, which then
invokes the page. As mentioned above, the first time the page is invoked,
the components specified by the page are generated and compiled by the
JSP container as one or more servlets. In the above example, the servlet
will contain code to configure the HttpServletResponse object,
produce a PrintWriter object (which is always named out), and then
turn the time calculation into a String which is sent to out. As you can
see, all this is accomplished with a very succinct statement, but the
average HTML programmer/Web designer will not have the skills to write
such code.
Implicit objects
Servlets include classes that provide convenient utilities, such as
HttpServletRequest, HttpServletResponse, Session, etc. Objects
of these classes are built into the JSP specification and automatically
available for use in your JSP without writing any extra lines of code. The
implicit objects in a JSP are detailed in the table below.
Implicit
Of Type (javax.servlet)
Description
Scope
variable
request
protocol dependent
The request that triggers request
subtype of
the service invocation.
HttpServletRequest
response
protocol dependent
The response to the
page
subtype of
request.
HttpServletResponse
pageContext
jsp.PageContext
The page context
page
encapsulates
implementation-
dependent features and
provides convenience
methods and namespace
access for this JSP.
session
Protocol dependent
The session object
session
subtype of
created for the
http.HttpSession
requesting client. See
servlet Session object.
application
ServletContext
The servlet context
app
obtained from the
962
Thinking in Java
img
servlet configuration
object (e.g.,
getServletConfig(),
getContext( ).
out
jsp.JspWriter
The object that writes
page
into the output stream.
config
ServletConfig
The ServletConfig for
page
this JSP.
page
java.lang.Object
The instance of this
page
page's implementation
class processing the
current request.
The scope of each object can vary significantly. For example, the session
object has a scope which exceeds that of a page, as it many span several
client requests and pages. The application object can provide services to
a group of JSP pages that together represent a Web application.
JSP directives
Directives are messages to the JSP container and are denoted by the "@":
<%@ directive {attr="value"}* %>
Directives do not send anything to the out stream, but they are important
in setting up your JSP page's attributes and dependencies with the JSP
container. For example, the line:
<%@ page language="java" %>
says that the scripting language being used within the JSP page is Java. In
fact, the JSP specification only describes the semantics of scripts for the
language attribute equal to "Java." The intent of this directive is to build
flexibility 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 scripting 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 communicates these attributes to the JSP
Chapter 15: Distributed Computing
963
img
container. These attributes include: language, extends, import,
session, buffer, autoFlush, isThreadSafe, info and errorPage. For
example:
<%@ page session="true" import="java.util.*" %>
This line first indicates that the page requires participation in an HTTP
session. Since we have not set the language directive the JSP container
defaults to using 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. If the
session variable is not specified, then it defaults to "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 ordinary
import expressions. This list is imported by the translated JSP page
implementation and is available to the scripting environment. Again, this
is currently only defined when the value of the language directive is
"java."
JSP scripting elements
Once the directives have been used to set up the scripting environment
you 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 syntax for each is:
<%! declaration %>
<%  scriptlet
%>
<%= expression  %>
White space is optional after "<%!", "<%", "<%=", and before "%>."
All these tags are based upon XML; you could even say that a JSP page
can be mapped to a XML document. The XML equivalent syntax for the
scripting elements above would be:
<jsp:declaration> declaration </jsp:declaration>
<jsp:scriptlet>
scriptlet
</jsp:scriptlet>
964
Thinking in Java
img
<jsp:expression>
expression
</jsp:expression>
In addition, there are two types of comments:
<%-- jsp comment --%>
<!-- html comment -->
The first form allows you to add comments to JSP source pages that will
not appear in any form in the HTML that is sent to the client. Of course,
the second form of comment is not specific to JSPs--it's just an ordinary
HTML comment. What's interesting is that you can insert JSP code inside
an HTML comment and the comment will be produced in the resulting
page, including the result from the JSP code.
Declarations are used to declare variables and methods in the scripting
language (currently Java only) used in a JSP page. The declaration must
be a complete Java statement and cannot produce any output in the out
stream. In the Hello.jsp example below, the declarations for the
variables loadTime, loadDate and hitCount are all complete Java
statements that declare and initialize new variables.
//:! 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>
Chapter 15: Distributed Computing
965
img
<H3>Page has been accessed <%= ++hitCount %>
times since <%= loadDate %></H3>
<%-- A "scriptlet" that writes to the server
console and to the client page.
Note that the ';' is required: --%>
<%
System.out.println("Goodbye");
out.println("Cheerio");
%>
</body></html>
///:~
When you run this program you'll see that the variables loadTime,
loadDate and hitCount hold their values between hits to the page, so
they are clearly fields and not local variables.
At the end of the example is a scriptlet that writes "Goodbye" to the Web
server console and "Cheerio" to the implicit JspWriter object out.
Scriptlets can contain any code fragments that are valid Java statements.
Scriptlets are executed at request-processing time. When all the scriptlet
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 code in the scriptlet. You should be aware
that scriptlets can produce side effects by modifying the objects that are
visible to them.
JSP expressions can found intermingled with the HTML in the middle
section of Hello.jsp. Expressions must be complete Java statements,
which are evaluated, coerced to a String, and sent to out. If the result of
the expression cannot be coerced to a String then a
ClassCastException is thrown.
Extracting fields and values
The following example is similar to one shown earlier in the servlet
section. The first time you hit the page it detects that you have no fields
and returns a page containing a form, using the same code as in the
servlet example, but in JSP format. When you submit the form with the
filled-in fields to the same JSP URL, it detects the fields and displays
966
Thinking in Java
img
them. This is a nice technique because it allows you to have both the page
containing the form for the user to fill out and the response code for that
page in a single file, thus making it easier to create and maintain.
//:! 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 flds = request.getParameterNames();
if(!flds.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>
<%} else {
while(flds.hasMoreElements()) {
String field = (String)flds.nextElement();
String value = request.getParameter(field);
%>
<li><%= field %> = <%= value %></li>
<%  }
} %>
</H3></body></html>
///:~
The most interesting feature of this example is that it demonstrates how
scriptlet code can be intermixed with HTML code, even to the point of
generating HTML within a Java for loop. This is especially convenient for
building any kind of form where repetitive HTML code would otherwise
be required.
Chapter 15: Distributed Computing
967
img
JSP page attributes and scope
By poking around in the HTML documentation for servlets and JSPs, you
will find features that report information about the servlet or JSP that is
currently running. The following example displays a few of these pieces of
data.
//:! 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>
Servlet Name: <%= config.getServletName() %><br>
Servlet container supports servlet version:
<% out.print(application.getMajorVersion() + "."
+ application.getMinorVersion()); %><br>
<%
session.setAttribute("My dog", "Ralph");
for(int scope = 1; scope <= 4; scope++) {  %>
<H3>Scope: <%= scope %> </H3>
<%  Enumeration e =
pageContext.getAttributeNamesInScope(scope);
while(e.hasMoreElements()) {
out.println("\t<li>" +
e.nextElement() + "</li>");
}
}
%>
</body></html>
///:~
This example also shows the use of both embedded HTML and writing to
out in order to output to the resulting HTML page.
The first piece of information produced is the name of the servlet, which
will probably just be "JSP" but it depends on your implementation. You
can also discover the current version of the servlet container by using the
application object. Finally, after setting a session attribute, the "attribute
names" in a particular scope are displayed. You don't use the scopes very
much in most JSP programming; they were just shown here to add
968
Thinking in Java
img
interest to the example. There are four attribute scopes, as follows: The
page scope (scope 1), the request scope (scope 2), the session scope (scope
3--here, the only element available in session scope is "My dog," added
right before the for loop), and the application scope (scope 4), 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. This is generally set up using a
configuration file.) At the application scope you will see objects that
represent paths for the working directory and temporary directory.
Manipulating sessions in JSP
Sessions were introduced in the prior section on servlets, and are also
available within JSPs. The following example exercises the session object
and allows you to manipulate the amount of time before the session
becomes invalid.
//:! c15:jsp:SessionObject.jsp
<%--Getting and setting session object values--%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<H3><li>This session was created at
<%= session.getCreationTime() %></li></H1>
<H3><li>Old MaxInactiveInterval =
<%= session.getMaxInactiveInterval() %></li>
<% session.setMaxInactiveInterval(5); %>
<li>New MaxInactiveInterval=
<%= session.getMaxInactiveInterval() %></li>
</H3>
<H2>If the session object "My dog" is
still around, this value will be non-null:<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 --%>
Chapter 15: Distributed Computing
969
img
<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>
///:~
The session object is provided by default so it is available without any
extra coding. The calls to getID( ), getCreationTime( ) and
getMaxInactiveInterval( ) are used to display information about this
session object.
When you first bring up this session you will see a MaxInactiveInterval
of, for example, 1800 seconds (30 minutes). This will depend on the way
your JSP/servlet container is configured. The MaxInactiveInterval is
shortened to 5 seconds to make things interesting. If you refresh the page
before the 5 second interval expires, then you'll see:
Session value for "My dog" = Ralph
But if you wait longer than that, "Ralph" will become null.
To see how the session information can be carried through to other pages,
and also to see the effect of invalidating a session object versus just letting
it expire, two other JSPs are created. The first one (reached by pressing
the "invalidate" button in SessionObject.jsp) reads the session
information and then explicitly invalidates that session:
//:! 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>
///:~
To experiment with this, refresh SessionObject.jsp, then immediately
click the "invalidate" button to bring you to SessionObject2.jsp. At this
970
Thinking in Java
img
point you will still see "Ralph," and right away (before the 5-second
interval has expired), refresh SessionObject2.jsp to see that the session
has been forcefully invalidated and "Ralph" has disappeared.
If you go back to SessionObject.jsp, refresh the page so you have a new
5-second interval, then press the "Keep Around" button, it will take you to
the following page, SessionObject3.jsp, which does NOT invalidate the
session:
//:! 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>
///:~
Because this page doesn't invalidate the session, "Ralph" will hang around
as long as you keep refreshing the page before the 5 second time interval
expires. This is not unlike a "Tomagotchi" pet--as long as you play with
"Ralph" he will stick around, otherwise he expires.
Creating and modifying cookies
Cookies were introduced in the prior section on servlets. Once again, the
brevity of JSPs makes playing with cookies much simpler here than when
using servlets. The following example shows this by fetching the cookies
that come with the request, reading and modifying their maximum ages
(expiration dates) and attaching a new cookie to the outgoing response:
//:! c15:jsp:Cookies.jsp
<%--This program has different behaviors under
different browsers! --%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<%
Cookie[] cookies = request.getCookies();
Chapter 15: Distributed Computing
971
img
for(int i = 0; i < cookies.length; i++) { %>
Cookie name: <%= cookies[i].getName() %> <br>
value: <%= cookies[i].getValue() %><br>
Old max age in seconds:
<%= cookies[i].getMaxAge() %><br>
<% cookies[i].setMaxAge(5); %>
New max age in seconds:
<%= cookies[i].getMaxAge() %><br>
<% } %>
<%! int count = 0; int dcount = 0; %>
<% response.addCookie(new Cookie(
"Bob" + count++, "Dog" + dcount++)); %>
</body></html>
///:~
Since each browser stores cookies in its own way, you may see different
behaviors with different browsers (not reassuring, but it might be some
kind of bug that could be fixed by the time you read this). Also, you may
experience different results if you shut down the browser and restart it,
rather than just visiting a different page and then returning to
Cookies.jsp. Note that using session objects seems to be more robust
than directly using cookies.
After displaying the session identifier, each cookie in the array of cookies
that comes in with the request object is displayed, along with its
maximum age. The maximum age is changed and displayed again to verify
the new value, then a new cookie is added to the response. However, your
browser may seem to ignore the maximum age; it's worth playing with
this program and modifying the maximum age value to see the behavior
under different browsers.
JSP summary
This section has only been a brief coverage of JSPs, and yet even with
what was covered here (along with the Java you've learned in the rest of
the book, and your own knowledge of HTML) you can begin to write
sophisticated web pages via JSPs. The JSP syntax isn't meant to be
particularly deep or complicated, so if you understand what was
presented in this section you're ready to be productive with JSPs. You can
972
Thinking in Java
img
find further information in most current books on servlets, or at
java.sun.com.
It's especially nice to have JSPs available, even if your goal is only to
produce servlets. You'll discover that if you have a question about the
behavior of a servlet feature, it's much easier and faster to write a JSP test
program to answer that question than it is to write a servlet. Part of the
benefit comes from having to write less code and being able to mix the
display HTML in with the Java code, but the leverage becomes especially
obvious when you see that the JSP Container handles all the
recompilation and reloading of the JSP for you whenever the source is
changed.
As terrific as JSPs are, however, it's worth keeping in mind that JSP
creation requires a higher level of skill than just programming in Java or
just creating Web pages. In addition, debugging a broken JSP page is not
as easy as debugging a Java program, as (currently) the error messages
are more obscure. This should change as development systems improve,
but we may also see other technologies built on top of Java and the Web
that are better adapted to the skills of the web site designer.
RMI (Remote Method
Invocation)
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 that you can send a message to
the remote 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.
Remote interfaces
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
Chapter 15: Distributed Computing
973
img
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.
When you create a remote interface, you must follow these guidelines:
1.
The remote interface must be public (it cannot have "package
access," that is, it cannot be "friendly"). Otherwise, a client will get
an error when attempting to load a remote object that implements
the remote interface.
2.
The remote interface must extend the interface
java.rmi.Remote.
3.
Each method in the remote interface must declare
java.rmi.RemoteException in its throws clause in addition to
any application-specific exceptions.
4.
A remote object passed as an argument or return value (either
directly or embedded within a local object) must be declared as the
remote interface, not the implementation class.
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.
Implementing the remote interface
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 are available to the
974
Thinking in Java
img
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. Throw
// exceptions out to the console.
public static void main(String[] args)
throws Exception {
System.setSecurityManager(
new RMISecurityManager());
PerfectTime pt = new PerfectTime();
Naming.bind(
"//peppy:2005/PerfectTime", pt);
Chapter 15: Distributed Computing
975
img
System.out.println("Ready to do time");
}
} ///:~
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:
1.
Create and install a security manager that supports RMI. The only
one available for RMI as part of the Java distribution is
RMISecurityManager.
2.
Create one or more instances of a remote object. Here, you can see
the creation of the PerfectTime object.
3.
Register at least one of the remote objects with the RMI 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.
Setting up the registry
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, the command 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 is located at 2005, so the rmiregistry should be
started like this under 32-bit Windows:
start rmiregistry 2005
976
Thinking in Java
img
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:5
1.
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.
2.
RMI will not work unless your computer has an active 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
5 Many brain cells died in agony to discover this information.
Chapter 15: Distributed Computing
977
img
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.
Creating stubs and skeletons
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
978
Thinking in Java
img
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.
Using the remote object
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.*;
Chapter 15: Distributed Computing
979
img
public class DisplayPerfectTime {
public static void main(String[] args)
throws Exception {
System.setSecurityManager(
new RMISecurityManager());
PerfectTimeI t =
(PerfectTimeI)Naming.lookup(
"//peppy:2005/PerfectTime");
for(int i = 0; i < 10; i++)
System.out.println("Perfect time = " +
t.getPerfectTime());
}
} ///:~
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).
CORBA
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
980
Thinking in Java
img
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.
CORBA fundamentals
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.
Chapter 15: Distributed Computing
981
img
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 Interface Definition Language (IDL)
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 are 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
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
982
Thinking in Java
img
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.
An example
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).
Writing the IDL source
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
Chapter 15: Distributed Computing
983
img
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.
Creating stubs and skeletons
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.
Implementing the server and the client
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,
984
Thinking in Java
img
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 {
// Throw exceptions to console:
public static void main(String[] args)
throws Exception {
// 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 =
Chapter 15: Distributed Computing
985
Table of Contents:
  1. Introduction to Objects:The progress of abstraction, An object has an interface
  2. Everything is an Object:You manipulate objects with references, Your first Java program
  3. Controlling Program Flow:Using Java operators, Execution control, true and false
  4. Initialization & Cleanup:Method overloading, Member initialization
  5. Hiding the Implementation:the library unit, Java access specifiers, Interface and implementation
  6. Reusing Classes:Composition syntax, Combining composition and inheritance
  7. Polymorphism:Upcasting revisited, The twist, Designing with inheritance
  8. Interfaces & Inner Classes:Extending an interface with inheritance, Inner class identifiers
  9. Holding Your Objects:Container disadvantage, List functionality, Map functionality
  10. Error Handling with Exceptions:Basic exceptions, Catching an exception
  11. The Java I/O System:The File class, Compression, Object serialization, Tokenizing input
  12. Run-time Type Identification:The need for RTTI, A class method extractor
  13. Creating Windows & Applets:Applet restrictions, Running applets from the command line
  14. Multiple Threads:Responsive user interfaces, Sharing limited resources, Runnable revisited
  15. Distributed Computing:Network programming, Servlets, CORBA, Enterprise JavaBeans
  16. A: Passing & Returning Objects:Aliasing, Making local copies, Cloning objects
  17. B: The Java Native Interface (JNI):Calling a native method, the JNIEnv argument
  18. Java Programming Guidelines:Design, Implementation
  19. Resources:Software, Books, My own list of books
  20. Index