Serializing GUI Components Across Network

When Swing came out, I was puzzled by the following warning in the javadocs:
Warning: Serialized objects of this class will not be compatible with future Swing releases. The current serialization support is appropriate for short term storage or RMI between applications running the same version of Swing. A future release of Swing will provide support for long term persistence.
My thoughts were: No one in their right mind would want to serialize GUI components, so why are they serializable in the first place? Luckily for me, some of my friends are not in their right mind :) Many thanks to twin-dad-to-be Niko Brummer for this extremely interesting, and probably totally useless idea, though he assures me that he is using it in a real program. Some of the code in this newsletter is from him, other parts I have added. You can easily spot his code by the absense of bugs.
There are some very interesting gotchas that occur because of the dear Swing event dispatch thread, who, having failed at all attempts to find a mate, is still single, and will be for the forseeable future. Due to his hermit nature, we have to be very careful that we do not read from the GUI components or write to them from any thread except the GUI thread, or else face the wrath of a thousand deadlocks (see first newsletter in this series).
If you are serializing the component in response to pushing a GUI button, it will work, because then you are using the event dispatch thread. If you are doing it from any other thread, you may get problems (in Niko's case the event dispatch thread crashed). The solution is to wrap the gui component in an object of which you control the serialization by specifying your own writeObject() method.
I have written a ComponentSerializer class which wraps the read write functionality and you can either write a Component to an OutputStream or read it from an InputStream without having to worry about what thread you are currently in.

//: ComponentSerializer.java
import java.io.*;
import java.awt.*;
import javax.swing.*;
import java.lang.reflect.*; // wouldn't be right for me to send
             // you a newsletter that doesn't use reflection :)

public class ComponentSerializer {
  public void write(Component comp, OutputStream out)
      throws IOException {
    System.out.println("writing " + comp);
    ObjectOutputStream oout = new ObjectOutputStream(out);
    oout.writeObject(new ComponentEncapsulator(comp));
    oout.reset();
    oout.flush();
  }
  public Component read(InputStream in)
      throws IOException, ClassNotFoundException {
    System.out.println("reading component");
    ObjectInputStream oin = new ObjectInputStream(in);
    ComponentEncapsulator enc =
      (ComponentEncapsulator)oin.readObject();
    return enc.getComponent();
  }
  private class ComponentEncapsulator implements Serializable {
    private final Component comp;
    public ComponentEncapsulator(Component comp) {
      this.comp = comp;
    }
    public Component getComponent() {
      return comp;
    }
    private IOException defaultWriteException;
    private void writeObject(final ObjectOutputStream out)
        throws IOException {
      if (SwingUtilities.isEventDispatchThread()) {
        // This is all that is necessary if we are already in
        // the event dispatch thread, e.g. a user clicked a
        // button which caused the object to be serialized
        out.defaultWriteObject();
      } else {
        try {
          // we want to wait until the object has been written
          // before continuing.  If we called this from the
          // event dispatch thread we would get an exception
          SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
              try {
                // easiest way to indicate to the enclosing class
                // that an exception occurred is to have a member
                // which keeps the IOException
                defaultWriteException = null;
                // we call the actual write object method
                out.defaultWriteObject();
              } catch(IOException ex) {
                // oops, an exception occurred, remember the
                // exception object
                defaultWriteException = ex;
              }
            }
          });
          if (defaultWriteException != null) {
            // an exception occurred in the code above, throw it!
            throw defaultWriteException;
          }
        } catch(InterruptedException ex) {
          // I'm not quite sure what do here, perhaps:
          Thread.currentThread().interrupt();
          return;
        } catch(InvocationTargetException ex) {
          // This can actually only be a RuntimeException or an
          // Error - in either case we want to rethrow them
          Throwable target = ex.getTargetException();
          if (target instanceof RuntimeException) {
            throw (RuntimeException)target;
          } else if (target instanceof Error) {
            throw (Error)target;
          }
          ex.printStackTrace(); // this should not happen!
          throw new RuntimeException(ex.toString());
        }
      }
    }
  }
}
I apologize for all the comments in the ComponentSerializer, as we all know, too many comments are often indicative of poorly written code, but I cannot think of a simpler, yet correct, way of doing this. This ComponentSerializer class should handle just about any java.awt.Component derivative thrown at it from any thread, except for components which reference non-serializable components.
We can then write a GUIServer, which accepts a ComponentEncapsulator via TCP/IP and constructs a JFrame containing the component within the ComponentEncapsulator. We only cater for one component per socket, but that could easily be changed.
//: GUIServer.java
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.net.*;

public class GUIServer {
  public static final int PORT = 4123;
  private static final ComponentSerializer compser =
    new ComponentSerializer();
  public GUIServer() throws IOException {
    System.out.println("Super-duper GUI SERVER started");
    ServerSocket ss = new ServerSocket(PORT);
    while(true) {
      Socket socket = ss.accept();
      try {
        JFrame frame = new JFrame(
          "Component received from " + socket);
        Component comp = compser.read(socket.getInputStream());
        frame.getContentPane().add(comp);
        frame.pack();
        frame.show();
      } catch(IOException ex) {
        ex.printStackTrace();
      } catch(ClassNotFoundException ex) {
        ex.printStackTrace();
      }
    }
  }
  public static void main(String[] args) throws IOException {
    new GUIServer();
  }
}
We can then take a Component, for example a JScrollPane containing a JTable, and send it to any OutputStream, e.g. to the network, to the file system for short-term storage, etc. The warning in the Swing source essentially tells us we should not write GUI components onto DAT tapes for long-term archiving.
//: GUIExample.java
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.net.*;

public class GUIExample extends JFrame {
  public static final int PORT = 4123;
  private static final ComponentSerializer compser =
    new ComponentSerializer();
  private JScrollPane scrollPane;
  public GUIExample() {
    super("GUIExample Frame");
    scrollPane = new JScrollPane(new JTable(3,4));
    getContentPane().add(scrollPane);
    getContentPane().add(new JButton(
      new AbstractAction("Serialize Table") {
        public void actionPerformed(ActionEvent e) {
          System.out.println("Now we serialize synchronously");
          try {
            Socket socket = new Socket("localhost", PORT);
            compser.write(scrollPane, socket.getOutputStream());
            socket.close();
          } catch(IOException ex) {
            ex.printStackTrace();
          }
        }
      }), BorderLayout.SOUTH);
    setSize(400, 200);
    show();
  }
  public static void main(String[] args) throws Exception {
    GUIExample ex = new GUIExample();
    ex.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }
}
Please try this out - I was quite amazed that it actually worked! Start the GUIServer class, then start the GUIExample and press the button. The GUIServer should now open up another JFrame containing the JTable. Now edit the original JTable (press enter after editing, otherwise you'll get an exception when you try to serialize the table), and click on the button again. The GUIServer should now open up yet another JFrame with the JTable containing the latest values. Though I cannot think of a good application for this at the moment, I think it's quite neat that you can do that.
I've also written a second example to show you how to do an asynchronous serialization of components via another thread.
//: GUIExample2
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.net.*;

public class GUIExample2 extends JFrame {
  public static final int PORT = 4123;
  private static final ComponentSerializer compser =
    new ComponentSerializer();
  private JPanel personalData;
  public GUIExample2() {
    super("Asynchronous Sending GUIExample2 Frame");
    personalData = new JPanel(new GridLayout(0, 2, 5, 5));
    personalData.add(new JLabel("Name: "));
    personalData.add(new JTextField());
    personalData.add(new JLabel("Age: "));
    personalData.add(new JTextField());
    getContentPane().add(personalData, BorderLayout.NORTH);
    getContentPane().add(new JButton(
      new AbstractAction("Serialize Personal Data") {
        public void actionPerformed(ActionEvent e) {
          asyncSerialize(personalData);
        }
      }), BorderLayout.SOUTH);
    setSize(400, 200);
    show();
  }
  private void asyncSerialize(final Component comp) {
    new Thread() { {start();} // start from initializer block
      public void run() {
        try {
          Socket socket = new Socket("localhost", PORT);
          compser.write(comp, socket.getOutputStream());
          socket.close();
        } catch(IOException ex) {
          ex.printStackTrace();
        }
      }
    };
  }
  public static void main(String[] args) throws Exception {
    GUIExample2 ex = new GUIExample2();
    ex.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }
}
When you try this out, you'll notice that the entire JPanel gets sent across the network to the server.
Were I to use this in a production environment framework, I would add the option of writing asynchronously to the ComponentSerializer and probably do the writing via a ThreadPool.
Next week I will demonstrate that the following can be true:
"hi there".equals("cheers !")


created by Dr. Heinz M. Kabutz

0 komentar:

Posting Komentar

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
This Theme Modified by Kapten Andre based on Structure Theme from MIT-style License by Jason J. Jaeger