Полезная информация

Exploring Java

Previous: 18.3 Hand-Coding with BeansChapter 18
Java Beans
Next: Glossary
 

18.4 Putting Reflection to Work

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.

18.4.1 Safety Implications

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.

18.4.2 Compile-Time versus Run-Time Hookups

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.


Previous: 18.3 Hand-Coding with BeansExploring JavaNext: Glossary
18.3 Hand-Coding with BeansBook IndexGlossary

Other Books in this LibraryJava in a NutshellJava Language ReferenceJava AWTJava Fundamental ClassesExploring Java