面向 Java 开发人员的 Scala 指南: Scala + Twitter = Scitter

通过 Scala 可以实现更好的社交网络

抽象地讨论 Scala 是一件有趣的事情,但对于本专栏的大多数读者而言,需要通过实践才能理解理论和应用之间的区别。在本期文章中,Ted Neward 将使用 Scala 为客户构建基础框架,用于访问流行的微型博客系统 Twitter。

Ted Neward, 主管, ThoughtWorks

Ted Neward 照片Ted Neward 是 ThoughtWorks 的顾问,ThoughtWorks 是一家向全球提供咨询服务的公司。他还是 Neward & Associates 的主管,负责有关 Java、.NET 和 XML 服务和其他平台的咨询、指导、培训和推介。他现在居住在华盛顿西雅图附近。



2009 年 8 月 20 日

Twitter 迅速占领了 Internet 市场。您肯定知道这个出色的社交网络工具允许订阅者提供关于他们自身以及当前正在执行的任务的简要状态更新。追随者 将接收到他们的 “Twitter 提要” 的更新,这与博客将更新生成到博客阅读者的提要中极为类似。

关于本系列

Ted Neward 将深入探讨 Scala 编程语言,并带领您一路随行。在本 系列 中,您将学习最新的热点以及 Scala 的语言功能。Scala 代码和 Java™ 代码将在必要时同时出现以方便进行比较,但您会发现 Scala 中的许多内容都与 Java 没有直接关系 — 这便是 Scala 的魄力所在!毕竟,如果 Java 能够做到,为什么还要大费周折来学习 Scala 呢?

就其本身而言,Twitter 是对社交网络的有趣讨论,并且是用户之间的新一代 “高度互联”,它具备您能想到的所有优点和缺点。

由于 Twitter 很早就发布了其 API,因此大量 Twitter 客户机应用程序涌入到 Internet 上。由于该 API 主要建立在直观和易于理解的基础上,因此许多开发人员都发现有必要构建一个自己的 Twitter 客户机,这与学习 Web 技术的开发人员构建自己的博客服务器极为类似。

考虑到 Scala 的功能性(这看上去能很好地协同 Twitter 的 REST 式特性)以及非常出众的 XML 处理特性,因此尝试构建一个用于访问 Twitter 的 Scala 客户机库应该是一个非常不错的体验。

何为 Twitter?

在详细讨论之前,我们先来看看 Twitter API。

简单来说,Twitter 是一个 “微型博客” — 关于您自己的简短个性化提要,不超过 140 个字符,任何 “追随者” 都可以通过 Web 更新、RSS、文本消息等方式接收它们。(140 字符的限制完全来自文本消息,它是 Twitter 的主要来源渠道,并受到类似的限制)。

最具 REST 特征 是什么意思?

一些读者会对我所使用的最具 REST 特征 短语感到好奇;这需要一些说明。“Twitter API 试图符合 Representational State Transfer (REST) 的设计原则”。并且在很大程度上说它做到了。该思想的创造者 Roy Fielding 可能不同意 Twitter 使用这个术语,但实现来说,Twitter 的方法将适合大多数人的 REST 定义。我只希望避免关于 REST 定义的激烈争论。因此,我使用了限定词 “最”。

从实际的角度来说,Twitter 是一个最具 REST 特征 的 API,您可以使用一些种类的消息格式 — XML、ATOM、RSS 或 JSON — 来发送或从 Twitter 服务器接收消息。不同的 URL,与不同的消息和它们所需及可选的消息部分相结合,可以发起不同的 API 调用。例如,如果您希望接收 Twitter 上所有人的所有 “Tweets”(Twitter 更新)的完整列表(也称作 “公共时间轴”),您需要准备一个 XML、ATOM、RSS 或 JSON 消息,将它发送给合适的 URL,并采用与 Twitter 网站(apiwiki.twitter.com)上相同的格式来使用结果:

------------------------------------------------------------
public_timeline
返回设定了自定义用户图标的
非保护用户的 20 条最新状态。不需要身份验证。

注意,公共时间轴将缓存 60 秒钟
因此频繁请求它不再浪费资源。

URL:http://twitter.com/statuses/public_timeline.format
格式:xml、json、rss、atom
方法:GET
API 限制:不适用
返回:状态元素列表
------------------------------------------------------------

从编程的角度来说,这意味着我们给 Twitter 服务器发送一个简单的 GET HTTP 请求,并且我们将获取一组封装在 XML、RSS、ATOM 或 JSON 消息中的 “状态” 消息。Twitter 站点将 “状态” 消息定义为类似清单 1 所示的内容:

清单 1. 您好世界,您在哪里?
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <title>Twitter / tedneward</title>
  <id>tag:twitter.com,2007:Status</id>
  <link type="text/html" rel="alternate" href="http://twitter.com/tedneward"/>
  <updated>2009-03-07T13:48:31+00:00</updated>
  <subtitle>Twitter updates from Ted Neward / tedneward.</subtitle>
  <entry>
	<title>tedneward: @kdellison Happens to the best of us...</title>
	<content type="html">tedneward: @kdellison Happens to the best of us...</content>
	<id>tag:twitter.com,2007:http://twitter.com/tedneward/statuses/1292396349</id>
	<published>2009-03-07T11:07:18+00:00</published>
	<updated>2009-03-07T11:07:18+00:00</updated>
	<link type="text/html" rel="alternate"
        href="http://twitter.com/tedneward/statuses/1292396349"/>
	<link type="image/png" rel="image"
        href="http://s3.amazonaws.com/twitter_production/profile_images/
         55857457/javapolis_normal.png"/>
	<author>
	  <name>Ted Neward</name>
	  <uri>http://www.tedneward.com</uri>
	</author>
  </entry>
</feed>

状态消息中的大部分元素(如果不是全部的话)都很直观,因此不再赘述。

由于我们可以采用三种基于 XML 的格式使用 Twitter 消息,以及 Scala 具备一些非常强大的 XML 特性,包括 XML 字面值和类似 XPath 的查询语法 API,因此编写可以发送和接收 Twitter 消息的 Scala 库只需要一些基础的 Scala 编码工作。举例来说,通过 Scala 使用清单 1 消息来提取状态更新的标题或内容可以利用 Scala 的 XML 类型和 \\\ 方法,如 清单 2 所示:

清单 2. 您好 Ted,您在哪里?
<![CDATA[
package com.tedneward.scitter.test
{
  class ScitterTest
  {
    import org.junit._, Assert._
    
    @Test def simpleAtomParse =
    {
      val atom =
        <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
          <title>Twitter / tedneward</title>
          <id>tag:twitter.com,2007:Status</id>
          <link type="text/html" rel="alternate" href="http://twitter.com/tedneward"/>
          <updated>2009-03-07T13:48:31+00:00</updated>
          <subtitle>Twitter updates from Ted Neward / tedneward.</subtitle>
          <entry>
            <title>tedneward: @kdellison Happens to the best of us...</title>
            <content type="html">tedneward: @kdellison
                                 Happens to the best of us...</content>
            <id>tag:twitter.com,2007:
                http://twitter.com/tedneward/statuses/1292396349</id>
            <published>2009-03-07T11:07:18+00:00</published>
            <updated>2009-03-07T11:07:18+00:00</updated>
            <link type="text/html" rel="alternate"
                  href="http://twitter.com/tedneward/statuses/1292396349"/>
            <link type="image/png" rel="image"
                  href="http://s3.amazonaws.com/twitter_production/profile_images/
                        55857457/javapolis_normal.png"/>
            <author>
              <name>Ted Neward</name>
              <uri>http://www.tedneward.com</uri>
            </author>
          </entry>
        </feed>
    
      assertEquals(atom \\ "entry" \ "title",
		 "tedneward: @kdellison Happens to the best of us...")
    }
  }
}
]]>

有关 Scala 的 XML 支持的更多详细信息,请参阅 “Scala 和 XML”(参见 参考资料)。

实际上,使用原始 XML 本身并不是一个有趣的练习。如果 Scala 的宗旨是让我们的生活更加轻松,那么可以创建一个或一组专用于简化 Scala 消息发送和接收任务的类。作为其中一个目标,应该能够在 “普通” Java 程序中方便地使用库(这意味着可以方便地从任何可理解普通 Java 语义的环境中来访问它,比如说 Groovy 或 Clojure)。

API 设计

在深入了解 Scala/Twitter 库的 API 设计之前(根据同事 ThoughtWorker Neal Ford 的建议,我将它称作 “Scitter”),需要明确一些需求。

首先,Scitter 显然会对网络访问有一些依赖 — 并且可扩展到 Twitter 服务器 — 这会使测试变得非常困难。

其次,我们需要解析(和测试)Twitter 发回的各种格式。

第三,我们希望隐藏 API 内部各种格式之间的差异,以便客户机不需要担心已记录的 Twitter 消息格式,但是可以仅使用标准类。

最后,由于 Twitter 依赖 “通过身份验证的用户” 才能使用大量 API,因此 Scitter 库需要适应 “验证” 和 “未验证” API 之间的差异,而不会让事情变得过于复杂。

网络访问需要一些形式的 HTTP 通信,以便联系 Twitter 服务器。虽然我们可以使用 Java 库本身(特别是 URL 类及其同胞),但由于 Twitter API 需要大量请求和响应主体连接,因此可以更加轻松地使用不同的 HTTP API,特别是 Apache Commons HttpClient 库。为了更便于测试客户机 API,实际通信将隐藏在一些 API 内部的 Scitter 库中,以便能够更加轻松地切换到另一个 HTTP 库(其必要性不太容易想到),并能模拟实际网络通信以简化测试(其作用很容易想到)。

结果,第一个测试是 Scala 化 HttpClient 调用,以确保基本通信模式就位;注意,由于 HttpClient 依赖另外两个 Apache 库(Commons Logging 和 Commons Codec),因此还需要在运行时提供这些库;对于那些希望开发相似种类代码的读者,确保类路径中包括所有三个库。

由于最易于使用的 Twitter API 是测试 API

因此在请求格式中返回 “ok”,并附带 200 OK HTTP 状态码。

我们将使用它作为 Scitter 测试中的保留条款。它位于 URL http://twitter.com/help/test.format(其中,“format” 为 “xml” 或 “json”;至于目前,我们将选择使用 “xml”),并且仅有的支持 HTTP 方法是 GET。HttpClient 代码简明易懂,如清单 3 所示:

清单 3. Twitter PING!
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...
  
    import org.apache.commons.httpclient._, methods._, params._, cookie._
    
    @Test def callTwitterTest =
    {
      val testURL = "http://twitter.com/help/test.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(testURL)

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      
      assertEquals(statusLine.getStatusCode(), 200)
      assertEquals(statusLine.getReasonPhrase(), "OK")
    }
  }
}

此代码中最重要的一部分是 HttpClient 样板 — 感兴趣的读者应该查阅 HttpClient API 文档了解详细信息。假设连接到公共 Internet 的网络可用(并且 Twitter 并未修改其公共 API),那么该测试应该能顺利通过。

鉴于此,我们详细分析 Scitter 客户机的第一部分。这意味着我们需要解决一个设计问题:如何构建 Scitter 客户机来处理经过验证和未经过验证的调用。目前,我将采用典型的 Scala 方式,假定验证是 “按对象” 执行的,因此将需要验证的调用放在类定义中,并在未验证的调用放在对象定义中:

清单 4. Scitter.test
package com.tedneward.scitter
{
  /**
   * Object for consuming "non-specific" Twitter feeds, such as the public timeline.
   * Use this to do non-authenticated requests of Twitter feeds.
   */
  object Scitter
  {
    import org.apache.commons.httpclient._, methods._, params._, cookie._

    /**
     * Ping the server to see if it's up and running.
     *
     * Twitter docs say:
     * test
     * Returns the string "ok" in the requested format with a 200 OK HTTP status code.
     * URL: http://twitter.com/help/test.format
     * Formats: xml, json
     * Method(s): GET
     */
    def test : Boolean =
    {
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/help/test.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      statusLine.getStatusCode() == 200
    }
  }
  /**
   * Class for consuming "authenticated user" Twitter APIs. Each instance is
   * thus "tied" to a particular authenticated user on Twitter, and will
   * behave accordingly (according to the Twitter API documentation).
   */
  class Scitter(username : String, password : String)
  {
  }
}

目前,我们将网络抽象放在一边 — 稍后,当离线测试变得更加重要时再添加它。当我们更好地理解如何使用 HttpClient 类时,这还将帮助避免 “过度抽象” 网络通信。

由于已经明确区分了验证和未验证 Twitter 客户机,因此我们将快速创建一个经过验证的方法。看上去 Twitter 提供了一个可验证用户登录凭证的 API。再次,HttpClient 代码将类似于之前的代码,除了将用户名和密码传递到 Twitter API 中之外。

这引出了 Twitter 如何验证用户的概念。快速查看 Twitter API 页面后,可以发现 Twitter 使用的是一种 Stock HTTP 验证方法,这与任何经过验证的资源在 HTTP 中的方法相同。这意味着 HttpClient 代码必须提供用户名和密码作为 HTTP 请求的一部分,而不是作为 POST 的主体,如清单 5 所示:

清单 5. 您好 Twitter,是我!
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    def testUser = "TwitterUser"
	def testPassword = "TwitterPassword"
  
    @Test def verifyCreds =
    {
      val client = new HttpClient()

      val verifyCredsURL = "http://twitter.com/account/verify_credentials.xml"
      val method = new GetMethod(verifyCredsURL)

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))

      client.getParams().setAuthenticationPreemptive(true)
      val defaultcreds = new UsernamePasswordCredentials(testUser, testPassword)
      client.getState().setCredentials(new AuthScope("twitter.com", 80,
                                                     AuthScope.ANY_REALM), defaultcreds)
      
      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      
      assertEquals(200, statusLine.getStatusCode())
      assertEquals("OK", statusLine.getReasonPhrase())
    }
  }
}

注意,要让此测试顺利通信,用户名和密码字段将需要输入 Twitter 能接收的内容 — 我在开发时使用了自己的 Twitter 用户名和密码,但显然您需要使用自己设定的用户名和密码。注册新的 Twitter 帐户相当简单,因此我假定您已经拥有一个帐户,或者知道如何注册(很好。我会等待完成此任务)。

完成后,使用用户名和密码构造函数参数将它映射到 Scitter 类非常简单,如清单 6 所示:

清单 6. Scitter.verifyCredentials
package com.tedneward.scitter
{
  import org.apache.commons.httpclient._, auth._, methods._, params._

  // ...

  /**
   * Class for consuming "authenticated user" Twitter APIs. Each instance is
   * thus "tied" to a particular authenticated user on Twitter, and will
   * behave accordingly (according to the Twitter API documentation).
   */
  class Scitter(username : String, password : String)
  {
    /**
     * Verify the user credentials against Twitter.
     *
     * Twitter docs say:
     * verify_credentials
     * Returns an HTTP 200 OK response code and a representation of the
     * requesting user if authentication was successful; returns a 401 status
     * code and an error message if not.  Use this method to test if supplied
     * user credentials are valid.
     * URL: http://twitter.com/account/verify_credentials.format
     * Formats: xml, json
     * Method(s): GET
     */
    def verifyCredentials : Boolean =
    {
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/help/test.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
        
      client.getParams().setAuthenticationPreemptive(true)
      val creds = new UsernamePasswordCredentials(username, password)
      client.getState().setCredentials(
        new AuthScope("twitter.com", 80, AuthScope.ANY_REALM), creds)

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      statusLine.getStatusCode() == 200
    }
  }
}

清单 7 中相应的 Scitter 类测试也相当简单:

清单 7. 测试 Scitter.verifyCredentials
package com.tedneward.scitter.test
{
  class ScitterTests
  {
    import org.junit._, Assert._
    import com.tedneward.scitter._

    def testUser = "TwitterUsername"
      def testPassword = "TwitterPassword"
    
      // ...
	
    @Test def verifyCreds =
    {
      val scitter = new Scitter(testUser, testPassword)
      val result = scitter.verifyCredentials
      assertTrue(result)
    }
  }
}

不算太糟。库的基本结构已经成形,但显然还有很长的路要走,特别是因为目前实际上未执行任何特定于 Scala 的任务 — 在面向对象设计中,库的构建并不像练习那样简单。因此,我们开始使用一些 XML,并通过更加合理的格式将它返回。

从 XML 到对象

现在可以添加的最简单的 API 是 public_timeline,它收集 Twitter 从所有用户处接收到的最新的 n 更新,并返回它们以便于进行使用。与之前讨论的另外两个 API 不同,public_timeline API 返回一个响应主体(而不是仅依赖于状态码),因此我们需要分解生成的 XML/RSS/ATOM/,然后将它们返回给 Scitter 客户机。

现在,我们编写一个探索测试,它将访问公共提要并将结果转储到 stdout 以便进行分析,如清单 8 所示:

清单 8. 大家都在忙什么?
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...
  
    @Test def callTwitterPublicTimeline =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
      
      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      assertEquals(statusLine.getStatusCode(), 200)
      assertEquals(statusLine.getReasonPhrase(), "OK")
      
      val responseBody = method.getResponseBodyAsString()
      System.out.println("callTwitterPublicTimeline got... ")
      System.out.println(responseBody)
    }
  }
}

运行后,结果每次都会有所不同,因为公共 Twitter 服务器上有许多用户,但通常应与清单 9 的 JUnit 文本文件转储类似:

清单 9. 我们的 Tweets 结果
<statuses type="array">
    <status>
      <created_at>Tue Mar 10 03:14:54 +0000 2009</created_at>
      <id>1303777336</id>
      <text>She really is. http://tinyurl.com/d65hmj</text>
      <source><a href="http://iconfactory.com/software/twitterrific">twitterrific</a>
        </source>
      <truncated>false</truncated>
      <in_reply_to_status_id></in_reply_to_status_id>
      <in_reply_to_user_id></in_reply_to_user_id>
      <favorited>false</favorited>
      <user>
          <id>18729101</id>
          <name>Brittanie</name>
          <screen_name>brittaniemarie</screen_name>
          <description>I'm a bright character. I suppose.</description>
          <location>Atlanta or Philly.</location>
          <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/
                             81636505/goodish_normal.jpg</profile_image_url>
          <url>http://writeitdowntakeapicture.blogspot.com</url>
          <protected>false</protected>
          <followers_count>61</followers_count>
      </user>
    </status>
    <status>
      <created_at>Tue Mar 10 03:14:57 +0000 2009</created_at>
      <id>1303777334</id>
      <text>Number 2 of my four life principles.  "Life is fun and rewarding"</text>
      <source>web</source>
      <truncated>false</truncated>
      <in_reply_to_status_id></in_reply_to_status_id>
      <in_reply_to_user_id></in_reply_to_user_id>
      <favorited>false</favorited>
      <user>
          <id>21465465</id>
          <name>Dale Greenwood</name>
          <screen_name>Greeendale</screen_name>
          <description>Vegetarian. Eat and use only organics. 
                       Love helping people become prosperous</description>
          <location>Melbourne Australia</location>
          <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/
                             90659576/Dock_normal.jpg</profile_image_url>
          <url>http://www.4abundance.mionegroup.com</url>
          <protected>false</protected>
          <followers_count>15</followers_count>
       </user>
     </status>
       (A lot more have been snipped)
</statuses>

通过查看结果和 Twitter 文档可以看出,调用的结果是一组具备一致消息结构的简单 “状态” 消息。使用 Scala 的 XML 支持分离结果相当简单,但我们会在基本测试通过后立即简化它们,如清单 10 所示:

清单 10. 大家都在忙什么?
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...
  
    @Test def simplePublicFeedPullAndParse =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
      val statusCode = client.executeMethod(method)
      val responseBody = new String(method.getResponseBody())
      
      val responseXML = scala.xml.XML.loadString(responseBody)
      val statuses = responseXML \\ "status"

      for (n <- statuses.elements)
      {
        n match
        {
          case <status>{ contents @ _*}</status> =>
          {
            System.out.println("Status: ")
            contents.foreach((c) =>
              c match
              {
                case <text>{ t @ _*}</text> =>
                  System.out.println("\tText: " + t.text.trim)
                case <user>{ contents2 @ _* }</user> =>
                {
                  contents2.foreach((c2) =>
                    c2 match
                    {
                      case <screen_name>{ u }</screen_name> =>
                        System.out.println("\tUser: " + u.text.trim)
                      case _ =>
						()
                    }
                  )
                }
                case _ =>
				  ()
              }
            )
          }
          case _ =>
            () // or, if you prefer, System.out.println("Unrecognized element!")
        }
      }
    }
  }
}

随着示例代码模式的变化,这并不值得推荐 — 这有点类似于 DOM,依次导航到各个子元素,提取文本,然后导航到另一个节点。我可以仅执行两个 XPath 样式的查询,如清单 11 所示:

清单 11. 替代解析方法
for (n <- statuses.elements)
{
     val text = (n \\ "text").text
       val screenName = (n \\ "user" \ "screen_name").text
}

这显然更加简短,但它带来了两个基本问题:

  • 我们可以强制 Scala 的 XML 库针对每个元素或子元素遍历一次图,其速度会随时间减慢。
  • 我们仍然需要直接处理 XML 消息的结构。这是两个问题中最为重要的。

也就是说,这种方式不具备可伸缩性 — 假设我们最终对 Twitter 状态消息中的每个元素都感兴趣,我们将需要分别从各状态中提取各个元素。

这又造成了另一个与各格式本身相关的问题。记住,Twitter 可以使用四种不同的格式,并且我们不希望 Scitter 客户机需要了解它们之间的任何差异,因此 Scitter 需要一个能返回给客户机的中间结构,以便未来使用,如清单 12 所示:

清单 12. Breaker,您的状态是什么?
abstract class Status
{
  val createdAt : String
  val id : Long
  val text : String
  val source : String
  val truncated : Boolean
  val inReplyToStatusId : Option[Long]
  val inReplyToUserId : Option[Long]
  val favorited : Boolean
  val user : User
}

这与 User 方式相类似,考虑到简洁性,我就不再重复了。注意,User 子元素有一个有趣的问题 — 虽然存在 Twitter 用户类型,但其中内嵌了一个可选的 “最新状态”。状态消息还内嵌了一个用户。对于这种情况,为了帮助避免一些潜在的递归问题,我选择创建一个嵌入在 Status 内部的 User 类型,以反映所出现的 User 数据;反之亦然,Status 也可以嵌入在 User 中,这样可以明确避免该问题。(至少,在没发现问题之前,这种方法是有效的)。

现在,创建了表示 Twitter 消息的对象类型之后,我们可以遵循 XML 反序列化的公共 Scala 模式:创建相应的对象定义,其中包含一个 fromXml 方法,用于将 XML 节点分离到对象实例中,如清单 13 所示:

清单 13. 分解 XML
  /**
   * Object wrapper for transforming (format) into Status instances.
   */
  object Status
  {
    def fromXml(node : scala.xml.Node) : Status =
    {
      new Status {
        val createdAt = (node \ "created_at").text
        val id = (node \ "id").text.toLong
        val text = (node \ "text").text
        val source = (node \ "source").text
        val truncated = (node \ "truncated").text.toBoolean
        val inReplyToStatusId =
          if ((node \ "in_reply_to_status_id").text != "")
            Some((node \"in_reply_to_status_id").text.toLong)
          else
            None
        val inReplyToUserId = 
          if ((node \ "in_reply_to_user_id").text != "")
            Some((node \"in_reply_to_user_id").text.toLong)
          else
            None
        val favorited = (node \ "favorited").text.toBoolean
        val user = User.fromXml((node \ "user")(0))
      }
    }
  }

其中最强大的一处是,它可以针对 Twitter 支持的其他任何格式进行扩展 —fromXml 方法可以在分解节点之前检查它是否保存了 XML、RSS 或 Atom 类型的内容,或者 Status 可以包含 fromXmlfromRssfromAtomfromJson 方法。实际上,后一种方法是我的优先选择,因为它会平等对待基于 XML 的格式和 JSON(基于文本)格式。

好奇和细心的读者会注意到在 Status 及其内嵌 UserfromXml 方法中,我使用的是 XPath 样式的分解方法,而不是之前建议的遍历内嵌元素的方法。现在,XPath 样式的方法看上去更易于阅读,但幸运的是,我后来改变了注意,良好的封装仍然是我的朋友 — 我可以在随后修改它,Scitter 外部的任何人都不会知道。

注意 Status 内部的两个成员如何使用 Option[T] 类型;这是因为这些元素通常排除在 Status 消息外部,并且虽然元素本身会出现,但它们显示为空(类似于 <in_reply_to_user_id></in_reply_to_user_id>)。这正是 Option[T] 的作用所在。当元素为空时,它们将使用 “None” 值。(这表示考虑到基于 Java 的兼容性,访问它们会更加困难,但惟一可行方法是对最终生成的 Option 实例调用 get(),这不太复杂并且能很好地解决 “非 null 即 0” 问题)。

现在已经可以轻而易举地使用公共时间轴:

清单 14. 分解公共时间轴
    @Test def simplePublicFeedPullAndDeserialize =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
      val statusCode = client.executeMethod(method)
      val responseBody = new String(method.getResponseBody())
      
      val responseXML = scala.xml.XML.loadString(responseBody)
      val statuses = responseXML \\ "status"

      for (n <- statuses.elements)
      {
        val s = Status.fromXml(n)
        System.out.println("\t'@" + s.user.screenName + "' wrote " + s.text)
      }
    }

显然,这看上去更加简洁,并且易于使用。

将所有这些结合到 Scitter 单一实例中相当简单,仅涉及执行查询、解析各个 Status 元素以及将它们添加到 List[Status] 实例中,如清单 15 所示:

清单 15. Scitter.publicTimeline
package com.tedneward.scitter
{
  import org.apache.commons.httpclient._, auth._, methods._, params._
  import scala.xml._

  object Scitter
  {
    // ...
  
    /**
     * Query the public timeline for the most recent statuses.
     *
     * Twitter docs say:
     * public_timeline
     * Returns the 20 most recent statuses from non-protected users who have set
     * a custom user icon.  Does not require authentication.  Note that the
     * public timeline is cached for 60 seconds so requesting it more often than
     * that is a waste of resources.
     * URL: http://twitter.com/statuses/public_timeline.format
     * Formats: xml, json, rss, atom
     * Method(s): GET
     * API limit: Not applicable
     * Returns: list of status elements     
     */
    def publicTimeline : List[Status] =
    {
      import scala.collection.mutable.ListBuffer
    
      val client = new HttpClient()

      val method =
          new GetMethod("http://twitter.com/statuses/public_timeline.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      if (statusLine.getStatusCode() == 200)
      {
        val responseXML =
          XML.loadString(method.getResponseBodyAsString())

        val statusListBuffer = new ListBuffer[Status]

        for (n <- (responseXML \\ "status").elements)
          statusListBuffer += (Status.fromXml(n))
        
        statusListBuffer.toList
      }
      else
      {
        Nil
      }
    }
  }
}

在实现功能全面的 Twiter 客户机之前,我们显然还有很长的路要走。但到目前为止,我们已经实现基本的行为。

结束语

构建 Scitter 库的工作进展顺利;目前,Scitter 测试实现相对比较简单,与产生 Scitter API 的探索测试相比时尤为如此。外部用户不需要担心 Twitter API 或者它的各种格式的复杂性,虽然目前测试 Scitter 库有点困难(对单元测试而言,依赖网络并不是个好方法),但我们会及时解决此问题。

注意,我故意在 Twitter API 中维持了面向对象的感觉,秉承了 Scala 的精神 — 因为 Scala 支持大量功能特性并不表示我们要放弃 Java 结构采用的对象设计方法。我们将接受有用的功能特性,同时仍然保留适用的 “旧方法”。

这并不是说我们在此处提供的设计是解决问题最好的方法,只能说这是我们决定采用的设计方法;并且,因为我是本文的作者,所以我采用的是自己的方式。如果不喜欢,您可以编写自己的库和文章(并将 URL 发送给我,我会在未来的文章中向您发起挑战)。事实上,在未来的文章中,我会将所有这些封装在一个 Scala “sbaz” 包中,并上传到网上供大家下载。

现在,我们又要暂时说再见了。下个月,我将在 Scitter 库中添加更多有趣的特性,并开始考虑如何简化它的测试和使用。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source, Web development
ArticleID=421091
ArticleTitle=面向 Java 开发人员的 Scala 指南: Scala + Twitter = Scitter
publish-date=08202009