构建一个集成了 Web 服务的可移植 Java 旅行应用程序

为了演示在云中构建和部署应用程序所带来的高级互操作性与可移植性,我决定构建一个有趣的小小旅行应用程序。

我的应用程序由两部分组成,利用保存在用户配置文件中的用户偏好设置来展现一幅地图,其中会显示一片区域内有哪些空闲的酒店房间。第一部分通过使用 MongoDB 服务来管理用户偏好,并将结果服务通过一个 API 向外部公开。第二部分将第一部分与外部服务集成在一个 Web 应用程序中。

云应用程序必须是可扩展和可移植的,并能够与内部服务轻松集成。它在整个生命周期内的使用和管理绝不能令人厌烦。

Note:

  • 单击 Run the app 后,您可以使用任意 ID 和密码登录。
  • 为了编写本次练习的代码,单击 Get the code at JazzHub 后,单击右上角的 EDIT CODE 按钮(如果尚未登录,请输入您的 JazzHub 证书),然后单击菜单上的 FORK 按钮创建一个新项目。另外还可以导出代码,具体操作是在左侧导航中选择 File > Export

构建一个类似的应用程序的前提条件

步骤 1. 创建云应用程序

我是在 Bluemix 上创建和部署这个示例应用程序的。对于这个示例,我选择使用 Java 和 Spring 框架。要创建应用程序,请访问 Bluemix 并选择应用程序的类型(Java 单机、Java Web、Ruby 等),在我们的示例中使用的是 Java Web。

图 1. 使用 Bluemix 站点创建一个应用程序的屏幕截图
使用 Bluemix 站点创建一个应用程序的屏幕截图

步骤 2. 安装和使用命令行工具

可以使用 Bluemix Web 界面或 Cloud Foundry 项目提供的命令行界面管理应用程序。我为这个示例选择了命令行界面 cf。使用命令行可以部署、关联服务,控制(启动和停止应用程序)等等。从 GitHub 下载 CLI,并运行安装程序。安装结果是一个可执行文件:cf.exe。首先必须设置目标 API 端点,然后登录。

图 2. 登录到 API 端点的屏幕截图
显示了如何登录到 API 端点的屏幕截图

现在可以列出应用程序、服务和绑定服务。


步骤 3. 准备开发环境

这个示例使用了 MVC Spring 框架。所使用的环境是 Spring Tool Suite 和 Cloud Foundry 插件。所使用的工具和技术包括:

  1. Spring 3.1.1
  2. JDK 7
  3. Spring Tool Suite 3.4.0+ Cloud Foundry Integration for Eclipse 1.5.1

创建 Java Web Project 时,需要在 Cloud Foundry 平台上进行部署,因此必须给项目添加 Cloud Foundry 性质。这样做可以创建描述应用程序的 manifest.yml 文件和它对 Cloud Foundry 运行时的资源需求。

我们将开发应用程序的两个部分:

  • 第一部分是 UserService。它公开用于管理用户信息的 API。它使用内部的云平台 MongoDB 实现数据持久化,这是一个流行的 。
  • 第二部分是 MyVacations,它允许登录用户使用一些参数搜索可用的酒店。UserService 应用程序提供一些搜索参数的值。Expedia Services 提供酒店的列表与详细信息。 在地图上定位酒店的列表。
图 3. 部署模型
样例中描述的应用程序的简要部署模型

UserService 显示了如何使用 MongoDB 服务将用户相关信息保存在一个集中的位置。

UserService 的功能包括:

  1. 登录功能 — UserService 接收用户名与密码,然后在数据库中搜索用户。如果找到了用户,UserService 会返回它;否则,它会创建一条新纪录。
  2. 配置功能 — UserService 接收用户名并搜索用户信息(偏好位置、成人数量和小孩数量),并将这些信息返回给客户端。

步骤 4. 绑定一个云服务 (MongoDB)

要使用 MongoDB 服务,必须先创建一个服务实例:

  1. 连接到 Bluemix 并在操作视图中选择 Add a Service
    图 4. Bluemix dashboard of applications
    应用程序的 Bluemix 仪表板
  2. 从可用服务列表中选择 MongoDB,然后创建服务实例。
    图 5. 创建用于 UserService 应用程序的 MongoDB 实例服务
    创建用于 UserService 应用程序的 MongoDB 实例服务

另外还可以使用命令行:

cf create-service mongodb 100 mongodb_ser1
(USAGE:    cf create-service SERVICE PLAN SERVICE_INSTANCE)

cf bind-service UserService mongodb_ser1
(USAGE:    cf bind-service APP SERVICE_INSTANCE)

现在,MongoDB 服务已准备就绪并被绑定到 UserService。

步骤 5. 在应用程序中使用 MongoDB 服务

创建服务并将它关联到应用程序之后,将它的配置作为一个只读环境变量添加到 VCAP_SERVICES,其中包含您在代码中连接服务时需要使用的信息。在这个例子中,内容如下:

{
  "mongodb-2.2": [
      {
         "name": "mongodb-ser1",
         "label": "mongodb-2.2",
         "plan": "100",
         "credentials": {
            "hostname": "10.0.116.106",
            "host": "10.0.116.106",
            "port": 10192,
            "username": "46c538a6-e6e1-4d02-8132-77b0a4b2dc1c",
            "password": "0ceea0ea-5548-46ad-9b09-1002683aeca7",
            "name": "946dc87b-b455-4d12-b977-1b1ee22f1ade",
            "db": "db",
            "url": "mongodb://46c538a6-e6e1-4d02-8132-77b0a4b2dc1c:
0ceea0ea-5548-46ad-9b09-1002683aeca7@10.0.116.106:10192/db"
         }
      }
   ]
}

要使用 VCAP_SERVICES 变量连接到 MongoDB 服务实例,提取 "url" JsonNode。请注意,该 URL 包含连接数据库需要的所有参数(用户证书、主机名、端口和 DB 名称)。

private static String getUrlConnection() {
			
	String env = System.getenv("VCAP_SERVICES");
	ObjectMapper mapper = new ObjectMapper();
	try {
		JsonNode node = mapper.readTree(env);
		Iterator<JsonNode> dbNode = node.get("mongodb-2.2").getElements();
			
		JsonNode cred =  (JsonNode)dbNode.next().get("credentials");
			
		String uri =cred.get("url").getTextValue();
			
		logger.debug ("url db: " + uri);
			
		return uri;
			
	} catch (JsonGenerationException e) {
		logger.debug(e.getMessage());
	} catch (JsonMappingException e) {
		logger.debug (e.getMessage());
	} catch (IOException e) {
		logger.debug (e.getMessage());
	}
			
	return null;
}

可以使用 MongoDB Java 驱动程序创建一个 Spring 配置类,用于创建 DB 连接,然后将 DB 对象返回给客户端。

@Configuration
public class MongoConfiguration {

	public @Bean DB mongoDb() throws Exception {
		MongoClientURI mcUri = new MongoClientURI(getUrlConnection());
		MongoClient mc = new MongoClient(mcUri);
		mc.getDB(mcUri.getDatabase());
		return mc.getDB(mcUri.getDatabase());
	}

UserManager 类使用 MongoConfiguration 与 DB 进行交互。init 方法获取 "users" 集合,或者在该集合不存在时创建一个集合。

private void init(){		
		ApplicationContext ctx = 
	    new AnnotationConfigApplicationContext(MongoConfiguration.class);
		db = (DB) ctx.getBean("mongoDb");
		coll = db.getCollection("users");
	}

MongoDB 并非关系 DBMS,而是面向文档的。这意味着我们拥有的不是表而是连接,不是行(或值组)而是文档,是字段而不是列。这些字段不是预先定义好的,这点和表中的列一样。您可以在一个集合中输入任意类型的数据。要找到文档必须创建 BasicDBObject

	BasicDBObject user = new BasicDBObject("username", userData.getUsername());
	
	return (DBObject)coll.findOne(user);

UserController 是 UserManager 的客户端, 它使用它的函数去获取和保存已登录的用户信息。

	@RequestMapping(value="/user", method = RequestMethod.GET)
	public  @ResponseBody String getUser(@RequestParam("username") String username, 
@RequestParam("password") String password)  {
		logger.debug("BEGIN: controller getUser - username:" + username);
		UserManager userManager = new UserManager();
		BasicDBObject user = (BasicDBObject) userManager.getUser(username, password);		
		logger.debug("END: controller getUser - user:" + user);

		return user.toString();
	}

步骤 6. 部署一个 Spring 应用程序

MyVacations 通过 UserService 获取用户信息,并使用 UserService 在用户上次登录期间保存的值配置搜索。用户可以看到酒店清单作为搜索结果出现在地图上。同样在这个例子中,控制器并非配置在 xml 配置文件中,而是由 Spring 框架动态监测到,因为 servlet 配置文件中包含这条指令。

     <context:component-scan base-package="com.myvacations.app" />

The first controller, HomeController, is called to display the login page.

	/**
	 * Simply selects the home view to render login page.
	 */
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {
		
		return "login";
	}

HotelsController 在登录页面提交时激活。

此控制器通过一个带有 "username" 参数的 HTTP get 请求进行调用,访问 UserService 以获取在此用户上次访问期间保存的所有用户偏好信息。

此控制器使用 RestTemplate 胡对 UserService 进行 RESTful 调用。RestTemplate 是一个 Spring 帮助器类,用于客户端的 HTTP 访问。getForObject() 方法负责接收和返回该对象,而 HttpMessageConverters 负责将它转化为 HTTP 请求与 HTTP 响应。在我们的例子中,这个类用于调用 RESTful 服务,比如 UserService。

@RequestMapping(value = "/hotels", method = RequestMethod.GET)
public String getHotels(@ModelAttribute("username") String username,Model model) {
		
	logger.debug("BEGIN HotelsController: username=" + username);

	RestTemplate restTemplate = applicationContext.getBean("restTemplate", 
	RestTemplate.class);

	user = (UserData) restTemplate.getForObject(new 
	URI("http://userservice.ng.Bluemix.net/userpref?username="+username), 
	UserData.class);

步骤 7. 集成服务

您需要集成服务以将更多的数据带给 MyVacations。我将会提供两个集成服务的例子:

  • Expedia RESTful 服务生成酒店的相关信息。
  • Google 地图服务将酒店形象地显示在地图上。

让我们首先集成 Expedia Web 服务,以获取酒店的相关信息。首先必须在 Expedia API Developer 上注册一个开发人员帐号和一个 API 密钥。

单击 Submit 按钮将激活 SearchController,它是 Hotels.jsp 中的一个 Ajax 调用。

    $.get('/search',
      $("#ricerca").serialize(),
      function (data, status) {
        if (status == 'success') {                	
            if (data.toString()==""){
                $("#map_canvas").hide();

SearchController 中调用了 ExpediaClient 来获取 HotelSummary List。

	List<HotelSummary> response=null;
	response = (new ExpediaClient()).getHotels(
			location, dateFrom, dateTo, numAdults, numChildren);

ExpediaClient 使用 UserService 获得的用户信息,提取出在 JSON 响应中解码的 ExpediaObjects。

ExpediaObjects hotels= (ExpediaObjects) 
restTemplate.getForObject(buildQueryHotelsURL(location,dal,al,numAdulti,numBambini),
ExpediaObjects.class);

示例接着使用 Google 地图服务在地图上以可视化方式显示从 Expedia 获得的酒店列表。

Google Maps API 支持在 Web 页面上嵌入一幅 Google 地图图像。在开始之前,您需要 Google 提供的一个特殊 API 密钥。该密钥是免费的,但您必须创建一个 Google 帐号。

<script src="http://maps.googleapis.com/maps/api/js?
key=YOUR_API_KEY&sensor=TRUE_OR_FALSE"></script>

为了与 Google Maps API 进行交互,我选择使用 jQuery-ui-map。这是一个优秀的 jQuery 插件,用于在 Web 和移动应用程序中嵌入地图。它支持用户查看地图和标签,并利用踪迹的高级服务与管理、街景模式视图和 JSON 中地理数据的动态加载。

当创建另一个 div 或另一个类似的 HTML 容器时,便可运行 gmap(插件的关键方法,允许我们调用 Google Maps API 的函数)在地图上显示标记引用的坐标。

var map =  $('#map_canvas').gmap({
     'center': new google.maps.LatLng(data[0].latitude,data[0].longitude),
     'minZoom': 5,
     'zoom': 8
         });

我们已经创建了一幅地图,它以第一家酒店的地理坐标为中心。

现在,我们为结果列表中的每家酒店都创建一个标记,并把它们放在地图上。

      $.each(data, function (i, m) {
         $('#map_canvas').gmap('addMarker', {
            'position': new google.maps.LatLng(m.latitude,
                            m.longitude),
            'bounds': true
         }) .click(function () {…

注册一个函数 on-click 事件,用于加载对酒店进行简要描述的信息窗口。

  $('#map_canvas').gmap('openInfoWindow', {
                                        'content': descr
                                    },

步骤 8. 将应用程序推送到云中

构造和创建应用程序之后,我们就可以在 Bluemix 上部署它们。部署是自动的,即将应用程序从本地 VM 迁移到基于云 的VM 上。

使用 Cloud Foundry CLI cf push 命令启动部署。(在 Cloud Foundry 文档中,部署过程通常称为 推送应用程序。)

cf push MyVacations –path MYDIR\MyVacations.war

push 命令执行多个分阶段的任务,比如找到一个容器来运行应用程序,为容器提供合适的软件和系统资源,启动应用程序的一个或多个实例,并在 Cloud Controller 数据库中保存应用程序的预期状态。

步骤 9. 测试应用程序

为了达到测试的目的,您需要对应用程序进行一个简单的运行测试,然后测试应用程序是否是可移植的。

为了简单起见,登录页面允许使用任意用户名和密码进行访问,如图用户在系统中不存在,就会被创建。

登录后,访问搜索页面,插入搜索参数并单击 Search

图 6. 带有结果地图的搜索页面
带有结果地图的搜索页面

如果单击某个标记,就会显示与该酒店相关的简要信息。

图 7. 带有关于旅店的简要信息的窗口
带有关于旅店的简要信息的窗口

为了在不同的云平台上测试可移植性,我选择在 Pivotal 平台和 Google App Engine 上部署 MyVacations。

要在 GoogleAE 上进行部署,所使用的工具和技术包括:

  1. Google App Engine Java SDK 1.8.8
  2. Spring 3.1.1
  3. Eclipse 4.2+ Google plugin for Eclipse

因为 Google App Engine 支持基于 Spring 框架的 Java Web 应用程序,我的应用程序不需要做任何改动。

Google App Engine SDK (安装在 Eclipse 上) 包含一个 Web 服务器,用于在模拟的本地环境中测试应用程序,这样便可在没有 Google 用户帐号的情况下测试应用程序。(还可以在远程的 Google 服务器上运行应用程序)。

用于 Eclipse 的 Google 插件在 Run 菜单中添加了启动这台服务器的选项。在这种场景中,安装在 Google App Engine 上的 MyVacations 将通过其 RESTful API 调用安装在 Bluemix 上的 UserService 应用程序,这演示了不使用内部平台服务的应用程序的高级可移植性。

图 8. 本地主机上的 Google App Engine
>本地主机上的 Google App Engine

在 Pivotal 上进行部署使用的工具和技术包括:

  1. Spring 3.1.1
  2. JDK 7
  3. Spring Tool Suite 3.4.0+ Cloud Foundry Integration for Eclipse 1.5.1

Cloud Foundry plugin for Eclipse 支持在 Pivotal 平台上进行部署。这样便可在目标环境上直接测试应用程序,无需离开 IDE。您需要一个有效的 Pivotal 用户帐号。

图 9. Pivotal Cloud Foundry 上的部署
Pivotal Cloud Foundry 上的部署

在这个平台上,您无需进行任何改动便可部署应用程序,也可以进行小小改动后部署 UserService 应用程序。

结束语

这个应用程序仅仅显示了将内部和外部服务与云应用程序集成在一起的一些可能性。我利用了 Bluemix 具有的一些优点:

  • 提供需求降低(应用程序或基础架构)
  • 可伸缩性
  • 轻松集成内部服务
  • 管理轻松
  • 在类似云平台上的可移植性

在可移植性问题上,因为 Bluemix 基于 Cloud Foundry,您可以自由地将它迁移到其他平台。让我们通过两个例子演示一下这种可移植性:

  • 将 MyVacations 部署到 Pivotal 云时无需任何改动 Pivotal 同样基于 Cloud Foundry,因此几乎可以完全兼容。UserService 使用 Bluemix 平台上的 MongoDB 服务;在 Pivotal 上也有一个类似的 MongoDB 服务,但您必须修改来自 MongoConfiguration 类中 VCAP_SERVICES 变量的 URL 连接。
  • 对于不是基于 Cloud Foundry 的云而言(这个例子中就是 Google 的云),部署 MyVacations 也很简单。(MongoDB 不在 Google 平台提供的服务中,因此我选择了另一个解决方案作为 "Big Table" 服务(和 MongoDB 不同),它是一个用于数据持久的专门解决方案。)只要将 appengine-web.xmlfile 添加到 web-inf 目录便可启用 Google Application Engine,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
      <application>_your_app_id_</application>
      <version>1</version>
      <threadsafe>true</threadsafe>
    </appengine-web-app>

最后要注意的是,您必须将云应用程序设计为能够利用分布式平台的潜力,同时提供可靠、高效与快速的服务。我认为我的 MyVacations 应用程序大体上做到了这一点。当然,在 UserService 遇到大量请求时,可能导致用户响应速度较慢。但您可以尝试针对这一点进行性能提升(比如使用异步的消息收发对组件进行去耦,这样任务就不会在收到响应之前一直阻塞)。适用这类应用程序的性能提升手段有好几种,您有时间可以好好地研究一下它们。

致谢

参考资料

学习

讨论

  • 加入 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, Cloud computing, Web development
ArticleID=968990
ArticleTitle=构建一个集成了 Web 服务的可移植 Java 旅行应用程序
publish-date=04242014