Return-Path: Received: from fort-point-station.mit.edu by po10.mit.edu (8.9.2/4.7) id SAA14265; Tue, 27 Mar 2001 18:54:36 -0500 (EST) Received: from hermes.java.sun.com (hermes.java.sun.com [204.160.241.85]) by fort-point-station.mit.edu (8.9.2/8.9.2) with SMTP id SAA08767 for ; Tue, 27 Mar 2001 18:55:09 -0500 (EST) Date: Tue, 27 Mar 2001 18:55:09 -0500 (EST) Message-Id: <200103272355.SAA08767@fort-point-station.mit.edu> From: "JDC Tech Tips" To: alexp@mit.edu Subject: JDC Tech Tips March 27, 2001 Reply-To: JDCTechTips@hermes.java.sun.com Errors-To: bounced_mail@hermes.java.sun.com Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii X-Mailer: Beyond Email 2.2 J D C T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips, March 27, 2001. This issue of the JDC Tech Tips covers the following topics: * Deserializing Marshalled Objects * JNDI Lookup in Distributed Systems This tip was developed using Java(tm) 2 SDK, Standard Edition, v 1.3. This issue of the JDC Tech Tips is written by Stuart Halloway, a Java specialist at DevelopMentor (http://www.develop.com/java). You can view this issue of the Tech Tips on the Web at http://java.sun.com/jdc/JDCTechTips/2001/tt0327.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DESERIALIZING MARSHALLED OBJECTS When you deserialize a Java(tm) object from a stream, all reference objects are deserialized as well. An important exception to this is the marshalled object, that is, an instance of the class java.rmi.MarshalledObject. The constructor of this class takes as a parameter an object to be serialized. The constructor creates a marshalled object that contains the serialized representation of the supplied object. In other words, a marshalled object is a container for the serialized representation of another object. A marshalled object has a special property: when you deserialize a marshalled object, the contained object is not automatically deserialized. Instead, the contained object must be deserialized in an explicit step. This explicit step means more work for you, but it provides two major advantages, as follows: o With marshalled objects, you can postpone deserialization until you actually need the object. If an object must pass through many virtual machines before it reaches its destination, normal serialization requires that you deserialize the object at each step. This, in turn, requires that the necessary classes be available and loaded on each intermediate machine, which is not always possible or desirable. o Marshalled objects can contain a codebase URL where the class code can be found. When you use a marshalled object, any needed class file is automatically located. To see these advantages in practice, let's compare a marshalled object with a "plain" serialized object. First, here's the plain variety: //file PlainContainer.java import java.io.*; public class PlainContainer implements Serializable { public PlainContainer(Object containee) { this.containee = containee; } private Object containee; public String toString() { return "This container holds " + containee; } } //file WritePlain.java import java.io.*; public class WritePlain { public static void main(String[] args) throws IOException { ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new BufferedOutputStream( new FileOutputStream(args[0]))); oos.writeObject(new PlainContainer("a string")); } finally { if (oos != null) oos.close(); } System.out.println("Wrote to " + args[0]); } } //file ReadPlain.java import java.io.*; public class ReadPlain { public static void main(String[] args) throws Exception { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new BufferedInputStream( new FileInputStream(args[0]))); Object o = ois.readObject(); System.out.println("Read from file " + args[0]+ ":"); System.out.println(o); } catch (Exception e) { e.printStackTrace(); } finally { if (ois != null) ois.close(); } } } The PlainContainer class is a simple serializable class. The WritePlain and ReadPlain classes test reading and writing an instance of PlainContainer. Assuming that you compile all three classes to a subfolder named "classes", you should be able to first write and then read a PlainContainer, as follows: >java -cp classes WritePlain plain.ser Wrote to plain.ser >java -cp classes ReadPlain plain.ser Read from file plain.ser: This container holds a string This is fine if the PlainContainer class is available when the instance is deserialized. Let's see what happens if the class isn't available. Copy the ReadPlain class to a separate subfolder named "otherdir". Then try to read from the plain.ser file again, using otherdir as the classpath. You should see the following: >java -cp otherdir ReadPlain plain.ser java.lang.ClassNotFoundException: PlainContainer at java.net.URLClassLoader$1.run(URLClassLoader.java:200) (etc...) The default serialization mechanism preserves the state of the instance, but not how to locate the class, so deserialization fails. You can use a marshalled object instead to solve this problem: //file WriteMarshalled.java import java.io.*; import java.rmi.*; public class WriteMarshalled { public static void main(String[] args) throws IOException { ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new BufferedOutputStream( new FileOutputStream(args[0]))); oos.writeObject(new MarshalledObject(new PlainContainer( "a marshalled string"))); } finally { if (oos != null) oos.close(); } System.out.println("Wrote to " + args[0]); } } //file ReadMarshalled.java import java.io.*; import java.rmi.*; public class ReadMarshalled { public static void main(String[] args) throws Exception { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new BufferedInputStream( new FileInputStream(args[0]))); MarshalledObject o = (MarshalledObject) ois.readObject(); System.out.println("Read MarshalledObject from file " + args[0]+ ", contents are:"); Object containee = o.get(); System.out.println(containee); System.out.println("Marshalled object loaded by: " + o.getClass().getClassLoader()); System.out.println("Containee loaded by: " + containee.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } finally { if (ois != null) ois.close(); } } } The WriteMarshalled and ReadMarshalled classes are almost identical to the original versions, but have a few subtle differences: o WriteMarshalled wraps a PlainContainer object in a marshalled object, that is, and instance of MarshalledObject. o ReadMarshalled casts the read object to MarshalledObject, and then makes an explicit call to get() to retrieve the contained object. ReadMarshalled also prints various lines to to show what is happening. Compile the new classes to the classes folder, and copy the ReadMarshalled class to the otherdir folder. Now you are almost ready to see the marshalled object use dynamic class loading to find the classes that it needs. However, to use dynamic class loading you must also set the codebase in the process that first serializes the object, and configure security in any process that wants to dynamically load classes. See the JDC Tech Tip for February 7, 2001, "Dynamic Class Loading in RMI" (http://java.sun.com/jdc/JDCTechTips/2001/tt0227.html#dynamic) for details on how to set the codebase and how to configure security for dynamic class loading. Here are the necessary command lines: >java -Djava.rmi.server.codebase=file:/yourcode/classes/ \ -cp classes WriteMarshalled marshalled.ser Wrote to marshalled.ser >java -Djava.security.policy=SimpleRMI.policy \ -Djava.security.manager -cp otherdir ReadMarshalled \ marshalled.ser Read MarshalledObject from file marshalled.ser, contents are: This container holds a marshalled string Marshalled object loaded by: null Containee loaded by: sun.rmi.server.LoaderHandler$Loader@30c221 If you wonder why the output indicates that the marshalled object is loaded by "null," it's because the object is loaded by the bootstrap class loader, which does not have an identity in the Java environment, and so appears as "null" in the output. To use these command lines, you need to replace the string "yourcode" with your actual working directory. You also need to create a SimpleRMI.policy file in your working directory. Use the template below, replacing "yourcode" with the path to your working directory. grant { permission java.net.SocketPermission "*:1024-", "accept, connect"; permission java.io.FilePermission "${/}yourcode${/}-", "read"; }; As you can see from the output above, the Containee is found, even though it was not on the class path. The Containee is loaded by a dynamic class loader (sun.rmi.server.LoaderHandler$Loader). This might seem like a lot of work for a small benefit, especially because the security properties and policy file syntax are tricky when you first encounter them. However, the initial effort you make to set the codebase and process security will benefit all the objects in your process. Moreover, these process configuration steps are essential if you are building distributed systems. Using marshalled objects is the preferred approach if your objects might be deserialized in environments where the class files are not on the class path. It's also the preferred approach if your objects might pass through many intermediate hosts before they need to be fully deserialized. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - JNDI LOOKUP IN DISTRIBUTED SYSTEMS The Java Naming and Directory Interface(tm) (JNDI) is the Java platform solution for looking up objects by name or by attributes. You should use JNDI to access resources such as database connections, transactions, remote objects, and Enterprise JavaBeans(tm) components. Many of the technologies associated with these resources were designed before the JNDI specification was completed, and because of that, have their own name lookup schemes. You should always consider JNDI a better approach than these resource-specific naming schemes. To illustrate the superiority of JNDI, consider the following RMI application. //file Echo.java import java.rmi.*; public interface Echo extends Remote { public String echo(String value) throws RemoteException; } //file EchoClient.java import java.rmi.*; public class EchoClient { public static void main(String [] args) { try { System.out.println("Connecting to echo server..."); Echo e = (Echo) Naming.lookup("ECHO"); String result = e.echo("Hello"); System.out.println("Echo returned " + result); } catch (Exception e) { e.printStackTrace(); } } } //file EchoServer.java import java.io.*; import java.rmi.*; import java.rmi.registry.*; import java.rmi.server.*; public class EchoServer extends RemoteObject implements Echo { public String echo(String value) { return value; } public static void main(String[] args) { try { EchoServer serv = new EchoServer(); Echo remoteObj = (Echo) UnicastRemoteObject.exportObject( serv); Registry r = LocateRegistry.getRegistry("localhost", 1099); r.rebind("ECHO", remoteObj); BufferedReader rdr = new BufferedReader( new InputStreamReader(System.in)); while (true) { System.out.println("Type EXIT to shutdown the server."); if ("EXIT".equals(rdr.readLine())) { break; } } r.unbind("ECHO"); UnicastRemoteObject.unexportObject(serv, true); } catch (Exception e) { e.printStackTrace(); } } } If this application looks familiar, it's because it was used to illustrate the February 7, 2001 JDC Tech Tip "The Lifecycle of an RMI Server" (http://java.sun.com/jdc/JDCTechTips/2001/tt0227.html#lifecycle). As described in that Tech Tip, the server binds a name into the rmiregistry through the Registry class, and the client finds this object with Naming.lookup. If you rewrite this application to use JNDI lookup, you only need to make a few changes in the code. You need to import the core JNDI package, javax.naming, in the client and server. You also need to change the lookup code in the client to the following: Context ctxt = new InitialContext(); Echo e = (Echo) ctxt.lookup("ECHO"); In the server, you use the following JNDI code to bind and unbind into the naming service: Context ctxt = new InitialContext(); ctxt.rebind("ECHO", remoteObj); //object available in naming service... ctxt.unbind("ECHO"); Here are the new versions of the client and server, rewritten for JNDI: //new version of EchoClient.java import java.rmi.*; import javax.naming.*; public class EchoClient { public static void main(String [] args) { try { System.out.println("Connecting to echo server..."); Context ctxt = new InitialContext(); Echo e = (Echo) ctxt.lookup("ECHO"); String result = e.echo("Hello"); System.out.println("Echo returned " + result); } catch (Exception e) { e.printStackTrace(); } } } //new version of EchoServer.java import java.io.*; import java.rmi.*; import java.rmi.registry.*; import java.rmi.server.*; import javax.naming.*; public class EchoServer extends RemoteObject implements Echo { public String echo(String value) { return value; } public static void main(String[] args) { try { EchoServer serv = new EchoServer(); Echo remoteObj = (Echo) UnicastRemoteObject.exportObject( serv); Context ctxt = new InitialContext(); ctxt.rebind("ECHO", remoteObj); BufferedReader rdr = new BufferedReader( new InputStreamReader(System.in)); while (true) { System.out.println("Type EXIT to shutdown the server."); if ("EXIT".equals(rdr.readLine())) { break; } } ctxt.unbind("ECHO"); UnicastRemoteObject.unexportObject(serv, true); } catch (Exception e) { e.printStackTrace(); } } } The difference in syntax is small, but the implications are large. While the rmiregistry APIs commit you at compile time to a specific naming service on a specific machine, the JNDI APIs are generic, and allow you (or an administrator) to specify a specific naming service at runtime. In this example, you can tell the InitialContext to use the rmiregistry service by creating a jndi.properties file with the following entries: java.naming.factory.initial= com.sun.jndi.rmi.registry.RegistryContextFactory java.naming.provider.url=rmi://localhost When the initial context is created, JNDI reads the properties file and uses the value of java.naming.factory.initial to load a specific provider. It then uses java.naming.provider.url to select the top-level naming context to search. (There are several other ways to specify this configuration information. See the "JNDI API Tutorial and Reference" (http://java.sun.com/docs/books/jndi/) for details.) To test the application, you need to create a stub class: rmic EchoServer Then open three console windows to start the RMI registry, run the RMI server, and run the RMI client, respectively. Start the RMI registry in one console window: rmiregistry Run the RMI server in the second console window: java EchoServer Run the RMI client in the third console window: java EchoClient You have now come full circle. At this point, you are still using the same naming service you were using before, so there is no immediate benefit to this approach. However, you might choose to bind your RMI objects into some other naming service, such as CORBA or LDAP, or some other service not even invented yet. Note however that switching to CORBA involves several subtleties in addition to the choice of a naming service. For more about these subtleties, see "Java(tm) RMI-IIOP Documentation" (http://java.sun.com/j2se/1.3/docs/guide/rmi-iiop/index.html). But aside from the CORBA subtleties, you only need to edit the jndi.properties file, and continue to use your existing code in order to bind your RMI objects into another naming service. No code changes or recompilation are needed. For further information about JNDI, see the book "JNDI API Tutorial and Reference" by Rosanna Lee and Scott Seligman (http://java.sun.com/docs/books/jndi/). Also see the book "Mastering RMI: Developing Enterprise Applications in Java and EJB" by Rickard Oberg. . . . . . . . . . . . . . . . . . . . . . . . - NOTE Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun Microsystems purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page (http://developer.java.sun.com/subscription/), uncheck the appropriate checkbox, and click the Update button. - SUBSCRIBE To subscribe to a JDC newsletter mailing list, go to the Subscriptions page (http://developer.java.sun.com/subscription/), choose the newsletters you want to subscribe to, and click Update. - FEEDBACK Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com - ARCHIVES You'll find the JDC Tech Tips archives at: http://java.sun.com/jdc/TechTips/index.html - COPYRIGHT Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA. This document is protected by copyright. For more information, see: http://java.sun.com/jdc/copyright.html - LINKS TO NON-SUN SITES The JDC Tech Tips may provide, or third parties may provide, links to other Internet sites or resources. Because Sun has no control over such sites and resources, You acknowledge and agree that Sun is not responsible for the availability of such external sites or resources, and does not endorse and is not responsible or liable for any Content, advertising, products, or other materials on or available from such sites or resources. Sun will not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such Content, goods or services available on or through any such site or resource. JDC Tech Tips March 27, 2001 Sun, Sun Microsystems, Java, Java Developer Connection, Java Remote Method Invocation, Java Naming and Directory Interface, and Enterprise JavaBeans are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.