從Java走進Scala:Twitter API與Scala的交互
本文是IBMDW上Ted Neward的Scala教學(xué)系列,本文是第16篇,標題為《用 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)。
注意,當您看到本文的時候,Scitter 庫將位于一個 公共源代碼控制庫 中。當然,我還將在本文中包括 源代碼,但是要知道,源代碼庫可能發(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)在我們清楚了目標。我們將通過實現(xiàn)兩個 “只讀” Twitter API 來達到目標:end_session API(結(jié)束用戶會話)和 rate_limit_status API(描述在某一特定時段內(nèi)用戶帳戶還剩下多少可用的 post)。
end_session API 與它的同胞 verify_credentials 相似,也是一個非常簡單的 API:只需用一個經(jīng)過驗證的請求調(diào)用它,它將 “結(jié)束” 當前正在運行的會話。在 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ā)送的。現(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 中,每當提到名稱/值對,自然會想到 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 為空,那么可以認為應(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ù)組引用被標記為 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ā)布到認證用戶的 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 條目。但是,默認的 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ù)” 部分越多,就認為這個版本越吸引人。)
但是,相對于 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 編程的工作,別忘了告訴我!

















