面向 Java 开发人员的 Node.js

用于 Web 应用程序的轻量级的、事件驱动的 I/O

Node.js 通过将 Java 平台的标准多线程方法替换为单线程、事件驱动的 I/O 来解决并发性问题。在本文中,Andrew Glover 将会介绍 Node.js,并解释事件驱动并发性为什么会引起这么多关注,甚至连铁杆的 Java 开发人员都会对它有兴趣。然后作者会演示如何利用 Node 的 Express 框架、Mongolian DeadBeef 和 MongoDB 来构建一个并发的、可扩展的、稳定的 Web 应用程序。

Andrew Glover, CTO, App47

Andrew GloverAndrew Glover 是在行为驱动开发、持续集成和敏捷软件开发方面很有激情的开发人员、作家、演说家和企业家。他是 easyb 行为驱动开发(Behavior-Driven Development,BDD)框架的创建者和三本书的合著者:持续集成Groovy 在行动Java 测试模式。您可以通过他的 博客 与他保持联系,并在 Twitter 上关注他。



2011 年 12 月 28 日

在过去的几年中,JavaScript 已经成为 Web 应用程序开发的幕后英雄。这样的情景让那些习惯将 JavaScript 称为 “玩具式语言” 的软件开发人员大跌眼镜。尽管有其他更流行的语言(这里是说开发人员忠实拥护的那些),而 JavaScript 作为标准且浏览器中立的脚本语言的地位从未动摇过。在客户端 Web 开发方面,它可能是世界上运用最广泛的语言。

Java 开发人员的 JavaScript

JavaScript 是现代 Java 开发人员的必备工具,而且并不难学。Java 开发 2.0:面向 Java 开发人员的 JavaScript Andrew Glover,他将会介绍用来构建一流的 Web 应用程序所需的语法,其中包括 JavaScript 变量、类型、函数和类。

JavaScript 在服务器端脚本也占有一席之地,而且影响还会越来越大。虽然过去 JavaScript 在服务器端做过一些尝试,但并没有什么大的收获,除了 Node.js 或 Node。

Node 专门用来协助开发人员构建可扩展的网络程序,它是服务器端的编程环境,但几乎重建了整个 JavaScript 以服务于新一代的开发人员。对很多 Java 开发人员而言,Node 最大的吸引力在于软件并发的新方法。尽管 Java 平台在并发方式上还会不断进步(预计在 Java 7 和 Java 8 中会有大幅提升),但事实是 Node 满足了一个需求,它以更轻量级方式进行处理,这是众多 Java 开发人员想要的。就像客户端脚本使用 JavaScript 一样,在 Node 环境下编辑服务器端脚本非常棒,因为它非常有效,而且它能够在很多 Java 开发人员遇到挑战的领域中发挥作用。

在本文中,我将为您介绍服务器端脚本的革命,那就是 Node。首先我们从整体结构上总揽全局,看看是什么让 Node 与众不同,然后会演示如何快速构建一个使用 MongoDB 持久化数据的可扩展性 Web 应用程序。您将会亲身体验 Node 是多么有趣,并且看到用它来组建一个有效的 Web 应用程序有多快。

Node 的事件驱动并发

Node 是建立在 Google 的 V8 JavaScript 引擎基础上的一个可扩展、事件驱动 I/O 环境。Google V8 实际上在执行之前就将 JavaScript 编译到本机机器代码中,从而达到快速的运行时性能,实际上这与 JavaScript 关系不大。Node 本身能够让您快速构建速度极快、高度并发的网络应用程序。

事件驱动 I/O 对 Java 开发人员来说可能有些陌生,但并非是全新的概念。与 Java 平台中使用的多线程编程模型不同,Node 处理并发的方法是单线程加上事件循环。Node 结构可以确保不会不堵塞和异步 I/O 传输。那些通常会引起堵塞的调用如等待数据库查询结果等在 Node 中不会发生。Node 应用程序不是等待代价高昂的 I/O 活动来完成操作,而是会发出一个回调(roll back)。当资源返回后,会异步调用关联的回调。

为什么选择 Node.js?

Java 平台处理并发的方式帮助确立了其在企业开发方面的领导地位,这很难发生改变。诸如 Netty(以及 Gretty;参阅 参考资料)的框架和诸如 NIO 和 java.util.concurrent 的核心库,使得 JVM 成为处理并发的首选。Node 的特别之处就在于它是专门为应对并发编程而设计的现代开发环境。Node 的事件驱动编程方式意味着,您无需添加额外的库来处理并发性,这对关注多核硬件的开发人员来说是个好消息。

并发性在 Node 程序中照样工作。如果在 Java 平台上运行之前的场景,我可能会选择复杂且耗时的方法,从传统的线程到 Java NIO 中较新的库,甚至是改善和更新的 java.util.concurrent 包。尽管 Java 的并发很强大,但却难以理解,还要用它来编码!相比而言,Node 的回调机制是嵌在语言内部的;您不需要添加诸如 synchronized 这样额外的结构使它工作。Node 的并发模型极其简单,让广大的开发人员都能够接受。

Node 的 JavaScript 语法也为您减少敲键的次数,节省大量时间。只需少量代码,您就能构建快速的、可扩展的 Web 应用程序,并且它能处理大量的并发连接。当然您也可以在 Java 平台上实现,但将会需要更多代码行和一大堆库和结构。如果您担心对新的编程环境不熟悉,那么大可不必:如果已经了解一些 JavaScript,那么 Node 很容易学习,这一点我可以向您保证。


开始使用 Node

如前所述,Node 很容易上手,而且有很多好的在线教程可以为您提供帮助。在本文中(以及我的 Java 开发 2.0:面向 Java 开发人员的 JavaScript 中),我重点关注的是帮助 Java 开发人员理解 Node 的优势。我不会从标准的 “Hello, world” Web 服务器应用程序讲起,我会直接讲解一个有实际意义的应用程序:将它想象成在 Node 上构建的 Foursquare 网站。

视频演示:开始使用 Node.js

想要换种方式学习这个方便的框架?观看此 视频演示,这也是由 Andrew Glover 演示的,通过它来学习关于 Node.js 的更多内容,并了解它能为您做些什么。(阅读 视频演示的文字版。)

安装 Node 需要您遵循特定平台的说明;如果您使用的是 UNIX 一类的平台,如 OSX,那么我推荐您使用 Node Version Manager 或 NVM,它能处理安装正确版本 Node 的细节信息。无论哪种情况,请现在就 下载并安装 Node

我们还使用了第三方库来构建此应用程序,因此您可能还想 安装 NPM,它是 Node 的包管理器。NPM 允许您指定项目的版本依赖关系,这样就可以下载并包含在您的构建路径中。NPM 在很多方面很像 Java 平台的 Maven,或者是 Ruby 的 Bundler。


Node Express

Node 在 Web 开发人员心目中占据一席之地,一是因为它对并发的处理能力,二是它是按 Web 开发的需求构建的。最流行的一个第三方 Node 工具是轻量级 Web 开发框架 Express,我们将会用它来开发应用程序(参阅 参考资料 以了解关于 Express 的更多内容)。

Express 有很多特性,包括复杂路由、动态模板视图(参见 Node 框架 du jour: Jade)和内容协商。Express 是非常轻量级的,没有内嵌的 ORM 或类似东西来加重其负担。在这一方面,Express 无法与 Rails、Grails 或其他有完整堆栈的 Web 框架相比。

安装并使用 Express 的一个简单方法是通过 NPM package.json 文件将其声明为依赖关系,如清单 1 所示。该文件与 Maven 的 pom.xml 或 Bundler 的 Gemfile 类似,但它是 JSON 格式的。

清单 1. 清单 1.NPM 的 package.json 文件
{
  "name":"magnus-server",
  "version":"0.0.1",
  "dependencies":{
    "express":"2.4.6"
  }
}

清单 1 中,我赋予 Node 项目一个名称 (magnus-server) 和一个版本号 (0.0.1)。我还将 Express 2.4.6 版本声明为一个依赖关系。NPM 的一个好处就是它会获取 Express 所有的传递依赖关系,迅速加载 Express 所需的其他所有第三方 Node 库。

通过 package.json 定义项目依赖关系之后,就可以通过在命令行输入 npm install 来安装所需的包。您会看到 NPM 安装 Express 和依赖关系,如 connectmime 等等。

编写网络应用程序

我们通过创建一个 JavaScript 文件来编写示例应用程序;我将其命名为 web.js,但实际上可以随便起一个名字。在您最喜欢的编辑器或 IDE 中打开此文件;例如,您可以使用 Eclipse JavaScript 插件 JSDT(参阅 参考资料)。

在此文件中,添加清单 2 中的代码:

清单 2. Magnus Server:第一步
var express = require('express');

var app = express.createServer(express.logger());

app.put('/', function(req, res) {
  res.contentType('json');
  res.send(JSON.stringify({ status:"success" }));
});

var port = process.env.PORT || 3000;

app.listen(port, function() {
  console.log("Listening on " + port);
});

这一小段代码发挥了很大的作用,因此我从头讲起。首先,如果想要在 Node 中使用第三方库,就要用到 require 短语;在 清单 2 中,我们需要 Express 框架,并通过 express 变量获得其句柄。下一步,我们通过 createServer 调用创建一个应用程序实例,它会创建一个 HTTP 服务器。

然后我们通过 app.put 定义一个端点。在本例中,我们定义一个 HTTP PUT 作为在应用程序根部 (/) 监听的 HTTP 方法。put 调用有两个参数:路由和调用路由时相应的回调。第二个参数是在运行时端点 / 被启动时的调用函数。记住,此回调就是 Node 所谓的事件驱动或事件 I/O,即异步调用回调函数。终端可以同时处理大量请求而不必手动创建线程。

作为端点定义的一部分,我们创建处理 /PUT 逻辑。为了简单起见,我们将响应类型设置为 JSON,然后发送一个简单的 JSON 文档:({"status":"success"})。请注意这里的 stringify 方法恰到好处,它会接收哈希表然后将其转换成 JSON 格式。

JavaScript 和 JSON

JSON 和 JavaScript 是同胞兄弟,这种关系延续到了 Node 中。在 Node 应用程序中解析 JSON 无需特别的库或结构;您可以使用与对象图类似的逻辑调用。简而言之,Node 对待 JSON 就像自有的类型一样,这就使得编写基于 JSON 的 Web 应用程序变得非常简单。

下一步,创建用来表示应用程序所监听的端口的变量;可以通过获取 PORT 环境变量或直接设置为 3000 来完成。最后,通过调用 listen 方法来启动此应用程序。我们再次传入一个回调函数,它将会在应用程序启动并运行至将消息打印到控制台时被调用(本例中,是 standard out)。

试一下!

这个完美的应用程序会对所有 PUT 作出响应,因此只要在命令行输入 node web.js 即可运行。如果您想要进一步测试该应用程序,我建议您下载 WizTools.org 的 RESTClient。有了 RESTClient,您就可以通过对 http://localhost:3000 执行 HTTP PUT 以快速测出 Magnus Server 是否工作正常。如果正常,您会看到表示执行成功的 JSON 响应。(参阅 参考资料 了解更多有关安装和使用 RESTClient 的信息。)


在 Express 中处理 JSON

JavaScript 和 JSON 紧密关联,这使得在 Express 中管理 JSON 变得非常简单。在这一节,我们将在 清单 2 的主应用程序中添加一些代码,从而能获取传入的 JSON 文档并将其打印到 standard out。然后,我们将所有内容持久化为 MongoDB 实例。

传入的文档与清单 3 类似(请注意,简单起见,我省略了位置信息):

清单 3. Freddie Fingers 的免费午餐!
{
 "deal_description":"free food at Freddie Fingers",
 "all_tags":"free,burgers,fries"    
}

清单 4 添加了解析传入文档的功能:

清单 4. 用 Express 解析 JSON
app.use(express.bodyParser());

app.put('/', function(req, res) {
  var deal = req.body.deal_description;
  var tags = req.body.all_tags;

  console.log("deal is :"  + deal + " and tags are " + tags);
	
  res.contentType('json');
  res.send(JSON.stringify({ status:"success" }));
});

请注意,清单 4 中包含了一行代码指引 Express 使用 bodyParser。这会让我们能轻松地(我是说轻松地)获取传入的 JSON 文档的属性。

put 回调函数中的代码是用来获取传入文档的 deal_descriptionall_tags 属性值。请注意,我们获得请求文档的每个元素时是多么轻松:在本例中,req.body.deal_description 获取 deal_description 的值。

测试一下!

你也可以对此实现进行测试。关闭 magnus-server 实例并重启,然后使用一个 HTTP PUT 将一个 JSON 文档提交到 Express 应用程序中。首先,您会看到一个成功响应。其次,Express 会把您提交的值发送到 standard out。通过使用我的 Freddie Fingers 文档,我获取了输出结果。

deal is : free food at Freddie Fingers and tags are free, burgers, fries.

Node 的持久性

我们已经拥有了一个工作正常的应用程序,它能接收和解析 JSON 文档,并返回响应。现在要做的就是增加一些持久性逻辑。由于我偏爱 MongoDB(参见 参考资料),因此我选择通过 MongoDB 实例持久化数据。为了让处理过程更简单一些,我们将会用到第三方库 Mongolian DeadBeef,我们将用它来存储传入 JSON 文档的值。

Mongolian DeadBeef 是众多的用于 Node 的 MongoDB 库中的一个。我之所以选择它是因为我觉得它名字很有趣,还因为它对本机 MongoDB 驱动的监控功能让我非常满意。

现在,您已经知道,使用 Mongolian DeadBeef 的第一步是升级 package.json 文件,如清单 5 所示:

清单 5. 添加 JSON 解析
{
  "name":"magnus-server",
  "version":"0.0.1",
  "dependencies":{
    "express":"2.4.6",
	"mongolian":"0.1.12"
  }
}

由于我们将会使用 MongoDB 数据存储,因此需要通过运行 npm install 来更新项目的硬件依赖关系。为了提高 Mongolian DeadBeef MongoDB 驱动的性能,我们还需要安装本机 C++ bson 解析器,NPM 能指导我们完成这一过程。

在开始使用 Mongolian DeadBeef 之前,在现有的实现中再添加一个 require,然后连接到所需的 MongoDB 实例上(如清单 6 所示)。在本示例中,我们将连接到由 MongoHQ 托管的实例上,它是 MongoDB 的云供应者。

清单 6. 将 Mongolian DeadBeef 添加到 magnus-server
var mongolian = require("mongolian");
var db = new mongolian("mongo://a_username:a_password@flume.mongohq.com:23034/magnus");

PUT 回调函数中,我们持久化来自传入的 JSON 文档的值,如清单 7 所示:

清单 7. 添加 Mongolian insert 逻辑
app.put('/', function(req, res) {
  var deal = req.body.deal_description;
  var tags = req.body.all_tags;

  db.collection("deals").insert({
     deal: deal,
     deal_tags: tags.split(",")
  })
	
  res.contentType('json');
  res.send(JSON.stringify({ status:"success" }));
});

仔细看,您会发现 insert 语句和 MongoDB shell 中的 insert 一样。这不是巧合,MongoDB 的 shell 也使用了 JavaScript!因此,我们能够轻松地持久化拥有两个字段的文档:dealdeal_tags。请注意,我们通过对 tags 字符串使用 split 方法将 deal_tags 设置为数组。

可以测试吗?(当然可以!)

如果您想要测试一下(有谁不想呢?),那么重启实例,再发送一个 JSON 文档,然后再检查 MongoDB 中的 deals 集合。您应该会看到一个与您发送的 JSON 文档几乎一模一样的文档。

清单 8. 添加 Mongolian insert 逻辑
{
  deal:"free food at Freddie Fingers",
  deal_tags:["free", "burgers", "fries"],
  _id:"4e73ff3a41258b7423000001"
}

结束语(成功!)

如果您以为我偷懒,这么快就结束这篇 Node.js 简介,那么我要告诉您:确实结束了!虽然只写了 20 多行代码,但已经足以构成一个完整、稳定的应用程序,这就是 Node 的美妙之处。编写和理解代码都非常容易,而且异步回调函数使其功能非常强大。应用程序编写完成后,就可以部署在任意个 PaaS 提供程序上,从而达到最大的可扩展性。

参见 参考资料 一节了解本文中所讨论技术的更多信息,包括 Node.js、 MongoDB 和 PaaS 选项,如 Google App Engine、Amazon 的 Elastic Beanstalk 和 Heroku。

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

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, Web development
ArticleID=783265
ArticleTitle=面向 Java 开发人员的 Node.js
publish-date=12282011