Chapter 12. Exception Callback

IN THIS CHAPTER

Exceptions can be thrown just about anywhere in a Java program. Methods must declare all of the exceptions they might throw except for RuntimeException and its subclasses. A RuntimeException can occur on just about every line of code (just think of the fact that NullPointerException can be thrown anytime a method is invoked if the object reference is null!).

When an exception occurs and is not immediately caught, it propagates up the call stack until it either is caught by one of the invoking methods, or remains uncaught. For methods called from the main() method, the exception can potentially float all the way back up to main() and get reported there. However, when a new thread is spawned, a brand new call stack is created. This call stack starts with the run() method. The run() method does not declare that it throws any exceptions and should therefore catch all of them.

Many times Java developers are frustrated by the fact that they cannot catch exceptions that occur in other threads. In this chapter, I'll show you a way to pass exceptions that occur inside an active class to another class to signal it that a potential problem has occurred.

ExceptionListener Interface

Classes that want to be notified when an exception occurs in an active class can implement the ExceptionListener interface shown in Listing 12.1.

Example 12.1. ExceptionListener.java—The Interface to Be Implemented for Exception Callback

1: public interface ExceptionListener {
2:     public void exceptionOccurred(Exception x, Object source);
3: }

This interface declares that classes of the type ExceptionListener must implement an exceptionOccurred() method. An active class will invoke this method on the listener whenever a notable exception should be relayed. The first parameter, x, is a reference to the actual exception. The second parameter, source, is a reference to the active class that caught the exception. Having a reference to the source object allows one ExceptionListener to monitor more than one active object and take a different course of action for each object monitored. If this is not necessary, the source parameter can simply be ignored.

Additional Methods to Support ExceptionListener

In the monitored class, three new methods are needed to support the existence of optional external ExceptionListener observers:

public void addExceptionListener(ExceptionListener l)
public void removeExceptionListener(ExceptionListener l)
private void sendException(Exception x)

The method addExceptionListener() adds a new listener for exceptions within the active object. The method removeExceptionListener() is used to clear out the current listener, if it exists, without setting a new one so that there are no longer any listeners.

Inside the active object, the private method sendException() should be invoked whenever an exception occurs that should be noted. Not all exceptions should be reported. For example, when InterruptedException is caught, it is generally not reported but used as a signal that someone has requested that the internal thread clean up and die soon.

For those exceptions that are reported, sendException() passes the exception on to all of the ExceptionListeners that have been added. If no listeners have been added, it is generally a good idea to instead print a stack trace to the console for the exception.

After the exception is reported, the internal thread can take different actions. If the exception was not very serious and can be worked around, the internal thread should proceed. If the exception is a critical error, the thread should clean up and return from run() soon.

The class ExceptionCallback, shown in Listing 12.2, supports monitoring by an ExceptionListener.

Example 12.2. ExceptionCallback.java—An Active Object That Supports Monitoring by an ExceptionListener

  1: import java.io.*;
  2: import java.util.*;
  3:
  4: public class ExceptionCallback extends Object {
  5:     private Set exceptionListeners;
  6:     private Thread internalThread;
  7:     private volatile boolean noStopRequested;
  8:
  9:     public ExceptionCallback(ExceptionListener[] initialGroup) {
 10:         init(initialGroup);
 11:     }
 12:
 13:     public ExceptionCallback(ExceptionListener initialListener) {
 14:         ExceptionListener[] group = new ExceptionListener[1];
 15:         group[0] = initialListener;
 16:         init(group);
 17:     }
 18:
 19:     public ExceptionCallback() {
 20:         init(null);
 21:     }
 22:
 23:     private void init(ExceptionListener[] initialGroup) {
 24:         System.out.println("in constructor - initializing...");
 25:
 26:         exceptionListeners =
 27:                 Collections.synchronizedSet(new HashSet());
 28:
 29:         // If any listeners should be added before the internal
 30:         // thread starts, add them now.
 31:         if ( initialGroup != null ) {
 32:             for ( int i = 0; i < initialGroup.length; i++ ) {
 33:                 addExceptionListener(initialGroup[i]);
 34:             }
 35:         }
 36: 
 37:         // Just before returning from the constructor,
 38:         // the thread should be created and started.
 39:         noStopRequested = true;
 40:
 41:         Runnable r = new Runnable() {
 42:                 public void run() {
 43:                     try {
 44:                         runWork();
 45:                     } catch ( Exception x ) {
 46:                         // in case ANY exception slips through
 47:                         sendException(x);
 48:                     }
 49:                 }
 50:             };
 51:
 52:         internalThread = new Thread(r);
 53:         internalThread.start();
 54:     }
 55:
 56:     private void runWork() {
 57:         try {
 58:             makeConnection(); // will throw an IOException
 59:         } catch ( IOException x ) {
 60:             sendException(x);
 61:             // Probably in a real scenario, a "return"
 62:             // statement should be here.
 63:         } 
 64:
 65:         String str = null;
 66:         int len = determineLength(str); // NullPointerException
 67:     }
 68:
 69:     private void makeConnection() throws IOException {
 70:         // A NumberFormatException will be thrown when
 71:         // this String is parsed.
 72:         String portStr = "j20";
 73:         int port = 0;
 74: 
 75:         try {
 76:             port = Integer.parseInt(portStr);
 77:         } catch ( NumberFormatException x ) {
 78:             sendException(x);
 79:             port = 80; // use default;
 80:         } 
 81:
 82:         connectToPort(port); // will throw an IOException
 83:     }
 84:
 85:     private void connectToPort(int portNum) throws IOException {
 86:         throw new IOException("connection refused");
 87:     }
 88:
 89:     private int determineLength(String s) {
 90:         return s.length();
 91:     }
 92:
 93:     public void stopRequest() {
 94:         noStopRequested = false;
 95:         internalThread.interrupt();
 96:     }
 97:
 98:     public boolean isAlive() {
 99:         return internalThread.isAlive();
100:     }
101:
102:     private void sendException(Exception x) {
103:         if ( exceptionListeners.size() == 0 ) {
104:             // If there aren't any listeners, dump the stack
105:             // trace to the console.
106:             x.printStackTrace();
107:             return;
108:         }
109:
110:         // Used "synchronized" to make sure that other threads
111:         // do not make changes to the Set while iterating.
112:         synchronized ( exceptionListeners ) {
113:             Iterator iter = exceptionListeners.iterator();
114:             while ( iter.hasNext() ) {
115:                 ExceptionListener l =
116:                         (ExceptionListener) iter.next();
117: 
118:                 l.exceptionOccurred(x, this);
119:             }
120:         }
121:     }
122:
123:     public void addExceptionListener(ExceptionListener l) {
124:         // Silently ignore a request to add a "null" listener.
125:         if ( l != null ) {
126:             // If a listener was already in the Set, it will
127:             // silently replace itself so that no duplicates
128:             // accumulate.
129:             exceptionListeners.add(l);
130:         }
131:     }
132:
133:     public void removeExceptionListener(ExceptionListener l) {
134:         // Silently ignore a request to remove a listener
135:         // that is not in the Set.
136:         exceptionListeners.remove(l);
137:     }
138:
139:     public String toString() {
140:         return getClass().getName() +
141:             "[isAlive()=" + isAlive() + "]";
142:     }
143: }

This class uses the self-running, anonymous inner class pattern explained in Chapter 11, "Self-Running Objects." It expands on it to include support for monitoring by an ExceptionListener.

There are three constructors and they all invoke init(). The first constructor (lines 9–11) takes an ExceptionListener[] as a parameter. All of the listeners in the array will be added before the internal thread is started to ensure that no exceptions are missed. The second constructor (lines 13–17) takes a single ExceptionListener as a parameter. This listener will be added before the internal thread is started. The third constructor (lines 19–21) is used when no initial listeners are needed.

All three constructors end up calling init() (lines 23–54) to finish up the object initialization. The member variable exceptionListeners refers to a Set that is holding the group of current ExceptionListeners (line 5). I used a Set to automatically keep out duplicate listeners. Specifically, a HashSet is used and wrapped in synchronization for multithread safety (lines 26–27). For more information on using the Collections API in a multithreaded environment, see Chapter 7. If any ExceptionListeners should be added before the internal thread is started, they are passed one by one to addExceptionListener() (lines 31–35). Inside the anonymous inner class, if any exception slips through from runWork() (line 44), it is caught (line 45) and sent to sendException() (line 47).

Inside runWork() (lines 56–67), a few methods are called to produce some mock exceptions. First, makeConnection() is invoked. It declares that it might throw an IOException, so a try/catch block is used and if an IOException is thrown, it will be passed to sendException() (line 60). In the real world, this might be a serious enough error that a return should be used to let the thread die, but in this scenario, I'll let it proceed to create a null String reference (line 65). This reference is passed to determineLength() (line 66). Although determineLength() might throw a NullPointerException (and in this case it does!), no try/catch clause is required because NullPointerException is a subclass of RuntimeException. If the exception occurs, it will propagate up the call stack to run() in the inner class, and there it will be caught (line 45) and passed to sendException() (line 47).

The makeConnection() method (lines 69–83) first tries to parse the string "j20" (line 72) into an integer (line 76). It will fail because of the j in the string, and a NumberFormatException will be thrown. This exception is caught (line 77) and passed off to sendException() for logging purposes, and a default port number of 80 is used instead, and then processing proceeds. This might be more representative of a real-world situation: one where you want to report the error, but also want to continue with a reasonable default value. This default port number is then passed to the connectToPort() method (line 82). The connectToPort() method (lines 85–87) simply throws a new IOException with the message connection refused. This exception will propagate up to runWork(), will be caught there (line 59), and reported (line 60).

All that determineLength() (lines 89–91) does is to return the length of the string passed into it. In this case, it will be passed null, which will cause a NullPointerException to be thrown. This exception will propagate all the way back up to run() and be caught there (line 45).

The private method sendException() (lines 102–121) first checks to see if there are any listeners (line 103). If there currently are no listeners, a stack trace of the exception is printed to the console (line 106) and the method returns right away (line 107). If there are listeners in the set, each one has its exceptionOccurred() method invoked passing in the exception and a reference to this in case the listener needs to know in which object the exception occurred (lines 112–120).

The addExceptionListener() method (lines 123–131) is used to add an ExceptionListener to the set of listeners that will be called if an exception occurs. If null is passed in, it is silently ignored (line 125). Otherwise, the new listener is added to the set of current listeners. Because Set does not allow duplicates, if the new listener is already in the Set, it is silently replaced with itself (line 129). In addition, because the Set was wrapped in synchronization (lines 26–27), adding elements is thread-safe and will temporarily block if sendException() is currently notifying the listeners.

The removeExceptionListener() method (lines 133–137) is used to stop a specific ExceptionListener from receiving any more exceptions. If the specified listener is not in the Set, the request to remove it is silently ignored. In addition, because the Set was wrapped in synchronization (lines 26–27), removing elements is thread-safe and will temporarily block if sendException() is currently notifying the listeners.

ExceptionCallbackMain in Listing 12.3 demonstrates how ExceptionCallback can be monitored.

Example 12.3. ExceptionCallbackMain.java—Used to Demonstrate ExceptionCallback

 1: public class ExceptionCallbackMain
 2:         extends Object
 3:         implements ExceptionListener {
 4:
 5:     private int exceptionCount;
 6:
 7:     public ExceptionCallbackMain() {
 8:         exceptionCount = 0;
 9:     }
10:
11:     public void exceptionOccurred(Exception x, Object source) {
12:         exceptionCount++;
13:         System.err.println("EXCEPTION #" + exceptionCount +
14:                 ", source=" + source);
15:         x.printStackTrace();
16:     }
17:
18:     public static void main(String[] args) {
19:         ExceptionListener xListener = new ExceptionCallbackMain();
20:         ExceptionCallback ec = new ExceptionCallback(xListener);
21:     }
22: }

ExceptionCallbackMain implements the ExceptionListener interface (line 3) so that it will have its exceptionOccurred() method called when an object that it is monitoring throws an exception. The variable exceptionCount (line 5) is simply used to keep track of how many exceptions are reported.

When the exceptionOccurred() method is called, exceptionCount is incremented and a header message is printed along with the results of invoking toString() on the source object (lines 13–14). After that, the stack trace of the exception is printed (line 15).

In main(), an ExceptionListener reference is created by constructing an ExceptionCallbackMain (line 19). This reference is passed into the constructor of ExceptionCallback so that exceptions are reported right away (line 20). ExceptionCallback is a self-running object, so no further action is necessary to get things going.

When ExceptionCallbackMain is run, the following output occurs (your output should match):

 1: in constructor - initializing...
 2: EXCEPTION #1, source=ExceptionCallback[isAlive()=true]
 3: java.lang.NumberFormatException: j20
 4:     at java.lang.Integer.parseInt(Compiled Code)
 5:     at java.lang.Integer.parseInt(Integer.java:458)
 6:     at ExceptionCallback.makeConnection(ExceptionCallback.java:76)
 7:     at ExceptionCallback.runWork(ExceptionCallback.java:58)
 8:     at ExceptionCallback.access$0(ExceptionCallback.java:56)
 9:     at ExceptionCallback$1.run(ExceptionCallback.java:44)
10:     at java.lang.Thread.run(Thread.java:479)
11: EXCEPTION #2, source=ExceptionCallback[isAlive()=true]
12: java.io.IOException: connection refused
13:     at ExceptionCallback.connectToPort(ExceptionCallback.java:86)
14:     at ExceptionCallback.makeConnection(ExceptionCallback.java:82)
15:     at ExceptionCallback.runWork(ExceptionCallback.java:58)
16:     at ExceptionCallback.access$0(ExceptionCallback.java:56)
17:     at ExceptionCallback$1.run(ExceptionCallback.java:44)
18:     at java.lang.Thread.run(Thread.java:479)
19: EXCEPTION #3, source=ExceptionCallback[isAlive()=true]
20: java.lang.NullPointerException
21:     at ExceptionCallback.determineLength(ExceptionCallback.java:90)
22:     at ExceptionCallback.runWork(ExceptionCallback.java:66)
23:     at ExceptionCallback.access$0(ExceptionCallback.java:56)
24:     at ExceptionCallback$1.run(ExceptionCallback.java:44)
25:     at java.lang.Thread.run(Thread.java:479)

All of the rigged exceptions are passed back to ExceptionCallbackMain and printed.

Summary

It is very likely that you will write code for an active object that can throw exceptions. A typical case would be when worker threads are used to communicate across a network. Exceptions are important and should not be frivolously discarded, but the run() method cannot throw any exceptions back to another thread because it has its own call stack. The exception callback technique I showed you in this chapter can be used to pass any exceptions that might occur back to an object that can monitor them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.62.34