从Java走进Scala:Twitter API与Scala的交互

本文是IBMDW上Ted Neward的Scala教学系列,本文是第16篇,标题为《用 Scitter 更新 Twitter》。

51CTO编辑推荐:Scala编程语言专题

在撰写本文时,夏季即将结束,新的学年就要开始,Twitter 的服务器上不断涌现出世界各地的网虫和非网虫们发布的更新。对于我们很多身在北美的人来说,从海滩聚会到足球,从室外娱乐到室内项目,各种各样的想法纷至沓来。为了跟上这种形势,是时候重访 Scitter 这个用于访问 Twitter 的 Scala 客户机库了。

如果 到目前为止 您一直紧随 Scitter 的开发,就会知道,这个库现在能够利用各种不同的 Twitter API 查看用户的好友、追随者和时间线,以及其他内容。但是,这个库还不具备发布状态更新的能力。在这最后一篇关于 Scitter 的文章中,我们将丰富这个库的功能,增加一些有趣的内容(终止和评价)功能和重要方法 update()、show() 和 destroy()。在此过程中,您将了解更多关于 Twitter API 的知识,它与 Scala 之间的交互如何,您还将了解如何克服两者之间不可避免的编程挑战。

注意,当您看到本文的时候,Scitter 库将位于一个 公共源代码控制库 中。当然,我还将在本文中包括 源代码,但是要知道,源代码库可能发生改变。换句话说,项目库中的代码与您在这里看到的代码可能略有不同,或者有较大的不同。

POST 到 Twitter

到目前为止,我们的 Scitter 开发主要集中于一些基于 <font face="NSimsun">HTTP GET</font> 的操作,这主要是因为这些调用非常容易,而我想轻松切入 Twitter API。将 <font face="NSimsun">POST</font><font face="NSimsun">DELETE</font> 操作添加到库中对于可见性来说迈出了重要一步。到目前为止,可以在个人 Twitter 帐户上运行单元测试,而其他人并不知道您要干什么。但是,一旦开始发送更新消息,那么全世界都将知道您要运行 Scitter 单元测试。

如果继续测试 Scitter,那么需要在 Twitter 上创建自己的 “测试” 帐户。(也许用 Twitter API 编程的最大缺点是没有任何合适的测试或模拟工具。)

目前的进展

在开始着手这个库的新的 <font face="NSimsun">UPDATE</font> 功能之前,我们来回顾一下到目前为止我们已经创建的东西。(我不会提供完整的源代码清单,因为 Scitter 已经开始变得过长,不便于全部显示。但是,可以在阅读本文时,从另一个窗口查看 代码。)

大致来说,Scitter 库分为 4 个部分:

  • 来回发送的请求和响应类型(<font face="NSimsun">User</font><font face="NSimsun">Status</font> 等),包含在 API 中;它们被建模为 case 类。
  • <font face="NSimsun">OptionalParam</font> 类型,同样在 API 中的某些地方;也被建模为 case 类,这些 case 类继承基本的 <font face="NSimsun">OptionalParam</font> 类型。
  • <font face="NSimsun">Scitter</font> 对象,用于通信基础和对 Twitter 的匿名(无身份验证)访问。
  • <font face="NSimsun">Scitter</font> 类,存放一个用户名和密码,用于访问给定 Twitter 帐户时进行验证。

注意,在这最后一篇文章中,为了使文件大小保持在相对合理的范围内,我将请求/响应类型分开放到不同的文件中。

终止和评价

那么,现在我们清楚了目标。我们将通过实现两个 “只读” Twitter API 来达到目标:<font face="NSimsun">end_session</font> API(结束用户会话)和 <font face="NSimsun">rate_limit_status</font> API(描述在某一特定时段内用户帐户还剩下多少可用的 post)。

<font face="NSimsun">end_session</font> API 与它的同胞 <font face="NSimsun">verify_credentials</font> 相似,也是一个非常简单的 API:只需用一个经过验证的请求调用它,它将 “结束” 当前正在运行的会话。在 Scitter 类上实现它非常容易,如清单 1 所示:

清单 1. 在 Scitter 上实现 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 不一样,<font face="NSimsun">end_session</font> 要求传入的消息是用 <font face="NSimsun">HTTP POST</font> 语义发送的。现在,<font face="NSimsun">Scitter.execute</font> 方法做任何事情都是通过 <font face="NSimsun">GET</font>,这意味着需要将那些期望 <font face="NSimsun">GET</font> 的 API 与那些期望 <font face="NSimsun">POST</font> 的 API 区分开来。

现在暂不考虑这一点,另外还有一个明显的变化:<font face="NSimsun">POST</font> 的 API 调用还需将名称/值对传递到 <font face="NSimsun">execute()</font> 方法中。(记住,在其他 API 调用中,若使用 <font face="NSimsun">GET</font>,则所有参数可以作为查询参数出现在 URL 行;若使用 <font face="NSimsun">POST</font>,则参数出现在 HTTP 请求的主体中。)在 Scala 中,每当提到名称/值对,自然会想到 Scala <font face="NSimsun">Map</font> 类型,所以在考虑建模作为 <font face="NSimsun">POST</font> 一部分发送的数据元素时,最容易的方法是将它们放入到一个 <font face="NSimsun">Map[String,String]</font> 中并传递。

例如,如果将一个新的状态消息传递给 Twitter,需要将这个不超过 140 个字符的消息放在一个名称/值对 <font face="NSimsun">Status</font> 中,那么应该如清单 2 所示:

清单 2. 基本 map 语法

val map = Map("status" -> message)

在此情况下,我们可以重构 <font face="NSimsun">Scitter.execute()</font> 方法,使之用 一个 <font face="NSimsun">Map</font> 作为参数。如果 <font face="NSimsun">Map</font> 为空,那么可以认为应该使用 <font face="NSimsun">GET</font> 而不是 <font face="NSimsun">POST</font>,如清单 3 所示:

清单 3. 重构 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())

    }

<font face="NSimsun">execute()</font> 方法最大的变化是引入了 <font face="NSimsun">Map[String,String]</font> 参数,以及与它的大小有关的 “if” 测试。该测试决定是处理 <font face="NSimsun">GET</font> 请求还是 <font face="NSimsun">POST</font> 请求。由于 Apache Commons <font face="NSimsun">HttpClient</font> 要求 <font face="NSimsun">POST</font> 请求的主体放在 <font face="NSimsun">NameValuePairs</font> 中,因此我们使用 <font face="NSimsun">foreach()</font> 调用遍历 map 的元素。我们以二元组 pr 的形式传入 map 的键和值,并将它们分别提取到本地绑定变量 kv,然后使用这些值作为 <font face="NSimsun">NameValuePair</font> 构造函数的构造函数参数。

我们还可以使用 <font face="NSimsun">PostMethod</font> 上的 <font face="NSimsun">setParameter(name, value)</font> API 更轻松地做这些事情。出于教学的目的,我选择了清单 3 中的方法:以表明 Scala 数组和 Java 数组一样,仍然是可变的,即使数组引用被标记为 val 仍是如此。记住,在实际代码中,对于每个 (k,v) 元组,使用 <font face="NSimsun">PostMethod</font> 上的 <font face="NSimsun">setParameter(name, value)</font> 方法要好得多。

还需注意,对于 if/else 返回的 “method” 对象的类型,Scala 编译器会进行 does the right thing 类型推断。由于 Scala 可以看到 if/else 返回的是 <font face="NSimsun">GetMethod</font> 还是 <font face="NSimsun">PostMethod</font> 对象,它会选择最接近的基本类型 <font face="NSimsun">HttpMethodBase</font> 作为 “method” 的返回类型。这也意味着,在 <font face="NSimsun">execute()</font> 方法的其余部分中,<font face="NSimsun">HttpMethodBase</font> 中的任何不可用方法都是不可访问的。幸运的是,我们不需要它们,所以至少现在没有问题。

清单 3 中的实现的背后还潜藏着最后一个问题,这个问题是由这样一个事实引起的:我选择了使用 <font face="NSimsun">Map</font> 来区分 <font face="NSimsun">execute()</font> 方法是处理 <font face="NSimsun">GET</font> 操作,还是处理 <font face="NSimsun">POST</font> 操作。如果还需要使用其他 HTTP 动作(例如 <font face="NSimsun">PUT</font><font face="NSimsun">DELETE</font>),那么将不得不再次重构 <font face="NSimsun">execute()</font>。到目前为止,还没有这样的问题,但是今后要记住这一点。

测试

在实施这样的重构之前,先运行 <font face="NSimsun">ant test</font>,以确保原有的所有基于 <font face="NSimsun">GET</font> 的请求 API 仍可使用 — 事实确实如此。(这里假设生产 Twitter API 或 Twitter 服务器的可用性没有变化)。一切正常(至少在我的计算机上是这样),所以实现新的 <font face="NSimsun">execute()</font> 方法就非常容易:

清单 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

    }

这实在是再简单不过了。

接下来要做的是实现 <font face="NSimsun">rate_limit_status</font> API,它有两个版本,一个是经过验证的版本,另一个是没有经过验证的版本。我们将该方法实现为 <font face="NSimsun">Scitter</font> 对象和 <font face="NSimsun">Scitter</font> 类上的 <font face="NSimsun">rateLimitStatus</font>,如清单 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

      }

    }

  }

}

我觉得还是很简单。

更新

现在,有了新的 <font face="NSimsun">POST</font> 版本的 HTTP 通信层,我们可以来处理 Twitter API 的中心:<font face="NSimsun">UPDATE</font> 调用。毫不奇怪,需要一个 <font face="NSimsun">POST</font>,并且至少有一个参数,即 <font face="NSimsun">Status</font>

<font face="NSimsun">Status</font> 参数包含要发布到认证用户的 Twitter 提要的不超过 140 个字符的消息。另外还有一个可选参数:<font face="NSimsun">in_reply_to_status_id</font>,该参数提供另一个更新的 id,执行了 <font face="NSimsun">POST</font> 的更新将回复该更新。

<font face="NSimsun">UPDATE</font> 调用差不多就是这样了,如清单 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

      }

    }

  }

}

也许这个方法中最 “不同” 的部分就是其中定义的嵌套函数 — 与使用 <font face="NSimsun">GET</font> 的其他 Twitter API 调用不同,Twitter 期望传给 <font face="NSimsun">POST</font> 的参数出现在执行 <font face="NSimsun">POST</font> 的主体中,这意味着在调用 <font face="NSimsun">Scitter.execute()</font> 之前需要将它们转换成 <font face="NSimsun">Map</font> 条目。但是,默认的 <font face="NSimsun">Map</font>(来自 <font face="NSimsun">scala.collections.immutable</font>)是不可变的,这意味着可以组合 <font face="NSimsun">Map</font>,但是不能将条目添加到已有的 <font face="NSimsun">Map</font> 中。

解决这个小难题的最容易的方法是递归地处理传入的 <font face="NSimsun">OptionalParam</font> 元素的列表(实际上是一个 <font face="NSimsun">Array[]</font>)。我们将每个元素拆开,将它转换成各自的 <font face="NSimsun">Map</font> 条目。然后,将一个新的 <font face="NSimsun">Map</font>(由新创建的 <font face="NSimsun">Map</font> 和从递归调用返回的 <font face="NSimsun">Map</font> 组成)返回到 <font face="NSimsun">optionsToMap</font>

然后,将 <font face="NSimsun">OptionalParam</font><font face="NSimsun">Array[]</font> 传递到 <font face="NSimsun">optionsToMap</font> 嵌套函数。然后,将返回的 <font face="NSimsun">Map</font> 与我们构建的包含 <font face="NSimsun">Status</font> 消息的 <font face="NSimsun">Map</font> 连接起来。最后,将新的 <font face="NSimsun">Map</font> 和用户名、密码一起传递给 <font face="NSimsun">Scitter.execute()</font> 方法,以传送到 Twitter 服务器。

随便说一句,所有这些任务需要的代码并不多,但是需要更多的解释,这是比较优雅的编程方式。

潜在的重构

理论上,传给 <font face="NSimsun">UPDATE</font> 的可选参数与传给其他基于 <font face="NSimsun">GET</font> 的 API 调用的可选参数将受到同等对待;只是结果的格式有所不同(结果是用于 <font face="NSimsun">POST</font> 的名称/值对,而不是用于 URL 的名称/值对)。

如果 Twitter API 需要其他 HTTP 动作支持(<font face="NSimsun">PUT</font> 和/或 <font face="NSimsun">DELETE</font> 就是可能需要的动作),那么总是可以将 HTTP 参数作为特定参数 — 也许又是一组 case 类 — 并让 <font face="NSimsun">execute()</font> 以一个 HTTP 动作、URL、名称/值对的 map 以及(可选)用户名/密码作为 5 个参数。然后,必要时可以将可选参数转换成一个字符串或一组 <font face="NSimsun">POST</font> 参数。这些内容只需记在脑中就行了。

显示

<font face="NSimsun">show</font> 调用接受要检索的 Twitter 状态的 id,并显示 Twitter 状态。和 <font face="NSimsun">UPDATE</font> 一样,这个方法非常简单,无需再作说明,如清单 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 中是如何以另一种方式编写 <font face="NSimsun">show()</font> 方法的:

清单 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 的 “函数” 部分越多,就认为这个版本越吸引人。)

但是,相对于 if/else 版本,模式匹配版本有一个优势:如果 Twitter 返回新的条件(例如不同的错误条件或来自 HTTP 的响应代码),那么模式匹配版本在区分这些条件时可能更清晰。例如,如果某天 Twitter 决定返回 400 响应代码和一条错误消息(在主体中),以表明某种格式错误(也许是没有正确地重新 Tweet),那么与 if/else 方法相比,模式匹配版本可以更轻松(清晰)地同时测试响应代码和主体的内容。

还应注意,我们还可以使用清单 8 中的方式创建一些局部应用的函数,这些函数只需要 URL 和参数。但是,坦白说,这是一种自找麻烦的解放方案,所以我不会采用。

撤销

我们还想让 Scitter 用户可以撤销刚才执行的动作。为此,需要一个 <font face="NSimsun">destroy</font> 调用,它将删除已发布的 Twitter 状态,如清单 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” 版,至少实现一个简单的 Scitter 客户机。(按照惯例,这个任务就留给您来完成,作为一项 “读者练习”。)

结束语

编写 Scitter 客户机库是一项有趣的工作。虽然不能说 Scitter 已经可以完全用于生产,但是它绝对足以用于实现简单的、基于文本的 Twitter 客户机,这意味着它已经可以投入使用了。要发现什么人可以使用它,哪些特性是需要的,从而使之变得更有用,最好的方法就是将它向公众发布。

我已经将本文和之前关于 Scitter 的文章中的代码作为第一个修订版提交到 Google Code 上的 Scitter 项目主页。欢迎下载和试用这个库,并告诉我您的想法。同时也欢迎提供 bug 报告、修复和建议。

相关推荐