Naming and Directory ServicesA
Naming Service provides a mechanism for giving names to objects so that you can retrieve and use those objects without knowing the location of the object.
A
Directory Service associates names with objects but provides additional information by associating attributes with the objects.
Why Use a Naming ServiceNaming Services provide an indispensable mechanism for decoupling the provider of a service from the consumer of the service. Users of the service need only know the name of the service to use it.
What is JNDI?JNDI is a Java API that defines an interface to Naming and Directory Services for Java programs. To use JNDI, an implementation of a Naming and Directory must be available. JNDI provides a service-independent interface to underlying Service Provider implementation. Naming services can be plugged into the JNDI by implementing the
Service Provider Interface (SPI) for JNDI. Common naming services include DNS, NDS (Novell Directory Services), NIS (Network Information Service), LDAP, CORBA naming systems, and Windows Active Directory.
Use JNDIRunning a JNDI-aware program requires a JNDI service to be running and the classes for that service to be available to the program. By default, running a J2EE server starts a naming service on the same machine.
Obtaining an Initial ContextThe first step in using the JNDI name service is to get a context in which to add or find names. The context that represents the entire namespace is called the
Initial Context and is represented by a class called
javax.naming.InitialContext and is a sub-class of the
javax.naming.Context class.
A
Context object represents a context that you can use to look up objects or add new objects to the namespace. You can also interrogate the context to get a list of objects bound to that context.
The following code createa an initial context using the default JNDI service information:
Context ctx = new InitialContext();If something goes wrong when creating the initial context, a
NamingException is thrown.
Initial Context Naming ExceptionsThe runtime system reports errors in using JNDI as a subclass of
NamingException. The exceptions most likely to occur for accessing the initial context include:
CommunicationException, NoInitialContextException, and
ServiceUnavailableException.
(1) Defining the JNDI Service.
Parameters used to define for the JNDI service include: JNDI service classname, server's DNS host name, socket port number.
There are several ways of defining the JNDI service properties for a program including:
- Add the properties to the JNDI properties file in the Java runtime home directory;
- Provide an application resource file for the program. An application resource file called
jndi.properties defines the JNDI service. The JNDI system automatically reads the application resource files from all components in the program's CLASSPATH and from
lib/jndi.properties in the Java runtime home directory (
jre sub-directory of the JDK home directory). An example file of
jndi.properties is as follows:
java.naming.factory.initial = com.sun.enterprise.naming.SerialInitContextFactoryjava.naming.provider.url = localhost:1099java.naming.factory.url.pkgs = com.sun.enterprise.namingWe can either configure every client's Java home directory to include the necessary JNDI properties or include a suitable JNDI properties file with the client program and distribute everything as a JAR file;
- Specify command-line parameters to be passed to an application. Using the
-D option, you can supply the JNDI properties on the
java command line of an application. Here's an example:
java -D java.naming.factory.initial = com.sun.enterprise.naming.SerialInitContextFactory -D java.naming.provider.url = localhost:1099 MyClass- Specify parameters to be passed into an applet. For example:
<applet code="MyApplet.class" width="640" height="480"><param name="java.naming.factory.initial" value="com.sun.enterprise.naming.SerialInitContextFactory><param name="java.naming.provider.url" value="localhost:1099"></applet>- Hard-code the parameters into the program. This is the least desirable way to specify the JNDI properties. The mechanism for defining the service in code is via a hash table of properties passed into the
InitialContext constructor. Here is an example:
Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.enterprise.naming.SerialInitContextFactory");env.put(Context.PROVIDER_URL, "localhost:1099");Context ctx = new InitialContext(env);Here the code uses symbolic constants from the
Context class rather than using strings representing the properties (such as "
java.naming.factory.initial"), which makes the code more portable if the property names change in the future versions of Java.
The first two options are the most suited to production environments. They both require you to distribute simple text configuration files with the program.
Binding JNDI ObjectsAfter the initial JNDI context has been obtained, a program can look up existing objects and bind new objects to the context. Binding an object means adding a name to the JNDI service and associating that name with a Java object. The name and object are bound to a context. Here's an example of binding a text message to a name:
import javax.naming.*;public class JNDIBind{private final static String JNDI = "Yelei/j2ee";public static void main(String[] args){try{Context ic = new InitialContext();ic.bind(JNDI, "Learning J2EE as quick as possible");System.out.println("Bound "+JNDI);}catch(NamingException ex){System.err.println(ex);System.exit(1);}}}Note that the object to be bound must implement the
Serializable interface so that the name server can store a copy of the object.
A bound object normally remains in the namespace until it is unbound. If the bound name remains across server restarts, it is said to be persistent. Commercial servers like NDS, Active Directory, and LDAP are persistent name servers and store the bound names and ancilliary information on disk. Usually the naming services for J2EE product is transient, and it reloads bound objects from configuration files in the SDK home directory whenever it is restarted. The naming service will not retain objects bound with the
Context.bind() method when server restarts.
We can use the
rebind() method to solve the problem of
bind() failing if a name is already bound. For example:
ic.rebind("Yelei/j2ee", "Learn J2EE as quick as possible");The code unbinds any existing object bound to that name and replace it with the new object.
We can remove an object from a namespace by using the
Context.unbind() method. Another use for
unbind() is to test if a name is already in use and unbind the old object before binding a new object. The advantage of using
unbind() over use
rebind() is that you can verify that the object to be unbound is at least of the same type as the new object to be bound:
String JNDI = "Yelei/j2ee";try{Object o = ic.lookup(JNDI);if(o instanceof String)ic.unbind(JNDI);}catch(NameNotFoundException nnfe){}ic.bind(JNDI, "Learn J2EE as quick as possible");We can rename objects using
Context.rename() by specifying the old name and then the new name as parameters. An object must be bound to the old name and the new name must not have a bound object. Otherwise, a
NamingException is thrown.
ic.rename("Yelei/j2ee", "Yelei/learnJ2EE");JNDI Name LookupThe most common use of JNDI is to look up objects that have been bound to a name. To do this, you require two pieces of information: the JNDI name, and the class of the bound object. Here's an example:
import javax.naming.*;public class JNDILookup{private final static String JNDI = "Yelei/j2ee";public static void main(String[] args){try{Context ic = new InitialContext();String name = (String)ic.lookup(JNDI);System.out.println(JNDI+" = "+name);}catch(NamingException ne){... ...}catch(ClassCastException cce){... ...}}}The above example is an example of a
Composite Name. If you need to look up many names in the same context of a composite name (names of the form
Yelei/...), it's better to change to sub-context and look up the simple name with that context. Here's an example:
import javax.naming.*;public class JNDILookupYelei{public static void main(String[] args){try{Context ic = new InitialContext();Context ctx = (Context)ic.lookup("Yelei");String name = (String)ctx.look("j2ee");System.out.println(name);}catch(NamingException ne){}catch(ClassCastException cce){}}}When dealing with RMI over IIOP object, we cannot cast the returned object the required class; instead, we must
narrow it. The implementation of J2EE requires the use of RMI-IIOP to implement the remote interfaces to EJB components. RMI-IIOP uses a portable remote object to encapsulate information about the real remote object. A portable remote object contains information about the real bound object in a portable format that can be interogated by the recipient to find the real remote object. This process of obtaining the real object from the portable remote object is called
narrowing.
We can use the
PortableRemoteObject.narrow() method in the javax.rmi package to narrow a portable remote object to obtain the actual remote object. This method takes 2 parameters: the object to narrow, a
java.lang.Class object defining the real remote object's class. For example:
InitialContext ic = new InitialContext();Object lookup = ic.lookup("java:com/env/ejb/Agency");AgencyHome home = (AgencyHome) PortableRemoteObject.narrow(lookup, AgencyHome.class);ContextsContexts provide a hierarchical structure to JNDI names, and composite names group together related names. The initial context provides a top-level view of the namespace and any sub-contexts reflect the hierarchical composite name structure.
You can obtain a listing of the names in a context by using
Context.list(). This method returns a list of name and class bindings of type
javax.naming.NamingEnumeration, where each element in the enumeration is a
javax.NameClassPair object. Here's an example:
import javax.naming.*;
public class JNDIList { public static void main(String[] args){try{ Context ctx=new InitialContext(); NamingEnumeration list = ctx.list("Yelei"); while(list.hasMore()){ NameClassPair item=(NameClassPair)list.next(); String cl= item.getClassName(); String name=item.getName(); System.out.println(cl+":"+name); } }catch(NamingException ne){ ne.printStackTrace(); System.exit(1); } }}The parameter to the
list() method defines the name of the context to list. It this is the empty string, the method lists the current context. If you want to list the initial context of the J2EE RI namespace, you must have the J2EE RI classes in your search path. Otherwise, you will get an
org.omg.CORBA.BAD_PARAM exception caused by another CORBA exception.
The list() method returns the name and the bound object's class name, but not the object itself. It is a lightweight interface designed for browsing the namespace. A second method, called Context.listBindings(), retrieve the object itself. This method returns a NamingEnumeration, where each element is of type javax.naming.Binding. Here's an example:
import javax.naming.*;public class JNDITree{public static void main(String[] args){Context ctx = null;try{ctx = new InitialContext();listContext(ctx, "");}catch(NamingException ne){ne.printStackTrace();System.exit(1);}}private static void listContext(Context ctx, String indent){try{NamingEnumeration list = ctx.listBindings("");while(list.hasMore()){Binding item = (Binding)list.next();String className = item.getClassName();String name = item.getName();System.out.println(indent+className+" "+name);Object o = item.getObject();if(o instanceof javax.naming.Context)listContext((Context)o, indent+"");}}catch(NamingException ex){System.err.println("List error: "+ex);System.exit(1);}}}Binding a composite name will automatically create an intermediate sub-context required to bind the name. For example, binding the name
Yelei/j2ee creates sub-contexts "Yelei" and then "Yelei/j2ee" if they don't already exist. We can also create contexts using the
Context.createSubcontext() method. This method require a single parameter, the name of the context. If this is a composite name, all intermediate contexts must already exist.
The
Context.destroySubcontext() method can destroy contexts. The method removes from the namespace any bound names and sub-contexts with the destroyed context.
More on JNDI NamesIt is conventional to use composite names to group related names. As an example, JDBC data sources take names of
jdbc/xxx, and EJBs uses the form
ejb/xxx.
Incidentally, JNDI calls structured names like the DNS and LDAP
compound names. JNDI does not interpret compound names, but simply passes them through to corresponding name service providers.
Besides forward slash (/), JNDI also treats backslash (\), single quote ('), and double quote (") as special characters. If a compound name or a component of a name contains any of these characters, they must be escaped using the backslash character.
You can specify a URL as a parameter to the
lookup() and
bind() methods in the
Context class. For example:
Context ic = new InitialContext();
Object obj = ic.lookup("ldap://localhost:389/cn=Winston,dc=my-domain,dc=com");
This overrides the default context and forces JNDI to perform the lookup against the specified server. Here the class path must contain the necessary service provider classes, and these must be able to process the request bind or lookup operation. In practice, this means that the URL must use the same service provider classes as the initial context.
AttributesAttributes are a feature of a Directory service and are not available with simple name servers. Typically, you use attributes with an LDAP server. CORBA name server does not support attributes.
An attribute is additional information stored with a name. Directory services usually support searching for names (objects) that have certain attributes defined (or not defined). LDAP uses a schema system to control which attributes an object must define and those that it may define. Any attributes that you add or delete must not break the schema's requirements.
Overview of LDAP X.500 NamesLDAP names conform to the X.500 standard that requires a hierarchical namespace. A
Distinguished Name (DN) unambiguously identifies each entry in the directory. The DN consists of the concatenation of the names from the root of the directory tree down to the specific entry. Different implementations of X.500 may use different syntax for representing object names.
LDAP uses a comma-separated list of names with the names specified from the lowest entry up the tree to the higher entries. Names consist of name and value pairs with names typically being those in the following list:
- c (countryName): ISO two letter code for country such as cn, us.
- o (organizationName): organization or company name.
- ou (organizationUnitName): orgainational unit, typically a division or departement within an organization.
- l (localityName): typically defines a location within an organizational unit.
- cn (commonName): sometimes called personal name, usually the name of the user or client.
- dc (domainComponent): a component part of a domain name.
- uid (userid): typically represents a login name.
An example LDAP DN looks like the following:
cn=Yelei Zhang, ou=Developers, o=Tick, c=cnUsing an LDAP Directory Service requires the JNDI properties to specify the JNDI Service Provider from Sun Microsystems and to have an LDAP server running. To configure the JNDI properties to use LDAP, the simplest way is to create an empty
jndi.properties file in current directory and add the following lines to this file:
java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactoryjava.naming.provider.url=ldap://localhost:389The operations on LDAP service are similar to above examples except that there are more operations like manipulating attributes, searching objects, and so forth.
More on ObjectsLoading Classes from a Code BaseJava class files can be made available via a web server. A
javaCodebase attribute supplies the details of the web server location when binding the object into the JNDI directory namespace. Here is an example of binding a
Book object to the LDAP namespace (Book class file is available via a web server):
import javax.naming.*;
import javax.naming.directory.*;
public class JNDICodebase{
private final static String JNDI = "cn=book, o=Agency, c=us";
private final static String codeURL = "http://localhost:8000/classes";
public static void main(String[] args){
try{
DirContext ic = new InitialDirContext();
Book book = new Book("J2EE");
Attributes attrs = new BasicAttributes();
attrs.put("javaCodebase", codeURL);
attrs.put("cn", "book");
ic.rebind(JNDI, book, attrs);
System.out.println("Bound "+JNDI);
}catch(NamingException ex){
System.err.println(ex);
System.exit(1);
}
}
}
With the name registered and the HTTP server running, the client can look up the bound object without having the
Book class file in the search path.
ReferencesSometimes, storing a serialized copy of an object in the Directory Service is inappropriate. JNDI references provide a mechanism for storing an object by reference rather than by value. This mechanism only works if the underlying JNDI Service Provider supports
Referenceable objects. A reference to an object in this case require 2 related classes:
- the
Object class that must implement the javax.naming.Referenceable interface.
- a
Factory class that can create the required objects.
What Else can JNDI Do?JNDI EventsJNDI supports an event model similar to the event listeners in the Java GUI programming. However, the underlying JNDI service provider must also provide support for this model for a client to register event handlers.
The
javax.naming.event package support two types of JNDI event listeners:
NamespaceChangeListener (reports on changes of namespace), and
ObjectChangeListener (reports on changes on object bindings). You use the
EventContext.addNamingListener() method to register a
NamingListener object against a context. Adding and removing a listener requires the context to implement the
EventContext. Here is an example:
Context ic = new InitialContext();EventContext ctx = (EventContext)ic.lookup("Yelei");NamingListener listener = new MyNamingListener();ctx.addNamingListener("book", EventContext.ONELEVEL_SCOPE, listener);SecurityJNDI security depends on the underlying service provider. Simple services such as RMI and CORBA name service do not support security.
Most live J2EE implementations will make use of LDAP to provide a naming service that supports security. LDAP security is based on 3 categories: anonymous, simple, and
Simple Authentication and Security Layer (SASL). The security information is provided in the JNDI properties
java.naming.security.authentication,
java.naming.security.principal,
java.naming.security.credentials.
If you do not define any of these properties, the implementation uses anonymous authentication. You can use a JNDI properties file to supply client authentication information, but more usually you code the information within the client program. If you use strong authentication, the
java.naming.security.authentication value can consist of a space-separated list of authentication mechanisms. JNDI can support authentication schems such as external,
GSSAPI (Kerberos v5), and
Digest MD5.