I've discussed reflection largely in terms of how design tools use it to analyze your classes. But it would be a shame to end this discussion without showing you how to use reflection for your own purposes. It's a powerful tool that lets you do many things that wouldn't be possible otherwise. In this section, we'll build a dynamic event adapter that can be configured at run-time.
In Chapter 13 we saw how adapter classes could be built to connect event firings to arbitrary methods in our code, allowing us to cleanly separate GUI and logic in our applications. In this chapter, I have described how the BeanBox interposes adapters between Beans to do this for us. I have also described how the BeanBox uses adapters to bind and constrain properties between Beans.
One of the primary motivations behind the new AWT event model was to reduce the need to subclass components to perform simple hookups. But if we start relying heavily on special adapter classes, we can quickly end up with as many adapters as objects. Anonymous inner classes let us hide the existence of these classes, but they're still there. A potential solution for large or specialized applications is to create generic event adapters that can serve a number of event sources and sinks simultaneously.
The following example,
DynamicActionAdapter, is a generic adapter
for ActionEvents. A single instance of
DynamicActionAdapter can be used to hook
up a number of ActionEvent sources.
DyanicActionAdapter uses reflection so
that each event source can be directed to an arbitrary method of the
target object.
Here's the code:
import java.awt.*;
import java.awt.event.*;
import java.util.Hashtable;
import java.lang.reflect.Method;
class DynamicActionAdapter implements ActionListener {
Hashtable actions = new Hashtable();
public void hookup( Object sourceObject, Object targetObject,
String targetMethod ) {
actions.put( sourceObject, new Target( targetObject, targetMethod ) );
invokeReflectedMethod( sourceObject, "addActionListener",
new Object [] { this }, new Class [] { ActionListener.class } );
}
public void actionPerformed(ActionEvent e) {
Target target = (Target)actions.get( e.getSource() );
if ( target == null )
throw new RuntimeException("unknown source");
invokeReflectedMethod( target.object, target.methodName, null, null );
}
private void invokeReflectedMethod(
Object target, String methodName, Object [] args, Class [] argTypes ) {
try {
Method method = target.getClass().getMethod( methodName, argTypes );
method.invoke( target, args );
} catch ( Exception e ) {
throw new RuntimeException("invocation problem: "+e);
}
}
class Target {
Object object;
String methodName;
Target( Object object, String methodName ) {
this.object = object;
this.methodName = methodName;
}
}
}Once we have an instance of
DynamicActionAdapter, we can use its
hookup() method to connect an
ActionEvent source to some method of a
target object. The target object doesn't have to be an
ActionListener--or any other particular
kind of object. The following applet,
DynamicTest, uses an instance of our
adapter to connect a button to its own
launchTheMissiles() method:
public class DynamicHookupTest extends java.applet.Applet {
DynamicActionAdapter actionAdapter = new DynamicActionAdapter();
public void init() {
Button launchButton = new Button("Launch!");
actionAdapter.hookup( launchButton, this, "launchTheMissiles" );
add( launchButton );
}
public void launchTheMissiles() {
System.out.println("Fire...");
}
}Here we simply call the dynamic adapter's
hookup() method,
passing it the ActionEvent source, the
target object, and a string with the name of the method to invoke when
the event arrives.
As for the code, it's pretty straightforward.
DynamicActionAdapter implements the
ActionListener interface. When
hookup() is called,
it registers itself with the event source, using reflection to invoke
the source's addActionListener() method.
It stores information about the target object and method in a
Target object, using an inner class. This
object is stored in a Hashtable called
actions.
When an action event arrives, the dynamic adapter looks up the target
for the event source in the Hashtable. It
then uses reflection to look up the requested method in the target
object and invoke that method. Our adapter can only invoke a method
that takes no arguments. If the method doesn't exist, the adapter
throws a RuntimeException.
The heart of the adapter is the
invokeReflectedMethod() method. This is a
private method that uses reflection to
look up and invoke an arbitrary method in an arbitrary class. First,
it calls the target's getClass() method to
get the target's Class object. It uses
this object to call getMethod(), which
returns a Method object. Once we have a
Method, we can call
invoke() to invoke the method.
The dynamic adapter is important because it has almost no built-in
knowledge. It doesn't know what object will be the event source; it
doesn't even know what kind of object will be the event source.
Likewise, it doesn't know what object will receive the event, or what
method it should call to deliver the event. All this information is
provided at run-time, in the call to
hookup(). We use reflection to look up
and invoke the event source's
addActionListener() method, and to look up
and invoke the target's event handler. All this is done on the fly.
Therefore, you can use this adapter in almost any situation requiring
an ActionEvent.
If the target's event-handling method isn't found, the adapter throws
a RuntimeException. Therein lies the
problem with this technique. By using reflection to locate and invoke
methods, we abandon Java's strong typing and head off in the direction
of scripting languages. We add power at the expense of safety.
Our dynamic event adapter is limited to handling
ActionEvents. What if we want to build
something like the BeanBox, that can hook up arbitrary
event sources to arbitrary destinations? Can we build an adapter that
can listen to any kind of event?
The short answer is "no," not without the ability to compile Java code
on the fly. Event listener registration is statically typed. Your
adapter simply has to be of the correct type to register itself as a
listener for any given type of event. But this isn't good enough for
a tool like the BeanBox, which may need to deal with events (like our
DialEvent) that aren't part of the Java
specification and that it has never seen before. The BeanBox solves
this problem
by doing just what we suggested: generating and compiling Java code on
the fly. This is what's happening when you see the message that the
BeanBox is "generating and compiling an adapter class."
One of the sample Beans, the EventMonitor, also generates adapters on
the fly so it can register itself as a listener for all types of
events coming from a Bean. If you really need to deal with arbitrary
event types or you have some other reason for generating Java code at
run-time, look at the EventMonitor Bean and the
sunw.demo.encapsulatedevents package. This will give you
some ideas about generating the code and invoking the compiler.