從Java走進Scala:Twitter API與Scala的交互
本文是IBMDW上Ted Neward的Scala教學(xué)系列,本文是第16篇,標(biāo)題為《用 Scitter 更新 Twitter》。
51CTO編輯推薦:Scala編程語言專題
在撰寫本文時,夏季即將結(jié)束,新的學(xué)年就要開始,Twitter 的服務(wù)器上不斷涌現(xiàn)出世界各地的網(wǎng)蟲和非網(wǎng)蟲們發(fā)布的更新。對于我們很多身在北美的人來說,從海灘聚會到足球,從室外娛樂到室內(nèi)項目,各種各樣的想法紛至沓來。為了跟上這種形勢,是時候重訪 Scitter 這個用于訪問 Twitter 的 Scala 客戶機庫了。
如果 到目前為止 您一直緊隨 Scitter 的開發(fā),就會知道,這個庫現(xiàn)在能夠利用各種不同的 Twitter API 查看用戶的好友、追隨者和時間線,以及其他內(nèi)容。但是,這個庫還不具備發(fā)布狀態(tài)更新的能力。在這最后一篇關(guān)于 Scitter 的文章中,我們將豐富這個庫的功能,增加一些有趣的內(nèi)容(終止和評價)功能和重要方法 update()、show() 和 destroy()。在此過程中,您將了解更多關(guān)于 Twitter API 的知識,它與 Scala 之間的交互如何,您還將了解如何克服兩者之間不可避免的編程挑戰(zhàn)。
注意,當(dāng)您看到本文的時候,Scitter 庫將位于一個 公共源代碼控制庫 中。當(dāng)然,我還將在本文中包括 源代碼,但是要知道,源代碼庫可能發(fā)生改變。換句話說,項目庫中的代碼與您在這里看到的代碼可能略有不同,或者有較大的不同。
POST 到 Twitter
到目前為止,我們的 Scitter 開發(fā)主要集中于一些基于 HTTP GET
的操作,這主要是因為這些調(diào)用非常容易,而我想輕松切入 Twitter API。將 POST
和 DELETE
操作添加到庫中對于可見性來說邁出了重要一步。到目前為止,可以在個人 Twitter 帳戶上運行單元測試,而其他人并不知道您要干什么。但是,一旦開始發(fā)送更新消息,那么全世界都將知道您要運行 Scitter 單元測試。
如果繼續(xù)測試 Scitter,那么需要在 Twitter 上創(chuàng)建自己的 “測試” 帳戶。(也許用 Twitter API 編程的最大缺點是沒有任何合適的測試或模擬工具。)
目前的進展
在開始著手這個庫的新的 UPDATE
功能之前,我們來回顧一下到目前為止我們已經(jīng)創(chuàng)建的東西。(我不會提供完整的源代碼清單,因為 Scitter 已經(jīng)開始變得過長,不便于全部顯示。但是,可以在閱讀本文時,從另一個窗口查看 代碼。)
大致來說,Scitter 庫分為 4 個部分:
- 來回發(fā)送的請求和響應(yīng)類型(
User
、Status
等),包含在 API 中;它們被建模為 case 類。 OptionalParam
類型,同樣在 API 中的某些地方;也被建模為 case 類,這些 case 類繼承基本的OptionalParam
類型。Scitter
對象,用于通信基礎(chǔ)和對 Twitter 的匿名(無身份驗證)訪問。Scitter
類,存放一個用戶名和密碼,用于訪問給定 Twitter 帳戶時進行驗證。
注意,在這最后一篇文章中,為了使文件大小保持在相對合理的范圍內(nèi),我將請求/響應(yīng)類型分開放到不同的文件中。
終止和評價
那么,現(xiàn)在我們清楚了目標(biāo)。我們將通過實現(xiàn)兩個 “只讀” Twitter API 來達(dá)到目標(biāo):end_session
API(結(jié)束用戶會話)和 rate_limit_status
API(描述在某一特定時段內(nèi)用戶帳戶還剩下多少可用的 post)。
end_session
API 與它的同胞 verify_credentials
相似,也是一個非常簡單的 API:只需用一個經(jīng)過驗證的請求調(diào)用它,它將 “結(jié)束” 當(dāng)前正在運行的會話。在 Scitter 類上實現(xiàn)它非常容易,如清單 1 所示:
清單 1. 在 Scitter 上實現(xiàn) end_session
package com.tedneward.scitter { import org.apache.commons.httpclient._, auth._, methods._, params._ import scala.xml._ // ... class Scitter { /** * */ def endSession : Boolean = { val (statusCode, statusBody) = Scitter.execute("http://twitter.com/account/end_session.xml", username, password) statusCode == 200 } } } |
好吧,我失言了。也不是那么容易。
POST
和我們到目前為止用過的 Twitter API 中的其他 API 不一樣,end_session
要求傳入的消息是用 HTTP POST
語義發(fā)送的?,F(xiàn)在,Scitter.execute
方法做任何事情都是通過 GET
,這意味著需要將那些期望 GET
的 API 與那些期望 POST
的 API 區(qū)分開來。
現(xiàn)在暫不考慮這一點,另外還有一個明顯的變化:POST
的 API 調(diào)用還需將名稱/值對傳遞到 execute()
方法中。(記住,在其他 API 調(diào)用中,若使用 GET
,則所有參數(shù)可以作為查詢參數(shù)出現(xiàn)在 URL 行;若使用 POST
,則參數(shù)出現(xiàn)在 HTTP 請求的主體中。)在 Scala 中,每當(dāng)提到名稱/值對,自然會想到 Scala Map
類型,所以在考慮建模作為 POST
一部分發(fā)送的數(shù)據(jù)元素時,最容易的方法是將它們放入到一個 Map[String,String]
中并傳遞。
例如,如果將一個新的狀態(tài)消息傳遞給 Twitter,需要將這個不超過 140 個字符的消息放在一個名稱/值對 status
中,那么應(yīng)該如清單 2 所示:
清單 2. 基本 map 語法
val map = Map("status" -> message) |
在此情況下,我們可以重構(gòu) Scitter.execute()
方法,使之用 一個 Map
作為參數(shù)。如果 Map
為空,那么可以認(rèn)為應(yīng)該使用 GET
而不是 POST
,如清單 3 所示:
清單 3. 重構(gòu) execute()
private[scitter] def execute(url : String) : (Int, String) = execute(url, Map(), "", "") private[scitter] def execute(url : String, username : String, password : String) : (Int, String) = execute(url, Map(), username, password) private[scitter] def execute(url : String, dataMap : Map[String,String]) : (Int, String) = execute(url, dataMap, "", "") private[scitter] def execute(url : String, dataMap : Map[String,String], username : String, password : String) = { val client = new HttpClient() val method = if (dataMap.size == 0) { new GetMethod(url) } else { var m = new PostMethod(url) val array = new Array[NameValuePair](dataMap.size) var pos = 0 dataMap.elements.foreach { (pr) => pr match { case (k, v) => array(pos) = new NameValuePair(k, v) } pos += 1 } m.setRequestBody(array) m } method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false)) if ((username != "") && (password != "")) { client.getParams().setAuthenticationPreemptive(true) client.getState().setCredentials( new AuthScope("twitter.com", 80, AuthScope.ANY_REALM), new UsernamePasswordCredentials(username, password)) } client.executeMethod(method) (method.getStatusLine().getStatusCode(), method.getResponseBodyAsString()) } |
execute()
方法最大的變化是引入了 Map[String,String]
參數(shù),以及與它的大小有關(guān)的 “if” 測試。該測試決定是處理 GET
請求還是 POST
請求。由于 Apache Commons HttpClient
要求 POST
請求的主體放在 NameValuePairs
中,因此我們使用 foreach()
調(diào)用遍歷 map 的元素。我們以二元組 pr 的形式傳入 map 的鍵和值,并將它們分別提取到本地綁定變量 k 和 v,然后使用這些值作為 NameValuePair
構(gòu)造函數(shù)的構(gòu)造函數(shù)參數(shù)。
我們還可以使用 PostMethod
上的 setParameter(name, value)
API 更輕松地做這些事情。出于教學(xué)的目的,我選擇了清單 3 中的方法:以表明 Scala 數(shù)組和 Java 數(shù)組一樣,仍然是可變的,即使數(shù)組引用被標(biāo)記為 val 仍是如此。記住,在實際代碼中,對于每個 (k,v) 元組,使用 PostMethod
上的 setParameter(name, value)
方法要好得多。
還需注意,對于 if/else 返回的 “method” 對象的類型,Scala 編譯器會進行 does the right thing 類型推斷。由于 Scala 可以看到 if/else 返回的是 GetMethod
還是 PostMethod
對象,它會選擇最接近的基本類型 HttpMethodBase
作為 “method” 的返回類型。這也意味著,在 execute()
方法的其余部分中,HttpMethodBase
中的任何不可用方法都是不可訪問的。幸運的是,我們不需要它們,所以至少現(xiàn)在沒有問題。
清單 3 中的實現(xiàn)的背后還潛藏著最后一個問題,這個問題是由這樣一個事實引起的:我選擇了使用 Map
來區(qū)分 execute()
方法是處理 GET
操作,還是處理 POST
操作。如果還需要使用其他 HTTP 動作(例如 PUT
或 DELETE
),那么將不得不再次重構(gòu) execute()
。到目前為止,還沒有這樣的問題,但是今后要記住這一點。
測試
在實施這樣的重構(gòu)之前,先運行 ant test
,以確保原有的所有基于 GET
的請求 API 仍可使用 — 事實確實如此。(這里假設(shè)生產(chǎn) Twitter API 或 Twitter 服務(wù)器的可用性沒有變化)。一切正常(至少在我的計算機上是這樣),所以實現(xiàn)新的 execute()
方法就非常容易:
清單 4. Scitter v0.3: endSession
def endSession : Boolean = { val (statusCode, statusBody) = Scitter.execute("http://twitter.com/account/end_session.xml", Map("" -> ""), username, password) statusCode == 200 } |
這實在是再簡單不過了。
接下來要做的是實現(xiàn) rate_limit_status
API,它有兩個版本,一個是經(jīng)過驗證的版本,另一個是沒有經(jīng)過驗證的版本。我們將該方法實現(xiàn)為 Scitter
對象和 Scitter
類上的 rateLimitStatus
,如清單 5 所示:
清單 5. Scitter v0.3: rateLimitStatus
package com.tedneward.scitter { object Scitter { // ... def rateLimitStatus : Option[RateLimits] = { val url = "http://twitter.com/account/rate_limit_status.xml" val (statusCode, statusBody) = Scitter.execute(url) if (statusCode == 200) { Some(RateLimits.fromXml(XML.loadString(statusBody))) } else { None } } } class Scitter { // ... def rateLimitStatus : Option[RateLimits] = { val url = "http://twitter.com/account/rate_limit_status.xml" val (statusCode, statusBody) = Scitter.execute(url, username, password) if (statusCode == 200) { Some(RateLimits.fromXml(XML.loadString(statusBody))) } else { None } } } } |
我覺得還是很簡單。
更新
現(xiàn)在,有了新的 POST
版本的 HTTP 通信層,我們可以來處理 Twitter API 的中心:update
調(diào)用。毫不奇怪,需要一個 POST
,并且至少有一個參數(shù),即 status
。
status
參數(shù)包含要發(fā)布到認(rèn)證用戶的 Twitter 提要的不超過 140 個字符的消息。另外還有一個可選參數(shù):in_reply_to_status_id
,該參數(shù)提供另一個更新的 id,執(zhí)行了 POST
的更新將回復(fù)該更新。
update
調(diào)用差不多就是這樣了,如清單 6 所示:
清單 6. Scitter v0.3: update
package com.tedneward.scitter { class Scitter { // ... def update(message : String, options : OptionalParam*) : Option[Status] = { def optionsToMap(options : List[OptionalParam]) : Map[String, String]= { options match { case hd :: tl => hd match { case InReplyToStatusId(id) => Map("in_reply_to_status_id" -> id.toString) ++ optionsToMap(tl) case _ => optionsToMap(tl) } case List() => Map() } } val paramsMap = Map("status" -> message) ++ optionsToMap(options.toList) val (statusCode, body) = Scitter.execute("http://twitter.com/statuses/update.xml", paramsMap, username, password) if (statusCode == 200) { Some(Status.fromXml(XML.loadString(body))) } else { None } } } } |
也許這個方法中最 “不同” 的部分就是其中定義的嵌套函數(shù) — 與使用 GET
的其他 Twitter API 調(diào)用不同,Twitter 期望傳給 POST
的參數(shù)出現(xiàn)在執(zhí)行 POST
的主體中,這意味著在調(diào)用 Scitter.execute()
之前需要將它們轉(zhuǎn)換成 Map
條目。但是,默認(rèn)的 Map
(來自 scala.collections.immutable
)是不可變的,這意味著可以組合 Map
,但是不能將條目添加到已有的 Map
中。
解決這個小難題的最容易的方法是遞歸地處理傳入的 OptionalParam
元素的列表(實際上是一個 Array[]
)。我們將每個元素拆開,將它轉(zhuǎn)換成各自的 Map
條目。然后,將一個新的 Map
(由新創(chuàng)建的 Map
和從遞歸調(diào)用返回的 Map
組成)返回到 optionsToMap
。
然后,將 OptionalParam
的 Array[]
傳遞到 optionsToMap
嵌套函數(shù)。然后,將返回的 Map
與我們構(gòu)建的包含 status
消息的 Map
連接起來。最后,將新的 Map
和用戶名、密碼一起傳遞給 Scitter.execute()
方法,以傳送到 Twitter 服務(wù)器。
隨便說一句,所有這些任務(wù)需要的代碼并不多,但是需要更多的解釋,這是比較優(yōu)雅的編程方式。
潛在的重構(gòu)
理論上,傳給 update
的可選參數(shù)與傳給其他基于 GET
的 API 調(diào)用的可選參數(shù)將受到同等對待;只是結(jié)果的格式有所不同(結(jié)果是用于 POST
的名稱/值對,而不是用于 URL 的名稱/值對)。
如果 Twitter API 需要其他 HTTP 動作支持(PUT
和/或 DELETE
就是可能需要的動作),那么總是可以將 HTTP 參數(shù)作為特定參數(shù) — 也許又是一組 case 類 — 并讓 execute()
以一個 HTTP 動作、URL、名稱/值對的 map 以及(可選)用戶名/密碼作為 5 個參數(shù)。然后,必要時可以將可選參數(shù)轉(zhuǎn)換成一個字符串或一組 POST
參數(shù)。這些內(nèi)容只需記在腦中就行了。
顯示
show
調(diào)用接受要檢索的 Twitter 狀態(tài)的 id,并顯示 Twitter 狀態(tài)。和 update
一樣,這個方法非常簡單,無需再作說明,如清單 7 所示:
清單 7. Scitter v0.3: show
package com.tedneward.scitter { class Scitter { // ... def show(id : Long) : Option[Status] = { val (statusCode, body) = Scitter.execute("http://twitter.com/statuses/show/" + id + ".xml", username, password) if (statusCode == 200) { Some(Status.fromXml(XML.loadString(body))) } else { None } } } } |
還有問題嗎?
另一種顯示方法
如果想再試一下模式匹配,那么可以看看清單 8 中是如何以另一種方式編寫 show()
方法的:
清單 8. Scitter v0.3: show redux
package com.tedneward.scitter { class Scitter { // ... def show(id : Long) : Option[Status] = { Scitter.execute("http://twitter.com/statuses/show/" + id + ".xml", username, password) match { case (200, body) => Some(Status.fromXml(XML.loadString(body))) case (_, _) => None } } } } |
這個版本比起 if/else 版本是否更加清晰,這很大程度上屬于審美的問題,但公平而論,這個版本也許更加簡潔。(很可能查看代碼的人看到 Scala 的 “函數(shù)” 部分越多,就認(rèn)為這個版本越吸引人。)
但是,相對于 if/else 版本,模式匹配版本有一個優(yōu)勢:如果 Twitter 返回新的條件(例如不同的錯誤條件或來自 HTTP 的響應(yīng)代碼),那么模式匹配版本在區(qū)分這些條件時可能更清晰。例如,如果某天 Twitter 決定返回 400 響應(yīng)代碼和一條錯誤消息(在主體中),以表明某種格式錯誤(也許是沒有正確地重新 Tweet),那么與 if/else 方法相比,模式匹配版本可以更輕松(清晰)地同時測試響應(yīng)代碼和主體的內(nèi)容。
還應(yīng)注意,我們還可以使用清單 8 中的方式創(chuàng)建一些局部應(yīng)用的函數(shù),這些函數(shù)只需要 URL 和參數(shù)。但是,坦白說,這是一種自找麻煩的解放方案,所以我不會采用。
撤銷
我們還想讓 Scitter 用戶可以撤銷剛才執(zhí)行的動作。為此,需要一個 destroy
調(diào)用,它將刪除已發(fā)布的 Twitter 狀態(tài),如清單 9 所示:
清單 9. Scitter v0.3: destroy
package com.tedneward.scitter { class Scitter { // ... def destroy(id : Long) : Option[Status] = { val paramsMap = Map("id" -> id.toString()) val (statusCode, body) = Scitter.execute("http://twitter.com/statuses/destroy/" + id.toString() + ".xml", paramsMap, username, password) if (statusCode == 200) { Some(Status.fromXml(XML.loadString(body))) } else { None } } def destroy(id : Id) : Option[Status] = destroy(id.id.toLong) } } |
有了這些東西,我們可以考慮將這個 Scitter 客戶機庫作為 “alpha” 版,至少實現(xiàn)一個簡單的 Scitter 客戶機。(按照慣例,這個任務(wù)就留給您來完成,作為一項 “讀者練習(xí)”。)
結(jié)束語
編寫 Scitter 客戶機庫是一項有趣的工作。雖然不能說 Scitter 已經(jīng)可以完全用于生產(chǎn),但是它絕對足以用于實現(xiàn)簡單的、基于文本的 Twitter 客戶機,這意味著它已經(jīng)可以投入使用了。要發(fā)現(xiàn)什么人可以使用它,哪些特性是需要的,從而使之變得更有用,最好的方法就是將它向公眾發(fā)布。
我已經(jīng)將本文和之前關(guān)于 Scitter 的文章中的代碼作為第一個修訂版提交到 Google Code 上的 Scitter 項目主頁。歡迎下載和試用這個庫,并告訴我您的想法。同時也歡迎提供 bug 報告、修復(fù)和建議。
您也無需受我的代碼庫的束縛。見證了之前三篇文章中進行的 Scitter 開發(fā),您應(yīng)該對 Twitter API 的使用有很好的理解。如果對于使用該 API 有不同的想法,那么盡管去做:拋開 Scitter,構(gòu)建自己的 Scala 客戶機庫。畢竟,做做這些內(nèi)部項目也是挺有樂趣的。
現(xiàn)在,我們要向 Scitter 揮手告別,開始尋找新的用 Scala 解決的項目。愿您從中找到樂趣,如果發(fā)現(xiàn)了用 Scala 編程的工作,別忘了告訴我!