中国领先的IT技术网站
|
|

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

Scitter 客户机库即将发布,但是还差最后一步。在这一期 面向 Java 开发人员的 Scala 指南 中,Ted Neward 展示如何将更新、显示和删除功能添加到用于访问 Twitter 的基于 Scala 的库中。

作者:Ted Neward来源:IBMDW|2009-12-09 09:15

沙龙活动 | 去哪儿、陌陌、ThoughtWorks在自动化运维中的实践!10.28不见不散!


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

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

目前的进展

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

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

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

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

终止和评价

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

end_session API 与它的同胞 verify_credentials 相似,也是一个非常简单的 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 不一样,end_session 要求传入的消息是用 HTTP POST 语义发送的。现在,Scitter.execute 方法做任何事情都是通过 GET,这意味着需要将那些期望 GET 的 API 与那些期望 POST 的 API 区分开来。

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

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

清单 2. 基本 map 语法


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

在此情况下,我们可以重构 Scitter.execute() 方法,使之用 一个 Map 作为参数。如果 Map 为空,那么可以认为应该使用 GET 而不是 POST,如清单 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())

    }

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

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

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

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

测试

在实施这样的重构之前,先运行 ant test,以确保原有的所有基于 GET 的请求 API 仍可使用 — 事实确实如此。(这里假设生产 Twitter API 或 Twitter 服务器的可用性没有变化)。一切正常(至少在我的计算机上是这样),所以实现新的 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

    }

这实在是再简单不过了。

接下来要做的是实现 rate_limit_status API,它有两个版本,一个是经过验证的版本,另一个是没有经过验证的版本。我们将该方法实现为 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

      }

    }

  }

}

我觉得还是很简单。

更新

现在,有了新的 POST 版本的 HTTP 通信层,我们可以来处理 Twitter API 的中心:update 调用。毫不奇怪,需要一个 POST,并且至少有一个参数,即 status

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

update 调用差不多就是这样了,如清单 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

      }

    }

  }

}

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

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

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

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

潜在的重构

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

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

显示

show 调用接受要检索的 Twitter 状态的 id,并显示 Twitter 状态。和 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 的 “函数” 部分越多,就认为这个版本越吸引人。)

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

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

撤销

我们还想让 Scitter 用户可以撤销刚才执行的动作。为此,需要一个 destroy 调用,它将删除已发布的 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 报告、修复和建议。

您也无需受我的代码库的束缚。见证了之前三篇文章中进行的 Scitter 开发,您应该对 Twitter API 的使用有很好的理解。如果对于使用该 API 有不同的想法,那么尽管去做:抛开 Scitter,构建自己的 Scala 客户机库。毕竟,做做这些内部项目也是挺有乐趣的。

现在,我们要向 Scitter 挥手告别,开始寻找新的用 Scala 解决的项目。愿您从中找到乐趣,如果发现了用 Scala 编程的工作,别忘了告诉我!

【责任编辑:杨赛 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

数据库系统工程师考试全程指导

为了满足广大考生的需要,我们组织了参与过多年资格考试命题或辅导的教师,以新的考试大纲为依据,编写了《数据库系统工程师考试全程指导》...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× Python最火的编程语言