命名和目录操作
您可以使用 JNDI 执行以下操作: 读取操作和更新命名空间的操作本节介绍这两个操作:
l 查询对象
l 列出上下文内容
l 添加覆盖和移除绑定
l 重命名对象
l 创建和销毁子上下文
配置
在命名和目录服务中执行操作之前, 需要得到初始化上下文命名空间的开始点因为命名和目录服务的所有方法都相对于一些上下文执行
为了得到初始化上下文, 必须执行以下步骤:
1. 选择想要访问的访问提供者
2. 指定需要的初始化上下文
3. 调用 InitialContext 构造函数
第一步: 为初始化上下文选择服务提供者
您可以为初始化上下文指定服务提供者, 创建一个环境变量集合(Hashtable), 同时将服务提供者的名称加入其中环境属性在 JNDI 教程中有详细的介绍
如果您使用 Sun 的 LDAP 服务提供者, 代码如下所示:
Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); |
要指定 Sun 的文件系统服务提供者, 代码如下所示:
Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.fscontext.RefFSContextFactory"); |
您可以使用一些系统属性描述使用的服务提供者在 JNDI 教程中有详细描述
第二步: 提供初始化上下文需要的信息
不同目录的客户端可能需要提供不同的信息用来连接目录例如, 您需要指定服务器运行的机器以及识别目录中的用户这些信息通过环境属性传递给服务提供者 JNDI 指定服务提供者使用的一般环境参数您的服务提供者文档会为需要提供的参数进行详细的说明
LDAP 提供者需要程序提供 LDAP 服务器的位置, 以及认证信息要提供这些信息, 需要如下代码:
env.put(Context.PROVIDER_URL, "ldap://ldap.wiz.com:389"); env.put(Context.SECURITY_PRINCIPAL, "joeuser"); env.put(Context.SECURITY_CREDENTIALS, "joepassword"); |
本教程中使用 Sun 的 LDAP 服务提供者例子中假设服务器设置在本机, 使用 389 端口, 根辨别名是 o=JNDITutorial, 修改目录不需要认证这些信息是设置环境所需要的
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial"); |
如果您使用不同设置的目录, 需要设置相应的环境属性您需要使用机器名称替换 localhost 您可以在任何公共的目录服务器或在其他机器上的自己的服务器运行例子您需要将 localhost 替换成那台机器的名字, 将 o=JNDITutorial 替换成相应的命名上下文
第三步: 创建初始化上下文
您已经创建了初始化上下文为了做到这一点, 您将之前创建的环境属性放到 InitialContext 构造函数中:
Context ctx = new InitialContext(env); |
现在您有了一个上下文对象的引用, 可以开始访问命名服务了
为了完成目录操作, 需要使用 InitialDirContext 为了做到这一点, 使用它的一个构造函数:
DirContext ctx = new InitialDirContext(env); |
这句话返回了用来进行目录操作的 DirContext 对象引用
命名异常
JNDI 包中的很多方法当抛出异常时, 说明操作请求不能执行一般情况下, 您将看到可以抛出 NamingException 的方法使用 try/catch 进行包装
try { Context ctx = new InitialContext(); Object obj = ctx.lookup("somename"); } catch (NamingException e) { // Handle the error System.err.println(e); } |
异常类结构
JNDI 有丰富的异常结构, 所有异常都从 NamingException 类中继承异常类名都是自解释的, 在下文中进行列举
如果要处理特定的 NamingException 子类, 需要分别 catch 子类例如, 以下代码特别的对待 AuthenticationException 及其子类
try { Context ctx = new InitialContext(); Object obj = ctx.lookup("somename"); } catch (AuthenticationException e) { // attempt to reacquire the authentication information ... } catch (NamingException e) { // Handle the error System.err.println(e); } |
枚举
诸如 Context.list()和 DirContext.search()这种操作返回 NamingEnumeration 在这些情况下, 如果出现错误并且没有返回结果, NamingException 或它的子类会在方法请求时抛出如果出现错误并且返回了部分结果, 返回 NamingEnumeration 这样您可以取得这些结果当所有结果都取出来后, 再请求 NamingEnumeration.hasMore()会导致抛出 NamingException(或其子类)异常, 表示出现错误在这种情况下, 枚举变得非法并且不能再请求其中任何方法
例如, 如果执行 search()并且指定最多返回多少结果, 那么 search()最多返回 n 个结果如果结果超过 n 个, 那么当第 n+1 次请求 NamingEnumeration.hasMore()时, 抛出 SizeLimitExceededException 请参见本节中的有关 limit 的示例代码
本手册中的例子
在本手册文件中的在线示例代码中, 通常为了便于阅读省略了 try/catch 语句通常, 因为只有部分代码片段在这里展示, 所以只展示直接表示概念的行如果您查看本教程附带的源码文件, 将看到 try/catch 语句的合适位置
javax.naming 包中异常在这里可以看到
查询对象
要从命名服务中查询对象, 使用 Context.lookup()方法并且传入您想取得的对象名假设命名服务中有一个对象的名称是 cn=Rosanna Lee,ou=People 要取得这个对象, 您只需要编写:
Object obj = ctx.lookup("cn=Rosanna Lee,ou=People"); |
lookup()返回的对象类型依赖于命名服务以及对象关联的数据命名服务可以包含许多不同类型的对象, 同时在系统的不同部分查询的对象可能得到不同的类型例如, cn=Rosanna Lee,ou=People 绑定到上下文对象中 (javax.naming.ldap.LdapContext) 您可以对 lookup()方法的结果 cast 成需要的类
例如, 以下代码查询 cn=Rosanna Lee,ou=People 对象并且 cast 成 LdapContext
import javax.naming.ldap.LdapContext; ... LdapContext ctx = (LdapContext) ctx.lookup("cn=Rosanna Lee,ou=People"); |
完整的例子在 Lookup.java 文件中
在 Java SE 6 中查询名称引入了两个新的静态方法:
- l InitialContext.doLookup(Name name)
- l InitialContext.doLookup(String name)
这些方法提供了不实例 InitialContext 查找对象的快捷方式
列举上下文
代替 Context.lookup()一次取得一个对象的方法, 您可以在一个单一的操作中列举整个剩下文列举上下文有两个方法: 一个返回了绑定关系, 另一个只返回名 - 对象类型名
Context.List()方法
Context.list()返回 NameClassPair 的枚举每个 NameClassPair 包含对象名和对象类型名下列代码列举了 ou=People 目录的内容(例如, ou=People 目录中找到的文件和目录)
NamingEnumeration list = ctx.list("ou=People"); while (list.hasMore()) { NameClassPair nc = (NameClassPair)list.next(); System.out.println(nc); } |
返回值如下:
# java List cn=Jon Ruiz: javax.naming.directory.DirContext cn=Scott Seligman: javax.naming.directory.DirContext cn=Samuel Clemens: javax.naming.directory.DirContext cn=Rosanna Lee: javax.naming.directory.DirContext cn=Maxine Erlund: javax.naming.directory.DirContext cn=Niels Bohr: javax.naming.directory.DirContext cn=Uri Geller: javax.naming.directory.DirContext cn=Colleen Sullivan: javax.naming.directory.DirContext cn=Vinnie Ryan: javax.naming.directory.DirContext cn=Rod Serling: javax.naming.directory.DirContext cn=Jonathan Wood: javax.naming.directory.DirContext cn=Aravindan Ranganathan: javax.naming.directory.DirContext cn=Ian Anderson: javax.naming.directory.DirContext cn=Lao Tzu: javax.naming.directory.DirContext cn=Don Knuth: javax.naming.directory.DirContext cn=Roger Waters: javax.naming.directory.DirContext cn=Ben Dubin: javax.naming.directory.DirContext cn=Spuds Mackenzie: javax.naming.directory.DirContext cn=John Fowler: javax.naming.directory.DirContext cn=Londo Mollari: javax.naming.directory.DirContext cn=Ted Geisel: javax.naming.directory.DirContext |
Context.listBindings()方法
Context.listBindings()方法返回绑定的枚举绑定是 NameClassPair 的子类绑定不止包含对象名和对象类名, 还包含对象以下代码片段枚举了 ou=People 上下文, 打印出每一个绑定名称和对象
NamingEnumeration bindings = ctx.listBindings("ou=People"); while (bindings.hasMore()) { Binding bd = (Binding)bindings.next(); System.out.println(bd.getName() + ":" + bd.getObject()); } |
返回的结果如下:
# java ListBindings cn=Jon Ruiz: com.sun.jndi.ldap.LdapCtx@1d4c61c cn=Scott Seligman: com.sun.jndi.ldap.LdapCtx@1a626f cn=Samuel Clemens: com.sun.jndi.ldap.LdapCtx@34a1fc cn=Rosanna Lee: com.sun.jndi.ldap.LdapCtx@176c74b cn=Maxine Erlund: com.sun.jndi.ldap.LdapCtx@11b9fb1 cn=Niels Bohr: com.sun.jndi.ldap.LdapCtx@913fe2 cn=Uri Geller: com.sun.jndi.ldap.LdapCtx@12558d6 cn=Colleen Sullivan: com.sun.jndi.ldap.LdapCtx@eb7859 cn=Vinnie Ryan: com.sun.jndi.ldap.LdapCtx@12a54f9 cn=Rod Serling: com.sun.jndi.ldap.LdapCtx@30e280 cn=Jonathan Wood: com.sun.jndi.ldap.LdapCtx@16672d6 cn=Aravindan Ranganathan: com.sun.jndi.ldap.LdapCtx@fd54d6 cn=Ian Anderson: com.sun.jndi.ldap.LdapCtx@1415de6 cn=Lao Tzu: com.sun.jndi.ldap.LdapCtx@7bd9f2 cn=Don Knuth: com.sun.jndi.ldap.LdapCtx@121cc40 cn=Roger Waters: com.sun.jndi.ldap.LdapCtx@443226 cn=Ben Dubin: com.sun.jndi.ldap.LdapCtx@1386000 cn=Spuds Mackenzie: com.sun.jndi.ldap.LdapCtx@26d4f1 cn=John Fowler: com.sun.jndi.ldap.LdapCtx@1662dc8 cn=Londo Mollari: com.sun.jndi.ldap.LdapCtx@147c5fc cn=Ted Geisel: com.sun.jndi.ldap.LdapCtx@3eca90 |
结束 NamingEnumeration
NamingEnumeration 可以通过三种方式终止: 一般的, 显式的, 非显式的
l 当 NamingEnumeration.hasMore()返回 false, 枚举结束同时终止
l 您可以在枚举终止前请求 NamingEnumeration.close()方法显式的终止一个枚举这样做提示底层实现释放任何和枚举有关的资源
l 如果 hasMore()或 next()方法抛出任何异常, 枚举立即终止
不管如何终止枚举, 枚举一旦被终止就不能再使用在一个已终止的枚举中请求任何方法都会导致不确定的结果
为什么使用两个不同的方法?
list()为浏览类型的应用程序准备, 只返回上下文中对象的名字例如, 浏览器可能列出上下文中的名字, 期待用户选择一个或多个显示的名称来进行后续操作这些应用程序一般不需要访问上下文中所有的对象
listBindings()为需要在上下文对象中进行操作的应用程序准备例如, 备份程序需要在文件目录中所有对象中执行 file stats 操作或者, 一个打印机管理员程序可能想要重启建筑物内的所有打印机为了执行这些操作, 需要得到上下文中的所有对象因此, 将对象作为枚举的一部分返回是权宜之计
应用程序可以依据需要的信息类型选择 list()或 listBindings()
添加替换或删除绑定
Context 接口包含在上下文中添加替换删除绑定的方法
添加绑定
Context.bind()为了向上下文中添加绑定, 它以对象类型以及需要绑定的对象作为参数
在继续前: 本教程中的例子需要您对架构做额外的修改您必须关闭 LDAP 服务器的架构检测或将符合本教程的架构添加到服务器中这种工作一般由目录服务器管理员执行请看课程
// Create the object to be bound Fruit fruit = new Fruit("orange"); // Perform the bind ctx.bind("cn=Favorite Fruit", fruit); |
这个例子创建了一个 Fruit 类的对象同时在上下文 ctx 中将他绑定到名称 cn=Favorite Fruit 中如果您随后在 ctx 中查询 cn=Favorite Fruit, 那么您将得到 fruit 兑现注意, 编译 Fruit 类需要 FruitFactory 类
如果您运行这个例子两次, 那么第二次会因为 NameAlreadyBoundException 异常失败因为 cn=Favorite Fruit 已经绑定了要第二次运行时不失败, 需要使用 rebind()
添加或修改绑定
rebind()用来添加或替换绑定它的参数列表和 bind()一样, 但如果名称已经绑定, 那么首先会 unbound 然后再重新绑定新的对象
// Create the object to be bound Fruit fruit = new Fruit("lemon"); // Perform the bind ctx.rebind("cn=Favorite Fruit", fruit); |
当您运行这个例子时, 将会替换 bind()例子中已经创建的绑定关系
删除绑定
要删除绑定, 使用 unbind()
// Remove the binding ctx.unbind("cn=Favorite Fruit"); |
当这个例子运行时, 删除 bind()或 unbind()创建的绑定关系
重命名
您使用 Context.rename()对上下文中的对象进行重命名
// Rename to Scott S ctx.rename("cn=Scott Seligman", "cn=Scott S"); |
这个例子将绑定到 cn=Scott Seligman 的对象绑定到了 cn=Scott S 中在验证对象被重命名后, 程序再将其重命名回原来的名字(cn=Scott Seligman), 如下所示:
// Rename back to Scott Seligman ctx.rename("cn=Scott S", "cn=Scott Seligman"); |
更多关于 LDAP 中重命名的例子请参考 LDAP 的高级注意
创建和销毁子上下文
Context 接口包含创建和销毁一个子上下文子上下文是绑定到其他上下文的上下文
这个例子使用一个有属性的对象, 然后在目录中创建子上下文您可以使用 DirContext 的方法将属性和对象在绑定或子上下文添加到名字空间时进行关联例如, 您可以创建 Person 对象, 然后在为 Person 对象关联属性的同时将他绑定到命名空间中命名等于没有任何属性
createSubcontext()和 bind()不同, 他创建了一个新对象, 例如一个要绑定到目录中的新上下文, 但 bind()在目录中绑定了给定的对象
创建上下文
要创建命名上下文, 您向 createSubcontext()提供要创建上下文的名称要创建有属性的上下文, 向 DirContext.createSubcontext()提供想要创建上下文的名称以及需要的属性
在继续前: 本教程中的例子需要您对架构做额外的修改您必须关闭 LDAP 服务器的架构检测或将符合本教程的架构添加到服务器中这种工作一般由目录服务器管理员执行请看课程
// Create attributes to be associated with the new context Attributes attrs = new BasicAttributes(true); // case-ignore Attribute objclass = new BasicAttribute("objectclass"); objclass.add("top"); objclass.add("organizationalUnit"); attrs.put(objclass); // Create the context Context result = ctx.createSubcontext("NewOu", attrs); |
这个例子创建了名称为 ou=NewO 的上下文, 并且有属性 objectclass, 属性 top 和 organizationalUnit, 其中 objectclass 有两个值
# java Create ou=Groups: javax.naming.directory.DirContext ou=People: javax.naming.directory.DirContext ou=NewOu: javax.naming.directory.DirContext |
这个例子创建了一个新的上下文, 叫 NewOu, 是 ctx 的子上下文
销毁上下文
要销毁上下文, 需要向 destroySubcontext()提供需要销毁上下文的名称
// Destroy the context ctx.destroySubcontext("NewOu"); |
这个例子在上下文 ctx 中删除上下文 NewOu
属性名
属性由属性标识符和一组属性值组成属性表示符叫属性名, 是表示属性的字符串属性值是属性的内容, 它的类型不一定是字符串当您想要指定获取搜索或修改指定属性时, 您使用属性名名称同时在返回属性的操作中返回(例如, 当您执行目录的读取或搜索操作时)
当使用属性名时, 您需要知道特定目录服务器的特性, 所以您不会为结果惊奇这些特定在下一子节中描述
属性类型
在诸如 LDAP 之类的目录中, 属性名表示了属性的类型, 通常叫做属性类型名例如, 属性名 cn 同时叫做属性类型名属性类型定义了属性值的语法, 是否允许多值, 相等性, 以及对属性值执行比较和排序时的排序规则,
属性子类
一些目录实现支持目录子类型, 就是服务器允许属性类型使用其他属性类型定义例如, name 属性可能是所有 name 相关属性的超类型: commonName 是 name 的子类对于支持这种特斯娜格的目录实现, 访问 name 属性可能返回 commonName 属性
当访问支持子类型的属性的目录时, 要知道服务器可能返回和您请求不一致的类型为了减少这种几率, 使用最派生类
属性名同义词
一些目录实现支持属性名的同义词例如, cn 可能是 commonName 的同义词所以请求 cn 属性可能返回 commonName 属性
当访问支持属性同义词的目录, 您必须意识到服务器可能返回和您请求不同的属性名要防止这种情况发生, 使用官方的属性名代替使用同义词官方的属性名是定义属性的属性名; 同义词是是定义中引用官方属性名的名称
语言参数选择
LDAP v3 的扩展 (RFC 2596) 允许您和属性名一起指定语言编码类似于子类属性, 一个属性名可以表示多个不同的属性例如 description 属性有两个不同的语言变体:
description: software description;lang-en: software products description;lang-de: Softwareprodukte |
对 description 属性的请求会返回所有三种属性当访问支持这种特性的目录时, 您必须意识到服务器可能返回和请求时不同的名称
读取属性
为了从目录中读取对象的属性, 使用 DirContext.getAttributes()并且将您想读取的属性名称传递进去就可以了假设在命名服务中的一个对象的名称是 cn=Ted Geisel, ou=People 要获取对象的属性要使用如下代码:
Attributes answer = ctx.getAttributes("cn=Ted Geisel, ou=People"); |
您可以按照如下方式打印应答的内容:
for (NamingEnumeration ae = answer.getAll(); ae.hasMore();) { Attribute attr = (Attribute)ae.next(); System.out.println("attribute:" + attr.getID()); /* Print each value */ for (NamingEnumeration e = attr.getAll(); e.hasMore(); System.out.println("value:" + e.next())) ; } |
输出如下:
# java GetattrsAll attribute: sn value: Geisel attribute: objectclass value: top value: person value: organizationalPerson value: inetOrgPerson attribute: jpegphoto value: [B@1dacd78b attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: facsimiletelephonenumber value: +1 408 555 2329 attribute: telephonenumber value: +1 408 555 5252 attribute: cn value: Ted Geisel |
返回选中属性
为了读取选中子集的属性, 您需要提供想要获取的属性标识符的数组
// Specify the ids of the attributes to return String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"}; // Get the attributes requested Attributes answer = ctx.getAttributes("cn=Ted Geisel, ou=People", attrIDs); |
这个例子请求对象 cn=Ted Geisel, ou=People 的 sn,telephonenumber,golfhandicap 和 mail 属性, 所以应答中返回这三个属性
以下是输出结果:
# java Getattrs attribute: sn value: Geisel attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: telephonenumber value: +1 408 555 5252 |
修改属性
DirContext 接口包含修改目录中对象的属性和属性值的方法
使用修改列表
修改对象属性的一个方法是提供修改列表 (ModificationItem) 每一个 ModificationItem 包含数字常量表示修改的类型以及描述需要修改的属性以下是修改的类型
- l ADD_ATTRIBUTE
- l REPLACE_ATTRIBUTE
- l REMOVE_ATTRIBUTE
修改以列表中提供的类型进行或者所有的修改都执行, 或者一个都不执行
以下代码创建修改列表它将 mail 属性值替换成 geisel@wizards.com, 为 telephonenumber 属性添加附加值, 并且删除 jpegphoto 属性
// Specify the changes to make ModificationItem[] mods = new ModificationItem[3]; // Replace the "mail" attribute with a new value mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("mail", "geisel@wizards.com")); // Add an additional value to "telephonenumber" mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("telephonenumber", "+1 555 555 5555")); // Remove the "jpegphoto" attribute mods[2] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("jpegphoto")); |
Windows 活动目录: 活动目录将 telephonenumber 属性定义为单值属性, 这和 RFC 2256 不符为了让这个例子在活动目录中执行, 您必须或者使用其他属性代替 telephonenumber 或将 DirContext.ADD_ATTRIBUTE 改为 DirContext.REPLACE_ATTRIBUTE
在创建修改列表后, 您可以按照如下方式提供给 modifyAttributes():
// Perform the requested modifications on the named object ctx.modifyAttributes(name, mods); |
使用属性
可选的, 您可以通过指定修改类型以及修改属性的方式进行修改
例如, 以下行使用 orig 中的 name 关联需要替换的属性:
ctx.modifyAttributes(name, DirContext.REPLACE_ATTRIBUTE, orig); |
其他属性的名称没有改变
两种对于 modifyAttributes()的使用在示例程序中都有使用修改列表修改属性的程序在第二部分 modifyAttributes()中恢复了原来的属性
有属性的添加删除绑定
命名的例子讨论如何使用 bind()和 unbind(),DirContext 这两方法的重载版本您使用 DirContext 的方法关联对象的属性, 在绑定或子上下文时将她们添加到名字空间中例如, 您可能创建了 Person 对象, 然后在为 Person 对象关联属性时将他绑定到名字空间中
添加有属性的绑定
DirContext.bind()用来将属性绑定添加到上下文中它的参数为需要绑定的对象名称, 以及属性集合
// Create the object to be bound Fruit fruit = new Fruit("orange"); // Create attributes to be associated with the object Attributes attrs = new BasicAttributes(true); // case-ignore Attribute objclass = new BasicAttribute("objectclass"); objclass.add("top"); objclass.add("organizationalUnit"); attrs.put(objclass); // Perform bind ctx.bind("ou=favorite, ou=Fruits", fruit, attrs); |
这个例子创建了 Fruit 类的对象并且将他绑定到 ou=Fruits 上下文中, 名称为 ou=favorite 绑定有 objectclass 属性如果接下来在 ctx 中查询 ou=favorite, ou=Fruits, 那么您将得到 fruit 对象如果您想得到 ou=favorite, ou=Fruits 的属性, 您将得到刚才为对象添加的属性以下是例子的输出:
# java Bind orange attribute: objectclass value: top value: organizationalUnit value: javaObject value: javaNamingReference attribute: javaclassname value: Fruit attribute: javafactory value: FruitFactory attribute: javareferenceaddress value: #0#fruit#orange attribute: ou value: favorite |
显示的多于属性使用来保存关于对象 (fruit) 的一些信息这些多于信息随后会进行详细介绍
如果您两次运行这个例子, 那么第二次运行时将会失败并抛出 NameAlreadyBoundException 这是因为 ou=favorite 已经绑定到上下文 ou=Fruits 中如果要成功, 需要使用 rebind()
替换有属性的绑定
DirContext.rebind()的作用是添加或修改绑定以及属性它接受和 bind()一样的参数然而, 使用 rebind()时如果名称已经存在, 那么将会首先 Unbind 然后再绑定新的对象和属性
// Create the object to be bound Fruit fruit = new Fruit("lemon"); // Create attributes to be associated with the object Attributes attrs = new BasicAttributes(true); // case-ignore Attribute objclass = new BasicAttribute("objectclass"); objclass.add("top"); objclass.add("organizationalUnit"); attrs.put(objclass); // Perform bind ctx.rebind("ou=favorite, ou=Fruits", fruit, attrs); |
运行这个例子时, 它替换了 bind()例子中创建的绑定关系
# java Rebind lemon attribute: objectclass value: top value: organizationalUnit value: javaObject value: javaNamingReference attribute: javaclassname value: Fruit attribute: javafactory value: FruitFactory attribute: javareferenceaddress value: #0#fruit#lemon attribute: ou value: favorite |
搜索
目录提供的最有用的好处就是黄页功能, 或搜索服务您可以组合一个包含条目属性的查询, 然后提交查询到目录中然后目录返回满足查询的条目列表例如, 您可以访问目录, 查询出保龄球平均成绩大于 200 的条目, 或所有姓以 Sch 开头的人
DirContext 接口提供查询条目的方法, 这些方法很复杂也很强大搜索目录的不同方面在以下章节中描述:
l 基本搜索
l 搜索过滤器
l 搜索控制
基本搜索
最简单的查询需需要您指定条目必须含有的属性集合以及进行查询的目标上下文
以下代码创建了属性集合 matchAttrs, 其中有两个属性 sn 和 mail 表名搜索条目必须有姓 (sn) 属性, 其值为 Geisel, 以及 mail 属性可以是任意值然后调用 DirContext.search()查询上下文 ou=People 中和 matchAttrs 匹配的条目
// Specify the attributes to match // Ask for objects that has a surname ("sn") attribute with // the value "Geisel" and the "mail" attribute Attributes matchAttrs = new BasicAttributes(true); // ignore attribute name case matchAttrs.put(new BasicAttribute("sn", "Geisel")); matchAttrs.put(new BasicAttribute("mail")); // Search for objects that have those matching attributes NamingEnumeration answer = ctx.search("ou=People", matchAttrs); You can then print the results as follows. while (answer.hasMore()) { SearchResult sr = (SearchResult)answer.next(); System.out.println(">>>" + sr.getName()); printAttrs(sr.getAttributes()); } |
printAttrs()方法和 getAttributes()方法打印出属性集合类似
返回结果如下:
# java SearchRetAll >>>cn=Ted Geisel attribute: sn value: Geisel attribute: objectclass value: top value: person value: organizationalPerson value: inetOrgPerson attribute: jpegphoto value: [B@1dacd78b attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: facsimiletelephonenumber value: +1 408 555 2329 attribute: cn value: Ted Geisel attribute: telephonenumber value: +1 408 555 5252 |
返回选中属性
上一个例子返回满足指定查询条件条目的所有属性您可以通过向 search()传递属性标识符数组的方式选择想要包含在结果集中的属性在创建 matchAttrs 只有, 您应该创建属性标识符的数组, 如下所示:
// Specify the ids of the attributes to return String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"}; // Search for objects that have those matching attributes NamingEnumeration answer = ctx.search("ou=People", matchAttrs, attrIDs); |
这个例子返回条目的属性 sn,telephonenumber,golfhandicap 以及 mail, 其中条目有属性 mail 并且 sn 属性的值是 Geisel 这个例子产生如下结果(条目中没有 golfhandicap 属性, 所以没有返回)
# java Search >>>cn=Ted Geisel attribute: sn value: Geisel attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: telephonenumber value: +1 408 555 5252 |
过滤器
除了使用指定属性集合进行搜索外, 您可以用搜索过滤器的形式进行搜索搜索过滤器是一种搜索用的逻辑表达式 DirContext.search()可接受的过滤器语法在 RFC 2254 中定义
以下搜索过滤器指定了满足查询条件的条目必须有值为 Geisel 的 sn 属性, 以及值为任意的 mail 属性:
(&(sn=Geisel)(mail=*)) |
以下代码创建了过滤器以及默认的 SearchControls, 使用他们执行查询这个查询的结果和基本查询中的一致
// Create the default search controls SearchControls ctls = new SearchControls(); // Specify the search filter to match // Ask for objects that have the attribute "sn" == "Geisel" // and the "mail" attribute String filter = "(&(sn=Geisel)(mail=*))"; // Search for objects using the filter NamingEnumeration answer = ctx.search("ou=People", filter, ctls); |
搜索结果如下:
# java SearchWithFilterRetAll >>>cn=Ted Geisel attribute: sn value: Geisel attribute: objectclass value: top value: person value: organizationalPerson value: inetOrgPerson attribute: jpegphoto value: [B@1dacd75e attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: facsimiletelephonenumber value: +1 408 555 2329 attribute: cn value: Ted Geisel attribute: telephonenumber value: +1 408 555 5252 |
搜索过滤器语法概述
搜索过滤器是前缀标记的搜索表达式 (逻辑运算符在表达式前面) 下表列举了创建过滤器使用的符号
符号 < span lang="EN-US" oh="14" ow="0"> | 描述 < span lang="EN-US" oh="14" ow="0"> |
& | 与(列表中所有项必须为 < span lang="EN-US" ow="28" oh="14">true ) |
| | 或(列表中至少一个必须为 < span lang="EN-US" ow="28" oh="14">true ) |
! | 非(求反的项不能为 < span lang="EN-US" ow="28" oh="14">true ) |
= | 相等(根据属性的匹配规则) |
~= | 近似等于(根据属性的匹配规则) |
>= | 大于(根据属性的匹配规则) |
<= | 小于(根据属性的匹配规则) |
=* | 存在(条目中必须有这个属性,但值不做限制) |
* | 通配符(表示这个位置可以有一个或多个字符),当指定属性值时用到 < span lang="EN-US" ow="0" oh="14"> |
\ | 转义符(当遇到 “* ”,“(”,“)” 时进行转义) |
过滤器中的每一个条目使用属性标识符和属性值或符号表示属性值组成例如, 项 sn=Geisel 表示 sn 属性的值必须为 Geisel, 同时项 mail=* 表示 mail 属性必须存在
每项必须包含在括号之内, 例如 (sn=Geisel) 这些项使用逻辑运算符, 例如 &, 创建逻辑表达式, 例如(& (sn=Geisel) (mail=*))
每一个逻辑表达式可以进一步组成其他项, 例如 (| (& (sn=Geisel) (mail=*)) (sn=L*)) 这个例子请求的条目中或者含有值为 Geisel 的 sn 属性和 mail 属性或者 sn 属性以字母 L 开头
关于语法的详细描述, 请参考 RFC 2254
返回选中属性
上一个例子返回满足指定过滤器的条目中的所有属性您可以通过设置搜索控制参数的方法选择返回属性您创建想要包含在结果中的属性标识符集合, 然后将他传递到 SearchControls.setReturningAttributes()中如下所示:
// Specify the ids of the attributes to return String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"}; SearchControls ctls = new SearchControls(); ctls.setReturningAttributes(attrIDs); |
这个例子和基本搜索一节中返回选择的属性部分的结果一致运行后的结果如下(这个条目没有 golfhandicap 属性, 所以没有返回)
# java SearchWithFilter >>>cn=Ted Geisel attribute: sn value: Geisel attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: telephonenumber value: +1 408 555 5252 |
范围
默认的 SearchControls 指定搜索只在命名空间中进行 (SearchControls.ONELEVEL_SCOPE) 这个默认选项在搜索过滤器一节中使用
除了默认选项之外, 您可以指定搜索在整个子树或只在命名对象中执行
搜索子树
对于整个子树的搜索不但搜索命名对象而且搜索它的后代要按照这种方式进行搜索, 按照下面的方式向 SearchControls.setSearchScope()中传递 SearchControls.SUBTREE_SCOPE 参数:
// Specify the ids of the attributes to return String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"}; SearchControls ctls = new SearchControls(); ctls.setReturningAttributes(attrIDs); ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); // Specify the search filter to match // Ask for objects that have the attribute "sn" == "Geisel" // and the "mail" attribute String filter = "(&(sn=Geisel)(mail=*))"; // Search the subtree for objects by using the filter NamingEnumeration answer = ctx.search("", filter, ctls); |
这个例子搜索了 ctx 上下文的子树, 得到满足搜索过滤器的条目它在子树中找到了满足过滤器的 cn= Ted Geisel, ou=People 条目
# java SearchSubtree >>>cn=Ted Geisel, ou=People attribute: sn value: Geisel attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: telephonenumber value: +1 408 555 5252 |
搜索命名对象
您也可以搜索命名对象这样做是很有用的, 例如, 测试命名对象是否满足搜索过滤器为了搜索命名对象, 将 SearchControls.OBJECT_SCOPE 传递到 setSearchScope()中
// Specify the ids of the attributes to return String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"}; SearchControls ctls = new SearchControls(); ctls.setReturningAttributes(attrIDs); ctls.setSearchScope(SearchControls.OBJECT_SCOPE); // Specify the search filter to match // Ask for objects that have the attribute "sn" == "Geisel" // and the "mail" attribute String filter = "(&(sn=Geisel)(mail=*))"; // Search the subtree for objects by using the filter NamingEnumeration answer = ctx.search("cn=Ted Geisel, ou=People", filter, ctls); |
这个例子测试是否对象 cn=Ted Geisel, ou=People 满足给定的过滤器
# java SearchObject >>> attribute: sn value: Geisel attribute: mail value: Ted.Geisel@JNDITutorial.com attribute: telephonenumber value: +1 408 555 5252 |
例子找到了一个结果并进行打印注意结果中的名称字段为空因为对象的名称是进行查询的上下文(cn=Ted Geisel, ou=People)
结果数量
有时, 查询可能返回了太多的结果同时您想限制结果集的大小这时您可以使用限制数量的搜索控制默认情况下, 搜索没有数量限制它将会返回找到的所有结果要设置搜索的数量限制, 将数字传递到 SearchControls.setCountLimit()中
以下例子设置数量限制为 1
// Set the search controls to limit the count to 1 SearchControls ctls = new SearchControls(); ctls.setCountLimit(1); |
如果程序尝试得到更多的结果, 那么会抛出 SizeLimitExceededException 如果程序设置数量限制, 那么或者将这个异常和 NamingExceptions 异常区别对待或者按照数量限制的大小, 不请求超过数量的结果
满足搜索数量限制是控制程序消耗资源的一种方法 (例如内存和网络带宽) 其他控制资源的方法是尽量使用小的搜索过滤器, 在合适的上下文中开始搜索, 使用合适的范围
时间限制
搜索时间限制是搜索操作等待应答的时间上限当您不想为应答等待太长时间时这很有用如果搜索操作超过时间限制还没完成, 那么将会抛出 TimeLimitExceededException 异常
为了设置搜索的时间限制, 将毫秒数传递到 SearchControls.setTimeLimit()即可以下例子将时间限制设置为 1 秒
// Set the search controls to limit the time to 1 second (1000 ms) SearchControls ctls = new SearchControls(); ctls.setTimeLimit(1000); |
要让这个例子运行超时, 需要重新配置, 或者使用一个慢的服务器或这使用有很多条目的服务器您还可以使用其他方式让搜索超过 1 秒
时间限制为 0 表示不进行时间限制, 这样请求将会进行无限等待
常见问题解答
当您使用 JNDI 类运行成功编译的程序时可能遇到的主要问题如下:
l 没有初始化上下文
l 连接被拒绝
l 连接失败
l 程序挂起
l 名字没有找到
l 不能连接任意主机
l 不能配置访问系统属性
l 不能使用 CRAM-MD5 认证
1. 得到 NoInitialContextException
原因: 您没有指定使用的初始化上下文特别的, Context.INITIAL_CONTEXT_FACTORY 环境变量没有设置成为初始化上下文的工厂类名后者, 找不到 Context.INITIAL_CONTEXT_FACTORY 配置的可得到的服务提供者
解决方案: 将环境变量 Context.INITIAL_CONTEXT_FACTORY 设置为您使用的初始化上下文类名详细信息请参考配置一节
如果属性已经设置, 那么确认类名没有输入错误, 并且类在您的程序中可见(或者在 classpath 中或者在 JRE 的 jre/lib/ext 目录下)Java 平台包含服务提供者有 LDAP,COS 命名, DNS 以及 RMI 注册所有其他的服务提供者必须安装并且添加到执行环境中
2. 得到 CommunicationException 异常, 表示连接被拒绝
原因: Context.PROVIDER_URL 环境参数表示的服务器和端口没有提供访问可能有人禁用或关闭了服务或者输入了错误的服务器名称或端口号
解决方案: 检查端口上确实运行了服务, 如果需要就重启服务器这种检查依赖于您使用的 LDAP 服务器通常, 可以使用管理控制台或工具管理服务器您可以使用工具确认服务器的状态
3. LDAP 服务器向其他工具应答 (例如管理控制台) 但不应答您程序的请求
原因: 服务器没有应答 LDAP v3 的连接全逆光球一些服务器 (尤其是公共服务器) 不能正确的应答 LDAP v3, 使用忽略的方式代替拒绝同时, 一些 LDAP v3 服务器有错误处理机制, Sun 的 LDAP 服务提供者自动发送并且通常返回特定服务器错误码
解决方案: 尝试设置环境参数 java.naming.ldap.version 为 2LDAP 服务提供者默认尝试使用 LDAP v3 连接 LDAP 服务器, 然后使用 LDAP v2 如果服务器静默忽略 v3 的请求, 那么提供者假设请求生效了使用这种服务器, 您必须显式的设置协议版本, 确保服务器有正确的行为
如果服务器是 v3 服务器, 那么尝试在创建初始化上下文之前设置这些环境参数:
env.put(Context.REFERRAL, "throw"); |
这样关闭了 LDAP 提供者自动发送的控制(更多信息请参考 JNDI 教程)
4. 程序挂起
原因: 当您尝试执行的查实会生成太多结果或需要服务器查询很多条目才能生成结果时, 服务器 (尤其是公共的) 不应答 (不是一个失败应答) 这种服务器基于预请求计算花费的方式尝试减少资源消耗
或者, 您尝试使用安全套接字层 (SSL) 但服务器 / 端口不支持, 反之(您尝试使用普通套接字与 SSL 端口对话)
最终, 服务器或者因为负载原因非常慢的应答, 或完全不应答某些请求
解决方案: 如果程序因为服务器为了减少资源消耗而挂起, 那么重试请求会得到单一应答或只有很少的应答这样可以帮助您判断服务器是否还在活动这样, 您可以加宽原有查询, 重新发送
如果您的程序因为 SSL 问题挂起, 那么您需要找到 SSL 端口然后正确设置 Context.SECURITY_PROTOCOL 环境参数如果端口是 SSL 端口, 那么这个参数应该设置成 ssl 如果不是 SSL 端口, 这个参数不应该设置
如果程序不是因为上述原因挂起, 使用 com.sun.jndi.ldap.read.timeout 表示读取超时这个参数的值是一个字符串, 表示 LDAP 请求读取超时的毫秒数如果 LDAP 提供者不能在周期内得到应答, 那么放弃读取尝试数字应该大于 0 等于或小于 0 表示不指定读取超时时间, 等于无限等待得到应答
如果没有指定这个参数, 默认情况下会一直等待, 直到得到应答
例如:
env.put("com.sun.jndi.ldap.read.timeout", "5000"); |
表示如果 LDAP 服务器不能在 5 秒中内应答, 将放弃读取请求
5. 您得到 NameNotFoundException 异常
原因: 当您为 LDAP 初始化了初始上下文, 提供了根辨别名例如, 如果您为初始上下文设置 Context.PROVIDER_URL 为 ldap://ldapserver:389/o=JNDITutorial, 然后提供名称 cn=Joe,c=us, 那么您向 LDAP 服务传递的全名为 cn=Joe,c=us,o=JNDITutorial 如果这确实是您想要的名称, 那么您应该检验服务器确定包含这个条目
同时, 如果您在认证时提供错误的辨别名, Sun Java 目录服务器返回错误例如, 如果您设置 Context.SECURITY_PRINCIPAL 环境参数为 cn=Admin, o=Tutorial, 并且 cn=Admin, o=Tutorial 不是 LDAP 服务器的条目, LDAP 提供者将要抛出 NameNotFoundExceptionSun Java 目录服务器返回的实际上是一些认证异常, 不是 name not found
解决方案: 确认您提供的名字是服务器上存在的您可以通过列举父上下文的所有条目或使用其他工具例如服务器的管理员控制台来确认
以下是在部署 applet 时可能遇到的问题
6. 当您的 applet 尝试连接目录服务器时得到 AppletSecurityException 异常, 服务器正运行在和下载 applet 不同的机器上
原因: applet 没有签名, 所以只能连接到加载它的机器或者, 如果 appet 已经签名, 浏览器没有授予 applet 连接目录服务器的权限
解决方案: 如果您想允许 applet 连接任意机器上的目录服务器, 那么需要签名整个 applet 以及 applet 使用的 JNDI jar 关于 jar 的签名, 请参考签名和验证 jar 文件
7. 当您的 applet 尝试使用系统属性设置环境属性时出现 AppletSecurityException 异常
原因: 浏览器限制访问系统参数, 并且当您尝试读取时抛出 SecurityException
解决方案: 如果您需要为 applet 得到输入, 尝试使用 applet params 代替
8.
当 applet 运行在 Firefox 中尝试使用 CRAM-MD5 向 LDAP 进行认证时抛出 AppletSecurityException
原因: Firefox 禁止访问 java.security 包 LDAP 提供者使用 java.security.MessageDigest 提供的信息摘要功能来实现 CRAM-MD5
来源: https://juejin.im/post/5a7fe036f265da4e7e10b3da