将 Spring Web MVC 应用程序从 JSP 迁移到 AngularJS

工程 | Michael Isvy | 2015年8月19日 | ...

关于作者的说明

这篇文章是由 Han LimTony Nguyen 撰写的客座文章。Han 和 Tony 在我们的新加坡 Spring 用户组上做了关于 Spring + Angular JS 的精彩演讲。这篇博文基于他们的演讲。

摘要

在本文中,我们尝试描述将服务器端渲染视图技术(如 JSP、Struts 和 Velocity)迁移到使用 AngularJS(一个流行的现代浏览器 Javascript 框架)的客户端渲染视图技术的经验。我们将讨论在进行此更改时需要注意的一些事项以及可能遇到的潜在陷阱。如果您精通 Spring Web MVC 和 JSP 开发,并且想了解 Spring MVC 如何与 AngularJS 等客户端 Javascript 协同工作,那么本文可能适合您。

还有一个附录,提供了关于 AngularJS 的一些额外见解,这些见解对于来自 JSP 世界的人来说可能显得奇怪或不熟悉。

参考示例宠物诊所

我们创建了 Spring Petclinic 应用程序的一个分支,并尝试将其转换为 AngularJS(由 Andrew Abogado 提供新的设计)。我们的分支可以在这里找到 这里

准备

当您开始从 JSP 或 Thymeleaf 等服务器端模板引擎迁移到客户端的基于 Javascript 的模板引擎时,您需要采用向客户端-服务器架构转变的范式。您必须停止将视图视为 Web 应用程序的一部分,而是将 Web 应用程序视为两个独立的客户端和服务器端应用程序。因此,AngularJS 应用程序成为在 Web 浏览器上运行的独立应用程序,并与 Spring MVC 提供的后端服务进行通信。Spring MVC 应用程序和 AngularJS 之间的唯一共同点可能是它们部署在同一个 Java WAR 文件中,并且索引文件是从 JSP 提供的。

下图对此进行了说明,该图显示了 Spring 应用程序如何成为 RESTful Web 服务的提供者,为各种前端应用程序提供服务,包括基于 AngularJS 的浏览器应用程序,以及为平板电脑或智能手机等移动客户端提供服务的可能性。这些服务可能包括 OAuth、身份验证和其他业务逻辑服务,这些服务应与公众视图隔离。应该记住,以 JSON 或 javascript 文件形式发布的任何数据或业务逻辑都暴露给客户端查看。因此,如果存在任何不应公开的业务敏感逻辑或工作流,则应仅在后端执行。

使用 AngularJS 而不是 JSP 需要注意的另一个区别是,我们宁愿不使用 HTML 表单和传统的表单提交将数据传递到服务器端。相反,我们更愿意将表单提交封装在 JSON 对象中,该对象通过 AngularJS HTTP Post 方法调用发送到后端 RESTful 服务。事实上,我们更愿意使用开发 RESTful 服务中鼓励的全部 HTTP 动词。

如果您需要对用户输入执行验证,可以在前端使用 AngularJS 的内置验证或您自己的自定义输入验证来完成。在将数据发布到服务器之前,您应该始终验证数据。谨慎起见,也应在服务器端验证相同的数据,以确保未检查其数据的客户端不会损害服务器端数据的完整性。

Architecture

应用程序结构

现在让我们讨论如何组织您的 Spring + AngularJS 应用程序。在 WDS(我们公司)中,我们使用 Maven 作为 Java/Spring 的依赖项和包管理工具,这影响了我们决定放置 AngularJS javascript 应用程序的方式。AngularJS 应用程序是在 src/main/webapp 中创建的,主要文件是

components/ # the various components are stored here.
js/app.js   # where we bootstrap the application
plugins/	# additional external plugins e.g. jquery.
services/   # common services are stored here.
images/
videos/

您可以在下面的 Eclipse 中看到文件夹结构的图像捕获。

folders

此处的资源根据 功能分组 方法进行组织。还有其他方法可以根据类型对资源进行分组,例如将所有控制器、服务和视图分组到其同名文件夹中。每个选项都有其优缺点。

还有一些基于 Javascript 的包管理器,如 npmbower,您可以考虑使用它们来简化外部依赖项的管理。如果您使用的是 bower,则会创建一个名为 bower_components 的文件夹,所有依赖项资源都将安装在此文件夹中。然后,您需要像对待任何 Javascript 库一样在模板中包含它们。对于 npm,您可以使用它来管理所有 Javascript 服务器端系统工具,如 Grunt(一种类似于 Ant 的任务运行器)

使用 AngularJS 指令与 JSP 自定义标签

如果您在 JSP 中使用过 Spring 的自定义表单标签来开发表单,您可能想知道 AngularJS 是否提供了相同类型的便利来将表单输入映射到对象。答案是肯定的!事实上,将任何 HTML 元素绑定到 Javascript 对象很容易。唯一的区别是现在绑定发生在客户端而不是服务器端。

<form:form method="POST" commandName="user">
<table>
    <tr>
        <td>User Name :</td>
        <td><form:input path="name" /></td>
    </tr>
    <tr>
        <td>Password :</td>
        <td><form:password path="password" /></td>
    </tr>
    <tr>
        <td>Country :</td>
        <td>
            <form:select path="country">
            <form:option value="0" label="Select" />
            <form:options items="${countryList}" itemValue="countryId" itemLabel="countryName" />
            </form:select>
        </td>
    </tr>
</table>
</form:form>

以下是在 AngularJS 中使用相同表单的示例

<form name="UserForm" data-ng-controller="ExampleUserController">
  <table>
    <tr>
        <td>User Name :</td>
        <td><input data-ng-model="user.name" /></td>
    </tr>
    <tr>
        <td>Password :</td>
        <td><input type="password" data-ng-model="user.password" /></td>
    </tr>
    <tr>
        <td>Country :</td>
        <td>
            <select data-ng-model="user.country" data-ng-options="country as country.label for country in countries">
               <option value="">Select<option />
            </select>
        </td>
    </tr>
</table>
</form>

AngularJS 中的表单输入具有增强的功能,例如 ngRequired 指令,该指令根据某些条件使字段成为必填字段。还有用于检查范围、日期、模式等的内置验证。您可以在 AngularJS 的官方文档中找到更多信息 这里,其中提供了所有相关的表单输入指令。

从 JSP 迁移到 AngularJS 时需要考虑的事项

为了成功地将基于 JSP 的应用程序迁移到使用 AngularJS 的应用程序,需要考虑一些因素。

将 Spring 控制器转换为 RESTful 服务

您需要转换控制器,以便它们不再将响应转发到模板引擎以呈现视图到客户端,而是提供将序列化为 JSON 数据的服务。以下是如何使用 ModelAndView 对象呈现具有 url 映射中所述所有者的视图的标准 Spring MVC 控制器 RequestMapping 的示例。

@RequestMapping("/api/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
    ModelAndView mav = new ModelAndView("owners/ownerDetails");
    mav.addObject(this.clinicService.findOwnerById(ownerId));
    return mav;
}

这样的控制器 RequestMapping 可以转换为等效的 RESTful 服务,该服务根据 ownerId 返回所有者。然后,您的模板可以移动到 AngularJS 中,AngularJS 将所有者对象绑定到 AngularJS 模板。

@RequestMapping(value = "/api/owners/{id}", method = RequestMethod.GET)
public @ResponseBody Owner find(@PathVariable Integer id) {
    return this.clinicService.findOwnerById(id);
}

为了让 Spring MVC 将返回的对象(需要实现 Serializable 接口)转换为 JSON 对象,您可以使用 Jackson2 序列化库,该库是 Spring MVC 依赖项的一部分。在下面的示例中,我们必须通过 Jackson2 自定义日期序列化格式,因此我们在 Spring 上下文 XML 文件中添加了 XML 代码片段来描述 JSON ObjectMapper 工厂的日期格式,以便它知道 Jackson2 ObjectMapper 需要这种格式的日期。您可以在下面看到执行此 Spring 上下文配置的代码片段。如果没有自定义日期格式(或任何其他序列化要求),您可以使用默认的,这意味着您甚至不需要包含此部分,因为 Spring MVC 默认情况下会扫描 ObjectMapper 组件并通过自动装配将其注入到您的控制器类中。

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean" p:indentOutput="true" p:simpleDateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ"></bean>
<mvc:annotation-driven conversion-service="conversionService" >
 <mvc:message-converters>
  <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" >
   <property name="objectMapper" ref="objectMapper" />
  </bean>
 </mvc:message-converters>
</mvc:annotation-driven>

将您的控制器转换为 RESTful 服务后,您就可以从 AngularJS 应用程序访问这些资源了。

在 AngularJS 中访问 RESTful 服务的一种好方法是使用内置的 ngResource 指令,它允许您以优雅简洁的方式访问 RESTful 服务。以下示例说明了使用此指令访问 RESTful 服务的 Javascript 代码。

var Owner = ['$resource','context', function($resource, context) {
 return $resource(context + '/api/owners/:id');
}];
 
app.factory('Owner', Owner);
 
var OwnerController = ['$scope','$state','Owner',function($scope,$state,Owner) {
 $scope.$on('$viewContentLoaded', function(event){
  $('html, body').animate({
      scrollTop: $("#owners").offset().top
  }, 1000);
 });
 
 $scope.owners = Owner.query();
}];

以上代码片段展示了如何通过声明 Owner 资源并将其初始化为 Owner 服务来创建“资源”。然后,控制器可以使用此服务从 RESTful 端点查询 Owners。通过这种方式,您可以轻松创建应用程序所需的资源,并将其轻松映射到您的业务领域模型。此声明仅在 app.js 文件中执行一次。您实际上可以查看此实际文件 这里

迁移到 RestAPI 时,务必记住,RestAPI 是公共接口,而不是网站内容。JSON 模型对用户是**完全可见**的。例如,如果我们需要显示用户资料,则应在 JSON 对象而不是模板中进行密码屏蔽。为了做到这一点,有时我们需要为我们的 RestAPI 创建 DTO 对象。

后端与 AngularJS 应用程序之间状态同步

在开发客户端-服务器架构时,需要管理状态同步。您需要考虑应用程序如何从后端更新其状态,或者在某些状态发生变化时如何刷新其视图。

身份验证

将客户端代码公开会使您更需要认真思考如何对用户进行身份验证以及如何与应用程序维护会话。在决定身份验证方法时,一个重要的考虑因素是根据应用程序架构选择有状态会话还是无状态会话。

您可以查看 Dave Syer 关于如何将 AngularJS 与 Spring Security 集成的博客系列 这里

测试

AngularJS 提供了必要的工具来帮助您在 Javascript 开发的所有层级(从单元测试到功能测试)执行测试。规划您的测试方式以及包含这些测试的构建过程将决定前端客户端的质量。我们使用名为 frontend-maven-plugin 的 Maven 插件来协助我们的构建测试。

结论

从 JSP 迁移到 AngularJS 似乎令人生畏,但从长远来看,它可能非常有益,因为它可以使用户界面更易于维护和测试。向客户端渲染视图的趋势也鼓励构建更具响应性的 Web 应用程序,而这些应用程序以前受到服务器端渲染设计的阻碍。HTML 5 和 CSS3 的出现为我们 ushered in 了一个新的视图渲染技术时代,出现了不同的竞争框架,如 EmberJs、ReactJs、BackboneJs 等。然而,就发展势头而言,AngularJS 备受关注,并且在使用了一段时间后,我们了解了其原因。我们希望本文包含对打算尝试的人们有用的技巧。您可以检查 Spring Petclinic 的分支,其中包含一些代码示例,以了解我们是如何做到的。

附录

AngularJS 简介

AngularJS 是 Google 创建的一个 Javascript 框架,它自称为“超级英雄级 Web MVW 框架”(其中“MVW”中的“W”是对各种MVx 架构的戏谑引用,表示“无论什么”。由于它基于 MVx 架构,因此 AngularJS 为 Javascript 开发提供了结构,从而使 Javascript 相比于仅使用 Javascript 在用户界面上提供交互性的传统 Spring + JSP 应用程序具有更高的地位。

使用 AngularJS,您的基于 Javascript 的视图层还继承了诸如依赖注入、HTML 词汇扩展(通过使用自定义指令)、单元测试和功能测试集成以及类似 JQuery 的 DOM 选择器(使用 jqlite,因为它只提供 JQuery 的一个子集,但如果您愿意,也可以轻松使用 JQuery)等特性。AngularJS 还为您的 Javascript 代码引入了作用域,以便在代码中声明的变量仅绑定到所需的作用域。这可以防止 Javascript 代码规模增长时无意中出现的变量污染。

在使用 JSP 开发 Spring Web MVC 应用程序时,您可能会使用 Spring 提供的表单标签将表单输入绑定到服务器端模型。类似地,AngularJS 提供了一种将表单输入绑定到客户端模型的方法。事实上,它提供了从表单输入到 Javascript 应用程序模型的即时双向数据绑定。这意味着,您不仅可以利用视图根据 Javascript 模型中的更改更新的优势,而且您对 UI 的任何更改也会更新 Javascript 模型(以及因此绑定到该模型的任何其他视图)。看到绑定到应用程序上相同 JS 模型的所有视图自动更新模型,这几乎像是魔法一样。

此外,由于您的模型可以设置为特定作用域,因此只有属于相同作用域的视图才会受到影响,从而允许您隔离仅应属于视图特定部分的代码。(这是通过 HTML 模板中设置的 AngularJS 属性 ng-controller 完成的)。在后面比较 JSP 标签和 AngularJS 指令的部分中,您可以看到差异。

双向数据绑定

在 Spring-JSP Web 应用程序中,存在从 Spring 模型到 JSP 视图的单向数据绑定。模型的任何更改都将反映到 JSP 视图,但反之则不然。这是 Web 应用程序的特性。如果我们构建一个桌面应用程序,则可以使用 Swing UI 进行反向数据绑定。

但是,对于公开 REST 资源的 Web 应用程序,可能没有直接的数据绑定。数据以 JSON 对象的形式从服务器发送到浏览器。如果没有 AngularJS 等,开发人员需要编写 javascript 代码才能将 javascript 对象绑定到 html 控件。

由于手动数据绑定是一项繁琐的任务,一些开发者尝试通过创建 Javascript 数据绑定框架来自动化该任务。值得记住的是,这种数据绑定发生在客户端,并且数据绑定的模型是 Javascript 对象,而不是服务器端模型。

Angular 将此理念进一步扩展,创建了双向绑定。HTML 控件中值的更改将实时反映在对象中。

Scope

如果您需要处理复杂的 UI 组件(如 AJAX 表格),绑定是一个有用的概念。

例如:我们需要在一个 AngularJs 应用程序中渲染用户和角色列表,并使用以下 html 模板

<tr ng-repeat="user in users">
	<td>{{user.username}}</td>
	<td>{{user.role}}</td>
</tr>
...
<a ng-click="addUser()">Add new user</a>

添加用户的代码可以很简单

$scope.addUser = function(){
	newUser = {}
	$scope.users.push(newUser );
}

如果数组 users 多了一个元素,表格将自动多一行。

AngularJS 模板

使用 AngularJS,可以以一种组织化和优雅的方式编写相对复杂的用户界面,始终将所需的逻辑封装在组件中,并且永远不会冒错误的全局 Javascript 变量污染作用域的风险。它也非常易于测试,并且有内置机制在单元和功能级别执行测试,确保您的用户界面代码库经过与 Java/Spring 代码相同的严格测试,即使在用户界面级别也能保证质量。

使用 AngularJS 编写 html 模板的另一个优势是,即使将各种前端逻辑烘焙到视图中,模板本质上也类似于 html。可以将 AngularJS 逻辑整合到模板中,并仍然执行客户端验证控制。在 JSP 世界中,您可以尝试从浏览器查看包含所有模板逻辑的 JSP 文件,并且很可能您的浏览器将放弃渲染页面。您可以查看典型的 AngularJS 模板是什么样子

<div class="row thumbnail-wrapper">
  <div data-ng-repeat="pet in currentOwner.pets" class="col-md-3">
    <div class="thumbnail">
      <img data-ng-src="images/pets/pet{{pet.id % 10 + 1}}.jpg" 
        class="img-circle" alt="My Pet Image">
      <div class="caption">
        <h3 class="caption-heading" data-ng-bind="pet.name"></h3>
        <p class="caption-meta" data-ng-bind="pet.birthdate"></p>
        <p class="caption-meta"><span class="caption-label" 
           data-ng-bind="pet.type.name"></span></p>
      </div>
      <div class="action-bar">
        <a class="btn btn-default" data-toggle="modal" data-target="#petModal" 
          data-ng-click="editPet(pet.id)">
          <span class="glyphicon glyphicon-edit"></span> Edit Pet
        </a>
        <a class="btn btn-default">
          <span></span> Add Visit
        </a>
      </div>
    </div>
  </div>
</div>

您可能会发现模板中有一些非 HTML 添加内容。它包括像 data-ng-click 这样的属性,它将按钮上的点击映射到方法名称调用。还有 data-ng-repeat,它循环遍历 JSON 数组并生成渲染数组中每个项目的相同视图所需的 html 代码。但是,即使所有逻辑都到位,我们仍然能够从浏览器验证和查看 html 模板。AngularJS 将所有非 html 标签和属性称为“指令”,这些指令的目的是增强 HTML 的功能。AngularJS 还支持 HTML 4 和 5,因此如果您仍然依赖 HTML 4 DOCTYPE 的模板,它应该仍然可以正常工作(尽管 HTML 4 的验证器将无法识别 data-ng-x 属性)。

使用 AngularJS 和 JSP 之间的一个主要区别是 **渲染时间**。如果您使用 JSP,服务器会渲染 html 内容。相反,如果您使用 AngularJS,渲染则发生在浏览器中。因此,模板和 JSON 对象都将发送到客户端。值得注意的是,AngularJS 可能会在运行 DOM 操作生成内容之前短暂显示模板。例如,如果 AngularJS 尚未完成加载,页面中的出生日期将在显示真实值之前显示为空值。

AngularJS 中的作用域

在 AngularJS 中需要掌握的一个重要概念是作用域。过去,每当我需要为我的 Web 应用程序编写 Javascript 时,我都必须管理变量名称并构建特殊的命名空间对象以存储我的作用域属性。但是,AngularJS 基于其 MVx 概念自动为您执行此操作。每个指令都将从其控制器继承作用域(或者,如果您愿意,则是一个不继承其他作用域属性的隔离作用域)。在此作用域中创建的属性和变量不会污染其他作用域或全局上下文。

作用域用作 AngularJS 应用程序的“粘合剂”。AngularJS 中的控制器使用作用域与视图交互。作用域还用于在指令和控制器之间传递模型和属性。这样做的优点是我们现在被迫以一种组件自包含的方式设计应用程序,并且必须通过使用可以从父作用域原型继承的模型来仔细考虑组件之间的关系。

作用域可以以原型方式嵌套在另一个作用域中,就像 Javascript 通过原型实现其继承模型一样。但是,在子作用域中声明的任何与父作用域相似的属性名称都将从此以后隐藏父作用域中的父属性。下面代码中可以描述一个示例

<!DOCTYPE html>
<html>
  <head>
    <script data-require="angular.js@*" data-semver="1.4.0-rc.0" src="https://code.angularjs.org/1.4.0-rc.0/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body data-ng-app="demo">
    <h1>Scopes in AngularJS</h1>
    <div data-ng-controller="parentController">
      <div data-ng-controller="childController">
        <span>This is a demonstration of scopes</span>
        <div>
          Parent model: <span data-ng-bind="$parent.model.name"></span>
        </div>
        <div>
          Current model: <span data-ng-bind="model.name"></span>
        </div>
        <div>
          <button data-ng-click="updateModel()">Click me</button>
        </div>
      </div>
    </div>
  </body>
</html>

在作用域层次结构的最顶层是 $rootScope,一个全局可访问的作用域,可以用作在整个应用程序中共享属性和模型的最后手段。应尽量减少此用法,因为它引入了一种“全局”变量,过度使用时会带来同样的问题。

有关作用域的更多信息,可以从 AngularJS 文档中获取,请点击 这里

AngularJS 中的指令

指令是 AngularJS 中最重要的概念之一。它们将所有额外的自定义标记带到 HTML 元素、属性、类或注释中。它们是赋予标记新功能的因素。

以下代码片段演示了一个名为 wdsCustom 的自定义指令,它将替换标记元素 <wds-custom company="wds">,替换为包含有关名为 wds 的模型的信息的标记。该模型元素在包含指令的控制器作用域中声明。您可以查看文件 app.jsindex.html 和指令模板 wds-custom-directive.html,以了解此功能在可在此处获得的 plunkr 代码段 这里 中是如何工作的。

由于本文不尝试教您如何编写指令,您可以参考官方文档 这里

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部