|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Learn to find your bean's home interface without
fuss
Daniel
Would (mailto:wouldd@uk.ibm.com?cc=&subject=Navigate
the JNDI maze) Software Engineer, IBM 18 November 2003
Java developers who move from
single-machine programming to the wilder world of EJB technology and
distributed computing often run into trouble: it can be difficult to
write code that successfully navigates through the JNDI maze, and
multiple machines and configurations just increase the chance of
something going wrong. In this article, EJB developer Daniel Would
explains how you can write client code that successfully finds its way
to an EJB component published in a JNDI namespace. He shows you various
programming options that make the process easier, and offers some code
that you can use as a utility class in your own
applications.
If you've never had any problem at all getting your client apps to see
your EJB components, and really don't see any complexities in running your
client and bean under different installs of the Java platforms, or on
entirely different machines, then this article probably won't interest
you. However, if you have just started out and have begun to see the many
interesting and obscure error messages that seem to pop up the moment you
try to do anything with a real configuration, then read on.
EJB errors? Don't
panic! So, you've read the chapter in your favorite Java
book on Enterprise JavaBeans technology, and you've knocked out a quick
HelloWorld bean, followed the recommended deployment procedure, and
published it. Now you have to write a client that will invoke this
masterpiece. So you come up with something that looks like Listing
1: Listing 1. A very simple client for invoking a
bean
InitialContext ic = new InitialContext();
Object or = ic.lookup("ejb/HelloWorldHome");
if (or != null) {
// Narrow the return object to the Home class type
HelloWorldHome home =
(HelloWorldHome)PortableRemoteObject.narrow(or,
HelloWorldHome.class);
// Create an EJB object instance using the home interface.
HelloWorld hw = home.create();
// Invoke the method
System.out.Println(hw.hello());
}
|
You run this client on the command line, using the nearest Java
installation at hand -- namely, the one your app server is using.
Everything works perfectly! Beaming with success, you move to run your
client on a second machine. This time, you get a horrible error message.
At first, you probably get java.lang.NoClassDefFoundError:
javax/ejb/EJBObject , followed by a bunch of other
NoClassDefFoundError s, because you forgot to hand over a JAR
file with the required stubs and ties, and various other EJB-related
things were not present and accounted for. But eventually, you get your
client as far as the first interesting line (InitialContext ic = new
InitialContext(); ). The exception you see when you get to this line
-- and you almost certainly will get an exception -- will vary depending
on the particular context provider
that you pick up.
Context? What's that? Get your terms
straight Before we proceed any further, it will be helpful
to define a few terms. The computing world is littered with strange terms,
buzzwords, and acronyms, and Java technology is no exception. (Or should
that be JavaIsNoException ?) If you're running into the
problems discussed above, you're probably a little shaky on the
terminology in this area. So let's discuss the terms that we will be
throwing around in this article; it's a good idea to get them
straight.
- Namespace, context, initial context, and subcontext
- These terms are all about locations -- the conceptual location where
EJB components are available, from the point of view of your client.
Think of a namespace as a town, with the shops in the town
represented by EJB home interfaces (which we'll discuss in a moment). A
context is a location within the town. An initial context
is the location where you start -- the road into town, as it were. And a
subcontext is a street name.
- Home interface and remote interface
- There are three parts to an Enterprise JavaBean component. First,
there is the bean code itself. Then there is the home interface,
which defines the methods that allow you to create an EJB bean of your
type. The home interface is published in the namespace. When you have
the home interface, you can call
Create() to obtain the
remote interface from the application server. Having obtained the
remote interface, you can call the methods that make up your actual EJB
code.
How do these terms fit into our town analogy? Well, having
gotten to the right town and found the right address, you need to walk
into the shop or ring the bell (call Create() ). This
process is the same for all the shops you might go to; however, the
response you receive depends on who is waiting to provide a service -- a
butcher, a baker, or a candlestick maker, for instance. This response
represents the remote interface. Each person is different and can
be asked to do different things. You must know that type of person (that
is, the bean) that you are talking to in order to ask the right
questions (that is, to call the right methods) -- it's no good asking a
butcher for a loaf of bread.
- CosNaming, LDAP, and JNDI
- The Java Naming and Directory Interface (JNDI) provides a
standard interface that dictates how you need to interact with a
namespace. LDAP and CosNaming are, for our purposes, types
of JNDI namespaces. To extend our metaphor: JNDI is a template for a
town, while CosNaming and LDAP are specific towns. They operate in
similar ways but there are differences in their layouts.
Properties provide a
map So, let's see how we use all these elements to
successfully call methods on our EJB component from remote machines. In
order for a client program to connect to your carefully crafted EJB
component, it needs several things. First of all, it requires all the JAR
files for the client code, generic EJB-related JAR files like J2EE.jar,
and the stubs and ties that were generated during the deployment of your
bean. Those files get your client as far as the first initial context.
The next bits of information your client needs are values for some
properties. First, you'll need some value for
java.naming.factory.initial . This property points to a class
that provides an initial context factory. A typical value for this
property, which we'll be using for these examples, would be
com.sun.jndi.cosnaming.CNCtxFactory ; this class exists in
rt.jar and as such is part of the base JVM. This factory is
for use by CosNaming nameservers, but the JVM includes an LDAP factory as
well. Different application servers provide their own initial context
factories, as we'll see later.
This class, along with details of nameserver URL and port number, is
used to generate the InitialContext class that you use to
interact with the namespace. However, without the provider URL it will
simply connect to localhost at port 900 (or whatever the
default port number for your context factory is). To connect to a remote
server, you need a value for the property
java.naming.provider.url .
The reason all this is so tricky for new programmers is that when you
run everything local to your app server, this stuff generally just works.
The environment takes care of everything, and when you ask for an
InitialContext , the environment gives you the right one. But
once you move your client to a different machine, you're on your own. You
need to know which JAR files to copy, and what settings to make. I have
known people to copy an app server's entire roster of JAR files to a
second machine just to make their client work correctly!
By default, the InitialContext factory is defined in
jndi.properties , and the factory class has defaults for a
server URL and a port number. This file is found on the classpath (that
generally means in the local directory) or inside any JAR on your
classpath. Different app servers may provide their defaults in different
JAR files; WebSphere Application Server stores a default copy in
namingclient.jar . To specify your own defaults, you simply
need to edit the copy of the file that will be first in the classpath.
This is one way to configure the properties; in the absence of
command-line or code-driven setup, the client will use values from
jndi.properties . However, while this may be appropriate for a
simple set-up, you may want to configure on a client-by-client basis if
you are dealing with multiple servers and namespaces.
How do the values of these properties differ based on the namespace we
want to use? As mentioned above, there are two flavors of JNDI namespace:
CosNaming and LDAP. Each of these has a transport associated with it: IIOP
and LDAP, respectively. An LDAP namespace uses a transport of LDAP (you
would connect to it with a URL like ldap://myldapnameserver )
and CosNaming uses a transport of IIOP (you would connect to it with a URL
like iiop://mycosnamingserver ). The default port number for
CosNaming is 900, while the default for LDAP is 389. However, the default
used by any given server implementation of the namespace may differ.
Configuring properties from the command
line Let's see how to configure the properties from the
command line. If you'd like to play along at home, go to the
bin folder within your JDK install. In this folder, you
should find a program called tnameserv.exe (for Windows) or
simply tnameserv (for UNIX-based systems). Executing this
program will start up a sample CosNaming nameserver on port 900.
Now would be a good time to equip yourself with a utility capable of
viewing a CosNaming namespace. I personally use Eclipse as a development
environment, and I have provided a link to the JNDI Browser plug-in that I
use in the Resources
section below. In theory, you should be able to point a namespace browser
at port 900 on your machine and see a very boring empty namespace
(although some app servers will populate their namespace with many and
varied things by default). To liven up our namespace, we'll now write a
simple program to put something into it, illustrated in Listing 2: Listing 2. A simple cosNaming namespace
interaction
package example.publisher;
import javax.naming.InitialContext;
public class Publish {
public static void main(String[] args) {
//
//This example creates a subcontext in a namespace
//
try{
InitialContext ic = new InitialContext();
ic.createSubcontext("Test");
}catch(Exception e){
System.out.println(e);
e.printStackTrace();
}
}
}
|
This application will assume that all properties required to get the
right initial context will simply be available. So now you can run it from
the command line and supply those properties when you do so (adjusting the
URL to your environment):
java -Djava.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory
-Djava.naming.provider.url=iiop://mymachine:900
example.publisher.Publish
|
All being well, our client will find the context for the example
nameserver and create a subcontext called Test . You can
confirm this by using the namespace browser.
Now try with the nameserver running on one machine and the application
in Listing 2 running on another, using the same command line (though with
the URL adjusted again, of course). It should work with no problem. (You
might want to modify the sample to change what is bound, or even to delete
the subcontext instead of creating it, so that you can be sure that it has
worked the second time you run it.)
Configuring properties within the
application So, what if you don't want to set these options
on the command line? There is an alternative. You can explicitly state
these properties inside your program. This means you don't need to provide
special options to the java command. When we update the code
in Listing 2 to explicitly set the needed properties, it looks like the
code in Listing 3: Listing 3. Simple cosNaming
namespace interaction, with properties set within the app code
package example.publisher;
import javax.naming.InitialContext;
public class Publish {
public static void main(String[] args) {
//
//This example creates a subcontext in a namespace
//
try{
Properties prop = new Properties();
prop.setProperty("java.naming.factory.initial",
"com.sun.jndi.cosnaming.CNCtxFactory");
prop.setProperty("java.naming.provider.url",
"iiop://mymachine:900");
InitialContext ic = new InitialContext(prop);
ic.createSubcontext("Test");
}catch(Exception e){
System.out.println(e);
e.printStackTrace();
}
}
}
|
Now the program requires no lengthy command-line configuration; keep in
mind, however, that an application written this way is hardcoded to these
settings.
Finding your way to your
bean So far, we've seen a few basic examples proving that
we've connected to a remote namespace and done something, though that
something was rather boring -- the creation of a subcontext. In reality,
it is likely that your tooling will do all the creating and publishing of
things for you; what you really want to
do is look up an object. In this section, we will obtain the
Home interface to a HelloWorld bean published in a CosNaming
namespace. Then we'll see how to find one in an LDAP namespace as
well.
For the sake of argument, let's assume that you have a HelloWorld bean
deployed, and that its home interface, HelloWorldHome , is
published at example/HelloWorldHome . (If you want to try
this, but don't want to create a HelloWorld bean yourself, there is a link
to download a prepackaged bean JAR file, as well as a file for a client to
go with it, in the Resources.)
A context tip In the namespace URL
format, you can set your initial context to start further up the
tree than the default. For instance, if you use
iiop://mymachine:900/example for the provider URL for
our example, then you would only need to look up
HelloWorldHome , not
example/HelloWorldHome ; the initial context will be
within example . This can be helpful if you are making
several lookups under the same structure in a namespace; that way,
if you change your setup, the only part of your code that will have
to change is the provider URL. |
In the last section, we did the hard work of connecting to the
nameserver; now all we need to do is look up the EJB component. This
requires us to pass a string to the lookup method, which represents the
directions that get you from the InitialContext (where you
start in the town) to the HomeInterface (the house or shop)
you want. Sounds simple -- but this is where the specific context factory
that you chose could start to affect matters. The factory classes that
come with app servers like WebSphere do not always put you in the root of
the namespace. So the string that we need in order to look up the
HomeInterface changes depending on where in the town your
InitialContext puts you. What's more, the context factory
might put you in a different starting root for a local server than it
would for a remote server.
For this reason, I recommend that the lookup string you use not be
hardcoded as it was in Listing 3, but rather passed from a command line or
properties file. This is particularly true in architectures with multiple
steps. For instance, you might have a client that calls an EJB component;
that bean might in turn need to call a second EJB component, which could
potentially be on a different server! In such a situation, the properties
should be passed through for each step. This provides a simple mechanism
for trial-and-error lookups, and also leaves the end application flexible
to relatively minor changes, such as deployment on a new nameserver. So
let's look at a sample lookup application. In Listing 4, properties are
set programmatically, but based off command-line values. This results in a
slightly different looking command line than the one in our previous
example, as we'll see. Listing 4. Looking up a home
interface
package example.lookup;
import java.util.Properties;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import example.HelloWorld;
import example.HelloWorldBean;
import example.HelloWorldHome;
import javax.naming.InitialContext;
public class Lookup {
public static void main(String[] args) {
//
//This example looks up the home interface of an EJB to a namespace
//
try{
Properties prop = new Properties();
prop.setProperty("java.naming.factory.initial",args[0]);
prop.setProperty("java.naming.provider.url",args[1]);
InitialContext ic = new InitialContext(prop);
Object or = ic.lookup(args[2]);
if (or != null) {
// Narrow the return object to the Home class type
HelloWorldHome home =
(HelloWorldHome)PortableRemoteObject.narrow(or,
HelloWorldHome.class);
// Create an EJB object instance using the home interface.
HelloWorld hw = home.create();
// Invoke the method
System.out.Println(hw.hello());
}
}catch(Exception e){
System.out.println(e);
e.printStackTrace();
}
}
}
|
This program is invoked with three parameters: the context factory to
be used, the provider URL, and the string containing the name to look for.
We already know what the first two are; what is the third?
Well, if you are still using tnameserv as your name
server, then you will likely have published your bean directly to
/example/HelloWorldHome . In that case, simply passing
/example/HelloWorldHome as the third parameter should result
in a successful lookup. However, if the nameserver you are using has a
more complex namespace, you may have some additional levels imposed by the
deployment tool used. WebSphere, for instance, deploys JavaBean components
by default into ejb/ , but this is not at the root of the
namespace, and only if you use WebSphere's context factory will you be
placed at the right place in the namespace by passing in the string
/ejb/example/HelloWorldHome . The problem will be exacerbated
if you use a context factory that is different from the one supplied by
the application server (as you may need to do if you run a client from a
machine that just has a standard Java installation). However, the
nameserver documentation for your application server should explain where
you will start in the namespace when you make a lookup for an EJB
component. Look at the examples in the documentation and then look at the
namespace with a browser to establish where their clients'
InitialContext puts them. Namespaces tend to loop, so you can
try to follow a branch to infinity. This does mean that from most starting
contexts you can find a path that leads to the home you want.
At any rate, here's the command line that would pass the appropriate
parameters to the application in Listing 4:
java Lookup com.sun.jndi.cosnaming.CNCtxFactory
iiop://mymachine:900 example/HelloWorldHome
|
With CosNaming, the namespace subcontexts are separated by the slash
(/ ) character, like a standard URL. The syntax for LDAP is
different, as we'll see.
Introducing LDAP Now
let's add LDAP into the picture. For our purposes here, LDAP is another
JNDI namespace, but one whose structure is represented in a manner that's
quite different from that for a CosNaming namespace. It also requires a
different context factory -- but that's not a problem, because we'll be
specifying the appropriate factory on the command line anyway. (In our
example, we'll be using the one that is part of the base JVM, but again
keep in mind that different application servers may have their own
factories.) And it requires a pointer to a different nameserver -- and,
lucky us, we're specifying that on the command line, too. Of course, the
string that represents the location of the home interface is different,
but guess what? That's right, we specify that one on the command line as
well. You can see the benefits of using these command-line calls: all we
need to do is change how we call our test program, and in theory we can
get to any JNDI nameserver, even switching from CosNaming to LDAP without
a hitch and without changing any code. Well, that's the theory, anyway; of
course, the trick is getting the parameters right.
Some nameservers protect part of the namespace, meaning that you can
publish only to those areas where you are allowed. Let's assume that you
have an LDAP server running, and these are its details:
- URL:
ldap://mymachine:1389
- BaseDN:
c=myldap
The structure of the tree in the LDAP namespace will generally look
something like this:
ibm-wsnName=MyServer,ibm-wsnName=HelloWorldHome
|
However, when we pass the string to our program, we need to reverse it
(don't ask me why). So the string we use looks like this:
ibm-wsnName=HelloWorldHome,ibm-wsnName=MyServer,
|
The BaseDN represents the location where you want to start in the
namespace. This could be many things for a given LDAP nameserver,
depending on how it is structured. In this example, we go straight to a
root of c=myldap . But if we wanted to jump to a tree in the
namespace, we could specify ibm-wsnTree=myTree,c=myldap as a
BaseDN instead jumping to that point.
So, the command-line parameters we'll pass to our program will look
like this:
java Lookup com.sun.jndi.ldap.LdapCtxFactory ldap://mymachine:1389/c=myldap/
ibm-wsnName=HelloWorldHome,ibm-wsnName=MyServer
|
Potential error messages Despite
everything, you will no doubt become familiar with various exception
messages while deploying EJB components. Here are a few that you'll
probably see a lot (I know that I have), along with some of the
reasons you might have encountered them:
CORBA.OBJECT_NOT_EXIST : You've looked in the
wrong place for your EJB.
CORBA.MARSHALL _ something : CORBA marshall
exceptions occur often. They have different details but they all
basically mean the same thing: you got some data, but not what
your client was expecting. Maybe you looked up the wrong thing,
maybe the versions of the EJB component classes your client knows
about are different from the versions actually deployed. Or maybe
there is a problem and the client ORB just doesn't understand what
the server ORB sent.
javax.naming.NoInitialContextException : Whoops!
You failed to specify the context factory or provider URL, or
maybe the nameserver just isn't
running. |
Here we specify an LDAP context factory, then pass in the name of our
LDAP server with the point at which we want to start. This is followed by
the reversed path to the EJB component that we want to look up. And we can
use this command line to invoke the same code (Listing 4) that
we used in our CosNaming example.
Of course, there is no reason why the code used in this article could
not form a method of a helper class -- something that takes in three
parameters, and returns the Object or that is returned from
the ic.lookup(args[2]) call before attempting to do any
narrowing. Then, wherever you need to have a lookup done, you could simply
use the helper class, pass it the appropriate parameters for your current
situation, and get back the object reference you need, ready to narrow to
its real class. (Note: I do not
guarantee such a class's performance and am providing these code fragments
"as is," without warranty of any kind by me or IBM.) Of course, a
completely generic approach can be achieved using reflection, but that
makes life a lot more complicated, and it's outside the scope of this
article.
There's one final thing to consider before we conclude. You could write
a client that combines the techniques we used in Listings 3 and 4. It
would check to see if a value had been given at the command line; if so,
it would then set those values; if not, it would use the hardcoded values.
In that way, you could have sensible defaults in a program, but override
them with command-line options if you wanted. Only small and fairly
trivial adjustments to code would be required.
Safe at home So to review:
here are the top four things you should have or know to make any system
work with multiple EJB lookups and multiple app servers:
- At any given stage, all the stubs and ties of the next stage must be
on the classpath. You can't narrow an object for use unless the local
environment knows what the class looks like.
- Each stage requires the generic EJB-related JAR files, such as
J2EE.jar .
- Pass the context factory type, nameserver name, and JNDI lookup
string as parameters. This allows for easy accommodation of
change.
- Know your namespace. Remember that your JNDI lookup string needs to
take you from the location where you start in the namespace to the
location where your object is stored. But you won't always start in the
same place! Get a tool to browse the namespace, and learn where you
start for local and remote lookups.
Navigating through remote namespaces can be a baffling process for
developers used to writing code that all executes on the same machine.
Hopefully, the tips and code in this article will help you get your
distributed EJB applications up and running. Once you've mastered the JNDI
namespaces, go and check out developerWorks' EJB best
practices series by Brett McLaughlin (see Resources) for
great tips on optimizing your code.
Resources
About the
author Daniel Would joined IBM in July 2001. He worked as
a CTG System Tester for one year before moving into his current role
as a CICS System Tester. In his time at IBM, Daniel's work has
focused on Java technology and EJB components. Contact Daniel at wouldd@uk.ibm.com. |
|
|