Ruby 作为企业粘合剂
动态语言,以前被称为脚本或粘合语言,一直是每个严肃的企业开发人员工具箱中的宝贵工具。过去,大量的程序员使用 Perl、Python、Tcl 等来集成不同的数据库、消息队列、LDAP 存储库、Web 服务等等。但是,现在出现了一个名为 Ruby 的新秀。在本文中,我将展示如何比使用当今任何其他编程语言更快、更优雅地解决常见的企业集成问题。
为了使事情更具体,让我们解决一个典型的现实世界问题。一家移动电信服务提供商希望提供一种基于用户地理位置的新资费。当人们在家址 500 米半径范围内使用手机时,他们支付较低的费用。
为了满足这一要求,开发计费应用程序的团队需要一个新的 HTTP 服务。该服务获取客户 ID,并应以 XML 格式返回属于客户地址的坐标。我们公司已经拥有一个客户数据库,并且可以访问 SOAP 定位服务。目标系统架构如图 1 所示,我们的任务是构建新的 HTTP 服务。
逐步构建它,首先我们修改客户数据库并为其构建一个访问层。然后,我们为定位服务实现一个绑定,最后,我们将所有这些隐藏在一个友好的 HTTP 接口后面。正如您可能从文章标题中猜到的那样,我们使用 Ruby 来完成所有这些工作。
客户存储在名为 customers 的 MySQL 数据库中。它基本上只包含两个表:customer 和 address(清单 1)。customer 表中的每个条目都引用 address 表中的一个条目,反之亦然。这两个表都有一个名为 id 的主键,该主键由数据库自动生成。
清单 1. create_db.sql
drop table if exists customer;
create table customer(
id int unsigned not null auto_increment,
forename varchar(64) not null,
surname varchar(64) not null,
created_on timestamp not null,
primary key(id)
);
drop table if exists address;
create table address(
id int unsigned not null auto_increment,
customer_id int unsigned not null,
street varchar(64),
house_number varchar(16),
postal_code varchar(16),
city varchar(64),
state char(2),
primary key(id),
foreign key (customer_id) references customer(id)
on delete cascade
);
因为我们必须存储每个地址的坐标,所以我们添加了一个名为 locations 的新表(清单 2)。它包含属于每个地址的经度和纬度。
清单 2. add_location.sql
drop table if exists locations;
create table locations(
id int unsigned not null auto_increment,
address_id int unsigned not null,
longitude double not null,
latitude double not null,
primary key(id),
foreign key (address_id) references address(id)
on delete cascade
);
或者,我们可以将经度和纬度列添加到 address 表中,但我们的解决方案侵入性较小。可能有些应用程序执行 SQL 语句,例如 select * from address,我们不想破坏它们。
这就是我们目前在数据库端必须做的全部工作。现在,我们将了解如何在 Ruby 程序中访问这些表。
访问关系数据库有很多方法。例如,您可以使用数据库的本机接口或抽象层(如 DBI)。但在像 Ruby 这样的面向对象语言中,对象关系映射器 (ORM) 是迄今为止最方便的工具。ORM 将数据库表中的行映射到对象,反之亦然,而无需使用任何 SQL 语句。
ActiveRecord 是 Ruby 最先进的 ORM,并实现了 Martin Fowler 的企业应用程序模式之一(参见在线资源)。他将其定义如下:“[活动记录是] 一个对象,它包装数据库表或视图中的一行,封装数据库访问并在该数据上添加域逻辑。” 简而言之,活动记录是一个类,它为数据库表中的单行提供典型的 CRUD 方法(创建、检索、更新、删除)。
ActiveRecord 是著名的 Ruby on Rails 项目的一部分,但它完全独立于其余部分,可以单独获取和安装。我们将使用它将我们的三个表映射到类。
如果您以前使用过 ORM,您现在可能会期待一些枯燥的配置文件。我们如何将表映射到 Ruby 类?我们如何将列映射到属性?不要害怕!您不需要所有这些,因为 ActiveRecord 偏爱约定优于配置。清单 3 中的简短程序是您将 Customer、Address 和 Location 类映射到相应表所需的一切。
清单 3. database.rb
require 'rubygems' require 'active_record' class Customer < ActiveRecord::Base set_table_name 'customer' has_one :address end class Address < ActiveRecord::Base set_table_name 'address' belongs_to :customer has_one :location end class Location < ActiveRecord::Base belongs_to :address end ActiveRecord::Base.establish_connection( :adapter => 'mysql', :database => 'customers' )
您不必做很多事情。从 ActiveRecord::Base 派生每个类,您就可以免费获得每个列的访问器。这些访问器具有与相应列相同的名称。例如,Customer 类具有名为 id、forename、surname 和 created_on 的访问器。
默认情况下,ActiveRecord 将类映射到具有相同名称(复数形式)的表。名为 User 的类映射到 users 表,名为 Location 的类映射到 locations 表。当您使用遗留数据库时,您无法自己选择表名。在这种情况下,使用 set_table_name 方法指定表名,就像我们对两个遗留表所做的那样。
每个表都必须有一个数字主键 id,该主键由数据库自动填充。您可以使用 set_primary_key 方法更改主键的名称,但如果您的遗留表包含跨越多个列的复杂主键,则 ActiveRecord 可能不是您工作的正确工具。当您遵守其约定(惯例)时,ActiveRecord 确实会发光发热。
使用 belongs_to、has_one、has_many 和 has_and_belongs_to_many 来声明不同类之间的关系。命名对于指定关系也很重要。请注意我们用于外键的命名方案。例如,在 address 表中,外键名为 customer_id。按照宽松的约定,许多开发人员通过将 _id 附加到键引用的表名来构建外键列的名称。如果您也这样做,则无需再做任何事情。
在清单 3 的最后几行中,我们建立了与 MySQL 数据库的连接。如果需要,您可以传递 :host、:username 和 :password 选项。
清单 4 显示了如何将新的客户和地址插入到数据库中。这一切都非常直观,我们只需要澄清一些细节。在第 7 行中,我们将一个客户存储在数据库中。save 方法会自动创建一个新的客户 ID。我们在第 10 行中使用此 ID 将地址与客户关联起来。ActiveRecord 会自动为从属表创建访问器,也就是说,Customer 类的所有实例都有一个 address 属性,该属性引用 address 表中的相应条目。还有什么比这更简单的呢?
清单 4. create_customer.rb
require 'database' customer = Customer.new( :forename => 'Homer', :surname => 'Simpson' ) customer.save address = Address.new( :customer_id => customer.id, :street => 'Main Street', :house_number => '42', :postal_code => '75244', :city => 'Dallas', :state => 'TX' ) address.save
我们可以使用以下语句之一找到我们的新客户
customer = Customer.find(1)
customer = Customer.find_by_forename('Homer')
customer = Customer.find_by_surname('Simpson)
ActiveRecord 动态创建了大量有用的 find 方法。例如,Address.find(:all) 迭代 address 表中的所有条目。此外,您可以搜索列值的任意组合,即有诸如 find_by_forename_and_surname 之类的方法。
您不必再为 LEFT OUTER JOIN 子句等而烦恼了。ActiveRecord 隐藏了所有这些令人讨厌的东西,它甚至还有更多有用的功能,例如单表继承和验证。它已被移植到当今几乎所有可用的数据库系统,并且一个庞大的社区正在不断增强它。
现在我们知道如何将属于客户地址的坐标存储在数据库中。接下来要做的是计算这些坐标。通常,这将是一个难题,需要数字地图。幸运的是,我们可以将这项工作委托给 SOAP 定位服务。
SOAP 是 W3C 标准化的远程过程调用 (RPC) 协议。它允许您在远程主机上创建和使用对象,就好像它们是您本地进程的一部分一样。方法调用及其参数被转换为 XML 文档,并通过网络层发送。在接收过程中,它们再次被转换回方法调用。返回值和异常也表示为 XML 文档,并被发送回调用进程。虽然 SOAP 独立于传输层,但大多数应用程序使用 HTTP 或 HTTPS。
幸运的是,您通常不必知道所有这些细枝末节的细节就可以使用 SOAP 服务。知道您可以在网络上的哪里找到它、它支持哪些方法以及它使用什么传输层就足够了。为此,您通常会使用 Web 服务描述语言 (WSDL)。定位服务的接口在清单 5 中描述。即使您不熟悉 WSDL,您也应该可以轻松找到 LocalizationService 服务的 locate 函数的定义。它接受一个地址(街道、门牌号、邮政编码、城市和州),并返回一个包含其经度和纬度的双元素数组。
清单 5. loc_service.wsdl
<definitions
name="LocServiceImplementationDescription"
targetNamespace="example.com/wsdl/loc_service.wsdl"
xmlns="http://schemas.xmlsoap.org/wsdl/"
>
<message name="locate_in">
<part name="street" type="xsd:string"/>
<part name="house_number" type="xsd:string"/>
<part name="postal_code" type="xsd:string"/>
<part name="city" type="xsd:string"/>
<part name="state" type="xsd:string"/>
</message>
<message name="locate_out">
<part name="longitude" type="xsd:double"/>
<part name="latitude" type="xsd:double"/>
</message>
<portType name="LocServiceInterface">
<operation name="locate">
<input message="tns:locate_in"/>
<output message="tns:locate_out"/>
</operation>
</portType>
<binding
name="LocServiceBinding"
type="tns:LocServiceInterface">
<soap:binding style="rpc"/>
<operation name="locate">
<soap:operation soapAction="locate"/>
<input>
<soap:body namespace="urn:LocService"/>
</input>
<output>
<soap:body
namespace="urn:LocService"
use="encoded"/>
</output>
</operation>
</binding>
<service name="LocalizationService">
<documentation>
Calculates coordinates of a given address.
</documentation>
<port
binding="tns:LocServiceBinding"
name="LocServicePort">
<soap:address
location="https://:2000"/>
</port>
</service>
</definitions>
由于 SOAP4R 库(参见资源),Ruby 对 SOAP 具有出色的支持。它实现了 SOAP 规范的 1.1 版本,并且易于使用。如果您以前使用过 SOAP,您可能知道如何处理 WSDL 文件。通常,您会使用它来为您将要实现的 SOAP 服务器或客户端创建骨架代码。SOAP4R 附带一个名为 wsdl2ruby.rb 的工具,该工具将 WSDL 文件转换为 Ruby 代码。它可以创建代码,既可以用于访问具有文件中描述的接口的服务,也可以用于创建实现该接口的服务器。
我们需要一个使用定位服务的客户端,我们可以使用 wsdl2ruby.rb 从 WSDL 文件生成完整的代码。但在像 Ruby 这样的动态语言中,我们不需要这个中间步骤。从 WSDL 文件动态派生客户端更容易。清单 6 演示了如何执行此操作。
清单 6. loc_service.rb
require 'soap/wsdlDriver'
include SOAP
class LocalizationService
def initialize(wsdl_file)
factory = WSDLDriverFactory.new(wsdl_file)
@loc_service = factory.create_rpc_driver
end
def locate(address)
@loc_service.locate(
address.street,
address.house_number,
address.postal_code,
address.city,
address.state
)
end
end
initialize 方法期望一个 WSDL 文件,并从中创建一个驱动程序工厂。此工厂为 WSDL 文件中指定的每个服务绑定创建一个驱动程序(代理的同义词)。我们选择 RPC 驱动程序并将实例变量 @loc_service 视为好像它是 LocalizationService 类的本地对象一样。在 locate 方法中,我们只是将工作委托给定位服务。
您需要运行一个独立的 SOAP 服务器才能使这些示例工作,如清单 7 所示。
清单 7. 独立的 SOAP 服务器
require 'soap/rpc/standaloneServer'
class LocalizationServer < SOAP::RPC::StandaloneServer
def on_init
@log.level = Logger::Severity::DEBUG
add_method(
self,
'locate',
'street',
'house_number',
'postal_code',
'city',
'state'
)
end
def locate(street, house_number, postal_code, city, state)
[97.03, 32.90]
end
end
server = LocalizationServer.new(
'localization', 'urn:LocService', '0.0.0.0', 2000
)
trap(:INT) { server.shutdown }
server.start
在最后一步中,我们构建一个 HTTP 服务器,该服务器以 XML 文档形式返回属于特定地址的坐标。计算坐标需要一些时间,而且定位服务也不是免费的。因此,我们仅在必要时计算坐标,并将它们本地存储在我们的数据库中。
在互联网的早期,您必须使用诸如通用网关接口 (CGI) 之类的标准来创建动态网站。每当客户端请求非静态页面时,Web 服务器都会调用一个外部程序(通常是 Perl 或 bash 脚本)来创建内容。服务器将当前环境(包括客户端的请求参数)传递给它,并将脚本的输出返回给请求客户端。这种方法会导致严重的性能开销,因为脚本必须作为单独的进程启动。
CGI 程序有更多缺点。首先,它们不能轻易维护状态,因为它们在完成工作后立即关闭。其次,它们通常是安全问题,因为它们在或多或少不受控制的环境中运行。
随着 Java 的出现,一种替代技术变得相当流行——Servlet。Servlet 是由所谓的 Servlet 容器执行的小代码片段。它们仅被加载到内存中一次,并且可以根据需要多次重用。这极大地提高了性能,并允许开发人员管理 Servlet 中的状态信息。最终,Servlet 容器控制 Servlet 的环境,并可以阻止它们执行不需要的操作,例如删除文件。
Ruby 附带 WEBrick(参见资源),这是一个用于创建 HTTP 服务器的出色框架。它允许您遵循或多或少过时的 CGI 方法,但它强烈鼓励使用 Ruby Servlet。在清单 8 中,您可以看到一个 Servlet,它实现了我们服务的主要逻辑。
清单 8. servlet.rb
require 'rubygems'
require 'builder'
require 'database'
require 'loc_service'
require 'webrick'
include WEBrick
include WEBrick::HTTPServlet
class LocalizationServlet < AbstractServlet
def initialize(server, wsdl)
super(server)
@loc_service = LocalizationService.new(wsdl)
end
def do_GET(req, res)
customer_id = req.query['customer_id']
customer = Customer.find(customer_id)
address = customer.address
if address.location.nil?
lon, lat = @loc_service.locate(address)
address.location = Location.new(
:longitude => lon, :latitude => lat
)
customer.save
end
res['content-type'] = 'text/xml'
res.body = to_xml(address.location)
res.status = 200
end
def to_xml(location)
xml = ''
doc = Builder::XmlMarkup.new(
:target => xml, :indent => 2
)
doc.position(
:longitude => location.longitude,
:latitude => location.latitude
)
xml
end
end
我们从 AbstractServlet 类派生了我们的 Servlet。每当 WEBrick 服务器收到针对特定 URL 的 GET 请求时,它都会调用 do_GET 方法。相应地,对于其他 HTTP 请求方法,它会调用 do_POST、do_PUT 等。WEBrick 始终将 Request 和 Response 对象传递给它调用的方法。Request 对象包含客户端发送的所有查询参数和标头。您的任务是用正文和所有应发送回的标头填充 Response 对象。
在我们的例子中,Servlet 逻辑看起来像伪代码规范。我们尝试从数据库中读取具有 ID customer_id 的客户的地理位置。如果我们找不到它,我们将使用定位服务定位客户的地址,并将坐标存储在数据库中,这样我们就不必再次定位它。接下来,我们将坐标转换为 XML 文档。在方法的末尾,我们设置内容类型、HTTP 状态代码和正文。
您不必为 Servlet 定义 initialize 方法,但如果您这样做,它始终会将服务器实例作为其第一个参数。在我们的例子中,我们还期望 WSDL 的名称用于初始化定位服务。
to_xml 方法将位置转换为 XML 文档。开发人员经常使用原始字符串来创建 XML 文档。我建议永远不要这样做,即使对于看似微不足道的文档也是如此。创建 XML 文档从来没有看起来那么容易,因为您必须关心诸如良好格式和字符集编码之类的复杂概念。因此,我们使用 Jim Weirich 的 XmlBuilder 类(参见资源)来创建结果文档。
现在我们有一个 Servlet,它实现了我们的主要逻辑,但仅靠 Servlet 是不够的。我们仍然必须创建一个 HTTP 服务器来控制它。清单 9 是我们所需要的一切。我们指定服务器正在侦听的端口,并将我们的 LocalizationServlet 映射到路径 /。此外,我们使服务器在收到 SIGINT 或 SIGTERM 信号时终止。
清单 9. server.rb
require 'servlet'
server = HTTPServer.new(:Port => 4242)
server.mount(
'/', LocalizationServlet, 'loc_service.wsdl'
)
trap('INT') { server.shutdown }
trap('TERM') { server.shutdown }
server.start
现在是进行最终测试运行的时候了。将您最喜欢的浏览器指向 https://:4242/?customer_id=1 或使用诸如 wget 或 curl 之类的命令行工具来测试我们新创建的服务
mschmidt:/tmp $ curl https://:4242/?customer_id=1 <position longitude="97.03" latitude="32.9"/> mschmidt:/tmp $
这正是我们期望的结果。我们完成了!
毫无疑问,在企业集成方面,Ruby 已经为黄金时段做好了准备。即使在本文中,我们也能涵盖一些最重要的企业技术,例如关系数据库、SOAP 和 HTTP。您还可以轻松地集成现有的 Java 代码、访问 LDAP 服务器、创建 XML-RPC 服务或操作 XML 文档。
在许多方面,Ruby 无法与 J2EE 或 .NET 等平台竞争,但它不必如此,也不想如此。它的优势在于灵活性、可维护性和开发速度。虽然与其他动态语言相比,Ruby 平台可能不是最大的,但它很可能是一个增长最快的平台。而且,最重要的是,它非常有趣!
本文的资源: /article/9018。
Maik Schmidt 担任软件开发人员已有十多年。他靠为中型企业创建复杂的解决方案为生。除了日常工作外,他还为计算机科学杂志撰写书评和文章,并为开源项目贡献代码。最近,他撰写了他的第一本书,名为 使用 Ruby 进行企业集成。

