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


















