案例研究:利用 Grails 搭建 Feedlr.com 网站

阅读数:7287 2009 年 2 月 5 日

话题:JavaRubyWeb框架语言 & 开发

Feedlr 和 Grails

Feedlr:feed 驱动的多平台微博客机器人平台

微博客是由 Twitter 创造出的一种 web 2.0 时代的新事物。在微博客上,人们使用简短的语言随时随地的发表消息,并可以即时地受到好友的消息。由于易用,实时等特点,Twitter 在 06 年推 出至今逐步升温,已经拥有超过 300 万用户。特别在 08 年中,Twitter 一改起步阶段 geek 玩具的角色,明显地向主流进化。随着 Twitter 的兴 起,也出现了非常多其他的微博客。仅国内就有叽歪、饭否、以及做啥等等。微博客的兴起提供了一种全新的在线沟通方式。

Twitter 作为微博客的 鼻祖和最成功的例子,其优秀的 API 接口功不可没。通过 Twitter API,开发者们开发出了众多新奇又好用的 Twitter 第三方应用。我开发 Feedlr 的出发点是建立一个让用户可以自行定制 feed 机器人的服务,核 心功能类似 Twitter 上颇受欢迎的 twitterfeed,并且可以同时 Twitter,叽歪,饭否以及做啥共 4 种微博客平台。

通过 Feedlr,用户可以建立微博客广播帐号,来随时追踪自己感兴趣的 RSS/Atom Feed 内容。一旦有更新,Feedlr 就会自动把新的内容发送到指定的微博客平台上。Feedlr 上线至今,用户们建立了自定义的新闻播报机器 人,DIY 的免费天气预报机器人,不同微博客之间的消息同步机器人,甚至国内地震情况实时监控机器人等等。而通过国内微博客服务的短信通知服务,以上所有 的 Feed 内容国内用户都可以免费在手机上通过短信接收到。

Grails 框架的选择

Grails 是一个崭露头角的基于 Groovy 语言,运行与 JVM 之上,设计上类似于 Rails 的快速 web 开发框架,在 08 年初刚推出 1.0 版。通过 Groovy 语言和创新的架 构,Grails 把成熟的企业级 JEE 开源组件 Spring,Hibernate 等巧妙地整合起来,使用类似 Rails 的“按约定设计”(design by convention) 理念捆绑成一套完整的 web 开发框架。JEE 开发过程的繁琐被 Groovy 灵活多变的动态特性和按约定设计带来的精简配置所取代, 而又保留了企业级组件在稳定和性能方面的优势,可以说是把 Rails 式的快速开发带给了水深火热中的 JEE 开发者们。我来自 JEE 背景,对 Groovy 语 言也有一定基础,选用 Grails 搭建 Feedlr 是比较自然的选择,同时也是为了在一个没有过多约束的真实项目中体验 Grails 的完整开发过程。

如何用 Grails 实现 Feedlr 的核心功能

Feedlr 的核心功能

Feedlr 的核心功能主要包括定时查询用户提供的 feed 的更新,把更新的 feed 内容发布到微博客,再加上用来增强用户体验的多处 AJAX 实现以及 OpenID 登录等。这里逐一对这些功能的实现做一下介绍。

定时查询 feed 更新

Feedlr 最核心的功能就是定时轮询用户提交的 feed,发现新增的条目,从而通过微博客 API 发送到微博上去。只要使用 Grails 的 Quartz 插件就可以非常 方便的实现这一功能。Quartz 是一个用途广泛的开源 Java 库,用于精确地控制定时任务。由于兼容 Unix Cron 语法,Quartz 的功能非常强大。而在 Grails 中,Quartz 是框架自带的核心插件之一,通过 Quartz 插件来执行定时任务非常方便。 新建一个 Quartz 定时任务,只需要在 Grails 项目根目录下执行

grails create-job

根 据提示输入 job 名称,Grails 就会自动在 grails-app/jobs/ 目录下生成一个新的 job 程序文件。Grails job 都是以 XXXJob.groovy 命名,存放在 grails-app/jobs 目录下,Grails 启动时会自动遍历 jobs 目录,定时执行每个定 义好的 job。一个 job 文件用来定义一种定时执行模式,通过 Unix Cron 语法来定义定时逻辑。例如,Feedlr 用于轮询 feed 的 job 大致是这样的:

class UpdateFeedsJob {
    def feedService    
    def cronExpression = "0 * * * * ?" // 每分钟执行一次 
    def execute() {    
        feedService.updateFeeds()
    }
}

Cron 表达式“0 * * * * ?”表示每分钟执行一次。需要执行的逻辑通过定义一个 execute() 方法来指定。其中 feedService 是已经定义好的用来查询 feed 更新的一 个 Grails Service 类,使用 Rome 来解析 feed。注意此处不需要实例化 feedService 变量,只要通过按约定设计的规则定义需要使用的 Service 的变量名,Grails 会自动找到 FeedService 这个 Service 类,注入到 UpdateFeedsJob 中,并把 Service 实例付给 feedService 变量,听起来很神奇吧。这样,Grails 就会每分钟触发一次 UpdateFeedsJob,来查询 feed 更新了。

发布 feed 更新到微博客

目前流行的微博客 API 都是已 REST 风格设计,通过 GET 和 POST 方法来得到或者更新内容的。例如发布一条消息到 Twitter,就是通过 POST 方法发送到 Twitter 指定的 API 地址,简化的代码实例如下:

def conn = new URL('http://twitter.com/statuses/update.xml').openConnection()
conn.setRequestProperty ('Authorization', 'Basic ' + 'username:password'.bytes.encodeBase64())
conn.requestMethod = 'POST'
conn.doOutput = true
try{
    conn.outputStream.withWriter('UTF8'){               
        it << "status=" << newMessage
    }
}catch(Exception e){
    ...
}

以上 Groovy 代码很清晰易读。通过 Twitter RESTful API 发布新消息需要使用 Http Basic 验证用户登录信息,所以这里按照 Basic 验证规范在请求中加入了验证数据。其中 encodeBase64() 方法是 Grails 提供的神奇的 动态方法,对于合适类型的对象在 Grails 程序中直接就可以使用这些动态方法,其他的编码方法还包括 encodeAsURL() 等。

Ajax

在 web 2.0 时代没有 Ajax 的网站是不完整的。幸运的是,在 Grails 中使用 Ajax 非常方便。通过 Grails 内建的多才多艺的 render 方法,就可以轻松地给前端 Ajax 请求返回任何形式的输出。例如,

  • 直接返回简单的纯文本字串

class FooController{
...
    def ajaxResponse = {
        ...
        render("This is an Ajax response.")
    }
  • 指定返回内容的格式和编码

render(text:"<xml>some xml</xml>",contentType:"text/xml",encoding:"UTF-8")
  • 返回模板内容

render(template:"feeds", model:[feeds:feeds], contentType:"text/html", encoding:"UTF-8")
  • 返回 JSON,直接自动转换一个 object 为 JSON

import grails.converters.*
...
def jsonObj = [object:[collection:[[name:‘value1′],[name:‘value2′]]]]
render jsonObj as JSON
  • 返回 JSON,通过 JSON builder DSL 直接构造 JSON 数据

render(contentType:‘text/json’, , encoding:'UTF-8'){
        studio(name:‘Pixar’,website:‘pixar.com’)
        films{
                film(title:‘Toy Story’,year:‘1995′)
                film(title:‘Monsters, Inc.’,year:‘2001′)
                 film(title:‘Finding Nemo’,year:‘2003′)
        }
}

OpenID 支持

Feedlr 支持使用 OpenID 登录。由于 Grails 社区已经提供了OpenID 插件,通过 Grails 的插件机制,实现 OpenID 支持也是一件轻松的事情。

  • 首先,安装 OpenID 插件,在 Grails 应用根目录执行命令:

grails install-plugin openid
  • 然后,使用 openid 插件提供的 taglib 来编写 openid 登录表单

<openid:form success="[controller:'login',action:'openidSuccess']" error="[controller:'login',action:'openidError']">
                <openid:input size="30" value="http://" class="input-text"/>
...
</openid:form>

OpenID 插件代为处理了具体的 OpenID 登录验证过程,在 <openid:form> 中,通过 success 参数和 error 参数指定登录成功或失 败以后重定向到哪个 controller action。登录成功后,就可以在 controller 中直接得到当前登录的 openid 信息。

def openid = session.openidIdentifier

当然,需要实现完整的 OpenID 和普通帐号的整合还有更多工作要做,包括把登录的 OpenID 和已有的普通帐号关联起来,从普通帐号添加 OpenID 信息等。这些都是需要开发者根据自己的情况自行实现的。

测试

测 试是开发一个完整项目不可缺少的部分,幸运的是 Grails 已经为开发者考虑到了这点,测试 Grails 程序也能像开发一样轻松。Grails 中的测试建 立在 Groovy testing 的基础上,通过使用 Groovy 来编写 JUnit 测试代码,减轻程序员的负担。Grails 中的测试分为 unit test 和 integration test 两种,两者的区别主要在于 unit test 是相对独立的测试,而 integration test 执行的时候 Grails 会按照实际运行的方式启动框架程序。建立一个 unit test 或者 integration test 各自只需要一条命令即可。

grails create-unit-test
grails create-integration-test

Grails 会自动在 grails-app/test/unit 或者 grails-app/test/integration 下面建立相应的 XXXTests.groovy 文件。具体的 test case 定义在 XXXTests.groovy 里,通过定义继承 groovy.util.GroovyTestCase 的类来实现,这些其实都是 Groovy 测试的内容,通过 JUnit 方式编写测试代码即可。

准备好了 test case 之后,Grails 同样已经为你准备好的命令来自动执行测试。

grails test-app

执行这条命令,Grails 就会自动按照 unit test 到 integration test 的顺序来执行定义好的所有 test case,并将测试结果整理成 HTML 格式展现出来。test-app 命令还有更多具体用法,可以参考 Grails 文档。

Feedlr 的部署

使用 Grails 的开发过程是很令人感到愉快的。那么,一切都搞定以后,怎么部署呢?

Grails 文档中说明 Grails 经测试可以部署到大多数常用的 Java 应用服务器上,但是具体有关部署的资料文档比较缺乏。Feedlr 选择的是 Tomcat 6,相对来讲比较常用,资料也比较丰富。准备部署 Grails 应用的时候,首先通过 Grails 把项目打包成 war 文件。

grails prod war feedlr.war

这 里,"prod"参数用来指定打包的时候使用 Config.groovy 里针对生产环境的配置。部署环境的配置在 Config.groovy 中设定,包 括"prodcution",“development"和"testing"三种,主要用于对不同环境指定不同的数据源和特定的环境变量。具体用法可以 参考 Grails 文档。war 命令默认的打包环境就是"prod",所以次数"prod"也可以省略。如需指定其他环境只需要将"prod"替换 成"dev"或者"test"即可。

最后指定 war 文件的文件名是可选的。在 Tomcat 下如果想让应用跑在 URL 根路径下,可以指定文件名为 ROOT.war。打包完成后,把 war 文件复制到 Tomcat 应用目录下,启动 Tomcat,正常的话 Grails 应用就能跑起来了。

在 系统性能和伸缩性方面,我其实没有花太多力气去优化。最主要的优化工作就是在内存占用方面。Feedlr 目前使用的是一台 540MB 内存的 VPS 服务器, 在初期曾使用 340MB 内存的配置,遇到了内存资源紧张的问题,导致 JVM 性能底下。由于 Feedlr 的特定用途,需要解析大量 feed 内容,所以在内存 方面要求不低。后来经过一系列的优化措施,目前在这个环境下运行的相对比较稳定了。我之前也总结过一些 Grails 服务器优化的经验,有兴趣的朋友可以参考我的这篇博客文章

实际开发中的一些困难

在 Feedlr 的开发中,可以体会到目前阶段使用 Grails 进行完整 Web 项目开发的一些困难和问题。

开发环境的不完善

Feedlr 是在 Eclipse 加上 Groovy 插件的环境下开发的。但是这个环境目前还非常不完善,主要是 Groovy 插件的可用程度还比较低,而且没有 Grails 支持,不支持 GSP 语法等,只是能够支持 Groovy 语言,加上 Eclipse 本身的强大功能,能够给开发提供一定帮助。不过好在使用 Groovy 语言开发比 Java 要省力不少,所以不需要非常强大的 IDE 也可以不错的完成任务。IDE 方面另外的选择主要包括 IntelliJ IDEA 和 NetBeans。简单来讲收费的 IDEA 对 Groovy/Grails 的支持最为完整强大,但是代价也不菲。开源方面的选择,Eclipse 方面还是比刚刚开始支持 Groovy 的 Netbeans 好过不少。

Grails 本身尚不够成熟

在开发过程中也遇到过若干 Grails 的 bug,有的 bug 甚至导致某功能无法正常运行。使用一个尚不成熟的框架,遇到 bug 也是正常的事情。解决 bug 的话基本上可以先到 Grails 邮件列表里提问和寻找答案,需要的话提交 bug 报告到 Grails 官方 JIRA,提供 bug 数据等待修复。不过如果遇到紧急问题,还是自己动 手更好。可以从 Grails 主页下载带源码的 Grails 安装包,就可以直接调试 bug 并编译修改代码,这样不用等官方发布下一个小版本就可以直接解决问 题了。在 Feedlr 开发过程中遇到过若干比较紧急的 bug,我随着 bug 报告也提供过若干补丁程序,在最新的 Grails 1.0 中都已被集成。在这里也要建议大家在使用过程中多多提交 bug 报告和提供补丁程序,这是良好的参与建设开源社区的方式。

总结

要 完整地实现 Feedlr 显然还有很多工作要做,但 Grails 确实大大减轻了程序员编码工作的负荷。通过 grails stats 命令可以看到,除去测试代码,feedlr 最终的代码规模约是 1.9kloc。对于一个完整地具备了包括全文检索,RSS/Atom feed 生成(Feedlr 提供最新机器人服务列表的 feed),Tag 标签功能,OpenID+ 普通登录方式整合等功能的 web 2.0 网站来说,这确实意味着我省去了不少打字的工夫,避免了传统 JEE 繁琐的开发方式。

那么,Grails 到底是不是 JEE 世界的圣杯呢?我将在本系列的下篇文章中进一步进行分析。

参考资料

作者简介

侯雍容,毕业于复旦大学计算机专业,毕业后在 IBM 中国开发中心从事软件开发工作,目前创立了上海睿谷信息科技有限公司,从事软件开发和咨询工作。对于敏捷 Web 开发、动态程序语言、云计算等方面都有特别的兴趣和经验。可以从这里看到关于他的详细资料:http://www.linkedin.com/in/houyr,也可以访问他的个人博客:http://damienh.org


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。