四、 如果你在Eclipse或JBuilder中開發的話,你需要在你的Web應用程序的WEB-INF\Web.xml文件中注冊數據源,文件添加如下內容:
<resource-ref>
<res-ref-name>jdbc/northwind</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
一定注意:同時檢查一下你部署到Tomcat中對應的
彩色的加粗文字是添加上的,用來注冊數據源的JNDI,在這我注冊了兩個數據源,一個是oracle的,一個是MSSQL Server 2000的。
在做任何配置時最好不要修改Tomcat服務器的任何文件,如servel.xml或web.xml文件,而所有的操作和配置都可以在你自己的應用配置文件中來完成,這樣即使培植錯誤也不至于服務器的崩潰。
按以上步驟就可以完成數據源的配置,你可以寫一些程序來測試。
用JSP來測試,Index.jsp文件程序如下:
<%@ page language="java" import="java.util.*" %>
<%@ page import="javax.sql.*" %>
<%@ page import="java.sql.*" %>
<%@ page import="javax.naming.*" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
out.println(basePath);
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>My JSP 'index.jsp' starting page</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<!--
<link rel="stylesheet" type="text/css" href="styles.css">
-->
</head>
<body>
This is my JSP page. <br>
<%
Context ctx=null;
Connection cnn=null;
java.sql.Statement stmt=null;
ResultSet rs=null;
try
{
ctx=new InitialContext();
if(ctx==null)
throw new Exception("initialize the Context failed");
DataSource ds=(DataSource)ctx.lookup("java:comp/env/jdbc/northwind");
out.println(ds);
if(ds==null)
throw new Exception("datasource is null");
try{
cnn=ds.getConnection();
out.println("<br> connection:"+cnn);
}catch(Exception e){
e.printStackTrace();
}
}
finally
{
if(rs!=null)
rs.close();
if(stmt!=null)
stmt.close();
if(cnn!=null)
cnn.close();
if(ctx!=null)
ctx.close();
}
%>
</body>
</html>
在你的瀏覽器中運行http://10.0.0.168:8888/WebDemo/web/即可以看到結果:如下:
你看到連接成功的標志,就意味這你的數據源配置成功!!!
記住:要想配置成功,就要認真檢查需要配置的每一個細節。
JNDI(The Java Naming and Directory Interface,Java命名和目錄接口)是一組在Java應用中訪問命名和目錄服務的API.命名服務將名稱和對象聯系起來,使得我們可以用名稱訪問對象。目錄服務是一種命名服務,在這種服務里,對象不但有名稱,還有屬性。
命名或目錄服務使你可以集中存儲共有信息,這一點在網絡應用中是重要的,因為這使得這樣的應用更協調、更容易管理。例如,可以將打印機設置存儲在目錄服務中,以便被與打印機有關的應用使用。
本文用代碼示例的方式給出了一個快速教程,使你可以開始使用JNDI.它:
l 提供了JNDI概述 l 描述了JNDI的特點 l 體驗了一下用JNDI開發應用 l 表明了如何利用JNDI訪問LDAP服務器,例如,Sun ONE 目錄服務器 l 表明了如何利用JNDI訪問J2EE服務 l 提供了示例代碼,你可以將其改編為自己的應用
JNDI概述
我們大家每天都不知不覺地使用了命名服務。例如,當你在web瀏覽器輸入URL,http://java.sun.com時,DNS(Domain Name System,域名系統)將這個符號URL名轉換成通訊標識(IP地址)。命名系統中的對象可以是DNS記錄中的名稱、應用服務器中的EJB組件(Enterprise JavaBeans Component)、LDAP(Lightweight Directory Access Protocol)中的用戶Profile.
目錄服務是命名服務的自然擴展。兩者之間的關鍵差別是目錄服務中對象可以有屬性(例如,用戶有email地址),而命名服務中對象沒有屬性。因此,在目錄服務中,你可以根據屬性搜索對象。JNDI允許你訪問文件系統中的文件,定位遠程RMI注冊的對象,訪問象LDAP這樣的目錄服務,定位網絡上的EJB組件。
對于象LDAP 客戶端、應用launcher、類瀏覽器、網絡管理實用程序,甚至地址薄這樣的應用來說,JNDI是一個很好的選擇。
JNDI架構
JNDI架構提供了一組標準的獨立于命名系統的API,這些API構建在與命名系統有關的驅動之上。這一層有助于將應用與實際數據源分離,因此不管應用訪問的是LDAP、RMI、DNS、還是其他的目錄服務。換句話說,JNDI獨立于目錄服務的具體實現,只要你有目錄的服務提供接口(或驅動),你就可以使用目錄。如圖1所示。 圖1:JNDI架構
關于JNDI要注意的重要一點是,它提供了應用編程接口(application programming interface,API)和服務提供者接口(service provider interface,SPI)。這一點的真正含義是,要讓你的應用與命名服務或目錄服務交互,必須有這個服務的JNDI服務提供者,這正是JNDI SPI發揮作用的地方。服務提供者基本上是一組類,這些類為各種具體的命名和目錄服務實現了JNDI接口?很象JDBC驅動為各種具體的數據庫系統實現了JDBC接口一樣。作為一個應用開發者,你不必操心JNDI SPI.你只需要確認你要使用的每一個命名或目錄服務都有服務提供者。
J2SE和JNDI
Java 2 SDK 1.3及以上的版本包含了JNDI.對于JDK 1.1和1.2也有一個標準的擴展。Java 2 SDK 1.4.x的最新版本包括了幾個增強和下面的命名/目錄服務提供者:
l LDAP(Lightweight Directory Access Protocol)服務提供者 l CORBA COS(Common Object Request Broker Architecture Common Object Services)命名服務提供者 l RMI(Java Remote Method Invocation)注冊服務提供者 l DNS(Domain Name System)服務提供者
更多的服務提供者
可以在如下網址找到可以下載的服務提供者列表:
http://java.sun.com/products/jndi/serviceproviders.html 特別有意思的或許是如下網址提供的Windows 注冊表JNDI服務提供者:http://cogentlogic.com/cocoon/CogentLogicCorporation/JNDI.xml 這個服務提供者使你可以訪問Windows XP/2000/NT/Me/9x的windows注冊表。
也可以在如下網址下載JNDI/LDAP Booster Pack:http://java.sun.com/products/jndi/ 這個Booster Pack包含了對流行的LDAP控制的支持和擴展。它代替了與LDAP 1.2.1服務提供者捆綁在一起的booster pack.關于控制和擴展的更多信息可以在如下網站看到: http://java.sun.com/products/jndi/tutorial/ldap/ext/index.html 另一個有趣的服務提供者是Sun的支持DSML v2.0(Directory Service Markup Language,目錄服務標記語言)的服務提供者。DSML的目的是在目錄服務和XML之間架起一座橋梁。
JNDI API
JNDI API由5個包組成:
l Javax.naming:包含了訪問命名服務的類和接口。例如,它定義了Context接口,這是命名服務執行查詢的入口。 l Javax.naming.directory:對命名包的擴充,提供了訪問目錄服務的類和接口。例如,它為屬性增加了新的類,提供了表示目錄上下文的DirContext接口,定義了檢查和更新目錄對象的屬性的方法。 l Javax.naming.event:提供了對訪問命名和目錄服務時的時間通知的支持。例如,定義了NamingEvent類,這個類用來表示命名/目錄服務產生的事件,定義了偵聽NamingEvents的NamingListener接口。 l Javax.naming.ldap:這個包提供了對LDAP 版本3擴充的操作和控制的支持,通用包javax.naming.directory沒有包含這些操作和控制。 l Javax.naming.spi:這個包提供了一個方法,通過javax.naming和有關包動態增加對訪問命名和目錄服務的支持。這個包是為有興趣創建服務提供者的開發者提供的。
JNDI 上下文
正如在前面提到的,命名服務將名稱和對象聯系起來。這種聯系稱之為綁定(binding)。一組這樣的綁定稱之為上下文(context),上下文提供了解析(即返回對象的查找操作)。其他操作包括:名稱的綁定和取消綁定,列出綁定的名稱。注意到一個上下文對象的名稱可以綁定到有同樣的命名約定的另一個上下文對象。這稱之為子上下文。例如,如果UNIX中目錄/home是一個上下文,那么相對于這個目錄的子目錄就是子上下文?例如,/home/guests中guests就是home的子上下文。在JNDI中,上下文用接口javax.naming.Context表示,這個接口是與命名服務交互的關鍵接口。在Context(或稍后討論的
DirContext)接口中的每一個命名方法都有兩種重載形式:
l Lookup(String name):接受串名 l Lookup(javax.naming.Name):接受結構名,例如,CompositeName(跨越了多個命名系統的名稱)或CompondName(單個命名系統中的名稱);它們都實現了Name接口。Compound name的一個例子是:cn=mydir,cn=Q Mahmoud,ou=People,composite name的一個例子是:cn=mydir,cn=Q Mahmoud,ou=People/myfiles/max.txt(這里,myfiles/max.txt是表示第二部分的文件名) Javax.naming.InitialContext是實現了Context接口的類。用這個類作為命名服務的入口。為了創建InitialContext對象,構造器以java.util.Hashtable或者是其子類(例如,Properties)的形式設置一組屬性。下面給出了一個例子:
Hashtable env = new Hashtable(); // select a service provider factory env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContext"); // create the initial context Context contxt = new InitialContext(env);
INITIAL_CONTEXT_FACTORY指定了JNDI服務提供者中工廠類(factory class)的名稱。Factory負責為其服務創建適當的InitialContext對象。在上面的代碼片斷中,為文件系統服務提供者指定了工廠類。表1給出了所支持的服務提供者的工廠類。要注意的是文件系統服務提供者的工廠類需要從Sun公司單獨下載,J2SE 1.4.x沒有包含這些類。
表1:上下文INITIAL_CONTEXT_FACTORY的值 Name Service Provider Factory File System com.sun.jndi.fscontext.RefFSContextFactory LDAP com.sun.jndi.ldap.LdapCtxFactory RMI com.sun.jndi.rmi.registry.RegistryContextFactory CORBA com.sun.jndi.cosnaming.CNCtxFactory DNS com.sun.jndi.dns.DnsContextFactory
為了用名稱從命名服務或目錄中取得或解析對象,使用Context的lookup方法:Object obj=contxt.lookup(name)。Lookup方法返回一個對象,這個對象表示的是你想要找的上下文的兒子。
轉載:轉載請保留本信息,本文來自http://www.matrix.org.cn/resource/article/1/1038.html感謝譯者的辛勤工作,請大家參加Matrix的翻譯計劃:http://www.matrix.org.cn/translation/Wiki.jsp?page=Main
{關鍵字}
測試驅動開發/Test Driven Development/TDD
測試用例/TestCase/TC
設計/Design
重構/Refactoring
{TDD的目標}
Clean Code That Works
這句話的含義是,事實上我們只做兩件事情:讓代碼奏效(Work)和讓代碼潔凈(Clean),前者是把事情做對,后者是把事情做好。想想看,其實我們平時所做的所有工作,除去無用的工作和錯誤的工作以外,真正正確的工作,并且是真正有意義的工作,其實也就只有兩大類:增加功能和提升設計,而TDD 正是在這個原則上產生的。如果您的工作并非我們想象的這樣,(這意味著您還存在第三類正確有意義的工作,或者您所要做的根本和我們在說的是兩回事),那么這告訴我們您并不需要TDD,或者不適用TDD。而如果我們偶然猜對(這對于我來說是偶然,而對于Kent Beck和Martin Fowler這樣的大師來說則是辛勤工作的成果),那么恭喜您,TDD有可能成為您顯著提升工作效率的一件法寶。請不要將信將疑,若即若離,因為任何一項新的技術——只要是從根本上改變人的行為方式的技術——就必然使得相信它的人越來越相信,不信的人越來越不信。這就好比學游泳,唯一能學會游泳的途徑就是親自下去游,除此之外別無他法。這也好比成功學,即使把卡耐基或希爾博士的書倒背如流也不能擁有積極的心態,可當你以積極的心態去成就了一番事業之后,你就再也離不開它了。相信我,TDD也是這樣!想試用TDD的人們,請遵循下面的步驟:
編寫TestCase --> 實現TestCase --> 重構 (確定范圍和目標) ? (增加功能) ? (提升設計)
[友情提示:敏捷建模中的一個相當重要的實踐被稱為:Prove it With Code,這種想法和TDD不謀而合。]
{TDD的優點}
『充滿吸引力的優點』
『不顯而易見的優點』
『有爭議的優點』
{TDD的步驟}
編寫TestCase --> 實現TestCase --> 重構 (不可運行) ? (可運行) ? (重構)
步驟 | 制品 |
(1)快速新增一個測試用例 | 新的TestCase |
(2)編譯所有代碼,剛剛寫的那個測試很可能編譯不通過 | 原始的TODO List |
(3)做盡可能少的改動,讓編譯通過 | Interface |
(4)運行所有的測試,發現最新的測試不能編譯通過 | -(Red Bar) |
(5)做盡可能少的改動,讓測試通過 | Implementation |
(6)運行所有的測試,保證每個都能通過 | -(Green Bar) |
(7)重構代碼,以消除重復設計 | Clean Code That Works |
{FAQ}
[什么時候重構?]
如果您在軟件公司工作,就意味著您成天都會和想通過重構改善代碼質量的想法打交道,不僅您如此,您的大部分同事也都如此。可是,究竟什么時候該重構,什么情況下應該重構呢?我相信您和您的同事可能有很多不同的看法,最常見的答案是“該重構時重構”,“寫不下去的時候重構”,和“下一次迭代開始之前重構”,或者干脆就是“最近沒時間,就不重構了,下次有時間的時候重構吧”。正如您已經預見到我想說的——這些想法都是對重構的誤解。重構不是一種構建軟件的工具,不是一種設計軟件的模式,也不是一個軟件開發過程中的環節,正確理解重構的人應該把重構看成一種書寫代碼的方式,或習慣,重構時時刻刻有可能發生。在TDD中,除去編寫測試用例和實現測試用例之外的所有工作都是重構,所以,沒有重構任何設計都不能實現。至于什么時候重構嘛,還要分開看,有三句話是我的經驗:實現測試用例時重構代碼,完成某個特性時重構設計,產品的重構完成后還要記得重構一下測試用例哦。
[什么時候設計?]
這個問題比前面一個要難回答的多,實話實說,本人在依照TDD開發軟件的時候也常常被這個問題困擾,總是覺得有些問題應該在寫測試用例之前定下來,而有些問題應該在新增一個一個測試用例的過程中自然出現,水到渠成。所以,我的建議是,設計的時機應該由開發者自己把握,不要受到TDD方式的限制,但是,不需要事先確定的事一定不能事先確定,免得捆住了自己的手腳。
[什么時候增加新的TestCase?]
沒事做的時候。通常我們認為,如果你要增加一個新的功能,那么先寫一個不能通過的 TestCase;如果你發現了一個bug,那么先寫一個不能通過的TestCase;如果你現在什么都沒有,從0開始,請先寫一個不能通過的 TestCase。所有的工作都是從一個TestCase開始。此外,還要注意的是,一些大師要求我們每次只允許有一個TestCase亮紅燈,在這個 TestCase沒有Green之前不可以寫別的TestCase,這種要求可以適當考慮,但即使有多個TestCase亮紅燈也不要緊,并未違反TDD 的主要精神。
[TestCase該怎么寫?]
測試用例的編寫實際上就是兩個過程:使用尚不存在的代碼和定義這些代碼的執行結果。所以一個 TestCase也就應該包括兩個部分——場景和斷言。第一次寫TestCase的人會有很大的不適應的感覺,因為你之前所寫的所有東西都是在解決問題,現在要你提出問題確實不大習慣,不過不用擔心,你正在做正確的事情,而這個世界上最難的事情也不在于如何解決問題,而在于ask the right question!
[TDD能幫助我消除Bug嗎?]
答:不能!千萬不要把“測試”和“除蟲”混為一談!“除蟲”是指程序員通過自己的努力來減少bug的數量(消除bug這樣的字眼我們還是不要講為好^_^),而“測試”是指程序員書寫產品以外的一段代碼來確保產品能有效工作。雖然TDD所編寫的測試用例在一定程度上為尋找bug提供了依據,但事實上,按照TDD的方式進行的軟件開發是不可能通過TDD再找到bug的(想想我們前面說的“完工時完工”),你想啊,當我們的代碼完成的時候,所有的測試用例都亮了綠燈,這時隱藏在代碼中的bug一個都不會露出馬腳來。
但是,如果要問“測試”和“除蟲”之間有什么聯系,我相信還是有很多話可以講的,比如TDD事實上減少了bug的數量,把查找bug戰役的關注點從全線戰場提升到代碼戰場以上。還有,bug的最可怕之處不在于隱藏之深,而在于滿天遍野。如果你發現了一個用戶很不容易才能發現的bug,那么不一定對工作做出了什么杰出貢獻,但是如果你發現一段代碼中,bug的密度或離散程度過高,那么恭喜你,你應該拋棄并重寫這段代碼了。TDD避免了這種情況,所以將尋找bug的工作降低到了一個新的低度。
[我該為一個Feature編寫TestCase還是為一個類編寫TestCase?]
初學者常問的問題。雖然我們從TDD 的說明書上看到應該為一個特性編寫相應的TestCase,但為什么著名的TDD大師所寫的TestCase都是和類/方法一一對應的呢?為了解釋這個問題,我和我的同事們都做了很多試驗,最后我們得到了一個結論,雖然我不知道是否正確,但是如果您沒有答案,可以姑且相信我們。
我們的研究結果表明,通常在一個特性的開發開始時,我們針對特性編寫測試用例,如果您發現這個特性無法用TestCase表達,那么請將這個特性細分,直至您可以為手上的特性寫出TestCase為止。從這里開始是最安全的,它不會導致任何設計上重大的失誤。但是,隨著您不斷的重構代碼,不斷的重構 TestCase,不斷的依據TDD的思想做下去,最后當產品伴隨測試用例集一起發布的時候,您就會不經意的發現經過重構以后的測試用例很可能是和產品中的類/方法一一對應的。
[什么時候應該將全部測試都運行一遍?]
Good Question!大師們要求我們每次重構之后都要完整的運行一遍測試用例。這個要求可以理解,因為重構很可能會改變整個代碼的結構或設計,從而導致不可預見的后果,但是如果我正在開發的是一個ERP怎么辦?運行一遍完整的測試用例可能將花費數個小時,況且現在很多重構都是由工具做到的,這個要求的可行性和前提條件都有所動搖。所以我認為原則上你可以挑幾個你覺得可能受到本次重構影響的TestCase去run,但是如果運行整個測試包只要花費數秒的時間,那么不介意你按大師的要求去做。
[什么時候改進一個TestCase?]
增加的測試用例或重構以后的代碼導致了原來的TestCase的失去了效果,變得無意義,甚至可能導致錯誤的結果,這時是改進TestCase的最好時機。但是有時你會發現,這樣做僅僅導致了原來的TestCase在設計上是臃腫的,或者是冗余的,這都不要緊,只要它沒有失效,你仍然不用去改進它。記住,TestCase不是你的產品,它不要好看,也不要怎么太科學,甚至沒有性能要求,它只要能完成它的使命就可以了——這也證明了我們后面所說的“用Ctrl-C/Ctrl-V編寫測試用例”的可行性。
但是,美國人的想法其實跟我們還是不太一樣,拿托尼巴贊的MindMap來說吧,其實畫MindMap只是為了表現自己的思路,或記憶某些重要的事情,但托尼卻建議大家把MindMap畫成一件藝術品,甚至還有很多藝術家把自己畫的抽象派MindMap拿出來幫助托尼做宣傳。同樣,大師們也要求我們把TestCase寫的跟代碼一樣質量精良,可我想說的是,現在國內有幾個公司能把產品的代碼寫的精良??還是一步一步慢慢來吧。
[為什么原來通過的測試用例現在不能通過了?]
這是一個警報,Red Alert!它可能表達了兩層意思——都不是什么好意思——1)你剛剛進行的重構可能失敗了,或存在一些錯誤未被發現,至少重構的結果和原來的代碼不等價了。2)你剛剛增加的TestCase所表達的意思跟前面已經有的TestCase相沖突,也就是說,新增的功能違背了已有的設計,這種情況大部分可能是之前的設計錯了。但無論哪錯了,無論是那層意思,想找到這個問題的根源都比TDD的正常工作要難。
[我怎么知道那里該有一個方法還是該有一個類?]
這個問題也是常常出現在我的腦海中,無論你是第一次接觸TDD或者已經成為 TDD專家,這個問題都會纏繞著你不放。不過問題的答案可以參考前面的“什么時候設計”一節,答案不是唯一的。其實多數時候你不必考慮未來,今天只做今天的事,只要有重構工具,從方法到類和從類到方法都很容易。
[我要寫一個TestCase,可是不知道從哪里開始?]
從最重要的事開始,what matters most?從腳下開始,從手頭上的工作開始,從眼前的事開始。從一個沒有UI的核心特性開始,從算法開始,或者從最有可能耽誤時間的模塊開始,從一個最嚴重的bug開始。這是TDD主義者和鼠目寸光者的一個共同點,不同點是前者早已成竹在胸。
[為什么我的測試總是看起來有點愚蠢?]
哦?是嗎?來,握個手,我的也是!不必擔心這一點,事實上,大師們給的例子也相當愚蠢,比如一個極端的例子是要寫一個兩個int變量相加的方法,大師先斷言2+3=5,再斷言5+5=10,難道這些代碼不是很愚蠢嗎?其實這只是一個極端的例子,當你初次接觸TDD時,寫這樣的代碼沒什么不好,以后當你熟練時就會發現這樣寫沒必要了,要記住,謙虛是通往TDD的必經之路!從經典開發方法轉向TDD就像從面向過程轉向面向對象一樣困難,你可能什么都懂,但你寫出來的類沒有一個純OO的!我的同事還告訴我真正的太極拳,其速度是很快的,不比任何一個快拳要慢,但是初學者(通常是指學習太極拳的前10年)太不容易把每個姿勢都做對,所以只能慢慢來。
[什么場合不適用TDD?]
問的好,確實有很多場合不適合使用TDD。比如對軟件質量要求極高的軍事或科研產品——神州六號,人命關天的軟件——醫療設備,等等,再比如設計很重要必須提前做好的軟件,這些都不適合TDD,但是不適合TDD不代表不能寫TestCase,只是作用不同,地位不同罷了。
{Best Practise}
[微笑面對編譯錯誤]
學生時代最害怕的就是編譯錯誤,編譯錯誤可能會被老師視為上課不認真聽課的證據,或者同學間相互嘲笑的砝碼。甚至離開學校很多年的老程序員依然害怕它就像害怕遲到一樣,潛意識里似乎編譯錯誤極有可能和工資掛鉤(或者和智商掛鉤,反正都不是什么好事)。其實,只要提交到版本管理的代碼沒有編譯錯誤就可以了,不要擔心自己手上的代碼的編譯錯誤,通常,編譯錯誤都集中在下面三個方面:
(1)你的代碼存在低級錯誤
(2)由于某些Interface的實現尚不存在,所以被測試代碼無法編譯
(3)由于某些代碼尚不存在,所以測試代碼無法編譯
請注意第二點與第三點完全不同,前者表明設計已存在,而實現不存在導致的編譯錯誤;后者則指僅有TestCase而其它什么都沒有的情況,設計和實現都不存在,沒有Interface也沒有Implementation。
另外,編譯器還有一個優點,那就是以最敏捷的身手告訴你,你的代碼中有那些錯誤。當然如果你擁有Eclipse這樣可以及時提示編譯錯誤的IDE,就不需要這樣的功能了。
[重視你的計劃清單]
在非TDD的情況下,尤其是傳統的瀑布模型的情況下,程序員不會不知道該做什么,事實上,總是有設計或者別的什么制品在引導程序員開發。但是在TDD的情況下,這種優勢沒有了,所以一個計劃清單對你來說十分重要,因為你必須自己發現該做什么。不同性格的人對于這一點會有不同的反應,我相信平時做事沒什么計劃要依靠別人安排的人(所謂將才)可能略有不適應,不過不要緊,Tasks和Calendar(又稱效率手冊)早已成為現代上班族的必備工具了;而平時工作生活就很有計劃性的人,比如我:),就會更喜歡這種自己可以掌控Plan的方式了。
[廢黜每日代碼質量檢查]
如果我沒有記錯的話,PSP對于個人代碼檢查的要求是蠻嚴格的,而同樣是在針對個人的問題上, TDD卻建議你廢黜每日代碼質量檢查,別起疑心,因為你總是在做TestCase要求你做的事情,并且總是有辦法(自動的)檢查代碼有沒有做到這些事情 ——紅燈停綠燈行,所以每日代碼檢查的時間可能被節省,對于一個嚴格的PSP實踐者來說,這個成本還是很可觀的!
此外,對于每日代碼質量檢查的另一個好處,就是幫助你認識自己的代碼,全面的從宏觀、微觀、各個角度審視自己的成果,現在,當你依照TDD做事時,這個優點也不需要了,還記得前面說的TDD的第二個優點嗎,因為你已經全面的使用了一遍你的代碼,這完全可以達到目的。
但是,問題往往也并不那么簡單,現在有沒有人能告訴我,我如何全面審視我所寫的測試用例呢?別忘了,它們也是以代碼的形式存在的哦。呵呵,但愿這個問題沒有把你嚇到,因為我相信到目前為止,它還不是瓶頸問題,況且在編寫產品代碼的時候你還是會自主的發現很多測試代碼上的沒考慮到的地方,可以就此修改一下。道理就是如此,世界上沒有任何方法能代替你思考的過程,所以也沒有任何方法能阻止你犯錯誤,TDD僅能讓你更容易發現這些錯誤而已。
[如果無法完成一個大的測試,就從最小的開始]
如果我無法開始怎么辦,教科書上有個很好的例子:我要寫一個電影列表的類,我不知道如何下手,如何寫測試用例,不要緊,首先想象靜態的結果,如果我的電影列表剛剛建立呢,那么它應該是空的,OK,就寫這個斷言吧,斷言一個剛剛初始化的電影列表是空的。這不是愚蠢,這是細節,奧運會五項全能的金牌得主瑪麗蓮·金是這樣說的:“成功人士的共同點在于……如果目標不夠清晰,他們會首先做通往成功道路上的每一個細小步驟……”。
[嘗試編寫自己的xUnit]
Kent Beck建議大家每當接觸一個新的語言或開發平臺的時候,就自己寫這個語言或平臺的xUnit,其實幾乎所有常用的語言和平臺都已經有了自己的 xUnit,而且都是大同小異,但是為什么大師給出了這樣的建議呢。其實Kent Beck的意思是說通過這樣的方式你可以很快的了解這個語言或平臺的特性,而且xUnit確實很簡單,只要知道原理很快就能寫出來。這對于那些喜歡自己寫底層代碼的人,或者喜歡控制力的人而言是個好消息。
[善于使用Ctrl-C/Ctrl-V來編寫TestCase]
不必擔心TestCase會有代碼冗余的問題,讓它冗余好了。
[永遠都是功能First,改進可以稍后進行]
上面這個標題還可以改成另外一句話:避免過渡設計!
[淘汰陳舊的用例]
舍不得孩子套不著狼。不要可惜陳舊的用例,因為它們可能從概念上已經是錯誤的了,或僅僅會得出錯誤的結果,或者在某次重構之后失去了意義。當然也不一定非要刪除它們,從TestSuite中除去(JUnit)或加上Ignored(NUnit)標簽也是一個好辦法。
[用TestCase做試驗]
如果你在開始某個特性或產品的開發之前對某個領域不太熟悉或一無所知,或者對自己在該領域里的能力一無所知,那么你一定會選擇做試驗,在有單元測試作工具的情況下,建議你用TestCase做試驗,這看起來就像你在寫一個驗證功能是否實現的 TestCase一樣,而事實上也一樣,只不過你所驗證的不是代碼本身,而是這些代碼所依賴的環境。
[TestCase之間應該盡量獨立]
保證單獨運行一個TestCase是有意義的。
[不僅測試必須要通過的代碼,還要測試必須不能通過的代碼]
這是一個小技巧,也是不同于設計思路的東西。像越界的值或者亂碼,或者類型不符的變量,這些輸入都可能會導致某個異常的拋出,或者導致一個標示“illegal parameters”的返回值,這兩種情況你都應該測試。當然我們無法枚舉所有錯誤的輸入或外部環境,這就像我們無法枚舉所有正確的輸入和外部環境一樣,只要TestCase能說明問題就可以了。
[編寫代碼的第一步,是在TestCase中用Ctrl-C]
這是一個高級技巧,呃,是的,我是這個意思,我不是說這個技巧難以掌握,而是說這個技巧當且僅當你已經是一個TDD高手時,你才能體會到它的魅力。多次使用TDD的人都有這樣的體會,既然我的TestCase已經寫的很好了,很能說明問題,為什么我的代碼不能從TestCase拷貝一些東西來呢。當然,這要求你的TestCase已經具有很好的表達能力,比如斷言f (5)=125的方式顯然沒有斷言f(5)=5^(5-2)表達更多的內容。
[測試用例包應該盡量設計成可以自動運行的]
如果產品是需要交付源代碼的,那我們應該允許用戶對代碼進行修改或擴充后在自己的環境下run整個測試用例包。既然通常情況下的產品是可以自動運行的,那為什么同樣作為交付用戶的制品,測試用例包就不是自動運行的呢?即使產品不需要交付源代碼,測試用例包也應該設計成可以自動運行的,這為測試部門或下一版本的開發人員提供了極大的便利。
[只亮一盞紅燈]
大師的建議,前面已經提到了,僅僅是建議。
[用TestCase描述你發現的bug]
如果你在另一個部門的同事使用了你的代碼,并且,他發現了一個bug,你猜他會怎么做?他會立即走到你的工位邊上,大聲斥責說:“你有bug!”嗎?如果他膽敢這樣對你,對不起,你一定要冷靜下來,不要當面回罵他,相反你可以微微一笑,然后心平氣和的對他說:“哦,是嗎?那么好吧,給我一個TestCase證明一下。”現在局勢已經倒向你這一邊了,如果他還沒有準備好回答你這致命的一擊,我猜他會感到非常羞愧,并在內心責怪自己太莽撞。事實上,如果他的TestCase沒有過多的要求你的代碼(而是按你們事前的契約),并且亮了紅燈,那么就可以確定是你的bug,反之,對方則無理了。用TestCase描述bug的另一個好處是,不會因為以后的修改而再次暴露這個bug,它已經成為你發布每一個版本之前所必須檢查的內容了。
{關于單元測試}
單元測試的目標是
Keep the bar green to keep the code clean
這句話的含義是,事實上我們只做兩件事情:讓代碼奏效(Keep the bar green)和讓代碼潔凈(Keep the code clean),前者是把事情做對,后者是把事情做好,兩者既是TDD中的兩頂帽子,又是xUnit架構中的因果關系。
單元測試作為軟件測試的一個類別,并非是xUnit架構創造的,而是很早就有了。但是xUnit架構使得單元測試變得直接、簡單、高效和規范,這也是單元測試最近幾年飛速發展成為衡量一個開發工具和環境的主要指標之一的原因。正如Martin Fowler所說:“軟件工程有史以來從沒有如此眾多的人大大收益于如此簡單的代碼!”而且多數語言和平臺的xUnit架構都是大同小異,有的僅是語言不同,其中最有代表性的是JUnit和NUnit,后者是前者的創新和擴展。一個單元測試框架xUnit應該:1)使每個TestCase獨立運行;2)使每個TestCase可以獨立檢測和報告錯誤;3)易于在每次運行之前選擇TestCase。下面是我枚舉出的xUnit框架的概念,這些概念構成了當前業界單元測試理論和工具的核心:
[測試方法/TestMethod]
測試的最小單位,直接表示為代碼。
[測試用例/TestCase]
由多個測試方法組成,是一個完整的對象,是很多TestRunner執行的最小單位。
[測試容器/TestSuite]
由多個測試用例構成,意在把相同含義的測試用例手動安排在一起,TestSuite可以呈樹狀結構因而便于管理。在實現時,TestSuite形式上往往也是一個TestCase或TestFixture。
[斷言/Assertion]
斷言一般有三類,分別是比較斷言(如assertEquals),條件斷言(如isTrue),和斷言工具(如fail)。
[測試設備/TestFixture]
為每個測試用例安排一個SetUp方法和一個TearDown方法,前者用于在執行該測試用例或該用例中的每個測試方法前調用以初始化某些內容,后者在執行該測試用例或該用例中的每個方法之后調用,通常用來消除測試對系統所做的修改。
[期望異常/Expected Exception]
期望該測試方法拋出某種指定的異常,作為一個“斷言”內容,同時也防止因為合情合理的異常而意外的終止了測試過程。
[種類/Category]
為測試用例分類,實際使用時一般有TestSuite就不再使用Category,有Category就不再使用TestSuite。
[忽略/Ignored]
設定該測試用例或測試方法被忽略,也就是不執行的意思。有些被拋棄的TestCase不愿刪除,可以定為Ignored。
[測試執行器/TestRunner]
執行測試的工具,表示以何種方式執行測試,別誤會,這可不是在代碼中規定的,完全是與測試內容無關的行為。比如文本方式,AWT方式,swing方式,或者Eclipse的一個視圖等等。
{實例:Fibonacci數列}
下面的Sample展示TDDer是如何編寫一個旨在產生Fibonacci數列的方法。
(1)首先寫一個TC,斷言fib(1) = 1;fib(2) = 1;這表示該數列的第一個元素和第二個元素都是1。
(2)上面這段代碼不能編譯通過,Great!——是的,我是說Great!當然,如果你正在用的是Eclipse那你不需要編譯,Eclipse 會告訴你不存在fib方法,單擊mark會問你要不要新建一個fib方法,Oh,當然!為了讓上面那個TC能通過,我們這樣寫:
(3)現在那個TC亮了綠燈,wow!應該慶祝一下了。接下來要增加TC的難度了,測第三個元素。
不過這樣寫還不太好看,不如這樣寫:
(4)新增加的斷言導致了紅燈,為了扭轉這一局勢我們這樣修改fib方法,其中部分代碼是從上面的代碼中Ctrl-C/Ctrl-V來的:
(5)天哪,這真是個賤人寫的代碼!是啊,不是嗎?因為TC就是產品的藍本,產品只要恰好滿足TC就ok。所以事情發展到這個地步不是fib方法的錯,而是TC的錯,于是TC還要進一步要求:
(6)上有政策下有對策。
(7)好了,不玩了。現在已經不是賤不賤的問題了,現在的問題是代碼出現了冗余,所以我們要做的是——重構:
(8)好,現在你已經fib方法已經寫完了嗎?錯了,一個危險的錯誤,你忘了錯誤的輸入了。我們令0表示Fibonacci中沒有這一項。
then change the method fib to make the bar grean:
(9)下班前最后一件事情,把TC也重構一下:
(10)打完收工。
{關于本文的寫作}
在本文的寫作過程中,作者也用到了TDD的思維,事實上作者先構思要寫一篇什么樣的文章,然后寫出這篇文章應該滿足的幾個要求,包括功能的要求(要寫些什么)和性能的要求(可讀性如何)和質量的要求(文字的要求),這些要求起初是一個也達不到的(因為正文還一個字沒有),在這種情況下作者的文章無法編譯通過,為了達到這些要求,作者不停的寫啊寫啊,終于在花盡了兩個月的心血之后完成了當初既定的所有要求(make the bar green),隨后作者整理了一下文章的結構(重構),在滿意的提交給了Blog系統之后,作者穿上了一件綠色的汗衫,趴在地上,學了兩聲青蛙叫。。。。。。。^_^
{后記:Martin Fowler在中國}
從本文正式完成到發表的幾個小時里,我偶然讀到了Martin Fowler先生北京訪談錄,其間提到了很多對測試驅動開發的看法,摘抄在此:
Martin Fowler:當然(值得花一半的時間來寫單元測試)!因為單元測試能夠使你更快的完成工作。無數次的實踐已經證明這一點。你的時間越是緊張,就越要寫單元測試,它看上去慢,但實際上能夠幫助你更快、更舒服地達到目的。
Martin Fowler:什么叫重要?什么叫不重要?這是需要逐漸認識的,不是想當然的。我為絕大多數的模塊寫單元測試,是有點煩人,但是當你意識到這工作的價值時,你會欣然的。
Martin Fowler:對全世界的程序員我都是那么幾條建議:……第二,學習測試驅動開發,這種新的方法會改變你對于軟件開發的看法。……——《程序員》,2005年7月刊
{鳴謝}
fhawk
Dennis Chen
般若菩提
Kent Beck
Martin Fowler
c2.com
(轉載本文需注明出處:Brian Sun @ 爬樹的泡泡[http://www.aygfsteel.com/briansun])
MD5算法是將數據進行不可逆加密的算法有較好的安全性,在國內如壽信的安全支付平臺就采用此算法。這幾天花了點時間弄了個 db4o 連接池,比較簡單,連接池原型是論壇上面的一篇文章。很簡單,歡迎拍磚。
從 servlet 開始,在這里初始化連接池:
人生有三重境界,這三重境界可以用一段充滿禪機的語言來說明,這段語言便是:
看山是山,看水是水; ?
看山不是山,看水不是水; ?
看山還是山。看水還是水。 ?
“看山是山,看水是水”就是說一個人的人生之初純潔無瑕,初識世界,一切都是新鮮的,眼睛看見什么就是什么,人家告訴他這是山,他就認識了山;告訴他這是水,他就認識了水。這就是人生的第一重境界。
“看山不是山,看水不是水”,隨著年齡漸長,經歷的世事漸多,就發現這個世界的問題了。這個世界問題越來越多,越來越復雜,經常是黑白顛倒,是非混淆,無理走遍天下,有理寸步難行,好人無好報,惡人活千年。進人這個階段,人是激憤的,不平的,憂慮的,疑問的,警惕的,復雜的。人不愿意再輕易地相信什么。
人到了這個時候看山也感慨,看水也嘆息,借古諷今,指桑罵槐。山自然不再是單純的山,水自然不再是單純的水。一切的一切都是人的主觀意志的載體,所謂“好風憑借力,送我上青云”。一個人倘若停留在人生的這一階段,那就苦了這條性命了。人就會這山望了那山高,不停地攀登,爭強好勝,與人比較,怎么做人,如何處世,絞盡腦汁,機關算盡,永無休止和滿足的一天。因為這個世界原本就是一個圓的,人外還有人,天外還有天,循環往復,綠水長流。而人的生命是短暫的有限的,哪里能夠去與永恒和無限計較呢?
許多人到了人生的第二重境界就到了人生的終點。追求一生.勞碌一生,心高氣傲一生,最后發現自己并沒有達到自己的理想,于是抱恨終生。但是有些人通過自己的修煉,終于把自己提升到了第三重人生境界。茅塞頓開,回歸自然。人這個時候便會專心致志做自己應該做的事情,不與旁人有任何計較。任你紅塵滾滾,我自清風朗月。面對蕪雜世俗之事,一笑了之,了了有何不了,這個時候的人看山又是山,看水又是水了。
正是:人本是人,不必刻意去做人;世本是世,無須精心去處世;便也就是真正的做人與處世了。
package com.hoten.util;
import java.util.*;
import java.io.*;
/**
* <p>Title: Time </p>
* <p>Description: </p>
* 此類主要用來取得本地系統的系統時間并用下面5種格式顯示
* 1. YYMMDDHH 8位
* 2. YYMMDDHHmm 10位
* 3. YYMMDDHHmmss 12位
* 4. YYYYMMDDHHmmss 14位
* 5. YYMMDDHHmmssxxx 15位 (最后的xxx 是毫秒)
* <p>Copyright: Copyright (c) 2003</p>
* <p>Company: c-platform</p>
* @author WuJiaQian
* @version 1.0
*/
public class CTime {
public static final int YYMMDDhhmmssxxx = 15;
public static final int YYYYMMDDhhmmss = 14;
public static final int YYMMDDhhmmss = 12;
public static final int YYMMDDhhmm = 10;
public static final int YYMMDDhh = 8;
/**
* 取得本地系統的時間,時間格式由參數決定
* @param format 時間格式由常量決定
* @return String 具有format格式的字符串
*/
public static String getTime(int format) {
StringBuffer cTime = new StringBuffer(15);
Calendar time = Calendar.getInstance();
int miltime = time.get(Calendar.MILLISECOND);
int second = time.get(Calendar.SECOND);
int minute = time.get(Calendar.MINUTE);
int hour = time.get(Calendar.HOUR_OF_DAY);
int day = time.get(Calendar.DAY_OF_MONTH);
int month = time.get(Calendar.MONTH) + 1;
int year = time.get(Calendar.YEAR);
time = null;
if (format != 14) {
if (year >= 2000) year = year - 2000;
else year = year - 1900;
}
if (format >= 2) {
if (format == 14) cTime.append(year);
else cTime.append(getFormatTime(year, 2));
}
if (format >= 4)
cTime.append(getFormatTime(month, 2));
if (format >= 6)
cTime.append(getFormatTime(day, 2));
if (format >= 8)
cTime.append(getFormatTime(hour, 2));
if (format >= 10)
cTime.append(getFormatTime(minute, 2));
if (format >= 12)
cTime.append(getFormatTime(second, 2));
if (format >= 15)
cTime.append(getFormatTime(miltime, 3));
return cTime.toString().trim();
}
/**
* 產生任意位的字符串
* @param time int 要轉換格式的時間
* @param format int 轉換的格式
* @return String 轉換的時間
*/
public synchronized static String getYearAdd(int format, int iyear) {
StringBuffer cTime = new StringBuffer(10);
Calendar time = Calendar.getInstance();
time.add(Calendar.YEAR, iyear);
int miltime = time.get(Calendar.MILLISECOND);
int second = time.get(Calendar.SECOND);
int minute = time.get(Calendar.MINUTE);
int hour = time.get(Calendar.HOUR_OF_DAY);
int day = time.get(Calendar.DAY_OF_MONTH);
int month = time.get(Calendar.MONTH) + 1;
int year = time.get(Calendar.YEAR);
if (format != 14) {
if (year >= 2000) year = year - 2000;
else year = year - 1900;
}
if (format >= 2) {
if (format == 14) cTime.append(year);
else cTime.append(getFormatTime(year, 2));
}
if (format >= 4)
cTime.append(getFormatTime(month, 2));
if (format >= 6)
cTime.append(getFormatTime(day, 2));
if (format >= 8)
cTime.append(getFormatTime(hour, 2));
if (format >= 10)
cTime.append(getFormatTime(minute, 2));
if (format >= 12)
cTime.append(getFormatTime(second, 2));
if (format >= 15)
cTime.append(getFormatTime(miltime, 3));
return cTime.toString();
}
/**
* 產生任意位的字符串
* @param time int 要轉換格式的時間
* @param format int 轉換的格式
* @return String 轉換的時間
*/
private static String getFormatTime(int time, int format) {
StringBuffer numm = new StringBuffer(format);
int length = String.valueOf(time).length();
if (format < length)return null;
for (int i = 0; i < format - length; i++) {
numm.append("0");
}
numm.append(time);
return numm.toString().trim();
}
/**
* 本函數主要作用是返回當前年份
* @param len int 要轉換年的位數
* @return String 處理后的年
*/
public static String getYear(int len) {
Calendar time = Calendar.getInstance();
int year = time.get(Calendar.YEAR);
String djyear = Integer.toString(year);
if (len == 2) {
djyear = djyear.substring(2);
}
return djyear;
}
/*
#本函數作用是返回當前月份(2位)
*/
public static String getMonth() {
Calendar time = Calendar.getInstance();
int month = time.get(Calendar.MONTH) + 1;
String djmonth = "";
if (month < 10) {
djmonth = "0" + Integer.toString(month);
}
else {
djmonth = Integer.toString(month);
}
return djmonth;
}
/*
#本函數作用是返回上個月份(2位)
*/
public static String getPreMonth() {
Calendar time = Calendar.getInstance();
int month = time.get(Calendar.MONTH);
if (month == 0) month = 12;
String djmonth = "";
if (month < 10) {
djmonth = "0" + Integer.toString(month);
}
else {
djmonth = Integer.toString(month);
}
return djmonth;
}
/*
#本函數主要作用是返回當前天數
*/
public static String getDay() {
Calendar time = Calendar.getInstance();
int day = time.get(Calendar.DAY_OF_MONTH);
String djday = "";
if (day < 10) {
djday = "0" + Integer.toString(day);
}
else {
djday = Integer.toString(day);
}
return djday;
}
/*
本函數作用是返回當前小時
*/
public static String getHour() {
Calendar time = Calendar.getInstance();
int hour = time.get(Calendar.HOUR_OF_DAY);
String djhour = "";
if (hour < 10) {
djhour = "0" + Integer.toString(hour);
}
else {
djhour = Integer.toString(hour);
}
return djhour;
}
/*
#本函數作用是返回當前分鐘
*/
public static String getMin() {
Calendar time = Calendar.getInstance();
int min = time.get(Calendar.MINUTE);
String djmin = "";
if (min < 10) {
djmin = "0" + Integer.toString(min);
}
else {
djmin = Integer.toString(min);
}
return djmin;
}
/*
#本函數的主要功能是格式化時間,以便于頁面顯示
#time 時間 可為6位、8位、12位、15位
#return 返回格式化后的時間
#6位 YY年MM月DD日
#8位 YYYY年MM月DD日
#12位 YY年MM月DD日 HH:II:SS
#15位 YY年MM月DD日 HH:II:SS:CCC
*/
public static String formattime(String time) {
int length = 0;
length = time.length();
String renstr = "";
switch (length) {
case 6:
renstr = time.substring(0, 2) + "年" + time.substring(2, 4) +
"月" + time.substring(4) + "日";
break;
case 8:
renstr = time.substring(0, 4) + "年" + time.substring(4, 6) +
"月" + time.substring(6, 8) + "日";
break;
case 12:
renstr = time.substring(0, 2) + "年" + time.substring(2, 4) +
"月" + time.substring(4, 6) + "日 " + time.substring(6, 8) +
"時" + time.substring(8, 10) + "分" +
time.substring(10, 12) + "秒";
break;
case 14:
renstr = time.substring(0, 4) + "-" + time.substring(4, 6) +
"-" + time.substring(6, 8) + " " + time.substring(8, 10) +
":" + time.substring(10, 12) + ":" +
time.substring(12, 14) + "";
break;
case 15:
renstr = time.substring(0, 2) + "年" + time.substring(2, 4) +
"月" + time.substring(4, 6) + "日 " + time.substring(6, 8) +
":" + time.substring(8, 10) + ":" +
time.substring(10, 12) + ":" + time.substring(12);
break;
default:
renstr = time.substring(0, 2) + "年" + time.substring(2, 4) +
"月" + time.substring(4) + "日";
break;
}
return renstr;
}
}
許多語言,包括Perl、PHP、Python、JavaScript和JScript,都支持用正則表達式處理文本,一些文本編輯器用正則表達式實現高級“搜索-替換”功能。那么Java又怎樣呢?
本文寫作時,一個包含了用正則表達式進行文本處理的Java規范需求(Specification Request)已經得到認可,你可以期待在JDK的下一版本中看到它。
然而,如果現在就需要使用正則表達式,又該怎么辦呢?你可以從Apache.org下載源代碼開放的Jakarta-ORO庫。本文接下來的內容先簡要地介紹正則表達式的入門知識,然后以Jakarta-ORO API為例介紹如何使用正則表達式。
一、正則表達式基礎知識
我們先從簡單的開始。假設你要搜索一個包含字符“cat”的字符串,搜索用的正則表達式就是“cat”。如果搜索對大小寫不敏感,單詞“ctalog”、“Catherine”、“sophisticated”都可以匹配。也就是說:
1.1 句點符號
假設你在玩英文拼字游戲,想要找出三個字母的單詞,而且這些單詞必須以“t”字母開頭,以“n”字母結束。另外,假設有一本英文字典,你可以用正則表達式搜索它的全部內容。要構造出這個正則表達式,你可以使用一個通配符——句點符號“。”。這樣,完整的表達式就是“t.n”,它匹配“tan”、“ten”、“tin”和“ton”,還匹配“t#n”、“tpn”甚至“t n”,還有其他許多無意義的組合。這是因為句點符號匹配所有字符,包括空格、Tab字符甚至換行符:
1.2 方括號符號
為了解決句點符號匹配范圍過于廣泛這一問題,你可以在方括號(“[]”)里面指定看來有意義的字符。此時,只有方括號里面指定的字符才參與匹配。也就是說,正則表達式“t[aeio]n”只匹配“tan”、“Ten”、“tin”和“ton”。但“Toon”不匹配,因為在方括號之內你只能匹配單個字符:
1.3 “或”符號
如果除了上面匹配的所有單詞之外,你還想要匹配“toon”,那么,你可以使用“|”操作符。“|”操作符的基本意義就是“或”運算。要匹配“toon”,使用“t(a|e|i|o|oo)n”正則表達式。這里不能使用方擴號,因為方括號只允許匹配單個字符;這里必須使用圓括號“()”。圓括號還可以用來分組,具體請參見后面介紹。
1.4 表示匹配次數的符號
表一顯示了表示匹配次數的符號,這些符號用來確定緊靠該符號左邊的符號出現的次數:
假設我們要在文本文件中搜索美國的社會安全號碼。這個號碼的格式是999-99-9999.用來匹配它的正則表達式如圖一所示。在正則表達式中,連字符(“-”)有著特殊的意義,它表示一個范圍,比如從0到9.因此,匹配社會安全號碼中的連字符號時,它的前面要加上一個轉義字符“\”。
圖一:匹配所有123-12-1234形式的社會安全號碼
假設進行搜索的時候,你希望連字符號可以出現,也可以不出現——即,999-99-9999和999999999都屬于正確的格式。這時,你可以在連字符號后面加上“?”數量限定符號,如圖二所示:
圖二:匹配所有123-12-1234和123121234形式的社會安全號碼
下面我們再來看另外一個例子。美國汽車牌照的一種格式是四個數字加上二個字母。它的正則表達式前面是數字部分“[0-9]{4}”,再加上字母部分“[A-Z]{2}”。圖三顯示了完整的正則表達式。
圖三:匹配典型的美國汽車牌照號碼,如8836KV
1.5 “否”符號
“^”符號稱為“否”符號。如果用在方括號內,“^”表示不想要匹配的字符。例如,圖四的正則表達式匹配所有單詞,但以“X”字母開頭的單詞除外。
圖四:匹配所有單詞,但“X”開頭的除外
1.6 圓括號和空白符號
假設要從格式為“June 26, 1951”的生日日期中提取出月份部分,用來匹配該日期的正則表達式可以如圖五所示:
圖五:匹配所有Moth DD,YYYY格式的日期
新出現的“\s”符號是空白符號,匹配所有的空白字符,包括Tab字符。如果字符串正確匹配,接下來如何提取出月份部分呢?只需在月份周圍加上一個圓括號創建一個組,然后用ORO API(本文后面詳細討論)提取出它的值。修改后的正則表達式如圖六所示:
圖六:匹配所有Month DD,YYYY格式的日期,定義月份值為第一個組
1.7 其它符號
為簡便起見,你可以使用一些為常見正則表達式創建的快捷符號。如表二所示:
表二:常用符號
例如,在前面社會安全號碼的例子中,所有出現“[0-9]”的地方我們都可以使用“\d”。修改后的正則表達式如圖七所示:
圖七:匹配所有123-12-1234格式的社會安全號碼二、Jakarta-ORO庫
二、Jakarta-ORO庫
有許多源代碼開放的正則表達式庫可供Java程序員使用,而且它們中的許多支持Perl 5兼容的正則表達式語法。我在這里選用的是Jakarta-ORO正則表達式庫,它是最全面的正則表達式API之一,而且它與Perl 5正則表達式完全兼容。另外,它也是優化得最好的API之一。
Jakarta-ORO庫以前叫做OROMatcher,Daniel Savarese大方地把它贈送給了Jakarta Project.你可以按照本文最后參考資源的說明下載它。
我首先將簡要介紹使用Jakarta-ORO庫時你必須創建和訪問的對象,然后介紹如何使用Jakarta-ORO API.
▲ PatternCompiler對象
首先,創建一個Perl5Compiler類的實例,并把它賦值給PatternCompiler接口對象。Perl5Compiler是PatternCompiler接口的一個實現,允許你把正則表達式編譯成用來匹配的Pattern對象。
Pattern對象
要把正則表達式編譯成Pattern對象,調用compiler對象的compile()方法,并在調用參數中指定正則表達式。例如,你可以按照下面這種方式編譯正則表達式“t[aeio]n”:
默認情況下,編譯器創建一個大小寫敏感的模式(pattern)。因此,上面代碼編譯得到的模式只匹配“tin”、“tan”、 “ten”和“ton”,但不匹配“Tin”和“taN”。要創建一個大小寫不敏感的模式,你應該在調用編譯器的時候指定一個額外的參數:
創建好Pattern對象之后,你就可以通過PatternMatcher類用該Pattern對象進行模式匹配。
▲ PatternMatcher對象
PatternMatcher對象根據Pattern對象和字符串進行匹配檢查。你要實例化一個Perl5Matcher類并把結果賦值給PatternMatcher接口。Perl5Matcher類是PatternMatcher接口的一個實現,它根據Perl 5正則表達式語法進行模式匹配:
用PatternMatcher對象,你可以用多個方法進行匹配操作,這些方法的第一個參數都是需要根據正則表達式進行匹配的字符串:
“· boolean matches(String input, Pattern pattern):當輸入字符串和正則表達式要精確匹配時使用。換句話說,正則表達式必須完整地描述輸入字符串。
· boolean matchesPrefix(String input, Pattern pattern):當正則表達式匹配輸入字符串起始部分時使用。
· boolean contains(String input, Pattern pattern):當正則表達式要匹配輸入字符串的一部分時使用(即,它必須是一個子串)。
另外,在上面三個方法調用中,你還可以用PatternMatcherInput對象作為參數替代String對象;這時,你可以從字符串中最后一次匹配的位置開始繼續進行匹配。當字符串可能有多個子串匹配給定的正則表達式時,用PatternMatcherInput對象作為參數就很有用了。用PatternMatcherInput對象作為參數替代String時,上述三個方法的語法如下:。 boolean matches(PatternMatcherInput input, Pattern pattern)。 boolean matchesPrefix(PatternMatcherInput input, Pattern pattern)。 boolean contains(PatternMatcherInput input, Pattern pattern)
三、應用實例
下面我們來看看Jakarta-ORO庫的一些應用實例。
3.1 日志文件處理
任務:分析一個Web服務器日志文件,確定每一個用戶花在網站上的時間。在典型的BEA WebLogic日志文件中,日志記錄的格式如下:
分析這個日志記錄,可以發現,要從這個日志文件提取的內容有兩項:IP地址和頁面訪問時間。你可以用分組符號(圓括號)從日志記錄提取出IP地址和時間標記。
首先我們來看看IP地址。IP地址有4個字節構成,每一個字節的值在0到255之間,各個字節通過一個句點分隔。因此,IP地址中的每一個字節有至少一個、最多三個數字。圖八顯示了為IP地址編寫的正則表達式:
圖八:匹配IP地址
IP地址中的句點字符必須進行轉義處理(前面加上“\”),因為IP地址中的句點具有它本來的含義,而不是采用正則表達式語法中的特殊含義。句點在正則表達式中的特殊含義本文前面已經介紹。
日志記錄的時間部分由一對方括號包圍。你可以按照如下思路提取出方括號里面的所有內容:首先搜索起始方括號字符(“[”),提取出所有不超過結束方括號字符(“]”)的內容,向前尋找直至找到結束方括號字符。圖九顯示了這部分的正則表達式。
圖九:匹配至少一個字符,直至找到“]”
現在,把上述兩個正則表達式加上分組符號(圓括號)后合并成單個表達式,這樣就可以從日志記錄提取出IP地址和時間。注意,為了匹配“- -”(但不提取它),正則表達式中間加入了“\s-\s-\s”。完整的正則表達式如圖十所示。
圖十:匹配IP地址和時間標記
現在正則表達式已經編寫完畢,接下來可以編寫使用正則表達式庫的Java代碼了。
為使用Jakarta-ORO庫,首先創建正則表達式字符串和待分析的日志記錄字符串:
這里使用的正則表達式與圖十的正則表達式差不多完全相同,但有一點例外:在Java中,你必須對每一個向前的斜杠(“\”)進行轉義處理。圖十不是Java的表示形式,所以我們要在每個“\”前面加上一個“\”以免出現編譯錯誤。遺憾的是,轉義處理過程很容易出現錯誤,所以應該小心謹慎。你可以首先輸入未經轉義處理的正則表達式,然后從左到右依次把每一個“\”替換成“\\”。如果要復檢,你可以試著把它輸出到屏幕上。
初始化字符串之后,實例化PatternCompiler對象,用PatternCompiler編譯正則表達式創建一個Pattern對象:
現在,創建PatternMatcher對象,調用PatternMatcher接口的contain()方法檢查匹配情況:
接下來,利用PatternMatcher接口返回的MatchResult對象,輸出匹配的組。由于logEntry字符串包含匹配的內容,你可以看到類如下面的輸出:
3.2 HTML處理實例一
下面一個任務是分析HTML頁面內FONT標記的所有屬性。HTML頁面內典型的FONT標記如下所示:
程序將按照如下形式,輸出每一個FONT標記的屬性:
在這種情況下,我建議你使用兩個正則表達式。第一個如圖十一所示,它從字體標記提取出“"face="Arial, Serif" size="+2" color="red"”。
圖十一:匹配FONT標記的所有屬性
第二個正則表達式如圖十二所示,它把各個屬性分割成名字-值對。
圖十二:匹配單個屬性,并把它分割成名字-值對
分割結果為:
現在我們來看看完成這個任務的Java代碼。首先創建兩個正則表達式字符串,用Perl5Compiler把它們編譯成Pattern對象。編譯正則表達式的時候,指定Perl5Compiler.CASE_INSENSITIVE_MASK選項,使得匹配操作不區分大小寫。
接下來,創建一個執行匹配操作的Perl5Matcher對象。
假設有一個String類型的變量html,它代表了HTML文件中的一行內容。如果html字符串包含FONT標記,匹配器將返回true.此時,你可以用匹配器對象返回的MatchResult對象獲得第一個組,它包含了FONT的所有屬性:
接下來創建一個PatternMatcherInput對象。這個對象允許你從最后一次匹配的位置開始繼續進行匹配操作,因此,它很適合于提取FONT標記內屬性的名字-值對。創建PatternMatcherInput對象,以參數形式傳入待匹配的字符串。然后,用匹配器實例提取出每一個FONT的屬性。這通過指定PatternMatcherInput對象(而不是字符串對象)為參數,反復地調用PatternMatcher對象的contains()方法完成。PatternMatcherInput對象之中的每一次迭代將把它內部的指針向前移動,下一次檢測將從前一次匹配位置的后面開始。
本例的輸出結果如下:
3.3 HTML處理實例二
下面我們來看看另一個處理HTML的例子。這一次,我們假定Web服務器從widgets.acme.com移到了newserver.acme.com.現在你要修改一些頁面中的鏈接:
執行這個搜索的正則表達式如圖十三所示:
圖十三:匹配修改前的鏈接
如果能夠匹配這個正則表達式,你可以用下面的內容替換圖十三的鏈接:
注意#字符的后面加上了$1.Perl正則表達式語法用$1、$2等表示已經匹配且提取出來的組。圖十三的表達式把所有作為一個組匹配和提取出來的內容附加到鏈接的后面。
現在,返回Java.就象前面我們所做的那樣,你必須創建測試字符串,創建把正則表達式編譯到Pattern對象所必需的對象,以及創建一個PatternMatcher對象:
接下來,用com.oroinc.text.regex包Util類的substitute()靜態方法進行替換,輸出結果字符串:
Util.substitute()方法的語法如下:
這個調用的前兩個參數是以前創建的PatternMatcher和Pattern對象。第三個參數是一個Substiution對象,它決定了替換操作如何進行。本例使用的是Perl5Substitution對象,它能夠進行Perl5風格的替換。第四個參數是想要進行替換操作的字符串,最后一個參數允許指定是否替換模式的所有匹配子串(Util.SUBSTITUTE_ALL),或只替換指定的次數。
「結束語」
在這篇文章中,我為你介紹了正則表達式的強大功能。只要正確運用,正則表達式能夠在字符串提取和文本修改中起到很大的作用。另外,我還介紹了如何在Java程序中通過Jakarta-ORO庫利用正則表達式。至于最終采用老式的字符串處理方式(使用StringTokenizer,charAt,和substring),還是采用正則表達式,這就有待你自己決定了。
權限往往是一個極其復雜的問題,但也可簡單表述為這樣的邏輯表達式:判斷“Who對What(Which)進行How的操作”的邏輯表達式是否為真。針對不同的應用,需要根據項目的實際情況和具體架構,在維護性、靈活性、完整性等N多個方案之間比較權衡,選擇符合的方案。
目標:
直觀,因為系統最終會由最終用戶來維護,權限分配的直觀和容易理解,顯得比較重要,系統不辭勞苦的實現了組的繼承,除了功能的必須,更主要的就是因為它足夠直觀。
簡單,包括概念數量上的簡單和意義上的簡單還有功能上的簡單。想用一個權限系統解決所有的權限問題是不現實的。設計中將常常變化的“定制”特點比較強的部分判斷為業務邏輯,而將常常相同的“通用”特點比較強的部分判斷為權限邏輯就是基于這樣的思路。
擴展,采用可繼承在擴展上的困難。的Group概念在支持權限以組方式定義的同時有效避免了重定義時
現狀:
對于在企業環境中的訪問控制方法,一般有三種:
1.自主型訪問控制方法。目前在我國的大多數的信息系統中的訪問控制模塊中基本是借助于自主型訪問控制方法中的訪問控制列表(ACLs)。
2.強制型訪問控制方法。用于多層次安全級別的軍事應用。
3.基于角色的訪問控制方法(RBAC)。是目前公認的解決大型企業的統一資源訪問控制的有效方法。其顯著的兩大特征是:1.減小授權管理的復雜性,降低管理開銷。2.靈活地支持企業的安全策略,并對企業的變化有很大的伸縮性。
名詞:
粗粒度:表示類別級,即僅考慮對象的類別(the type of object),不考慮對象的某個特
定實例。比如,用戶管理中,創建、刪除,對所有的用戶都一視同仁,并不區分操作的具體對象實例。
細粒度:表示實例級,即需要考慮具體對象的實例(the instance of object),當然,細
粒度是在考慮粗粒度的對象類別之后才再考慮特定實例。比如,合同管理中,列表、刪除,需要區分該合同實例是否為當前用戶所創建。
原則:
權限邏輯配合業務邏輯。即權限系統以為業務邏輯提供服務為目標。相當多細粒度的權限問題因其極其獨特而不具通用意義,它們也能被理解為是“業務邏輯”的一部分。比如,要求:“合同資源只能被它的創建者刪除,與創建者同組的用戶可以修改,所有的用戶能夠瀏覽”。這既可以認為是一個細粒度的權限問題,也可以認為是一個業務邏輯問題。在這里它是業務邏輯問題,在整個權限系統的架構設計之中不予過多考慮。當然,權限系統的架構也必須要能支持這樣的控制判斷。或者說,系統提供足夠多但不是完全的控制能力。即,設計原則歸結為:“系統只提供粗粒度的權限,細粒度的權限被認為是業務邏輯的職責”。
需要再次強調的是,這里表述的權限系統僅是一個“不完全”的權限系統,即,它不提供所有關于權限的問題的解決方法。它提供一個基礎,并解決那些具有“共性”的(或者說粗粒度的)部分。在這個基礎之上,根據“業務邏輯”的獨特權限需求,編碼實現剩余部分(或者說細粒度的)部分,才算完整。回到權限的問題公式,通用的設計僅解決了Who+What+How 的問題,其他的權限問題留給業務邏輯解決。
概念:
Who:權限的擁用者或主體(Principal、User、Group、Role、Actor等等)
What:權限針對的對象或資源(Resource、Class)。
How:具體的權限(Privilege, 正向授權與負向授權)。
Role:是角色,擁有一定數量的權限。
Operator:操作。表明對What的How 操作。
說明:
User:與 Role 相關,用戶僅僅是純粹的用戶,權限是被分離出去了的。User是不能與 Privilege 直接相關的,User 要擁有對某種資源的權限,必須通過Role去關聯。解決 Who 的問題。
Resource:就是系統的資源,比如部門新聞,文檔等各種可以被提供給用戶訪問的對象。資源可以反向包含自身,即樹狀結構,每一個資源節點可以與若干指定權限類別相關可定義是否將其權限應用于子節點。
Privilege:是Resource Related的權限。就是指,這個權限是綁定在特定的資源實例上的。比如說部門新聞的發布權限,叫做"部門新聞發布權限"。這就表明,該Privilege是一個發布權限,而且是針對部門新聞這種資源的一種發布權限。Privilege是由Creator在做開發時就確定的。權限,包括系統定義權限和用戶自定義權限用戶自定義權限之間可以指定排斥和包含關系(如:讀取,修改,管理三個權限,管理 權限 包含 前兩種權限)。Privilege 如"刪除" 是一個抽象的名詞,當它不與任何具體的 Object 或 Resource 綁定在一起時是沒有任何意義的。拿新聞發布來說,發布是一種權限,但是只說發布它是毫無意義的。因為不知道發布可以操作的對象是什么。只有當發布與新聞結合在一起時,才會產生真正的 Privilege。這就是 Privilege Instance。權限系統根據需求的不同可以延伸生很多不同的版本。
Role:是粗粒度和細粒度(業務邏輯)的接口,一個基于粗粒度控制的權限框架軟件,對外的接口應該是Role,具體業務實現可以直接繼承或拓展豐富Role的內容,Role不是如同User或Group的具體實體,它是接口概念,抽象的通稱。
Group:用戶組,權限分配的單位與載體。權限不考慮分配給特定的用戶。組可以包括組(以實現權限的繼承)。組可以包含用戶,組內用戶繼承組的權限。Group要實現繼承。即在創建時必須要指定該Group的Parent是什么Group。在粗粒度控制上,可以認為,只要某用戶直接或者間接的屬于某個Group那么它就具備這個Group的所有操作許可。細粒度控制上,在業務邏輯的判斷中,User僅應關注其直接屬于的Group,用來判斷是否“同組” 。Group是可繼承的,對于一個分級的權限實現,某個Group通過“繼承”就已經直接獲得了其父Group所擁有的所有“權限集合”,對這個Group而言,需要與權限建立直接關聯的,僅是它比起其父Group需要“擴展”的那部分權限。子組繼承父組的所有權限,規則來得更簡單,同時意味著管理更容易。為了更進一步實現權限的繼承,最直接的就是在Group上引入“父子關系”。
User與Group是多對多的關系。即一個User可以屬于多個Group之中,一個Group可以包括多個User。子Group與父Group是多對一的關系。Operator某種意義上類似于Resource + Privilege概念,但這里的Resource僅包括Resource Type不表示Resource Instance。Group 可以直接映射組織結構,Role 可以直接映射組織結構中的業務角色,比較直觀,而且也足夠靈活。Role對系統的貢獻實質上就是提供了一個比較粗顆粒的分配單位。
Group與Operator是多對多的關系。各概念的關系圖示如下:
解釋:
Operator的定義包括了Resource Type和Method概念。即,What和How的概念。之所以將What和How綁定在一起作為一個Operator概念而不是分開建模再建立關聯,這是因為很多的How對于某What才有意義。比如,發布操作對新聞對象才有意義,對用戶對象則沒有意義。
How本身的意義也有所不同,具體來說,對于每一個What可以定義N種操作。比如,對于合同這類對象,可以定義創建操作、提交操作、檢查沖突操作等。可以認為,How概念對應于每一個商業方法。其中,與具體用戶身份相關的操作既可以定義在操作的業務邏輯之中,也可以定義在操作級別。比如,創建者的瀏覽視圖與普通用戶的瀏覽視圖要求內容不同。既可以在外部定義兩個操作方法,也可以在一個操作方法的內部根據具體邏輯進行處理。具體應用哪一種方式應依據實際情況進行處理。
這樣的架構,應能在易于理解和管理的情況下,滿足絕大部分粗粒度權限控制的功能需要。但是除了粗粒度權限,系統中必然還會包括無數對具體Instance的細粒度權限。這些問題,被留給業務邏輯來解決,這樣的考慮基于以下兩點:
一方面,細粒度的權限判斷必須要在資源上建模權限分配的支持信息才可能得以實現。比如,如果要求創建者和普通用戶看到不同的信息內容,那么,資源本身應該有其創建者的信息。另一方面,細粒度的權限常常具有相當大的業務邏輯相關性。對不同的業務邏輯,常常意味著完全不同的權限判定原則和策略。相比之下,粗粒度的權限更具通用性,將其實現為一個架構,更有重用價值;而將細粒度的權限判斷實現為一個架構級別的東西就顯得繁瑣,而且不是那么的有必要,用定制的代碼來實現就更簡潔,更靈活。
所以細粒度控制應該在底層解決,Resource在實例化的時候,必需指定Owner和GroupPrivilege在對Resource進行操作時也必然會確定約束類型:究竟是OwnerOK還是GroupOK還是AllOK。Group應和Role嚴格分離User和Group是多對多的關系,Group只用于對用戶分類,不包含任何Role的意義;Role只授予User,而不是Group。如果用戶需要還沒有的多種Privilege的組合,必須新增Role。Privilege必須能夠訪問Resource,同時帶User參數,這樣權限控制就完備了。
思想:
權限系統的核心由以下三部分構成:1.創造權限,2.分配權限,3.使用權限,然后,系統各部分的主要參與者對照如下:1.創造權限 - Creator創造,2.分配權限 - Administrator 分配,3.使用權限 - User:
1. Creator 創造 Privilege, Creator 在設計和實現系統時會劃分,一個子系統或稱為模塊,應該有哪些權限。這里完成的是 Privilege 與 Resource 的對象聲明,并沒有真正將 Privilege 與具體Resource 實例聯系在一起,形成Operator。
2. Administrator 指定 Privilege 與 Resource Instance 的關聯。在這一步, 權限真正與資源實例聯系到了一起, 產生了Operator(Privilege Instance)。Administrator利用Operator這個基本元素,來創造他理想中的權限模型。如,創建角色,創建用戶組,給用戶組分配用戶,將用戶組與角色關聯等等...這些操作都是由 Administrator 來完成的。
3. User 使用 Administrator 分配給的權限去使用各個子系統。Administrator 是用戶,在他的心目中有一個比較適合他管理和維護的權限模型。于是,程序員只要回答一個問題,就是什么權限可以訪問什么資源,也就是前面說的 Operator。程序員提供 Operator 就意味著給系統穿上了盔甲。Administrator 就可以按照他的意愿來建立他所希望的權限框架可以自行增加,刪除,管理Resource和Privilege之間關系。可以自行設定用戶User和角色Role的對應關系。(如果將 Creator看作是 Basic 的發明者, Administrator 就是 Basic 的使用者,他可以做一些腳本式的編程) Operator是這個系統中最關鍵的部分,它是一個紐帶,一個系在Programmer,Administrator,User之間的紐帶。
用一個功能模塊來舉例子。
一.建立角色功能并做分配:
1.如果現在要做一個員工管理的模塊(即Resources),這個模塊有三個功能,分別是:增加,修改,刪除。給這三個功能各自分配一個ID,這個ID叫做功能代號:
Emp_addEmp,Emp_deleteEmp,Emp_updateEmp。
2.建立一個角色(Role),把上面的功能代碼加到這個角色擁有的權限中,并保存到數據庫中。角色包括系統管理員,測試人員等。
3.建立一個員工的賬號,并把一種或幾種角色賦給這個員工。比如說這個員工既可以是公司管理人員,也可以是測試人員等。這樣他登錄到系統中將會只看到他擁有權限的那些模塊。
二.把身份信息加到Session中。
登錄時,先到數據庫中查找是否存在這個員工,如果存在,再根據員工的sn查找員工的權限信息,把員工所有的權限信息都入到一個Hashmap中,比如就把上面的Emp_addEmp等放到這個Hashmap中。然后把Hashmap保存在一個UserInfoBean中。最后把這個UserInfoBean放到Session中,這樣在整個程序的運行過程中,系統隨時都可以取得這個用戶的身份信息。
三.根據用戶的權限做出不同的顯示。
可以對比當前員工的權限和給這個菜單分配的“功能ID”判斷當前用戶是否有打開這個菜單的權限。例如:如果保存員工權限的Hashmap中沒有這三個ID的任何一個,那這個菜單就不會顯示,如果員工的Hashmap中有任何一個ID,那這個菜單都會顯示。
對于一個新聞系統(Resouce),假設它有這樣的功能(Privilege):查看,發布,刪除,修改;假設對于刪除,有"新聞系統管理者只能刪除一月前發布的,而超級管理員可刪除所有的這樣的限制,這屬于業務邏輯(Business logic),而不屬于用戶權限范圍。也就是說權限負責有沒有刪除的Permission,至于能刪除哪些內容應該根據UserRole or UserGroup來決定(當然給UserRole or UserGroup分配權限時就應該包含上面兩條業務邏輯)。
一個用戶可以擁有多種角色,但同一時刻用戶只能用一種角色進入系統。角色的劃分方法可以根據實際情況劃分,按部門或機構進行劃分的,至于角色擁有多少權限,這就看系統管理員賦給他多少的權限了。用戶—角色—權限的關鍵是角色。用戶登錄時是以用戶和角色兩種屬性進行登錄的(因為一個用戶可以擁有多種角色,但同一時刻只能扮演一種角色),根據角色得到用戶的權限,登錄后進行初始化。這其中的技巧是同一時刻某一用戶只能用一種角色進行登錄。
針對不同的“角色”動態的建立不同的組,每個項目建立一個單獨的Group,對于新的項目,建立新的 Group 即可。在權限判斷部分,應在商業方法上予以控制。比如:不同用戶的“操作能力”是不同的(粗粒度的控制應能滿足要求),不同用戶的“可視區域”是不同的(體現在對被操作的對象的權限數據,是否允許當前用戶訪問,這需要對業務數據建模的時候考慮權限控制需要)。
擴展性:
有了用戶/權限管理的基本框架,Who(User/Group)的概念是不會經常需要擴展的。變化的可能是系統中引入新的 What (新的Resource類型)或者新的How(新的操作方式)。那在三個基本概念中,僅在Permission上進行擴展是不夠的。這樣的設計中Permission實質上解決了How 的問題,即表示了“怎樣”的操作。那么這個“怎樣”是在哪一個層次上的定義呢?將Permission定義在“商業方法”級別比較合適。比如,發布、購買、取消。每一個商業方法可以意味著用戶進行的一個“動作”。定義在商業邏輯的層次上,一方面保證了數據訪問代碼的“純潔性”,另一方面在功能上也是“足夠”的。也就是說,對更低層次,能自由的訪問數據,對更高層次,也能比較精細的控制權限。
確定了Permission定義的合適層次,更進一步,能夠發現Permission實際上還隱含了What的概念。也就是說,對于What的How操作才會是一個完整的Operator。比如,“發布”操作,隱含了“信息”的“發布”概念,而對于“商品”而言發布操作是沒有意義的。同樣的,“購買”操作,隱含了“商品”的“購買”概念。這里的綁定還體現在大量通用的同名的操作上,比如,需要區分“商品的刪除”與“信息的刪除”這兩個同名為“刪除”的不同操作。
提供權限系統的擴展能力是在Operator (Resource + Permission)的概念上進行擴展。Proxy 模式是一個非常合適的實現方式。實現大致如下:在業務邏輯層(EJB Session Facade [Stateful SessionBean]中),取得該商業方法的Methodname,再根據Classname和 Methodname 檢索Operator 數據,然后依據這個Operator信息和Stateful中保存的User信息判斷當前用戶是否具備該方法的操作權限。
應用在 EJB 模式下,可以定義一個很明確的 Business層次,而一個Business 可能意味著不同的視圖,當多個視圖都對應于一個業務邏輯的時候,比如,Swing Client以及 Jsp Client 訪問的是同一個 EJB 實現的 Business。在 Business 層上應用權限較能提供集中的控制能力。實際上,如果權限系統提供了查詢能力,那么會發現,在視圖層次已經可以不去理解權限,它只需要根據查詢結果控制界面就可以了。
靈活性:
Group和Role,只是一種輔助實現的手段,不是必需的。如果系統的Role很多,逐個授權違背了“簡單,方便”的目的,那就引入Group,將權限相同的Role組成一個Group進行集中授權。Role也一樣,是某一類Operator的集合,是為了簡化針對多個Operator的操作。
Role把具體的用戶和組從權限中解放出來。一個用戶可以承擔不同的角色,從而實現授權的靈活性。當然,Group也可以實現類似的功能。但實際業務中,Group劃分多以行政組織結構或業務功能劃分;如果為了權限管理強行將一個用戶加入不同的組,會導致管理的復雜性。
Domain的應用。為了授權更靈活,可以將Where或者Scope抽象出來,稱之為Domain,真正的授權是在Domain的范圍內進行,具體的Resource將分屬于不同的Domain。比如:一個新聞機構有國內與國外兩大分支,兩大分支內又都有不同的資源(體育類、生活類、時事政治類)。假如所有國內新聞的權限規則都是一樣的,所有國外新聞的權限規則也相同。則可以建立兩個域,分別授權,然后只要將各類新聞與不同的域關聯,受域上的權限控制,從而使之簡化。
權限系統還應該考慮將功能性的授權與資源性的授權分開。很多系統都只有對系統中的數據(資源)的維護有權限控制,但沒有對系統功能的權限控制。
權限系統最好是可以分層管理而不是集中管理。大多客戶希望不同的部門能且僅能管理其部門內部的事務,而不是什么都需要一個集中的Administrator或Administrators組來管理。雖然你可以將不同部門的人都加入Administrators組,但他們的權限過大,可以管理整個系統資源而不是該部門資源。
正向授權與負向授權:正向授權在開始時假定主體沒有任何權限,然后根據需要授予權限,適合于權限要求嚴格的系統。負向授權在開始時假定主體有所有權限,然后將某些特殊權限收回。
權限計算策略:系統中User,Group,Role都可以授權,權限可以有正負向之分,在計算用戶的凈權限時定義一套策略。
系統中應該有一個集中管理權限的AccessService,負責權限的維護(業務管理員、安全管理模塊)與使用(最終用戶、各功能模塊),該AccessService在實現時要同時考慮一般權限與特殊權限。雖然在具體實現上可以有很多,比如用Proxy模式,但應該使這些Proxy依賴于AccessService。各模塊功能中調用AccessService來檢查是否有相應的權限。所以說,權限管理不是安全管理模塊自己一個人的事情,而是與系統各功能模塊都有關系。每個功能模塊的開發人員都應該熟悉安全管理模塊,當然,也要從業務上熟悉本模塊的安全規則。
技術實現:
1.表單式認證,這是常用的,但用戶到達一個不被授權訪問的資源時,Web容器就發
出一個html頁面,要求輸入用戶名和密碼。
2.一個基于Servlet Sign in/Sign out來集中處理所有的Request,缺點是必須由應用程序自己來處理。
3.用Filter防止用戶訪問一些未被授權的資源,Filter會截取所有Request/Response,
然后放置一個驗證通過的標識在用戶的Session中,然后Filter每次依靠這個標識來決定是否放行Response。
這個模式分為:
Gatekeeper :采取Filter或統一Servlet的方式。
Authenticator: 在Web中使用JAAS自己來實現。
用戶資格存儲LDAP或數據庫:
1. Gatekeeper攔截檢查每個到達受保護的資源。首先檢查這個用戶是否有已經創建
好的Login Session,如果沒有,Gatekeeper 檢查是否有一個全局的和Authenticator相關的session?
2. 如果沒有全局的session,這個用戶被導向到Authenticator的Sign-on 頁面,
要求提供用戶名和密碼。
3. Authenticator接受用戶名和密碼,通過用戶的資格系統驗證用戶。
4. 如果驗證成功,Authenticator將創建一個全局Login session,并且導向Gatekeeper
來為這個用戶在他的web應用中創建一個Login Session。
5. Authenticator和Gatekeepers聯合分享Cookie,或者使用Tokens在Query字符里。
————————————————————————————————————
權限表及相關內容大體可以用六個表來描述,如下:
1 角色(即用戶組)表:包括三個字段,ID,角色名,對該角色的描述;
2 用戶表:包括三個或以上字段,ID,用戶名,對該用戶的描述,其它(如地址、電話等信息);
3 角色-用戶對應表:該表記錄用戶與角色之間的對應關系,一個用戶可以隸屬于多個角色,一個角色組也可擁有多個用戶。包括三個字段,ID,角色ID,用戶ID;
4 權限列表:該表記錄所有要加以控制的權限,如錄入、修改、刪除、執行等,也包括三個字段,ID,名稱,描述;
5 權限-角色對應表:該表記錄權限與角色之間的對應關系,一個角色可以擁有多個權限,一個權限也可以隸屬多個角色。包括三個字段,ID, 角色ID,權限ID;