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 进行企业集成。