|
|||||
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
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
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
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
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
}
}
}
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
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
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
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
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
//:
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
//
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
(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
(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
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
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
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
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
}
///:~
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
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
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
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
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
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
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
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
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
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
!
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
"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
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
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
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
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
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
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
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
}
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
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
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
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
"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
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
//:
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
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
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
"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
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
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
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
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
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
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
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
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
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
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
}
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
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
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
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
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
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
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
<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
<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
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
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
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
<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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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:
|
|||||