|
Java Enterprise Breakthroughs, Part 1by Jim Farley, coauthor of Java Enterprise in a Nutshell, 2nd Edition05/22/2002 |
The second edition of Java Enterprise in a Nutshell includes an enormous amount of new material, reflecting the significant milestones that enterprise Java has achieved since the first edition was published in 1999. These milestones include the release of the full J2EE 1.2 spec in December 1999, the release of the J2EE 1.3 spec in the summer of 2001, the recent wave of Java support for Web services, and numerous other achievements in between.
In this article, I'll highlight just a few of the powerful new features found in the Java enterprise APIs that are described in the second edition.
As of JDK 1.3, the Java Naming and Directory Interface (JNDI) was added as
a standard part of the core Java environment. (Previously, it was available separately
as a "standard extension" API.) In this release, the JNDI API was
also extended to include support for event notification. Some naming and directory
services, such as LDAP, are capable of issuing notifications of changes to registered
listeners. The JNDI API provides support for this in the form of the javax.naming.event
sub-package.
If your JNDI provider supports event notification, its Context
or DirContext implementation will extend the javax.naming.event.EventContext
and/or javax.naming.event.EventDirContext interfaces. If you're
using the LDAP provider supplied with either JDK 1.3 or 1.4 to connect to an
LDAP server, for example, you can cast a Context generated from
this provider to an EventDirContext:
// Obtain Context from LDAP provider through a lookup
Context ctx = (Context)initCtx.lookup("ou=people");
EventDirContext evCtx = (EventDirContext)ctx;
The EventContext and EventDirContext interfaces provide
the additional functionality needed to register event listeners with the underlying
JNDI provider. All JNDI event listeners implement the javax.naming.event.NamingListener
interface. JNDI provides two subclasses of this interface: NamespaceChangeListeners,
to listen for changes in the namespace of the JNDI provider, and ObjectChangeListener,
to listen for changes to the actual objects stored in the JNDI provider.
To receive notifications of changes in a naming/directory service, you simply
implement a concrete subclass of one or both of these types of NamingListeners
and register them with a Context obtained from the JNDI provider.
If, for example, you wanted to log a message each time the namespace changed,
you could implement a NamespaceChangeListener such as the following:
import javax.naming.*;
import javax.naming.event.*;
public class NSChangeLogger implements NamespaceChangeListener {
// Default constructor
public NSChangeLogger() {}
// Callback for object addition events
public void objectAdded(NamingEvent ev) {
Binding b = ev.getNewBinding();
System.out.println("--> ADD: Object of type " + b.getClassName() +
" added at binding \"" + b.toString() + "\"");
}
// Callback for object removal events
public void objectRemoved(NamingEvent ev) {
Binding b = ev.getOldBinding();
System.out.println("--> REMOVE: Object of type " + b.getClassName() +
" removed from binding \"" + b.toString() + "\"");
}
// Callback for object rename events
public void objectRenamed(NamingEvent ev) {
Binding bNew = ev.getNewBinding();
Binding bOld = ev.getOldBinding();
System.out.println("--> RENAME: Object of type " + bNew.getClassName() +
" renamed from binding \"" + bOld.toString() +
"\" to binding \"" + bNew.toString() + "\"");
}
// Callback for errors in the naming service
public void namingExceptionThrown(NamingExceptionEvent ev) {
System.out.println("--> ERROR: An error occurred in the naming service:");
ev.getException().printStackTrace();
}
}
Each method on our NSChangeLogger is a callback that will be invoked
by the JNDI provider whenever a corresponding event occurs in the naming/directory
service. To register this listener with the JNDI provider, you invoke the addNamingListener()
method on either the EventContext or EventDirContext interface.
For example, to log namespace change events for any objects in the "people"
branch of an LDAP server that belongs to the organization "O'Reilly and
Associates", you might register one of our NSChangeLogger
listeners like so:
EventDirContext evCtx = (EventDirContext)initCtx.lookup("ou=people");
NSChangeLogger nsLogger = new NSChangeLogger();
evCtx.addNamingListener(“o=O'Reilly and Associates”,
EventContext.SUBTREE_SCOPE, nsLogger);
As of JDK 1.4, Java IDL (the standard implementation of the Java binding of
the CORBA specifications) supports the Interoperable Naming Service (INS) extensions
to the CORBA Naming Service. These extensions were added circa CORBA 2.3.1,
and they provide new, simpler approaches for clients to locate CORBA objects
registered in CORBA Naming Services. CORBA has always provided a scheme for
obtaining a "stringified object reference" for a registered object,
using the object_to_string() method on the ORB interface:
// Initialize the ORB
ORB myORB = ORB.init(...);
// Initialize a remote object and connect it to the ORB
org.omg.CORBA.Object myRmtObject = . . .;
// Get a stringified reference to the object and print it
String ior = myORB.object_to_string(myRmtObject);
System.out.println("The IOR for the object is:\n" + ior);
Running this example code (with the missing bits filled in, of course) will generate output like the following:
The IOR for the object is:
IOR:000000000000002349444c3a6f7265696c6c792f6a656e742f636f7262612f416
3636f756e743a312e300000000000010000000000000068000102000000000a313237
2e302e302e3100046600000021afabcb0000000020363f810b0000000100000000000
000000000000400000000030000000000000100000001000000200000000000010001
00000002050100010001002000101090000000100010100
A client can generate a remote reference to this CORBA object by passing this
"IOR:" string into the string_to_object() method on its
ORB:
Object rmtRef = clientORB.string_to_object("IOR:000...");
INS supports simpler, human-readable object URLs, similar to the URLs used
to locate RMI objects stored in an RMI registry. If a CORBA object is registered
in a CORBA Naming Service under the name "myObject" on a server named
inshost.org, for example, a client can obtain a remote reference
to this object by constructing a corresponding corbaname URL:
Object rmtRef2 = clientORB.string_to_object("corbaname:iiop:inshost.org#myObject");
Among other things, these human-readable object URLs can be much more easily assembled in a programmatic way, based on the component parts (protocol, host, object name). These simplified URLs are also easier to manage and distribute than the more complicated IORs. It also helps a great deal in eyeballing configuration parameters for distributed systems -- manually checking an IOR for correctness is akin to reading the machine code for an operating system kernel to find a bug.
|
J2EE 1.3 supports the 2.0 version of the Enterprise JavaBeans specification. EJB 2.0 added an important new feature to EJB components: local interfaces.
Prior to EJB 2.0, client interfaces to EJBs could only be remote -- every client had to invoke remote method calls in order to obtain an EJB reference, and each business method called on the EJB reference was also a remote method call. A common architectural pattern found in enterprise Java applications, however, is for a Web component to reference an EJB component within the same application. Another common pattern is for a session EJB to utilize one or more entity EJBs on the same server, to manage persistent information. In these cases, the client of the EJB component resides in the same Java virtual machine as the EJB component itself. It would be much more efficient for these local clients to use a local (non-remote) interface to the EJB.
Local home and client interfaces are defined for session and entity EJBs in
much the same way as their remote counterparts. A local interface for a ProfileServer
EJB, for example, might look like the following:
public interface ProfileServerLocal extends javax.ejb.EJBLocalObject {
public Profile getProfile(String userName)
throws NoSuchPersonException;
}
This EJB returns a Profile associated with a named user, through
its getProfile() method. It extends the EJBLocalObject
interface, which marks it as a local EJB client interface. A corresponding local
home interface might look like this:
public interface ProfileServerLocalHome extends javax.ejb.EJBLocalHome {
public ProfileServerLocal create() throws CreateException;
}
The local home interface for an EJB extends the EJBLocalHome interface,
and any create or find methods it may have will return the local client references
to the EJB. In this case, the create() method on ProfileServerLocalHome
returns a ProfileServerLocal interface.
A single EJB component can have both remote and local interfaces associated with it. Each home interface can be bound to separate JNDI names in the J2EE server, and a client can indicate which it requires by the JNDI lookup it performs. If the local home interface shown above is registered under the JNDI name "ejb/ProfileServerLocal", then a local client can use the local interface by making an appropriate JNDI lookup:
Context ctx = new InitialContext();
ProfileServerLocalHome profHome =
(ProfileServerLocalHome)ctx.lookup("java:comp/env/ejb/ProfileServerLocal");
ProfileServerLocal = profHome.create();
The local interface provides a much more efficient path for local clients to make use of the EJB component, since it avoids the overhead of constructing a remote method call with all of the corresponding marshalling/unmarshalling of method arguments and return values.
Both J2EE 1.2 and 1.3 support JMS 1.0. In J2EE 1.2, this support only included
the client APIs needed to interact with a separate JMS provider. In J2EE 1.3,
JMS support was extended to require that a J2EE server also provide JMS Destination
and ConnectionFactory implementations that support both point-to-point
and publish-subscribe messaging.
An interesting and powerful feature provided in JMS is the standard JMSReplyTo
message header. This header is meant to hold a Destination to be
used by the receiver to send any required response message. If we're using point-to-point
messaging, for example, I might set the JMSReplyTo header on a
message to a Queue, as a signal to the receiver that I want any
response message to be sent to the given Queue:
// Create a JMS session and get a Queue to be used for responses
QueueSession qSession = . . .;
Queue myQueue = . . .;
// Create a message and set its JMSReplyTo header to the Queue
TextMessage request = qSession.createTextMessage();
request.setJMSReplyTo(myQueue);
Another interesting feature provided by JMS is temporary queues. A messaging
client can dynamically create temporary destinations through its Session
with the JMS provider:
Queue tempQueue = qSession.createTemporaryQueue();
The combination of these two features makes it much easier to implement a request/response
messaging scheme in JMS. In our example above, if I wasn't able to create a
temporary destination, I would have to use some sort of administrative interface
to explicitly set up a response Queue on my JMS provider before
running this code. Then I'd access this specific Queue in my code
through a JNDI lookup, and use it as the JMSReplyTo in my message.
If I needed multiple request/response scenarios in my application, I'd have
to either configure multiple Destinations for the responses, or
I'd have to design some sort of message filtering scheme with a single reply-to
Destination, to ensure that the proper response is picked up by
each requestor.
With temporary destinations, I can dynamically create a Destination
to be used as the target of the response to my message:
Queue tempQueue = qSession.createTemporaryQueue();
TextMessage request = qSession.createTextMessage();
request.setJMSReplyTo(tempQueue);
The receiver of the message can access the JMSReplyTo Destination
by simply calling the getJMSReplyTo() method on the received Message.
Back on the request side, I can create a MessageConsumer for the
Destination and either receive the response synchronously by calling
its receive() method:
QueueReceiver qReceiver = qSession.createReceiver(tmpQueue);
Message response = qReceiver.receive();
or I can asynchronously receive the response by registering a MessageListener
with the receiver:
qReceiver.setMessageListener(new MyMessageListener(. . .));
Since the temporary destination is only accessible from this client, it effectively sets up a private request/response "channel" between the sender and receiver(s) of a message. Plus, it's a channel that's established dynamically at runtime, rather than statically through an a priori administrative step.
These are only a few of the powerful capabilities provided by the enterprise Java APIs. To see more details on these and other enterprise Java features, see the newest edition of Java Enterprise in a Nutshell. And watch for more enterprise Java tips and tricks in part 2 of this series.
Jim Farley is a technology architect, strategist and IT manager.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.