jndi step by step(2)
(4) 目錄服務操作
1、目錄服務的操作
我們會用LDAP作爲例子來講解目錄服務的操作。與命名服務不同,目錄服務的內容上下文的初始化方式需要
改變:
1.1、如何讀取屬性
你可以使用 DirContext.getAttributes() 方法來讀取一個對象的屬性,你需要給這個方法傳遞一個對象的
名字作爲參數。例如一個對象在命名系統中的名字是“cn=Ted Geisel, ou=People”,那麼我們可以使用如下
的代碼得到它的屬性:
你也可以輸出 “answer”來看看:
輸出結果是:
# java GetattrsAll
attribute: sn
value: Geisel
attribute: objectclass
value: top
value: person
value: organizationalPerson
value: inetOrgPerson
attribute: jpegphoto
value: [B@1dacd78b
attribute: mail
value: [email protected]
attribute: facsimiletelephonenumber
value: +1 408 555 2329
attribute: telephonenumber
value: +1 408 555 5252
attribute: cn
value: Ted Geisel
1.1.1、返回需要的屬性
有的時候我們只是需要得到一些屬性,而不是全部的屬性。這樣,你可以把屬性作爲一個數組,把這個數組
作爲參數傳遞給那個方法。
假設我們這個例子裏的這個對象,擁有 "sn", "telephonenumber"和"mail"屬性,但是沒有“golfhandicap”
屬性,那麼上面的代碼返回的結果就應該是:
# java Getattrs
attribute: sn
value: Geisel
attribute: mail
value: [email protected]
attribute: telephonenumber
value: +1 408 555 5252
1.2、改變屬性
DirContext 接口有一些改變對象屬性的方法。
1.2.1、批量改變屬性
改變屬性的方式之一就是批量改變屬性,也就是使用許多 ModificationItem 對象來修改屬性。
每個 ModificationItem 對象都會有一個常量,來表示對屬性進行什麼樣的操作。這些常量如下:
ADD_ATTRIBUTE
REPLACE_ATTRIBUTE
REMOVE_ATTRIBUTE
對屬性的改變會按照隊列的順序來執行,要麼所有的改變都生效,要麼都不生效。
下面的代碼演示了一個例子。它把“mail”這個屬性的值,改變成了“[email protected]”,給“telephonenumber”
屬性增加了一個值,並且刪除了“jpegphoto”屬性。
上面的代碼中,我們創建了一個修改屬性的 ModificationItem 對象的列表(其實就是一個數組),然後執行
modifyAttributes() 方法來修改屬性。
1.2.2 只修改某幾個屬性
你可以不使用上面的方式,而對屬性進行某一種操作:
1.3、在目錄服務中使用搜索功能
1.3.1、基本的搜索功能
最基本的搜索功能是可以指定一個對象的名字,和一些要搜索的屬性的名字。
下面的代碼演示了這個功能。我們要進行這麼一個搜索:對象必須有“sn”屬性,而且數值必須是“Geisel”,
而且必須有“mail”這個屬性。
你可以打印出這個結果:
輸出結果:
# 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: [email protected]
attribute: facsimiletelephonenumber
value: +1 408 555 2329
attribute: cn
value: Ted Geisel
attribute: telephonenumber
value: +1 408 555 5252
1.3.2、返回指定的屬性
上一個例子返回了滿足條件的全部屬性,當然你也可以只返回需要的屬性,這僅僅需要把需要返回的屬性,
作爲一個數組參數傳遞給 search() 方法。
1.4、搜索過濾
在這裏,你可以學到一個高級點的搜索方式,就是在搜索中使用過濾。在搜索中實現過濾,我們需要使用
表達式來實現這個功能。下面的這個表達式就表示搜索條件是:對象必須有“sn”屬性,而且數值必須是
“Geisel”,而且必須有“mail”這個屬性:
(&(sn=Geisel)(mail=*))
下面的例子告訴你如何使用表達式來搜索:
下面這個列表有助你使用表達式:
符號 描述
& conjunction (i.e., and -- all in list must be true)
| disjunction (i.e., or -- one or more alternatives must be true)
! negation (i.e., not -- the item being negated must not be true)
= equality (according to the matching rule of the attribute)
~= approximate equality (according to the matching rule of the attribute)
>= greater than (according to the matching rule of the attribute)
<= less than (according to the matching rule of the attribute)
=* presence (i.e., the entry must have the attribute but its value is irrelevant)
* wildcard (indicates zero or more characters can occur in that position);
used when specifying attribute values to match
/ escape (for escaping '*', '(', or ')' when they occur inside an attribute value)
表達式中的每一個項目,都必須使用屬性的名字,也可以使用屬性的數值。例如“sn=Geisel”意味着屬性
“sn”的值爲“Geisel”,而 "mail=*" 意味着屬性 “mail” 必須存在,但是可以爲任意值。
每一個項目必須是在括號中,例如 "(sn=Geisel)"。不同的括號間使用邏輯判斷符號來連接起來。
例如 "(| (& (sn=Geisel) (mail=*)) (sn=L*))"。這表示 屬性中 sn 必須等於Geisel 並且有 mail 這個
屬性 或者 有 sn 這個屬性。
詳細的內容,請參考 http://www.ietf.org/rfc/rfc2254.txt
當然了,你也可以只返回指定的屬性,而不是全部的屬性。
1.5 搜索控制
在上面的章節中,我們已經看到了一個類:SearchControls,通過這個類,你可以控制搜索的行爲。這裏
我們就來仔細地看看這個類。
1.5.1 搜索範圍
SearchControls 類默認會在整個內容上下文(SearchControls.ONELEVEL_SCOPE)搜索對象,通過設置,你
可以在某一個範圍內搜索對象。
1.5.1.1 在一個子樹上搜索
通過下面的代碼你可以清晰地瞭解這一功能:
1.5.1.2 根據名字來搜索
通過下面的代碼你可以清晰地瞭解這一功能:
1.5.2 數量的限制
通過下面的代碼你可以控制返回結果的數量:
1.5.3 時間的限制
如果一個搜索耗費了很長的時間,那可不是一個好方法。這裏你可以設置超時的時間。
參數的單位是毫秒。
如果發生超時現象,那麼就會拋出 TimeLimitExceededException。
1.6 結合命名服務和目錄服務的操作
我們已經這樣的一個概念,就是目錄服務是命名服務的一個擴展。例如,之前我們說過命名服務具有 bind(),
rebind(), createSubcontext() 等方法,但是在目錄服務裏卻沒有介紹這些方法。
其實目錄服務裏也有這些方法。下面就用 LDAP 作爲例子,介紹一下這些方法。
1.6.1 創建一個具有屬性的內容上下文
1.6.2 增加一個具有屬性的綁定
1.6.3 替換一個具有屬性的綁定
(5) 高級應用之Name
1、jndi 高級應用之 Name
1.1、什麼是 Name?
這之前的文檔中,我們所用的例子裏,對象的名字都是 java.lang.String 類型的,也就是字符串類型。
在這個文檔裏,我們則會介紹一些其他的對象名字類型,如 Name,以及他們的使用方法。
我們首先會講講什麼是字符串的名字和結構化的名字,以及它們的存在的必要性。
然後,我們會介紹2個結構化名字的類:(複合名字)composite 和 (混合名字)compound。
最後,我們介紹如何在對象名字中使用特殊字符,以及如何解析和構成一個複雜的對象的名字。
1.2、字符串名字 vs 結構化名字
在 Context 和 DirContext 接口裏,每一個命名服務的方法都有2種方式,一個是接受字符串類型的名字,
一個是接受結構化的名字(Name 類的對象)。例如:
lookup(java.lang.String)
lookup(javax.naming.Name)
1.2.1、字符串名字
使用字符串類型的名字,可以讓你不必再生成一個CompositeName類的對象。例如下面的代碼是相同的:
1.2.1、結構化的名字
結構化的名字的對象可以是 CompositeName 或 CompoundName 類的對象,或者是任何一個實現了 “Name ”
接口的類。
如果是 CompositeName 類的實例,那麼就被認爲是一個複合的名字(composite name)。所謂的複合名字就
是可以在一個名字裏使用多個命名服務系統,而不是僅僅一個。
如果是 compound 類的實例,那麼就被認爲是混合的名字(compound name)。混合的名字只包含一個命名服務
系統。
1.2.2、那麼該使用哪種名字呢?
一般來說,如果用戶可以提供字符串類型的名字,那麼就使用字符串類型的名字;可是如果用戶提供的是一
個組合的名字,那麼就應該使用結構化的名字了。
例如一個應用如果會涉及多個命名服務系統,那麼就應該使用結構化的名字了。
1.3 複合名字(composite name)
複合名字就是跨越多個命名系統的名字。例如:
cn=homedir,cn=Jon Ruiz,ou=People/tutorial/report.txt
這個名字裏包含了兩個命名系統:一個 LDAP 系統 "cn=homedir,cn=Jon Ruiz,ou=People" 和一個文件系統
"tutorial/report.txt"。當你把這個字符串傳遞給 Context 的 look 方法,那麼就會從 LDAP 系統裏查找
那個文件,然後返回那個對象(一個文件對象的實例)。當然,這取決於特定的SPI。例如:
1.3.1、字符串的表現形式
我們可以這樣想像,一個複合的名字是由不同的“組件”組成的。所謂的“組件”,我們可以想象成是一個名字
的一小部分,每一個組件表示一個命名服務系統。每一個組件都由正斜槓“/”分開。
例如 :cn=homedir,cn=Jon Ruiz,ou=People/tutorial/report.txt
包含三個組件:
cn=homedir,cn=Jon Ruiz,ou=People
tutorial
report.txt
第一個組件屬於LDAP命名服務系統,第2和第3個組件屬於文件命名服務系統。正如我們從這個例子裏看到的,
多個組件(tutorial 和 report.txt)可以屬於同一個命名服務系統,但是一個組件是不能跨越多個命名服務
系統的。
除了使用正斜槓“/”外,複合名字還允許使用其它三個特殊符號:反斜槓“/",單引號"'",雙引號" " "。
這三個符號是內置的符號,也就是說他們三個具有特殊的用途。
反斜槓“/”的作用是轉譯字符。反斜槓“/”之後的字符,會被認爲是普通字符。
例如“a//b”,其中的“/”是不會被認爲是不同的命名服務系統的分隔符號。
雙引號和單引號可以讓引號內的字符成爲普通字符,例如下面的用法都是一樣的:
a//b//c//d
"a/b/c/d"
'a/b/b/d'
可見,有的時候使用引號還是很方便的。
複合的名字也可以是空的,空的複合名字意味着沒有組件,一般用空的字符串表示。
複合名字的組件也可以是空的,例如:
/abc
abc/
abc//xyz
1.3.2、複合名字的類
CompositeName 類是用來構成複合名字的類。你可以給它的構造函數傳遞一個複合名字的字符串。
例如:
運行這個例子,輸出的結果就是:
a/b/c has 3 components:
a
b
c
CompositeName 類有許多方法,例如查看,修改,比較,以及得到一個複合名字的字符串表現形式。
1.3.3、訪問複合名字裏的組件
可以通過下列方法訪問復和名字裏的組件:
get(int posn)
getAll()
getPrefix(int posn)
getSuffix(int posn)
clone()
如果你想得到特定位置的組件的名字,那麼使用 get(int posn) 方法就非常合適。
getAll() 方法可以返回全部的組件的名字。
例如:
你可以用 getPrefix(int posn) 和 getSuffix(int posn)來從前端或者後端查找組件的名字,如:
運行結果:
two/three
one
1.3.4、修改一個複合名字
你可以通過下列方法修改一個複合名字:
add(String comp)
add(int posn, String comp)
addAll(Name comps)
addAll(Name suffix)
addAll(int posn, Name suffix)
remove(int posn)
當你創建了一個複合名字實例後,你可以對它進行修改。看看下面的例子:
1.3.4、比較複合名字
你可以通過以下的方法對複合名字進行比較:
compareTo(Object name)
equals(Object name)
endsWith(Name name)
startsWith(Name name)
isEmpty()
你可以使用 compareTo(Object name) 方法對一個複合名字的列表進行排序。下面是一個例子:
equals() 方法可以讓你比較兩個複合名字是否相同。只有兩個複合名字有相同的組件,而且順序一樣,
會返回 true。
使用 startsWith() 和 endsWith()方法,你可以判斷複合名字是以什麼字符串開頭和以什麼字符串結尾。
isEmpty() 方法可以讓你知道一個複合名字是否爲空。你也可以使用 size() == 0 來實現同樣的功能。
下面是一些例子:
1.3.5、複合名字的字符串表現形式
你可以使用 toString() 方法來實現這個功能。
下面是一個例子:
1.3.5、複合名字作爲Context的參數
直接看一個例子,非常簡單:
1.4、混合名字
混合名字不是跨越命名系統的,例如:
cn=homedir,cn=Jon Ruiz,ou=People
它和複合名字有些類似,我們也可以把它看成是由不同的組件組成的,這個名字裏包含了三個組件:
ou=People
cn=Jon Ruiz
cn=homedir
1.4.1、混合名字和複合名字的關係
當你給 Context.lookup() 傳遞一個字符串的時候,首先 lookup 方法會把這個字符串作爲複合名字來看
待,這個複合名字可能只包含一個組件。但是一個組件可能會包含幾個混合名字。
1.4.2、混合名字的字符串表現方式
正如上面所說的,一個混合名字是由很多的組件組成的。組件之間的分隔符號依賴於特定的命名服務系統。
例如在 LDAP 裏,分隔符好就是“,”,因此,下面的這個混合名字
ou=People
cn=Jon Ruiz
cn=homedir
的字符串形式就是 cn=homedir,cn=Jon Ruiz,ou=People
1.4.3、混合名字的類
處理混合名字,我們可以使用 CompoundName 類。你可以向它的構造函數傳遞混合名字的字符串,並且還
得設置一些必要的屬性,這些屬性一般都是特定的命名服務系統的一些規則。
實際上,只有當你準備編寫一個SPI的時候,纔會去使用裝個構造函數。作爲一個開發者,一般你只是會
涉及混合名字裏的各個組件而已。
下面是一個例子:
1.4.4、操作混合名字類
注意,上面的例子中,我們使用 NameParser.parse() 來返回一個實現了Name接口的對象。這個接口可以
是 CompositeName類 或 CompoundName類。這就意味着,你可以訪問或者修改一個混合名字對象,就好
像我們在複合名字裏做的一樣!
下面是一個例子:
輸出結果:
ou=People
cn=John,ou=Marketing,ou=East
cn=HomeDir,cn=John,ou=Marketing,ou=East
需要的注意的是,LDAP系統裏,組件的順序是從右到左的。也就是右邊是名字的開頭,而左邊是名字
的結尾!
下面這個例子是修改混合名字的:
這個例子使用了文件系統,而不是LDAP系統,輸出結果:
People
East/Marketing/John
East/Marketing/John/HomeDir
1.4.5、混合名字作爲Context的參數
我們只用一個例子就可以了:
1.4.6、取得完整的混合名字
有的時候,你可能需要根據一個混合名字來的完整的名字。例如一個DNS,你只知道一部分如“www.abc.com”,
但實際上它的完整的DNS是“www.abc.com.cn”。
其實這個功能已經超出了jndi 的 api 所控制的範圍了,因爲 jndi 是不能決定完整的名字到底是什麼樣子,
那必須依賴特定的命名服務系統。
但是,jndi 仍然提供了一個接口 Context.getNameInNamespace()。這個方法的返回結果,依賴於你所使用
的命名服務系統。
下面是一個例子:
輸出結果:
cn=Jon Ruiz,ou=people,o=JNDItutorial
1.5、名字解析
名字解析的意思就是根據一個名字的字符串形式,把它解析成結構化的形式,如混合名字或複合名字。
jndi API裏包含了這樣的接口,其接口的實現,依賴於特定的SPI。
1.5.1、解析複合名字
下面是一個例子:
其實這個用法我們早就在前面看過了。
1.5.2、解析混合名字
爲了解析混合名字,你必須使用 NameParser 接口,這個接口有一個方法:
首先,你必須從 SPI 裏得到一個 NameParser 接口的實現:
一旦你得到了 NameParser 實例,你就可以用它來把字符串形式的名字轉換成結構化的名字。
如果解析錯誤,那麼會得到 InvalidNameException 。
儘管 parse() 方法可以返回一個結構化的名字,但是我們還是建議僅僅用它來處理混合名字,而不要
處理複合名字。
parse()返回的對象未必一定是 CompoundName 類,只要返回的對象實現了 Name 接口就可以--這依賴
於特定的SPI。
1.6、動態生成複合名字
之前我們介紹了訪問,操作複合名字的方法。但是還有一些難以處理的情況需要我們去面對。
例如,如果你要給一個複合名字增加一個組件的話,你是增加複合名字的組件還是混合名字的組件?
如果是混合名字的組件,那麼使用什麼規則呢?
例如我們有這樣一個複合名字:
cn=homedir,cn=Jon Ruiz/tutorial
現在你需要增加一個文件名字,如果是windows系統,那麼就是:
cn=homedir,cn=Jon Ruiz/tutorial/report.txt
然後我們再增加一個LDAP混合名字 ou=People,那麼就需要使用LDAP的規則:
cn=homedir,cn=Jon Ruiz,ou=People/tutorial/report.txt
在這個例子中,我們使用了不同的命名服務系統,我們需要知道什麼時候因該使用什麼樣的命名系統
的規則,這真的很麻煩。
爲了解決這個問題,jndi API 提供了一個接口 Context.composeName(),它用來動態的組裝名字,
當然依賴於特定的SPI了。
你需要給它提供兩個參數:一個是需要追加的組件,一個是被追加的組件的名字。
下面是一個例子:
- // Create the initial context
- Context ctx = new InitialContext(env);
- // Compose a name within the LDAP namespace
- Context ldapCtx = (Context)ctx.lookup("cn=Jon Ruiz,ou=people");
- String ldapName = ldapCtx.composeName("cn=homedir", "cn=Jon Ruiz,ou=people");
- System.out.println(ldapName);
- // Compose a name when it crosses into the next naming system
- Context homedirCtx = (Context)ctx.lookup(ldapName);
- String compositeName = homedirCtx.composeName("tutorial", ldapName);
- System.out.println(compositeName);
- // Compose a name within the File namespace
- Context fileCtx = (Context)ctx.lookup(compositeName);
- String fileName = fileCtx.composeName("report.txt", compositeName);
- System.out.println(fileName);
(6) 高級應用之 環境屬性
1、環境屬性
在之前的文檔裏,我們已經學會如何去初始化一個內容上下文,例如:
這個例子中的 Hashtable,就是一個環境屬性,或者簡單的說是“環境”。
這個章節裏,我們要介紹的就是這個“環境屬性”,看看他們是如何發揮作用的,以及如何使用的等等。
JNDI其實只是一個接口,爲了訪問一個命名/目錄服務,你必須依賴特定的SPI。這就需要進行一些配置,
你得告訴JNDI,你需要的SPI是什麼。
下面是一些不同種類的環境屬性:
a> 標準的
b> 描述服務的
c> 描述特性的
d> 描述提供者的。
下面是標準的環境屬性:
1.1、環境屬性
配置環境屬性可以通過兩種方式:一個是把一個Hashtable傳遞給InitialContext的構造函數,另一個
是用一個 .properties 文件。
一些JNDI的環境屬性還可以通過系統變量或者Applet參數來設置。
1.1.1、應用程序資源文件(.properties文件)
你可以在 .properties文件 裏指定JNDI的配置。這個文件的名字應該是 jndi.properties。例如下面
就是一個 jndi.properties的例子:
java.naming.factory.object=com.sun.jndi.ldap.AttrsToCorba:com.wiz.from.Person
java.naming.factory.state=com.sun.jndi.ldap.CorbaToAttrs:com.wiz.from.Person
java.naming.factory.control=com.sun.jndi.ldap.ResponseControlFactory
java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
java.naming.provider.url=ldap://localhost:389/o=jnditutorial
com.sun.jndi.ldap.netscape.schemaBugs=true
首先,應用程序會從classpath 裏面載入 jndi.properties,如果沒有發現,那麼會載入JAVA_HOME/lib/
jndi.properties。
注意:如果你使用這種方式配置JNDI,那麼當找不到裝個文件的時候,會拋出異常。
當然,我們初始化內容上下文的用法也需要修改一下:
1.1.2、通過系統變量設置JNDI
我們可以使用如下的方式來實現這個功能:
# java -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory /
-Djava.naming.provider.url=ldap://localhost:389/o=jnditutorial /
List
1.1.3、Applet的方式設置JDNI
看這個例子:
同時初始內容上下文也需要修改:
1.2、內容上下文環境的探討
我們已經知道可以通過三種方式來設置JDNI的屬性。但是,如果我們同時使用了兩種方式會
怎麼樣呢?看看下面的例子:
我們同時配置一個jndi.properties在classpath裏:
java.naming.factory.object=com.sun.jndi.ldap.AttrsToCorba:com.wiz.from.Person
java.naming.factory.state=com.sun.jndi.ldap.CorbaToAttrs:com.wiz.from.Person
java.naming.factory.control=com.sun.jndi.ldap.ResponseControlFactory
java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
java.naming.provider.url=ldap://localhost:389/o=jnditutorial
com.sun.jndi.ldap.netscape.schemaBugs=true
然後看看運行的結果:
com.sun.jndi.ldap.netscape.schemaBugs=true
java.naming.factory.object=foo.bar.ObjFactory:com.sun.jndi.ldap.AttrsToCorba:com.wiz.from.Person
java.naming.factory.initial=com.sun.jndi.fscontext.FSContextFactory
foo=bar
java.naming.provider.url=file:/
java.naming.factory.state=com.sun.jndi.ldap.CorbaToAttrs:com.wiz.from.Person
java.naming.factory.control=com.sun.jndi.ldap.ResponseControlFactory
下面來分析一下這個結果:
a> 在 Hashtable 裏的“foo”項,和在文件裏的 “com.sun.jndi.ldap.netscape.schemaBugs”,
都出現在結果裏。
b> “java.naming.factory.object” 項是兩者的組合。
c> 其他的屬性如“java.naming.factory.initial” 都是用的 Hashtable裏的。
是不是有些混亂?如果我們要使用多個SPI該怎麼辦?沒關係,我們在下一個小結裏介紹這個問題。
1.3、定製使用SPI
你可以通過一個SPI的屬性文件,來爲某一個SPI進行單獨的設置。這個文件也是 .properties 文件。
SPI的屬性文件應該類似下面:
前綴/jndiprovider.properties
這個前綴是什麼呢?就是這個SPI實現的內容上下文類(Context)的包的結構。
例如,我們要使用的是 com.sun.jndi.ldap.LdapCtx 這個內容上下文,那麼對應於它的屬性配置
文件就應該是:"com/sun/jndi/ldap/jndiprovider.properties"。
一個應用可以使用多個這樣的屬性配置文件。
那麼我們爲什麼要使用屬性配置文件?
有兩個原因。第一,我們可以單獨的配置某一個命名/服務系統。第二,部署的時候會有用。例如,
你可以單獨配置一個針對LDAP的屬性,而不用去修改 jndi.properties,或者增加系統變量。
但是,我們也並不是可以在SPI屬性配置文件裏設置全部的屬性,我們可以設置的屬性如下:
java.naming.factory.object
java.naming.factory.state
java.naming.factory.control
java.naming.factory.url.pkgs
不過並不象 jndi.properties 或者 那個Hastable,SPI屬性配置文件裏的屬性不會自動載入到環境
裏,只有SPI調用了下列方法時纔會這樣做:
NamingManager.getObjectInstance(Object, Name, Context, Hashtable)
DirectoryManager.getObjectInstance(Object, Name, Context, Hashtable, Attributes)
NamingManager.getStateToBind(Object, Name, Context, Hashtable)
DirectoryManager.getStateToBind(Object, Name, Context, Hashtable, Attributes)
ControlFactory.getControlInstance(Control, Context, Hashtable)
例如,假設我們使用一個LDAP服務,它的實現類是com.sun.jndi.ldap.LdapCtx,當這個類調用
DirectoryManager.getObjectInstance() 方法時,JNDI將會從com/sun/jndi/ldap/jndiprovider.properties
裏找到“java.naming.factory.object”項,然後把它追加到環境裏已經定義的項裏(例如那個Hashtable
或者 jndi.properties)。