Download Packaging an applet into a JAR file An important use of the JAR

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
Packaging an applet into a JAR file
An important use of the JAR utility is to optimize applet loading.
JAR files solve the problem by compressing all of your .class files into a
single file that is downloaded by the browser. Now you can create the right
design without worrying about how many .class files it will generate, and the
user will get a much faster download time.
Consider TicTacToe.java. It looks like a single class, but in fact it contains
five inner classes, so that’s six in all. Once you’ve compiled the program, you
package it into a JAR file with the line:
jar cf TicTacToe.jar *.class
This assumes that the only .class files in the current directory are the ones
from TicTacToe.java (otherwise, you’ll get extra baggage).
Now you can create an HTML page with the new archive tag to indicate the
name of the JAR file. Here is the basic applet tag:
<head><title>TicTacToe Example Applet
</title></head>
<body>
<applet code=TicTacToe.class
archive=TicTacToe.jar
width=200 height=100>
</applet>
</body>
You’ll need to run this file through the HTMLconverter application that
comes with the JDK in order to get it to work.
Signing applets
Because of the sandbox security model, unsigned applets are prevented from
performing certain tasks on the client, like writing to a file or connecting to a
local network. A signed applet verifies to the user that the person who
claims to have created the applet actually did, and that the contents of the
JAR file have not been tampered with since that file left the server.
Consider an applet that wants to have access to the client’s file system and
read and write some files.
//: c14:signedapplet:FileAccessApplet.java
// Demonstration of File dialog boxes.
package c14.signedapplet;
import
import
import
import
import
javax.swing.*;
java.awt.*;
java.awt.event.*;
java.io.*;
com.bruceeckel.swing.*;
public class FileAccessApplet extends JApplet
{
private JTextField filename = new JTextField(), dir = new JTextField();
private JButton open = new JButton("Open"), save = new JButton("Save");
1
private JEditorPane ep = new JEditorPane();
private JScrollPane jsp = new JScrollPane();
private File file;
public void init()
{
JPanel p = new JPanel();
open.addActionListener(new OpenL());
p.add(open);
save.addActionListener(new SaveL());
p.add(save);
Container cp = getContentPane();
jsp.getViewport().add(ep);
cp.add(jsp, BorderLayout.CENTER);
cp.add(p, BorderLayout.SOUTH);
dir.setEditable(false);
save.setEnabled(false);
ep.setContentType("text/html");
filename.setEditable(false);
p = new JPanel();
p.setLayout(new GridLayout(2, 1));
p.add(filename);
p.add(dir);
cp.add(p, BorderLayout.NORTH);
}
class OpenL implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
JFileChooser c = new JFileChooser();
c.setFileFilter(new TextFileFilter());
// Demonstrate "Open" dialog:
int rVal = c.showOpenDialog(FileAccessApplet.this);
if (rVal == JFileChooser.APPROVE_OPTION)
{
file = c.getSelectedFile();
filename.setText(file.getName());
dir.setText(c.getCurrentDirectory().toString());
try
{
System.out.println("Url is " + file.toURL());
ep.setPage(file.toURL());
// ep.repaint();
} catch (IOException ioe)
{
throw new RuntimeException(ioe);
}
}
if (rVal == JFileChooser.CANCEL_OPTION)
{
filename.setText("You pressed cancel");
dir.setText("");
} else
{
save.setEnabled(true);
2
}
}
}
class SaveL implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
JFileChooser c = new JFileChooser(file);
c.setSelectedFile(file);
// Demonstrate "Save" dialog:
int rVal = c.showSaveDialog(FileAccessApplet.this);
if (rVal == JFileChooser.APPROVE_OPTION)
{
filename.setText(c.getSelectedFile().getName());
dir.setText(c.getCurrentDirectory().toString());
try
{
FileWriter fw = new FileWriter(file);
ep.write(fw);
} catch (IOException ioe)
{
throw new RuntimeException(ioe);
}
}
if (rVal == JFileChooser.CANCEL_OPTION)
{
filename.setText("You pressed cancel");
dir.setText("");
}
}
}
public class TextFileFilter extends javax.swing.filechooser.FileFilter
{
public boolean accept(File f)
{
return f.getName().endsWith(".txt") || f.isDirectory();
}
public String getDescription()
{
return "Text Files (*.txt)";
}
}
public static void main(String[] args)
{
Console.run(new FileAccessApplet(), 500, 500);
}
} // /:~
3
It appears to be an ordinary applet. However, as it stands, it would not be
allowed to open and close files on a client’s system. To make this run as a
signed applet, you need to put it into a JAR file and sign the JAR file.
Once you have a JAR file, you will need a certificate or a key to sign it with. If
you were a large corporation, you would apply to a signing authority like
Verisign or Thawte, and they would issue you a certificate. This is used to sign
code and thus identify to a user that you are indeed the provider of the code
they are downloading, and that the code that has been deployed hasn’t been
modified since you signed it. Essentially, the digital signature is a load of bits,
and the signing authority vouches for you when someone downloads that
signature.
A certificate from a signing authority costs money and requires regular
renewal. In our case we can just make a little self-signed one. This needs to be
stored in a file somewhere (it is usually called the keychain). If you type:
keytool –list
then it will try to access the default file. If there is no file, then you need to
create one, or specify an existing one. You might try to search for a file called
“cacerts,” and then try
keytool -list -file <path/filename>
The default location is usually
{java.home}/lib/security/cacerts
where the java.home property points to the JRE home.
You can also easily make a self-signed certificate for testing purposes using
the keytool. If you have your Java “bin” directory in your executable path,
4
you can type:
keytool –genkey –alias <keyname> -keystore <url>
where keyname is the alias name that you want to give the key, say
“mykeyname,” and url is the location of the file that stores your keys, usually
the cacerts file as described above.
You will now be prompted for the password. Unless you have changed the
default, this will be “changeit” (a hint to do just that). Next you will be asked
for your name, the organizational unit, the organization, city, state, and
country. This information is stored in the certificate. Lastly, you will be asked
for a password for that key. If you are really security conscious, you can give it
a separate password, but the default password is the same as the keystore
itself, and is usually adequate. The above information can be specified on the
command line from within a build tool such as Ant.
If you invoke the keytool utility with no parameters at the command
prompt, it will give you a list of its numerous options. You might like to use
the –valid option, for example, which enables you to specify how many days
the key will be valid for.
To confirm that your key is now in the cacerts file, type:
keytool –list –keystore <url>
and enter the password as before. Your key may be hidden among the other
keys already in your certificate files.
Your new certificate is self-signed and thus not actually trusted by a signing
authority. If you use this certificate to sign a JAR file, the end user will get a
warning, and a strong recommendation not to use your software. You and
your users will have to tolerate this until you are prepared to pay for a trusted
certificate for commercial purposes.
To sign your JAR file, use the standard Java jarsigner tool as follows:
jarsigner –keystore <url> <jarfile> <keyname>
where url is the location of your cacerts file, jarfile is the name of your JAR
file, and keyname is the alias that you gave to your key. You will again be
prompted for the password.
You now have a JAR file that can be identified as being signed with your key,
and that can guarantee it has not been tampered with (i.e., no files have been
changed, added, or removed) since you signed it.
All you have to do now is make sure that the applet tag in your HTML file has
an “archive” element, which specifies the name of your JAR file.
The applet tag is somewhat more complicated for the plugin, but if you create
a simple tag like:
<APPLET
CODE=package.AppletSubclass.class
ARCHIVE = myjar.jar
WIDTH=300
HEIGHT=200>
</APPLET>
and run the HTMLConverter tool on it (this is packaged with the freely
downloadable JDK), it will create the correct applet tag for you.
Now, when your applet is downloaded by a client, they will be informed that a
signed applet is being loaded, and given the option of trusting the signer. As
previously mentioned, your test certificate doesn’t have a very high degree of
trust, and the user will get a warning to this effect. If they opt to trust your
5
applet, it will have full access to their system and behave as if it were an
ordinary application.
JNLP and Java Web Start
Signed applets are powerful and can effectively take the place of an
application, but they must run inside a Web browser. This requires the extra
overhead of the browser running on the client machine, and also means that
the user interface of the applet is limited and often visually confusing. The
Web browser has its own set of menus and toolbars, which will appear above
the applet.
The Java Network Launch Protocol (JNLP) solves the problem without
sacrificing the advantages of applets. With a JNLP application, you can
download and install a standalone Java application onto the client’s machine.
This can be run from the command prompt, a desktop icon, or the application
manager that is installed with your JNLP implementation. The application
can even be run from the Web site from which it was originally downloaded.
A JNLP application can dynamically download resources from the Internet at
run time, and the version can be automatically checked (if the user is
connected to the Internet) . This means that it has all of the advantages of an
applet together with the advantages of standalone applications.
Because JNLP describes a protocol, not an implementation, you will need an
implementation in order to use it. Java Web Start, or JAWS, is Sun’s freely available
official reference implementation. All you need to do is download
and install it, and if you are using it for development, make sure that the JAR
files are in your classpath. If you are deploying your JNLP application from a
Web server, you have to ensure that your server recognizes the MIME type
application/x-java-jnlp-file.
Creating a JNLP application is not difficult. You create a standard application
that is archived in a JAR file, and then you provide a launch file, which is a
simple XML file that gives the client all the information it needs to download
and install your application. If you choose not to sign your JAR file, then you
must make use of the services supplied by the JNLP API for each type of
resource you want access to on the users machine.
Here is a variation of the example using the JFileChooser dialog, but this
time using the JNLP services to open it, so that the class can be deployed as a
JNLP application in an unsigned JAR file.
//: c14:jnlp:JnlpFileChooser.java
// Opening files on a local machine with JNLP.
// {Depends: javaws.jar}
// From 'Thinking in Java, 3rd ed.' (c) Bruce Eckel 2002
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
package c14.jnlp;
import
import
import
import
import
javax.swing.*;
java.awt.*;
java.awt.event.*;
java.io.*;
javax.jnlp.*;
6
public class JnlpFileChooser extends JFrame
{
private JTextField filename = new JTextField();
private JButton open = new JButton("Open"), save = new JButton("Save");
private JEditorPane ep = new JEditorPane();
private JScrollPane jsp = new JScrollPane();
private FileContents fileContents;
public JnlpFileChooser()
{
JPanel p = new JPanel();
open.addActionListener(new OpenL());
p.add(open);
save.addActionListener(new SaveL());
p.add(save);
Container cp = getContentPane();
jsp.getViewport().add(ep);
cp.add(jsp, BorderLayout.CENTER);
cp.add(p, BorderLayout.SOUTH);
filename.setEditable(false);
p = new JPanel();
p.setLayout(new GridLayout(2, 1));
p.add(filename);
cp.add(p, BorderLayout.NORTH);
ep.setContentType("text");
save.setEnabled(false);
}
class OpenL implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
FileOpenService fs = null;
try
{
fs = (FileOpenService) ServiceManager
.lookup("javax.jnlp.FileOpenService");
} catch (UnavailableServiceException use)
{
throw new RuntimeException(use);
}
if (fs != null)
{
try
{
fileContents = fs.openFileDialog(".", new String[] { "txt",
"*" });
if (fileContents == null)
return;
filename.setText(fileContents.getName());
ep.read(fileContents.getInputStream(), null);
} catch (Exception exc)
{
throw new RuntimeException(exc);
}
7
save.setEnabled(true);
}
}
}
class SaveL implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
FileSaveService fs = null;
try
{
fs = (FileSaveService) ServiceManager
.lookup("javax.jnlp.FileSaveService");
} catch (UnavailableServiceException use)
{
throw new RuntimeException(use);
}
if (fs != null)
{
try
{
fileContents = fs.saveFileDialog(".",
new String[] { "txt" }, new ByteArrayInputStream(ep
.getText().getBytes()), fileContents
.getName());
if (fileContents == null)
return;
filename.setText(fileContents.getName());
} catch (Exception exc)
{
throw new RuntimeException(exc);
}
}
}
}
public static void main(String[] args)
{
JnlpFileChooser fc = new JnlpFileChooser();
fc.setSize(400, 300);
fc.setVisible(true);
}
} // /:~
Note that the FileOpenService and the FileCloseService classes are
imported from the javax.jnlp package and that nowhere in the code is the
JFileChooser dialog box referred to directly. The two services used here
must be requested using the ServiceManager.lookup( ) method, and the
resources on the client system can only be accessed via the objects returned
from this method. In this case, the files on the client’s file system are being
written to and read from using the FileContent interface, provided by the
JNLP. Any attempt to access the resources directly by using, say, a File or a
FileReader object would cause a SecurityException to be thrown in the
same way that it would if you tried to use them from an unsigned applet. If
you want to use these classes and not be restricted to the JNLP service
interfaces, you must sign the JAR file.
8
Now that we have a runnable class that makes use of the JNLP services, all
that is needed is for the class to be put into a JAR file and a launch file to be
written. Here is an appropriate launch file for the preceding example.
<?xml version="1.0" encoding="UTF-8"?>
<jnlp codebase="__codebase__" href="jnlp/filechooser.jnlp">
<information>
<title>FileChooser demo application</title>
<vendor>Mindview Inc.</vendor>
<description>Jnlp File choose Application</description>
<description kind="short">A demonstration of opening, reading and writing
a text file</description>
<icon href="images/tijicon.gif"/>
<offline-allowed/>
</information>
<resources>
<j2se version="1.3+"/>
<jar href="jnlp/jnlpfilechooser.jar" download="eager"/>
</resources>
<application-desc main-class="c14.jnlp.JnlpFileChooser"/>
</jnlp>
This launch file needs to be saved as a .jnlp file, in this case,
filechooser.jnlp, in the same directory as the JAR file.
As you can see, it is an XML file with one <jnlp> tag. This has a few
subelements, which are mostly self-explanatory.
The spec attribute of the jnlp element tells the client system what version of
the JNLP the application can be run with. The codebase attribute points to
the directory where this launch file and the resources can be found. Typically,
it would be an HTTP URL pointing to a Web server, but in this case it points
to a directory on the local machine, which is a good means of testing the
application. The href attribute must specify the name of this file.
The information tag has various subelements that provide information
about the application. These are used by the Java Web Start administrative
console or equivalent, which installs the JNLP application and allows the user
to run it from the command line, make short cuts and so on.
The resources tag serves a similar purpose as the applet tag in an HTML
file. The j2se subelement specifies the version of the j2se that is needed to
run the application, and the jar subelement specifies the JAR file in which
the class is archived. The jar element has an attribute download, which can
have the values “eager” or “lazy” that tell the JNLP implementation whether
or not the entire archive needs to be downloaded before the application can
be run.
The application-desc attribute tells the JNLP implementation which class
is the executable class, or entry point, to the JAR file.
Another useful subelement of the jnlp tag is the security tag, not shown
here. Here’s what a security tag looks like:
<security>
<all-permissions/>
<security/>
9
You use the security tag when your application is deployed in a signed JAR
file. It is not needed in the preceding example because the local resources are
all accessed via the JNLP services.
There are a few other tags available, the details of which can be found in the
specification http://java.sun.com/products/javawebstart/downloadspec.
html.
Now that the .jnlp is written, you will need to add a hypertext link to it in an
HTML page. This will be its download page. You might have a complex layout
with a detailed introduction to your application, but as long as you have
something like:
<a href="filechooser.jnlp">click here</a>
in your HTML file, then you will be able to initiate the installation of the
JNLP application by clicking on the link. Once you have downloaded the
application once, you will be able to configure it by using the administrative
console. If you are using Java Web Start on Windows, then you will be
prompted to make a short cut to your application the second time you use it.
This behavior is configurable.
Only two of the JNLP services are covered here, but there are seven services
in the current release. Each is designed for a specific task such as printing, or
cutting, and pasting to the clipboard. An in-depth discussion of these services
is beyond the scope of this course.
Programming techniques
Binding events dynamically
One of the benefits of the Swing event model is flexibility. You can add and
remove event behavior with single method calls. The following example
demonstrates this:
//: c14:DynamicEvents.java
// You can change event behavior dynamically.
// Also shows multiple actions for an event.
// <applet code=DynamicEvents
// width=250 height=400></applet>
// From 'Thinking in Java, 3rd ed.' (c) Bruce Eckel 2002
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
public class DynamicEvents extends JApplet
{
private java.util.List list = new ArrayList();
private int i = 0;
private JButton b1 = new JButton("Button1"), b2 = new JButton("Button2");
private JTextArea txt = new JTextArea();
class B implements ActionListener
{
10
public void actionPerformed(ActionEvent e)
{
txt.append("A button was pressed\n");
}
}
class CountListener implements ActionListener
{
private int index;
public CountListener(int i)
{
index = i;
}
public void actionPerformed(ActionEvent e)
{
txt.append("Counted Listener " + index + "\n");
}
}
class B1 implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
txt.append("Button 1 pressed\n");
ActionListener a = new CountListener(i++);
list.add(a);
b2.addActionListener(a);
}
}
class B2 implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
txt.append("Button2 pressed\n");
int end = list.size() - 1;
if (end >= 0)
{
b2.removeActionListener((ActionListener) list.get(end));
list.remove(end);
}
}
}
public void init()
{
Container cp = getContentPane();
b1.addActionListener(new B());
b1.addActionListener(new B1());
b2.addActionListener(new B());
b2.addActionListener(new B2());
JPanel p = new JPanel();
p.add(b1);
p.add(b2);
cp.add(BorderLayout.NORTH, p);
cp.add(new JScrollPane(txt));
}
11
public static void main(String[] args)
{
Console.run(new DynamicEvents(), 250, 400);
}
} // /:~
The new twists in this example are:
1. There is more than one listener attached to each Button. Usually,
components handle events as multicast, meaning that you can register
many listeners for a single event. In the special components in which
an event is handled as unicast, you’ll get a TooManyListenersException.
2. During the execution of the program, listeners are dynamically added
and removed from the Button b2. Adding is accomplished in the way
you’ve seen before, but each component also has a
removeXXXListener( ) method to remove each type of listener.
This kind of flexibility provides much greater power in your programming.
You should notice that event listeners are not guaranteed to be called in the
order they are added (although most implementations do in fact work that
way).
Separating business logic from UI logic
In general, you’ll want to design your classes so that each one does “only one
thing.” This is particularly important when user-interface code is concerned,
since it’s easy to tie up “what you’re doing” with “how you’re displaying it.”
This way, not only can you reuse the business logic more easily, but it’s also easier to reuse the GUI.
Another issue is multitiered systems, where the “business objects” reside on a
completely separate machine. This central location of the business rules
12
allows changes to be instantly effective for all new transactions, and is thus a
compelling way to set up a system. However, these business objects can be
used in many different applications and so should not be tied to any
particular mode of display. They should just perform the business operations
and nothing more.
The following example shows how easy it is to separate the business logic
from the GUI code:
package c14;
//: c14:Separation.java
// Separating GUI logic and business objects.
// <applet code=Separation width=250 height=150></applet>
// From 'Thinking in Java, 3rd ed.' (c) Bruce Eckel 2002
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
import javax.swing.*;
import java.awt.*;
import javax.swing.event.*;
import java.awt.event.*;
import java.applet.*;
import com.bruceeckel.swing.*;
class BusinessLogic
{
private int modifier;
public BusinessLogic(int mod)
{
modifier = mod;
}
public void setModifier(int mod)
{
modifier = mod;
}
public int getModifier()
{
return modifier;
}
// Some business operations:
public int calculation1(int arg)
{
return arg * modifier;
}
public int calculation2(int arg)
{
return arg + modifier;
}
}
public class Separation extends JApplet
{
private JTextField t = new JTextField(15), mod = new JTextField(15);
private JButton calc1 = new JButton("Calculation 1"), calc2 = new JButton(
13
"Calculation 2");
private BusinessLogic bl = new BusinessLogic(2);
public static int getValue(JTextField tf)
{
try
{
return Integer.parseInt(tf.getText());
} catch (NumberFormatException e)
{
return 0;
}
}
class Calc1L implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
t.setText(Integer.toString(bl.calculation1(getValue(t))));
}
}
class Calc2L implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
t.setText(Integer.toString(bl.calculation2(getValue(t))));
}
}
// If you want something to happen whenever
// a JTextField changes, add this listener:
class ModL implements DocumentListener
{
public void changedUpdate(DocumentEvent e)
{
}
public void insertUpdate(DocumentEvent e)
{
bl.setModifier(getValue(mod));
}
public void removeUpdate(DocumentEvent e)
{
bl.setModifier(getValue(mod));
}
}
public void init()
{
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(t);
calc1.addActionListener(new Calc1L());
calc2.addActionListener(new Calc2L());
JPanel p1 = new JPanel();
p1.add(calc1);
14
p1.add(calc2);
cp.add(p1);
mod.getDocument().addDocumentListener(new ModL());
JPanel p2 = new JPanel();
p2.add(new JLabel("Modifier:"));
p2.add(mod);
cp.add(p2);
}
public static void main(String[] args)
{
Console.run(new Separation(), 250, 100);
}
} // /:~
You can see that BusinessLogic is a straightforward class that performs its
operations without even a hint that it might be used in a GUI environment. It
just does its job.
Separation keeps track of all the UI details, and it talks to BusinessLogic
only through its public interface. All the operations are centered around
getting information back and forth through the UI and the BusinessLogic
object. So Separation, in turn, just does its job. Since Separation knows
only that it’s talking to a BusinessLogic object (that is, it isn’t highly
coupled), it could be massaged into talking to other types of objects without
much trouble.
Thinking in terms of separating UI from business logic also makes life easier
when you’re adapting legacy code to work with Java.
Concurrency & Swing
It is easy to forget that you are using threads when you program with Swing.
The fact that you don’t have to explicitly create a Thread object means that
threading issues can catch you by surprise. Typically, when you write a Swing
program, or any GUI application with a windowed display, the majority of the
application is event driven, and nothing really happens until the user
generates and event by clicking on a GUI component with the mouse, or
striking a key.
Just remember that there is a Swing event dispatching thread, which is
always there, handling all the Swing events in turn. This needs to be
considered if you want to guarantee that your application won’t suffer from
deadlocking or race conditions.
This section looks at a couple of issues worth noting when working with
threads under Swing.
Runnable revisited
Previously, I suggested that you think carefully before making a class as an
15
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 following example 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 sleep( ) between color changes. By playing with these values,
you’ll discover some interesting and possibly inexplicable features of threads:
package c14;
//: 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>
// From 'Thinking in Java, 3rd ed.' (c) Bruce Eckel 2002
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
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.DARK_GRAY, Color.GRAY, Color.GREEN,
Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK,
Color.RED, Color.WHITE, Color.YELLOW };
private static Random rand = new Random();
private static final Color newColor()
{
return colors[rand.nextInt(colors.length)];
}
private Color cColor = newColor();
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();
}
public void run()
16
{
while (true)
{
cColor = newColor();
repaint();
try
{
t.sleep(pause);
} catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
}
}
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);
}
} // /:~
17
ColorBoxes is the usual applet/application with an init( ) that sets up the
GUI. This configures a 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 that 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.)
18
This program also makes an interesting benchmark, since it can and has
shown dramatic performance and behavioral differences between one JVM
threading implementation and another.
Managing concurrency
When you make changes to any Swing component properties from the main
method of your class or in a separate thread, be aware that the event
dispatching thread might be vying for the same resources.
The following program shows how you can get an unexpected result by not
paying attention to the event dispatching thread:
package c14;
//: c14:EventThreadFrame.java
// Race Conditions using Swing Components.
// From 'Thinking in Java, 3rd ed.' (c) Bruce Eckel 2002
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.Console;
public class EventThreadFrame extends JFrame
{
private JTextField statusField = new JTextField("Initial Value");
public EventThreadFrame()
{
Container cp = getContentPane();
cp.add(statusField, BorderLayout.NORTH);
addWindowListener(new WindowAdapter()
{
public void windowOpened(WindowEvent e)
{
try
{ // Simulate initialization overhead
Thread.sleep(2000);
} catch (InterruptedException ex)
{
throw new RuntimeException(ex);
}
statusField.setText("Initialization complete");
}
});
}
public static void main(String[] args)
{
EventThreadFrame etf = new EventThreadFrame();
Console.run(etf, 150, 60);
etf.statusField.setText("Application ready");
System.out.println("Done");
}
} // /:~
19
It is easy to see what is supposed to happen. In the main method, a new
EventThreadFrame class is created and run using the Console.run( )
method. After the frame has been created and run, the value of the text field
is set to “Application ready,” and then, just before exiting main( ), “Done” is
sent to the console.
When the frame is created, the text field is constructed with the value “Initial
Value” in the constructor of the frame, and an event listener is added that
listens for the opening of the window. This event will be received by the
JFrame as soon as the setVisible(true) method has been called (by
Console.run( )) and is the right place to do any initialization that affects the
view of the window. In this example, a call to sleep( ) simulates some
initialization code that might take a couple of seconds. After this is done, the
value of the text box is set to “Initialization complete.”
You would expect that the text field would display “Initial Value” followed by
“Initialization complete” and then “Application Ready.” Next the word
“Done” should appear on the command prompt. What really happens is that
the setText( ) method on the TextField is called by the main thread before
the EventThreadFrame has had a chance to process its events. This means
that the string “Application ready” might actually appear before
“Initialization complete.” In reality, things might not even appear in this
order. Depending on the speed of your system, the Swing event dispatching
thread may already be busy handling the windowOpened event, so you
won’t see the text field value until after that event, but by then the text will
have been changed to “Initialization Complete.” Since the text field was set to
this value last, the message “Application ready” is lost. To makes things
worse, the word “Done” appears on the command prompt before anything
else happens at all!
This undesirable and somewhat unpredictable effect is caused by the simple
fact that there are two threads that need some sort of synchronization. It
shows that you can sometimes get into trouble with threads and Swing. To
solve this problem, you must ensure that Swing component properties are
only ever updated by the event dispatch thread.
This is easier than it sounds, using one of Swing’s two mechanisms,
SwingUtilities.invokeLater( ) and SwingUtilities.invokeandWait( ).
They do most of the work, which means that you don’t have to do too much
complicated synchronization or thread programming.
They both take runnable objects as parameters and drive the run( ) with the
Swing event processing thread, after it has processed any pending events in
the queue.
package c14;
//: c14:InvokeLaterFrame.java
// Eliminating race Conditions using Swing Components.
// From 'Thinking in Java, 3rd ed.' (c) Bruce Eckel 2002
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
20
import com.bruceeckel.swing.Console;
public class InvokeLaterFrame extends JFrame
{
private JTextField statusField = new JTextField("Initial Value");
public InvokeLaterFrame()
{
Container cp = getContentPane();
cp.add(statusField, BorderLayout.NORTH);
addWindowListener(new WindowAdapter()
{
public void windowOpened(WindowEvent e)
{
try
{ // Simulate initialization overhead
Thread.sleep(2000);
} catch (InterruptedException ex)
{
throw new RuntimeException(ex);
}
statusField.setText("Initialization complete");
}
});
}
public static void main(String[] args)
{
final InvokeLaterFrame ilf = new InvokeLaterFrame();
Console.run(ilf, 150, 60);
// Use invokeAndWait() to synchronize output to prompt:
// SwingUtilities.invokeAndWait(new Runnable() {
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
ilf.statusField.setText("Application ready");
}
});
System.out.println("Done");
}
} // /:~
A Runnable anonymous inner class is passed to
SwingUtilities.invokeLater( ), which calls the setText( ) method of the
text field. This queues the runnable object as an event so that it is the event
dispatching thread that calls the setText( ) method after first processing any
pending events. This means that the windowOpening event will be
processed before the text field displays “Application ready,” which is the
intended result.
invokeLater( ) is asynchronous, so it returns right away. This can be useful
because it doesn’t block, so your code runs smoothly. However, it doesn’t
solve the problem with the “Done” string, which is still printed to the
21
command prompt before anything else happens.
The solution to this problem is to use invokeAndWait( ) instead of
invokeLater( ) to set the text field value to “Application Ready.” This
method is synchronous, which means that it will block until the event has
been processed before returning. The System.out.println(“Done”)
statement will only be reached after the text field value has been set, so it will
be the last statement to be executed. This gives us completely predictable and
correct behavior.
Using invokeAndWait( ) provides one of the necessary conditions for
deadlock, so make sure that you are careful about controlling shared
resources if you are using invokeAndWait( ), especially if you are calling it
from more than one thread.
You will probably use invokeLater( ) more often than invokeAndWait( ),
but remember that if you set the properties of a Swing component any time
after initialization, it should be done using one of these methods.
22