使用 XML 实现 REST 式的 SOA
如果公司有大量应用程序,这些程序供不同部门的承担不同责任的职员使用,那么就适合使用面向服务体系结构(Service Oriented Architecture,SOA)。这些应用程序可以共享功能,但是功能的组合、用户界面细节和易用性需求是不同的。与许多企业体系结构一样,SOA 也采用一个多层模型,但是它不只如此。在服务器中,功能分散在单独的服务上。一个客户机可以使用其中的一个或多个服务,而一个服务也可以由许多客户机使用。由此形成了一个松散耦合的体系结构,这大大提高了现有软件的可重用性。
常用缩写词
- API:应用程序编程接口(Application program interface)
- IT:信息技术(Information technology)
- XML:可扩展标记语言(Extensible Markup Language)
SOA 尤其适合大公司,大公司往往有数百个应用程序,应用程序之间缺少良好的集成,所以公司需要清理 IT 基础结构。SOA 是一种已经证明有效的实践,对于大型环境尤其有效。采用 SOA 的公司可以把遗留的应用程序转换为服务,并把服务集成为现代应用程序的后端。可以使用中间件技术对服务进行组合,并对服务中的特定功能进行访问控制。因为在大型环境中对 SOA 的需求最为强烈,所以中间件技术的厂商通常把产品的重点放在大型和重型解决方案上。
SOA 背后的思想对于小公司同样是有价值的。重型解决方案的设置成本和所需的人员技能可能使小公司不敢尝试 SOA — 但这是不应该的。暂且不要考虑重型实现,我们先来考察一下 SOA 的基本概念:
- 从现有的或新的应用程序中提取出服务
- 把服务集中起来,供许多客户机使用
并没有什么因素妨碍我们用轻量型技术实现这些思想。您可以先建立一个小型 SOA 并逐渐扩展它。如果您的公司以后发展成大型跨国公司,随时可以迁移到重型技术。
SOA 通常是用 SOAP 协议实现的,服务由一个 WSDL(Web Services Description Language, Web 服务描述语言)文档来描述。尽管有许多开发工具大大简化了对 SOAP 和 WSDL 的处理过程,但是我仍然把它们看作重型技术,因为如果不使用这些工具,SOAP 和 WSDL 是很难处理的。
也可以通过超文本传输协议(HTTP)发送简单的消息来实现 SOA。这基本上就是 REST 式 Web 服务 (RESTful Web services) 的工作方式。Representational State Transfer(简称 REST,中文翻译“具象状态传输”。REST 这个名称是由 Roy Fielding 首创的)并不是一个协议或技术;它是一种体系结构风格。REST 是 SOAP 的轻量型替代品,它是面向资源的,而不是面向操作的。它常常被归结为远程过程使用 HTTP 调用 GET
、POST
、PUT
和 DELETE
语句。我认为,这只是第二个重要的步骤。
第一个(也是最重要的)步骤是把所有资源建模为 URL 形式。URL 容易记忆,同时能够访问无数 Web 页面。至少,如果建模方式适当的话,很容易记住 URL(比如 http://www.ibm.com/developerworks/xml/)。如果过分重视 GET
、POST
、PUT
和 DELETE
,就可能产生不容易记忆的 URL,比如 http://www.longfakeurl.com/pol_srdm/70612/9,3993.32?id=78688&lang=cz&st=idx。
在实践中,使用 HTTP 的方法可以进一步限制为 GET
和 POST
两种方法,因为大多数浏览器对它们的支持很完善。可以对 http://domain.com/myresources/new 执行 POST
,以替代对 http://domain.com/myresources 执行 PUT
;对 http://domain.com/myresources/oldresource/delete 执行 POST
,以替代对 http://domain.com/myresources/oldresource 执行DELETE
。
在设计 REST 式 Web 服务时,可以采用以下四个步骤:
- 决定资源及其描述性 URL。
- 为每个 URL 上的通信选择一种数据格式。
- 指定每个资源上的方法。
- 指定返回的数据和状态码。
以下是具体的设计过程。假设您是一家航空公司的开发人员。公司有用于预订航班的软件,还有处理付款(现金和信用卡)的组件。它使用软件跟踪包裹、执行内部资源规划和执行许多其他任务。
假设机场登记处的职员使用一个客户机应用程序,这个程序访问包裹跟踪服务,还使用一个服务为乘客分配座位。处理包裹的地勤人员只需要包裹跟踪服务,不需要其他服务。他们的客户机只允许他们确认已经登记的包裹是否到达了。不允许他们登记新的包裹。
在这个示例中,我们将设计包裹跟踪服务。首先,决定资源:旅行者、航班和包裹(注意,在出现 {id}
的任何地方,都可以填写任意数字):
http://luggagetracking.airlinecompany.com/bags/{id} http://luggagetracking.airlinecompany.com/flights/{id} http://luggagetracking.airlinecompany.com/travellers/{id} |
为每个资源选择一种数据格式:
包裹:
<bag id="{id}"> <traveller id="{traveller-id}"/> <flight id="{flight-id}" /> <status>{current-status: departure/plane/arrival}</status> </bag> |
航班:
<flight id="{id}"> <travellers> <traveller id="{traveller-id-0}" /> <traveller id="{traveller-id-1}" /> <traveller id="{traveller-id-2}" /> </travellers> <bags> <bag id="{bag-id-0}" /> <bag id="{bag-id-1}" /> <bag id="{bag-id-2}" /> </bags> </flight> |
乘客:
<traveller id="{id}"> <flight id="{flight-id}" /> <bags> <bag id="{bag-id-0}" /> <bag id="{bag-id-1}" /> <bag id="{bag-id-2}" /> </bags> </traveller> |
显然,这个模型过于简单了。对于当前的示例,只需要支持两个方法,因此这个模型已经足够了。登记处应该能够为乘客登记新包裹。在把包裹装进飞机时,地勤人员应该能够修改包裹的状态:
- 对 http://luggagetrackingairlinecompany.com/travellers/{id}/newbag 执行
POST
,返回一个<bag>
XML 结构。 - 对 http://luggagetracking.airlinecompany.com/bags/{id}/status/{newstatus} 执行
POST
,返回修改后的 XML 结构。
使用标准的 HTTP 状态作为状态码。成功的操作都会返回 200。如果系统无法根据资源的 ID 找到它,就会返回 404。系统故障导致的任何错误都会返回 500。
可以使用多种方式把 URL 映射到实现方法。比较先进的方法可能更灵活,应该用在比较大的应用程序中。这个小示例使用最简单的方法:正则表达式。下面是 BagServlet 上的 post 方法示例,它把 URL 参数传递给底层 servlet。可以在本文的下载文件中找到完整的 servlet 代码。注意,这里没有实现实际的底层服务。 以下是该示例:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Pattern pattern = Pattern.compile("^/?.*?/bags/(.*)/status/(.*)$"); Matcher matcher = pattern.matcher(request.getRequestURI()); if(matcher.matches()) { String bagId = matcher.group(1); String newStatus = matcher.group(2); bagService.changeBagStatus(bagId, newStatus); } } |
在调用这个 URL 时,如果成功,就会隐式地返回状态码 200。更有意义的是,代码返回 XML 结构。这个示例使用 XStream API 把 Java™ 对象转换成 XML 结构。这个 API 需要的配置非常少,而且主要根据类中的字段名选择元素名。
这个示例代码使用下面这些简单的类:
航班:
package eu.adraandejonge.restfulsoa; public class Flight { String id; public Flight(String id) { super(); this.id = id; } } |
乘客:
package eu.adraandejonge.restfulsoa; public class Traveller { private String id; public Traveller(String id) { super(); this.id = id; } } |
包裹:
package eu.adraandejonge.restfulsoa; public class Bag { private String id; private Flight flight; private Traveller traveller; private String status; public Bag(String id, Flight flight, Traveller traveller, String status) { super(); this.id = id; this.flight = flight; this.traveller = traveller; this.status = status; } } |
假设底层的 BagService 返回一个包裹,包裹的航班 ID 是 1,乘客 ID 是 1,状态是 new。请考虑下面的 GET
实现:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Pattern pattern = Pattern.compile("^/?.*?/bags/(.*)$"); Matcher matcher = pattern.matcher(request.getRequestURI()); if (matcher.matches()) { String bagId = matcher.group(1); Bag bag = bagService.retrieveBag(bagId); XStream xstream = new XStream(); xstream.alias("bag", Bag.class); xstream.alias("traveller", Traveller.class); xstream.alias("flight", Flight.class); xstream.useAttributeFor(Bag.class, "id"); xstream.useAttributeFor(Traveller.class, "id"); xstream.useAttributeFor(Flight.class, "id"); String xml = xstream.toXML(bag); response.getWriter().write(xml); } } |
在查询这个 URL 时,它会返回以下信息:
<bag id="1"> <flight id="1"/> <traveller id="1"/> <status>new</status> </bag> |
我选择这些示例代码是为了说明,不需要很多底层通信,URL 也能够实现很多功能。对于其他服务,可能需要处理上传给 REST 服务的 XML 结构。XStream 也可以帮助完成这个任务。例如,要想对包裹的 XML 结构进行去序列化,应该调用:
Bag bag = (Bag) xstream.fromXML(xml); |
到目前为止,本文已经讨论了服务器端的实现。客户端上的代码非常相似。客户机可以共享数据类 Flight
、Traveller
和 Bag
,并使用 XStream API 对 XML 进行序列化和去序列化。客户机上惟一的新部分是连接 URL 并读取内容或发送内容。通过使用 Java 类库提供的 URL 连接,很容易完成这个任务:
String xml = "<newinput>input</newinput>"; URL url = new URL("http://luggagetracking.airlinecompany.com/bags/1/newmethod"); URLConnection connection = url.openConnection(); // set POST connection.setDoOutput(true); Writer output = new OutputStreamWriter(connectiongetOutputStream()); output.write(xml); output.close(); // display result BufferedReader input = new BufferedReader( new InputStreamReader(connection.getInputStream())); String decodedString; while ((decodedString = input.readLine()) != null) { System.out.println(decodedString); } input.close(); |
尽管 REST 并没有明确的规范来规定如何实现它,但是对 REST 的开箱即用支持越来越多了。因此,虽然没有需要遵循的标准,但是您需要遵守一些约定。例如,Ruby on Rails 提供 ActiveResource。如果遵守 Rails 对 URL 和输出格式的约定,就很容易用最小的开销把 Rails Web 客户机连接到 Java REST 式 Web 服务。
随着应用程序环境的增长,很可能会对越来越多的 REST 实现细节进行抽象。当增长和抽象发展到一定程度之后,从轻量型技术迁移到重型的 SOA 技术可能会节省成本。这需要把服务背后的实际业务逻辑提取出来,并重新包装在新环境中的一个 SOAP 包中,这个过程应该不是太难。
航空公司只是本文使用的一个示例。实际的航空公司规模都比较大,它们应该直接使用重型技术。如果您为小公司工作,可能需要发挥想像力,寻找到在实践中应用 SOA 和 REST 原则的最佳方式。花些时间考虑这个问题,这会带来长远的回报!