使用多层体系结构构建 REST 风格的 Web 服务和动态 Web 应用程序

继续学习如何使用多层体系结构构建 REST 风格的 Web 服务和动态的 Web 应用程序。本文将手把手指导您设计和构建各层中的组件,并讨论各组件之间的结合关系。它演示了 REST 风格的 Web 服务、Asynchronous JavaScript and XML (Ajax) 和 Spring Web Flow 如何共同生成了一个类似桌面的、快速响应的富 Web 界面。它还演示了 Ruby 脚本等客户机程序如何利用 REST 风格的 Web 服务来向服务器上传和下载用户数据。

Bruce Sun, Java 架构师, IBM  

http://www.ibm.com/developerworks/i/p-bsun.jpgBruce Sun 是 Sun Microsystems 的认证 Java 架构师。他从 1998 年就开始开发基于 Java 的 Web 应用程序。他目前是 National Center for Atmospheric Research (NCAR) 的高级软件工程师。



2009 年 8 月 10 日

简介

在之前的 文章 中,我讨论了构建 REST 风格的 Web 服务和动态 Web 应用程序的多层体系结构。我在表示层中提出了一种资源请求处理程序 (RRH),并提供了一种浏览器请求处理程序 (BRH) 用于处理来自浏览器的请求和生成输出在浏览器中显示。两个处理程序共享一个公共的业务逻辑层,后者又与数据访问层进行交互。在示例应用程序中,应用层是使用 Java™ 代码构建的。本文使用 Jersey 框架来实现 REST 风格的 Web 服务;使用 Spring 框架实现 MVC、导航和 JDBC;使用 MySQL 作为数据库。使用 Eclipse 作为 IDE。示例应用程序将在 Tomcat 中部署。示例应用程序是一个简单的虚拟应用程序,供 National Center for Atmospheric Research (NCAR) 管理员签约 NCAR 员工。


场景

在本场景中,管理员使用浏览器界面签约新 NCAR 员工。在 NCAR,共有四个实验室,每个实验室都有不同的部门:

  • 计算和信息系统实验室
  • 地球和太阳系实验室
  • 地球观察实验室
  • 研究应用程序实验室

应用程序中的签约界面包括以下字段:

  • 用户名
  • 密码
  • 姓(last name)
  • 名(first name)
  • 电子邮件
  • 员工工作的实验室和部门

在这些字段中,实验室和部门字段都是可以选择的菜单,并且用户名必须是唯一的。如果某个用户名已经被使用,而管理员尝试再次输入它,则浏览器会显示一个警告,同时用户名字段将被清空。

部门选项菜单中的项目列表由实验室选项菜单中所选的实验室决定。当界面首次打开时,部门字段将禁用。管理员选择实验室之后,部门选项菜单将启用,但其中仅包含所选实验室的部门。管理员填充信息并单击 Submit 之后,系统会将新用户添加到 MySQL 数据库中,并显示一条成功消息。

系统管理员需要能够运行批处理来上传新用户以及下载已经签约的用户。批处理程序可以使用 Ruby、Pearl 或 Java 代码来实现。我在本文中使用 Ruby 进行演示。浏览器界面或 REST 风格的 Web 服务都不需要身份验证。

注意:虚拟应用程序并不能用于生产。实际应用程序还需要异常处理、登录、身份验证和数据验证等功能。


组件

表 1 列出了各组件以及它们文件夹结构的组织方式。

表 1. 文件夹结构
分层分层和脚本文件位置
客户机层Ajax 脚本WebRoot/js
JSP 页面WebRoot/WEB-INF/jsp
Ruby 脚本Client/ruby
表示层表示层 - 浏览器请求处理程序src/edu/ucar/cisl/ncarUsers/presentation/brh
表示层 - 资源请求处理程序src/edu/ucar/cisl/ncarUsers /presentation/rrh
业务逻辑层src/edu/ucar/cisl/ncarUsers/bll
数据访问层src/edu/ucar/cisl/ncarUsers/dal
数据存储层MySql 脚本db/setup.sql

下载 源代码,并将其解压到 C 盘中。现在,您应该可以看到一个新文件夹,即 C:\ncarUsers。在此文件夹中,您可以看到如表 1 所示的完整的文件夹结构。下载文件包括包括源代码以及 MySQL Connector/J Driver 5.1、Jersey 1.0、Spring Framework 2.5.5 和 Spring Web Flow 2.0.2 所需要的所有库,所有这些对于本演示来说应该足够了,除非您需要尝试更新的发行版。


设置环境

下载以下软件包,并根据各网站上的安装指南安装它们。(参见 参考资料 获取链接)。

  1. Apache Tomcat 6.x
  2. MySQL 5.1
  3. Eclipse IDE for Java EE Developers(我使用 Eclipse Europa 发行版,但较新的版本也可以)。
  4. Ruby

完成 Ruby 安装之后,运行命令 gem install –remote 下载和安装 json(图 1)和 rest-open-uri 库。

图 1. Ruby gen 安装 json 库
安装 Ruby 之后执行的命令

创建数据库

我在本文中使用 MySQL 数据库。要创建一个 MySQL 服务器实例,使用 MySQL Server Instance Configuration Wizard。实例的默认名称是 MYSQL。要启动 MySQL 命令行工具,运行 mysql –u root –p MYSQL。(您需要在之前的步骤中提供密码集用于根用户登录)。接下来,在命令行工具中运行 source c:/ncarUsers/db/setup.sql 以创建数据库 (ncar_users)、一个带密码 (tutorial) 的 MySQL 用户 (tutorial) 以及本文的表(图 2)。脚本还将数据插入到了实验室和部门表中。

图 2. 运行脚本文件 setup.sql
显示 mysql -u root -p MYSQL 命令的命令窗口

在 Eclipse 中配置 Tomcat 服务器

配置 Tomcat 服务器:

  1. 打开 Eclipse 并选择 File > New > Other
  2. 从列表中选择 Server
    图 3. 在 Eclipse 中配置 Tomcat 服务器 —— 步骤 1
    向导中的 Server 菜单选择
  3. 单击 Next。在窗口中,选择 Apache > Tomcat v6.0 Server。
    图 4. 在 Eclipse 中配置 Tomcat 服务器 —— 步骤 2
    选择 Tomcat 6.0 服务器
  4. 单击 Next。在下一个窗口中,单击 Browse… 并选择 Tomcat 安装的位置。
    图 5. 在 Eclipse 中配置 Tomcat 服务器 —— 步骤 3
    Tomcat 服务器的配置。Tomcat 安装目录设置为 'C:\apache-tomcat-6-6.0.16'
  5. 单击 Finish

在 Eclipse 中创建 Web 项目 ncarUsers

创建 Web 项目:

  1. 选择 File > New > Project…。打开 Web
  2. 单击 Dynamic Web Project
    图 6. 在 Eclipse 中创建 Web 项目 ncarUsers —— 步骤 1
    选择 Dynamic Web Project
  3. 单击 Next。在新窗口中,在项目名称字段中输入 ncarUsers
    图 7. 在 Eclipse 中创建 Web 项目 ncarUsers —— 步骤 2
    -Dynamic Web Project 的配置。项目名称设置为 'ncarUsers'
  4. 单击 Next
  5. 在 Project Facets 窗口中再次单击 Next
  6. 在 Web Module 窗口中,在 Content Directory 字段中将 WebContent 修改为 WebRootfield。
  7. 单击 Finish
    图 8. 在 Eclipse 中创建 Web 项目 ncarUsers —— 步骤 3
    Web Module 配置。Content Directory 被设置为 'WebRoot'

从文章下载导入文件

要导入文件:

  1. 在 Project Explorer 中,右键单击 ncarUsers 并选择 Import > Import …
  2. 在 Import 窗口中,单击 General > File System(图 9)。
    图 9. 导入下载到 ncarUsers 项目 —— 步骤 1
    导入屏幕。文件系统已突出显示
  3. 单击 Next
  4. 在 File System 窗口中,单击 Browse ... 并选择 C:\ncarUsers
  5. 选中 ucarUsers 旁边的复选框(图 10)。
    图 10. 导入下载到 ncarUsers 项目 —— 步骤 2
    选中 'C:\ncarUsers\src\edu' 的文件系统导入屏幕。它旁边的复选框已被选中。

完成导入之后,Project Explorer 应如图 11 所示。

图 11. 项目导入的结果
Eclipse 屏幕

如果您希望跳过以下部分(实现域对象、数据访问层 (DAL)、业务逻辑层 (BLL)、表示层,包括浏览器请求处理程序、资源请求处理程序以及客户机应用程序),您可以直接阅读 在 Eclipse 中运行应用程序


实现域对象

域对象建模应用程序的问题域。我实现了三个域对象:User(清单 1)、Lab(清单 2)和 Division(清单 3)。

清单 1. edu.ucar.cisl.ncarUsers.domain.User
1.	package edu.ucar.cisl.ncarUsers.domain;

2.	import java.io.Serializable;

3.	public class User implements Serializable {
4.	   protected int ID;
5.	   protected String userName;
6.	   protected String password;
7.	   protected String firstName;
8.	   protected String lastName;
9.	   protected String email;
10.	   protected int lab;
11.	   protected int division;

12.	... //getters and setters
13.	}
清单 2. edu.ucar.cisl.ncarUsers.domain.Lab
1.	package edu.ucar.cisl.ncarUsers.domain;
2.	import java.io.Serializable;

3.	public class Lab implements Serializable {
4.	    protected int ID;
5.	    protected String shortName;
6.	    protected String name;
7.	    protected String description;

8.	    ... //getters and setters   
9.	}
清单 3. edu.ucar.cisl.ncarUsers.domain.Division
1.	package edu.ucar.cisl.ncarUsers.domain;

2.	import java.io.Serializable;

3.	public class Division implements Serializable {
4.	    protected int ID;
5.	    protected String shortName;
6.	    protected String name;
7.	    protected String description;
8.	    protected int labID;

9.	    ... //getters and setters    
10.	}

实现数据访问层

在数据访问层 (DAL) 中,我们创建了三个数据访问对象:UserDAOLabDAODivisionDAO。数据访问对象既可以匹配也可以不匹配域对象。清单 4 展示了 UserDAO 接口,清单 5 展示了它的实现,其中,Spring JDBC 框架用于执行插入/更新(21 行)和查询(30 行)。为查询实现了一个内部类(31-44 行)以将 ResultSet 对象映射到 User 对象。LabDAODivisionDAO 采用相同的方式实现。

清单 4. edu.ucar.cisl.ncarUsers.dal.UseDAO
1.    package edu.ucar.cisl.ncarUsers.dal;

2.    ...//imports

3.    public interface UserDAO
4.    {
5.      public User getUser(String s);    
6.      public void addUser(User user);
7.      public ArrayList<User> getAllUsers();
8.    }
清单 5. edu.ucar.cisl.ncarUsers.dal.UserDAOJDBCImpl
1.   package edu.ucar.cisl.ncarUsers.dal;
2.   ...//imports

3.   public class UserDAOJDBCImpl extends SimpleJdbcDaoSupport 
          implements UserDAO {
4.        public getUser(String s) {
5.            String criteria="USERNAME = '" + s + "'";
6.            ArrayList<User> users=getUsers(criteria);
7.            if (users.size() > 0)
8.                return users.get(0);
9.            else
10.                return null;
11.        }

12.        public void addUser(User user) {
13.            Object objs[] = new Object[7];
14.            objs[0] = user.getUserName();
15.            objs[1] = user.getPassword();
16.            objs[2] = user.getEmail();
17.            objs[3] = user.getFirstName();
18.            objs[4] = user.getLastName();
19.            objs[5] = user.getLab();
20.            objs[6] = user.getDivision();

21.            this.getJdbcTemplate().update("insert into USER (USERNAME, 
                    PASSWORD, EMAIL, FIRST_NAME, LAST_NAME, LAB, DIVISION )
                     values (?, ?, ?, ?, ?, ?, ?)", objs);
22.            }

23.        public ArrayList<User> getAllUsers(){
24.            return getUsers(null);
25.        }

26.        protected ArrayList<User> getUsers(String criteria)   {
27.            String query="select ID, USERNAME, PASSWORD, EMAIL, 
                      FIRST_NAME, LAST_NAME, LAB, DIVISION from USER";
28.            if (criteria != null && criteria.trim().length() > 0)
29.                query= query + " Where " + criteria;
30.                Collection users = this.getJdbcTemplate().query(query,
31.                    new RowMapper() {
32.                        public Object mapRow(ResultSet rs, int rowNum) throws 
                                 SQLException {
33.                            User user = new User();
34.                            user.setID(rs.getInt("ID"));
35.                            user.setUserName(rs.getString("USERNAME"));
36.                            user.setPassword(rs.getString("PASSWORD"));
37.                            user.setEmail(rs.getString("EMAIL"));
38.                            user.setFirstName(rs.getString("FIRST_NAME"));
39.                            user.setLastName(rs.getString("LAST_NAME"));
40.                            user.setLab(rs.getInt("LAB"));
41.                            user.setDivision(rs.getInt("DIVISION"));
42.                            return user;
43.                         }
44.                     });
45.                ArrayList<User> results= new ArrayList <User>();
46.                Iterator it=users.iterator();
47.                while (it.hasNext())
48.                    results.add((User)it.next());

49.                return results;      
50.            }
51.       }

实现业务逻辑层

业务逻辑层 (BLL) 是集中存放业务规则的地方。这个层还处理来自表示层的请求,并与 DAL 交互以便从后端检索数据并请求 DAL 执行数据持久性。我实现了三个管理器类:分别用于各个域对象。清单 6 和 7 展示了 UserManager 接口及其实现。LabManager 和 DivisionManager 的实现与 UserManager 极为相似。

清单 6. edu.ucar.cisl.ncarUsers.bll.UserManager
1.	package edu.ucar.cisl.ncarUsers.bll;

2.	...//imports

3.	public interface UserManager {
4.	    public User getUser(String userName);	
5.	    public void addUser(User user);	
6.	    public ArrayList<User> getAllUsers();
7.	}
清单 7. edu.ucar.cisl.ncarUsers.bll.UserManagerImpl
1.	package edu.ucar.cisl.ncarUsers.bll;

2.	...//imports

3.	public class UserManagerImpl implements UserManager {
4.	    protected UserDAO userDao;

5.	    public User getUser(String userName)	{
6.	        return userDao.getUser(userName);
7.	    }

8.	    public void addUser(User user)	{
9.	        userDao.addUser(user);
10.	    }
  
11.	    public UserDAO getUserDao() {
12.	        return userDao;
13.	    }

14.	    public void setUserDao(UserDAO userDao) {
15.	        this.userDao = userDao;
16.	    }

17.	    public ArrayList<User> getAllUsers() {
18.	        return userDao.getAllUsers();
19.	    }	
20.	 }

实现表示层

浏览器请求处理程序

需要一个浏览器接口来允许 NCAR 管理员在网络上添加用户。我使用 Spring MVC 和 Spring Web Flow 框架来实现浏览器请求处理程序。网络上发布了许多关于 Spring Web Flow 的文章和教程,您可以在 参考资料 中找到其中一部分。

清单 8 配置了 Spring MVC servlet。这个 servlet 将服务于来自浏览器的所有请求,除了来自 Ajax 的请求。一个良好的实践是,所有这些请求的 URI 始终以 /brh 开头,而来自 REST 风格的 Web 服务客户机程序(包括 Ajax 客户机)的所有请求都以 /rrh 开头。

清单 8. 在 /Web-Inf/web.xml 中使用 Spring MVC 和 Spring Web Flow 的 Servlet 定义
1.<servlet>
2.    <servlet-name>ncarUsers</servlet-name>
3.    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
4.    <init-param>
5.        <param-name>contextConfigLocation</param-name>
6.        <param-value>/WEB-INF/ncarUsers-spring-config.xml</param-value>
7.    </init-param>
8.</servlet>    
9.<servlet-mapping>
10.    <servlet-name>ncarUsers</servlet-name>
11.    <url-pattern>*.htm</url-pattern>
12.</servlet-mapping>

清单 9 配置了 Spring Web Flow。它将视图名称映射到 JavaServer Pages (JSP) 文件(6-9 行),并注册了流配置文件中定义的流(11-13行)。

清单 9. /WEB-INF/ncarUsers-spring-config.xml
1.<?xml version="1.0" encoding="UTF-8"?>
2.<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flow="http://www.springframework.org/schema/webflow-config"
xsi:schemaLocation="
      http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
      http://www.springframework.org/schema/webflow-config
      http://www.springframework.org/schema/webflow-config/spring-webflow-config-1.0.xsd">
3.    <bean name="/flow.htm" 
       class="org.springframework.webflow.executor.mvc.FlowController">
4.        <property name="flowExecutor" ref="flowExecutor"/>
5.    </bean>

6.    <bean id="viewResolver" 
       class="org.springframework.web.servlet.view.InternalResourceViewResolver">
7.        <property name="prefix" value="/WEB-INF/jsp/"/>
8.        <property name="suffix" value=".jsp"/>
9.    </bean>
10.    <flow:executor id="flowExecutor" registry-ref="flowRegistry"/>
11.    <flow:registry id="flowRegistry">
12.        <flow:location path="/WEB-INF/flows/**-flow.xml"/>
13.    </flow:registry>    
14.</beans>

我实现了一个表单类(清单 10)和一个表单操作类(清单 11),以便于添加 User 页面流。表单类包含在浏览器界面中显示的数据,并存储 NCAR 将在表单中填充的数据,如 addUser.jsp 所示(清单 12)。Spring 标记库用于绑定表单数据与 HTML 表单中的字段。表单操作类包含表单操作的行为。

清单 10. edu.ucar.cisl.ncarUsers.presentation.brh.AddUserForm
1.	package edu.ucar.cisl.ncarUsers.presentation.brh;

2.	...//imports

3.	public class AddUserForm implements Serializable {
4.	    protected ArrayList<Lab> labs;
5.	    protected User user;

6.	    public AddUserForm() {
7.	    }

8.	    ... //getters and setters

9.	}
清单 11. edu.ucar.cisl.ncarUsers.presentation.brh.AddUserFormAction
1.	package edu.ucar.cisl.ncarUsers.presentation.brh;

2.	...//imports

3.	public class AddUserFormAction extends FormAction {
4.	    protected UserManager userManager;
5.	    protected LabManager labManager;

6.	    public AddUserFormAction() {
7.	        userManager = null;
8.	    }

9.	    public Event initForm(RequestContext context) throws Exception {
10.	        AddUserForm form = (AddUserForm) getFormObject(context);
11.	        form.setLabs(this.labManager.getLabs());
12.	        form.setUser(new User());
13.	        return success();
14.	    }

15.	    public Event submit(RequestContext context) throws Exception {
16.	        AddUserForm form = (AddUserForm) getFormObject(context);
17.	        User user = form.getUser();
18.	        userManager.addUser(user);
19.	        return success();
20.	    }

21.	    public Event addNewUser(RequestContext context) throws Exception {
22.	        initForm(context);
23.	        return success();
24.	    }

25.	    ... //getters and setters

26.	}
清单 12. /WEB-INF/jsp/addUser.jsp
1.<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
2.<%@ page language="java"%>
3.<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
4.<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

5.<html>
6.<head>
7.<title>NCAR New User Registration</title>
8.<script language="JavaScript" src="js/addUserAjax.js"></script>
9.</head>
10.<body>
11.<h1>NCAR New User Registration</h1>
12.<form:form commandName="addUserForm" method="post" 
          action="flow.htm">
13.<input type="hidden" name="_flowExecutionKey" 
          value="${flowExecutionKey}">
14.<table>
15.<tr>
16.    <td>Username:</td>
17.    <td align="left">
18.        <form:input path="user.userName" id="userName" 
                     onblur="validateUsername();" />
19.   </td>
20.</tr>
21.<tr>
22.    <td>Password:</td>
23.    <td align="left">
24.        <form:input path="user.password" id="password" />
25.    </td>
26.</tr>
27.<tr>
28.     <td>&nbsp;</td>
29.</tr>
30.<tr>
31.    <td>First Name:</td>
32.    <td align="left">
33.        <form:input path="user.firstName" id="email" />
34.    </td>
35.</tr>
36.<tr>
37.    <td>Last Name:</td>
38.    <td align="left">
39.        <form:input path="user.lastName" id="email" />
40.    </td>
41.</tr>
42.<tr>
43.    <td>Email:</td>
44.    <td align="left">
45.        <form:input path="user.email" id="email" />
46.    </td>
47.</tr>
48.<tr>
49.    <td>Lab:</td>
50.    <td align="left">
51.        <form:select id="lab" path="user.lab" onclick="updateDivisions();">
52.            <form:option value="0" label="--Please Select--" />
53.            <form:options items="${addUserForm.labs}" itemValue="ID" 
                        itemLabel="name" />
54.        </form:select>
55.    </td>
56.</tr>
57.<tr>
58.    <td>Division:</td>
59.    <td align="left">
60.        <form:select id="division" path="user.division" disabled="true">
61.        </form:select>
62.    </td>
63.</tr>
64.<tr>
65.    <td>&nbsp;</td>
66.</tr>
67.<tr>
68.    <td colspan="2" align="center">
69.        <input type="submit" name="_eventId_submit" value="Submit">
70.    </td>
71.</tr>
72.</table>
73.</form:form>
74.</body>
75.</html>

清单 13 中配置了表单操作类 AddUserFormAction。它使用表单类(3 行),以及 BLL 中的 userManagerlabManager 类(行 6,7)。两个管理器类都是在 Spring 配置文件中配置的,这将在稍后讨论。

清单 13. /WEB-INF/flows/addUser-beans.xml
1.  <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
2.      <bean id="addUserFormAction" 
                class="edu.ucar.cisl.ncarUsers.presentation.brh.AddUserFormAction">
3.          <property name="formObjectName" value="addUserForm"/>
4.          <property name="formObjectClass" 
                    value="edu.ucar.cisl.ncarUsers.presentation.brh.AddUserForm"/>
5.          <property name="formObjectScope" value="FLOW"/>
6.          <property name="userManager" ref="userManager"/>  
7.          <property name="labManager" ref="labManager"/>          
8.      </bean>    
9.  </beans>

清单 14 定义了状态以及在流程中传递状态的操作。

清单 14. /WEB-INF/flows/addUser-flow.xml
1.	<?xml version="1.0" encoding="UTF-8"?>
2.	<flow xmlns=http://www.springframework.org/schema/webflow
         xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
              xsi:schemaLocation="http://www.springframework.org/schema/webflow
         http://www.springframework.org/schema/webflow/spring-webflow-1.0.xsd">

3.	    <start-state idref="addUser"/>

4.	    <view-state id="addUser" view="addUser">
5.	        <render-actions>
6.	            <action bean="addUserFormAction" method="initForm"/>
7.	        </render-actions>
8.	        <transition on="submit" to="submit">
9.	            <action bean="addUserFormAction" method="bind"/>
    </transition>
10.	    </view-state>

11.	    <view-state id="summary" view="summary">
12.	        <transition on="addNewUser" to="addNewUser" />
13.	    </view-state>

14.	    <action-state id="submit">
15.	        <action bean="addUserFormAction" method="submit"/>
    <transition on="success" to="summary"/>
16.	    </action-state>

17.	    <action-state id="addNewUser">
18.	        <action bean="addUserFormAction" method="addNewUser"/>
19.	        <transition on="success" to="addUser"/>
20.	    </action-state>    

21.	    <import resource="addUser-beans.xml"/>    

22.	</flow>

资源请求处理程序

资源类决定哪些内容将作为 REST 风格的 Web 服务向客户机应用程序公开。Jersey 简化了在 RRH 中实现 REST 风格的 Web 服务 的过程。它使用注释映射资源类与 URI,并将 HTTP 请求中的标准 HTTP 方法映射到资源类中的方法。要使用 Jersey,需要在 web.xml 文件中配置一个特殊的 servlet(清单 15)。当 servlet 初始化时,它会遍历 edu.ucar.cisl.ncarUsers.presentation.rrh 包中的类,以定位所有资源类并将它们映射到注释 URI。REST 风格的 Web 服务的所有请求都以 /rrh 作为开头,并且将由此 servlet 进行处理。

清单 15. /Web-Inf/web.xml 中使用 Jersey 的 Servlet 定义
1.  <servlet>
2.    <servlet-name>rrh</servlet-name>
3.    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
4.    <init-param>
5.        <param-name>com.sun.jersey.config.property.packages</param-name>
6.        <param-value>edu.ucar.cisl.ncarUsers.presentation.rrh</param-value>
7.    </init-param>
8.  </servlet>
9.  <servlet-mapping>
10.    <servlet-name>rrh</servlet-name>
11.    <url-pattern>/rrh/*</url-pattern>
12.  </servlet-mapping>

已经实现了三个资源类:UsersResource(清单 16)、UserResource(清单 17)和 DivisionResource(清单 18)。在清单 16 中,第 3 行类定义之前的注释 @PathUsersResource 类映射到 URI /用户。21、22 行的 getUsersAsJsonArray 方法之前的 @GET@Produces(application/json) 注释指示此方法将处理 HTTP GET 请求,并且它的响应内容类型是 JSON。41、42 行的 putUsers 方法之前的 @PUT@Consumes("text/plain") 注释指示方法将处理 HTTP PUT 请求,并且输入 HTTP 主体中的内容类型预期为纯文本。在本例中,它是每行一个用户的平面文件。每个用户的属性都由一条竖线分开。

清单 16. edu.ucar.cisl.ncarUsers.presentation.rrh.UsersResource
1.  package edu.ucar.cisl.ncarUsers.presentation.rrh;

2.  ...//imports

3.  @Path("/users/")
4.  public class UsersResource {
5.     protected @Context UriInfo uriInfo;
6.     protected UserManager userManager;
7.     protected DivisionManager divisionManager;
8.     protected LabManager labManager;

9.     public UsersResource() {
10.        userManager = (UserManager)
11.          BeanFactory.getInstance().getBean("userManager");
12.        divisionManager = (DivisionManager)
13.          BeanFactory.getInstance().getBean("divisionManager");
14.        labManager = (LabManager)
15.          BeanFactory.getInstance().getBean("labManager");
16.  }

17.     @Path("{username}/")
18.     public UserResource getUser(@PathParam("username") String userName) {
19.        return new UserResource(uriInfo, userManager, userName);
20.     }

21.     @GET
22.     @Produces("application/json")
23.     public JSONArray getUsersAsJsonArray() throws JSONException {
24.        ArrayList<User> users = this.userManager.getAllUsers();
25.        JSONArray usersArray = new JSONArray();
26.        for (User user : users) {
27.          JSONObject obj = new JSONObject();
28.          obj.put("USERNAME", user.getUserName());
29.          obj.put("PASSWORD", user.getPassword());
30.          obj.put("FIRST_NAME", user.getFirstName());
31.          obj.put("LAST_NAME", user.getLastName());
32.          obj.put("EMAIL", user.getEmail());
33.          String labName=labManager.getLabName(user.getLab());
34.          obj.put("LAB", labName);
35.          String divisionName= divisionManager.getDivisionName(user.getDivision());
36.          obj.put("DIVISION", divisionName);
37.          usersArray.put(obj);
38.        }
39.        return usersArray;
40.     }

41.     @PUT
42.     @Consumes("text/plain")
43.     public Response putUsers(String input) throws IOException {
44.        Reader reader=new StringReader(input);
45.        BufferedReader br=new BufferedReader(reader);
46.        while (true) {
47.          String line=br.readLine();
48.          if (line == null)
49.             break;
50.          processUser(line);      
51.        }    
52.        return Response.created(uriInfo.getAbsolutePath()).build();
53.  }

54.  /********************************
If the user exists, update it. Otherwise, create a new one
@param input
   */
55.     protected void processUser(String input)
56.     {
57.        StringTokenizer token=new StringTokenizer(input, "|");
58.        String userName=token.nextToken();
59.        String password=token.nextToken();
60.        String firstName=token.nextToken();
61.        String lastName=token.nextToken();
62.        String email=token.nextToken();
63.        String labName=token.nextToken();
64.        String divisionName=token.nextToken();

65.        int lab=this.labManager.getLabID(labName);
66.        int division=this.divisionManager.getDivisionID(divisionName);
67.        User user=this.userManager.getUser(userName);
68.        if (user == null)
69.          user=new User();

70.        user.setUserName(userName);
71.        user.setPassword(password);
72.        user.setFirstName(firstName);
73.        user.setLastName(lastName);
74.        user.setEmail(email);
75.        user.setLab(lab);
76.        user.setDivision(division);

77.        this.userManager.addUser(user);    
78.     }
79.  }

在清单 16 中,17 行的 getUser 方法之前的 @Path("{username}/") 注释指示 /users/ 之前以及下一个斜杠之前的字符串,如果存在于请求 URI 中,那么将作为 username 变量的值,并且 getUser 方法将返回子资源类 UserResource 的一个实例。然后,Jersey 调用 UserResource 类中的方法,它们分别针对 HTTP 方法添加了注释。在清单 17 中,getUser 方法,使用 @GETProduces("application/json") 注释,将由 Jersey 调用,以便 HTTP 请求中的 GET 方法以 JSON 格式返回用户数据(12-19 行)。

清单 17. edu.ucar.cisl.ncarUsers.presentation.rrh.UserResource
1. package edu.ucar.cisl.ncarUsers.presentation.rrh;

2. ...//imports

3. public class UserResource {
4.     protected String userName;
5.     protected UriInfo uriInfo;
6.     protected UserManager userManager;

7.     public UserResource(UriInfo uriInfo, UserManager userManager, String userName) {
8.         this.uriInfo = uriInfo;
9.         this.userName = userName;
10.         this.userManager = userManager;
11. }

12.     @GET
13.     @Produces("application/json")
14.     public JSONObject getUser() throws JSONException {
15.         JSONObject obj = new JSONObject();
16.         User user = this.userManager.getUser(userName);
17.         if (user != null) 
18.             obj.put("userName", user.getUserName()).put("email", user.getEmail());
           return obj;
19.     }
20. }

清单 18 中的资源类 DivisionsResource 采用类似的方法实现。类在第 3 行添加了 URI path /divisions/ 注释。getDivisions 方法,添加了 @GET@ProduceName 注释,将返回一个 JSON division 数组(10-23 行)。

清单 18. edu.ucar.cisl.ncarUsers.presentation.rrh.DivisionsResource
1.   package edu.ucar.cisl.ncarUsers.presentation.rrh;

2.   ...//imports

3.   @Path("/divisions/")

4.   public class DivisionsResource {
5.       protected DivisionManager divisionManager;

6.       public DivisionsResource() {
7.           divisionManager = (DivisionManager)
8.               BeanFactory.getInstance().getBean("divisionManager");
9.       }

10.       @GET
11.       @Produces("application/json")
12.       public JSONArray getDivisions(@QueryParam("labID") String labID) 
13.           throws JSONException {
14.           int id = Integer.parseInt(labID);
15.           ArrayList<Division> divisions = this.divisionManager.getDivisions(id);
16.           JSONArray divisionsArray = new JSONArray();
17.           for (Division division : divisions) {
18.               JSONObject obj = new JSONObject();
19.               obj.put("ID", division.getID()).put("name", division.getName());
20.               divisionsArray.put(obj);
21.           }
22.           return divisionsArray;
23.       }
24.   }

客户机应用程序

Ajax

Ajax 充当 REST 风格的 Web 服务的客户机。它们共同帮助创建类似桌面的、快速响应的富浏览器界面。在示例应用程序中,我在两处使用了 Ajax:在清单 12 的 18 行中,检查用户名称是否已经存在于数据库中,以及在 51 行中,异步请求特定实验室的部门列表并更新部门选项菜单,而不刷新页面。

JavaScripts 列出在清单 19 中。2 到 13 行的 validateUsername 函数设置了一个 XMLHttpRequest,并将它发送给 REST 风格的 Web 服务,以在浏览器中获取特定用户名的用户数据。14 到 27 行的 usernameCallback 函数是一个回调函数,它处理来自 REST 风格的 Web 服务服务器的响应。如果响应包含用户数据,则它表示特定用户名称的用户已经存在。将显示一条警告消息,并且浏览器中的用户名字段将被清空。

28 到 39 行的 updateDivisions 函数发送一个请求给 REST 风格的 Web 服务,以获取实验室 NCAR 管理员在实验室选项菜单中所选择的部门。40 到 55 行的回调函数 updateDivisionsCallback 处理响应并在 Division 选项菜单中显示返回的部门名称。

清单 19. js/addUserAjax.js
1.    var req;
2.    function validateUsername() {
3.        var username = document.getElementById("userName");
4.        var url = "rrh/users/" + escape(username.value);
5.        if (window.XMLHttpRequest) {
6.            req = new XMLHttpRequest();
7.        } else if (window.ActiveXObject) {
8.            req = new ActiveXObject("Microsoft.XMLHTTP");
9.        }

10.        req.open("Get", url, true);
11.        req.onreadystatechange = usernameCallback;
12.        req.send(null);
13.    }

14.    function usernameCallback() {
15.        if (req.readyState == 4 && if (req.status == 200) {
16.            var jsonData = req.responseText;
17.            var myJSONObject = eval("(" + jsonData + ")");
18.            var un = myJSONObject.userName;
19.            var username = document.getElementById("userName");
20.            if (username.value == un) {
21.                alert("Warning: " + username.value + 
22.                   " exists already. Choose another username.");
23.                username.value = "";
24.                username.focus();
25.            }
26.        }
27.    }

28.    function updateDivisions() {
29.        var labSel = document.getElementById("lab");
30.        var url = "rrh/divisions/?labID=" + escape(labSel.value);
31.        if (window.XMLHttpRequest) {
32.            req = new XMLHttpRequest();
33.        } else if (window.ActiveXObject) {
34.            req = new ActiveXObject("Microsoft.XMLHTTP");
35.        }
36.        req.open("Get", url, true);
37.        req.onreadystatechange = updateDivisionsCallback;
38.        req.send(null);
39.    }

40.    function updateDivisionsCallback() {
41.        if (req.readyState == 4)&& req.status == 200) {
42.            var jsonData = req.responseText;
43.            var divisionsData = eval("(" + jsonData + ")");
44.            var divisionSel = document.getElementById("division");
45.            var length = divisionSel.length;
46.            for (var b = 0; b < length; b++) {
47.                divisionSel.options[b] = null;
48.            }
49.            for (var a = 0; a < divisionsData.length; a++) {
50.                divisionSel.options[a] = new 
51.                   Option(divisionsData[a].name, divisionsData[a].ID);
52.            }
53.            divisionSel.disabled = "";
54.        }
55.    }

Ruby 脚本

REST 风格的 Web 服务的客户机可以轻松地通过 Perl、Ruby、Python、C、C# 或 Java 等语言实现。在本文中,我以 Ruby 为例。清单 20 展示了用于从 REST 风格的 Web 服务下载用户数据并采用由竖线 (|) 分隔属性的方式来保存各用户数据的 Ruby 脚本。清单 21 中的 Ruby 脚本将用户数据从文件上传到服务器。

清单 20. client/downloadUsersData.rb
54. #!/usr/bin/ruby

55. require 'rubygems'
56. require 'json'
57. require 'open-uri'
58. $KCODE = 'UTF8'

59. def download(filename)
60. file=File.new(filename, 'w')
61. base_uri = 'http://localhost:8080/ncarUsers/rrh/users/'

62. # Make the HTTP request and read the response entity-body as a JSON
63. # document.
64. json = open(base_uri).read

65. # Parse the JSON document into a Ruby data structure.
66. json = JSON.parse(json)

67. # Iterate over the data structure...
68. json.each { |r| file.puts r['USERNAME'] + '|' + r['PASSWORD'] + '|' +
                    r['FIRST_NAME'] +  '|' +  r['LAST_NAME'] + 
69. '|' + r['EMAIL'] + '|' +  r['LAB'] + '|' + r['DIVISION']; }
70. end

71. # Main program.
72. unless ARGV[0]
73. puts "Usage: #{$0} [file name]"
74. exit
75. end
76. download(ARGV[0])
清单 21. client/uploadUsersData.rb
1. #!/usr/bin/ruby
2. require 'rubygems'
3. require 'rest-open-uri'
4. require 'uri'
5. require 'cgi'


6. def uploadUsers(content)
7. base_uri = 'http://localhost:8080/ncarUsers/rrh/users'
8. begin
9. response = open(base_uri, :method => :put, 'Content-Type' => 
                      "text/plain", :body => content)
10. rescue OpenURI::HTTPError => e
11. response_code = e.io.status[0].to_i
12. puts response_code 
13. if response_code !=  "200" 
a. puts "Sorry, Can't post the users"
14. else
a. raise e
15. end
16. end

17. end

18. def upload(filename)
19. File.open(filename) do |file|
20. content = file.read
21. uploadUsers(content)
22. end
23. end


24. # Main program.
25. unless ARGV[0]
26. puts "Usage: #{$0} [file name]"
27. exit
28. end
29. upload(ARGV[0])

功能合并

Spring 框架用于将数据访问层、业务逻辑层和表示层中的各组件结合在一起。它使用 Inversion of Control (IoC) 具体化组件依赖关系的创建和管理。清单 22 展示了定义各组件及其依赖关系的 Spring 配置文件。它还配置了数据库和事务管理器。

清单 22. applicationContext.xml
1.    <beans xmlns=http://www.springframework.org/schema/beans
2.        xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
3.        xmlns:tx=http://www.springframework.org/schema/tx 
4.        xsi:schemaLocation="
5.           http://www.springframework.org/schema/beans
6.    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
7.    http://www.springframework.org/schema/tx
8.    http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

9.        <tx:annotation-driven/>
10.        <bean id="dataSource"
11.            class="org.springframework.jdbc.datasource.DriverManagerDataSource">
12.            <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
13.            <property name="url" value="jdbc:mysql://localhost:3306/ncar_users"/>
14.            <property name="username" value="tutorial"/>
15.            <property name="password" value="tutorial"/>
16.        </bean>

17.        <bean id="transactionManager" 
18.              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
19.            <property name="dataSource" ref="dataSource"/>
20.        </bean>    
21.        <bean id="userManager"
22.            class="edu.ucar.cisl.ncarUsers.bll.UserManagerImpl">
23.            <property name="userDao"><ref local="userDao"/></property>
24.        </bean>
25.        <bean id="labManager"
26.            class="edu.ucar.cisl.ncarUsers.bll.LabManagerImpl">
27.            <property name="labDao"><ref local="labDao"/></property>
28.        </bean>
29.        <bean id="divisionManager"
30.            class="edu.ucar.cisl.ncarUsers.bll.DivisionManagerImpl">
31.            <property name="divisionDao"><ref local="divisionDao"/></property>
32.        </bean>    
33.        <bean id="userDao" class="edu.ucar.cisl.ncarUsers.dal.UserDAOJDBCImpl">
34.            <property name="dataSource"><ref local="dataSource"/></property>
35.        </bean>     
36.        <bean id="labDao" class="edu.ucar.cisl.ncarUsers.dal.LabDAOJDBCImpl">
37.            <property name="dataSource"><ref local="dataSource"/></property>
38.        </bean>     
39.        <bean id="divisionDao"
40.            class="edu.ucar.cisl.ncarUsers.dal.DivisionDAOJDBCImpl">
41.            <property name="dataSource"><ref local="dataSource"/></property>
42.        </bean>       
43.    </beans>

web.xml 文件中配置了一个上下文加载器监听程序,用于在 Web 应用程序启动时加载 applicationContext.xml 文件(清单 23)。

清单 23. web.xml 中用于加载 applicationContext.xml 的上下文加载器监听程序配置
1. <context-param>
2.     <param-name>contextConfigLocation</param-name>
3.     <param-value>/WEB-INF/classes/applicationContext.xml</param-value>
4. </context-param>

5. <listener>
6.     <listener-class>
              org.springframework.web.context.ContextLoaderListener</listener-class>
7. </listener>

在 Eclipse 中运行应用程序

要在 Eclipse 中运行示例应用程序:

  1. 在 Project Explorer 中右键单击 ncarUsers 项目,并选择 Run As > Run On Server 或者 Debug On Server(如果希望在调试模式下运行)。
  2. 选择 localhost > Tomcat v6.0 Server at localhost
    图 12. 在 Eclipse 内部运行 Tomcat
    选中了 'Choose an existing server' 选项的 Run On Server 窗口
  3. 单击 Finish。这将打开 Servers 选项卡并显示示例应用程序已被部署到 Tomcat,且该服务器正在运行中。您可以切换到 Console 选项卡,查看由 Tomcat 服务器生成的消息。
  4. 打开浏览器并浏览到:http://localhost:8080/ncarUsers。单击链接 Sign Up New Users App
    图 13. NCAR 新用户注册浏览器界面
    NCAR 新用户注册屏幕快照

    您会注意到 Lab 选项菜单显示 --Please Select--,并且 Division 选项菜单已被禁用。在各文本字段中输入一些内容,并选择一个实验室。Division 选项菜单现在包含所选实验室的部门。接下来,选择一个部门。

  5. 单击 Submit。这将创建一个新用户,并且将显示 Signup Confirmation 页面。
    图 14. NCAR 新用户注册确认页面
    带有成功消息和 'Add New User' 按钮的注册确认页面
  6. 单击 Add New User
  7. NCAR New User Registration 页面中,在用户名字段输入相同的用户名,然后移动到下一个字段。此时将弹出一个窗口,警告用户名已经使用并且用户名字段将被清空。
  8. 创建一些用户之后,打开一个命令行提示。运行 downloadUsers.rb Ruby 脚本,下载用户的数据(图 15)。您可以使用下载的文件作为一个模板来添加一些新用户。然后使用 uploadUsers.rb 将新用户上传到应用服务器。
    图 15. 运行 downloadUsers.rb 脚本下载用户的数据
    downloadUsers.rb 脚本将数据转储到 userData.txt

内部原理

当您打开 Sign Up New User 应用程序时,表示层中的浏览器请求处理程序将处理来自 Web 界面的请求。在所有字段中输入数据并提交之后,浏览器请求处理程序中的 AddUserFormAction 对象将接受 Submit 请求。如图 16 所示,此对象将必要的数据传递给 BLL,后者随后请求数据访问层将数据保存到 MySQL 数据库。然后,浏览器中将显示一个确认页面。

图 16. 添加新用户序列图
数据从客户机流经资源请求处理程序、业务逻辑层、数据访问层和数据存储,然后返回资源请求处理程序,最后显示成功消息。

键入用户名之后,Ajax 脚本将调用一个 REST 风格的 Web 服务。这次,资源请求处理程序层中的 UsersResourceUserResource 对象将处理请求。如果特定用户名的用户在数据库中已经存在,则一个 JSON 数据结构将返回给浏览器。然后,Ajax 脚本显示一条警告消息,并清空用户名字段。

当您选择某实验室选项菜单字段时,Ajax 脚本将调用资源请求处理程序的 DivisionsResource 中的一个 GET Web 服务,后者将返回所选实验室的部门数组。图 17 展示了 Ajax 脚本发送 REST 风格的 Web 服务请求以获取特定实验室的部门并在 Division 选项菜单中显示它们之后,各分层之间的请求序列。

图 17. 获取部门序列图
获取部门列表的流程。

用于上传和下载用户数据的 Ruby 脚本也是 REST 风格的 Web 服务的客户机。Ruby 脚本 downloadUsers.rb 发送一个 HTTP GET 请求给服务器,后者以 JSON 格式返回用户数据,而 uploadUsers.rb 发送一个 HTTP PUT 请求(用户数据包含在 HTTP 主体中)给服务器。数据格式是每行一个用户。在各行中,各用户的属性由竖线分隔 (|)。

与浏览器请求处理程序相似,资源请求处理程序提供了一个到不同客户机以及业务逻辑层所处理的请求的接口。而业务逻辑层则请求数据访问层处理数据持久性。


结束语

越来越多的现代 Web 应用程序都需要提供一个丰富的界面以及 REST 风格的 Web 服务,以便客户机可以自动化流程。本文阐述了如何使用“用于构建 REST 风格的 Web 服务的多层体系结构”(见 参考资料)一文中所讨论的多层体系结构来构建动态 Web 应用程序和 REST 风格的 Web 服务。本文还描述了如何结合 Ajax 和 REST 风格的 Web 服务来创建类似桌面的、快速响应的富界面。它使用了 Eclipse、Jersey、Spring MVC 框架、Spring Web Flow、Spring JBDC 框架和 MySQL。Ruby 脚本用作 REST 风格的 Web 服务的客户机。

National Science Foundation 的研究支持是本文的基础(依照 University Corporation for Atmospheric Research 合作协议)。National Center for Atmospheric Research 由 National Science Foundation 提供支持。另外,我要感谢来自 NCAR 的 Markus Stobbs,他为应用程序的选择以及文件的编辑提供了宝贵的建议。


下载

描述名字大小
源代码ncarUsers.zip5987KB

参考资料

学习

获得产品和技术

条评论

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=Web development, SOA and web services
ArticleID=419508
ArticleTitle=使用多层体系结构构建 REST 风格的 Web 服务和动态 Web 应用程序
publish-date=08102009