將 Spring Web MVC 應用程式從 JSP 遷移到 AngularJS

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

作者說明

這篇文章是由 Han LimTony Nguyen 客座撰寫。Han 和 Tony 在我們新加坡 Spring User Group 的 Spring + Angular JS 發表了精彩的演講。這篇部落格文章是基於他們的演講。

摘要

在本文中,我們嘗試描述我們從伺服器端渲染視圖技術(如 JSP、Struts 和 Velocity)轉移到使用 AngularJS(一種適用於現代瀏覽器的流行 Javascript 框架)的客戶端渲染視圖技術的經驗。我們將討論在進行此變更時需要注意的一些事項以及可能遇到的潛在陷阱。如果您在 Spring Web MVC 和 JSP 開發方面擁有豐富的經驗,並且想了解 Spring MVC 如何與客戶端 Javascript(如 AngularJS)協同工作,那麼這篇文章可能正適合您。

還有一個附錄,其中提供了一些關於 AngularJS 的額外見解,這些見解對於來自 JSP 世界的人來說可能看起來很奇怪或不熟悉。

參考範例 Petclinic

我們建立了一個 Spring Petclinic 應用程式的分支,並嘗試將其轉換為 AngularJS(採用 Andrew Abogado 設計的新設計)。我們的分支可以在這裡找到。

準備

當您開始從 JSP 或 Thymeleaf 等伺服器端範本引擎遷移到客戶端基於 Javascript 的範本引擎時,您需要採用範例轉移到客戶端-伺服器架構。 您必須停止將視圖視為 Web 應用程式的一部分,而是將 Web 應用程式設想為 2 個獨立的客戶端和伺服器端應用程式。 因此,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

此處的資源是按照 feature-grouping 方法組織的。 還有一些方法可以根據類型對資源進行分組,例如將所有控制器、服務和視圖分組到其同名資料夾中。 每個選項都有優點和缺點。

還有一些基於 Javascript 的套件管理器,例如 npmbower,您可能需要考慮使用它們來簡化外部依賴項的管理。 如果您使用 bower,您將建立一個名為 bower_components 的資料夾,所有依賴資源都將安裝在其中。 然後,您需要將它們包含在範本中,就像對任何 Javascript 庫所做的那樣。 至於 npm,您可以使用它來管理所有 Javascript 伺服器端系統工具,例如 Grunt(一種類似 Ant 的任務運行程式)

使用 AngularJS 指令 vs 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 Controllers 轉換為 RESTful Services

你需要轉換你的 controllers,不再將回應轉發到樣板引擎以將 view 渲染到客戶端,而是提供將序列化為 JSON 數據的服務。以下是一個標準 Spring MVC controller RequestMapping 如何使用 ModelAndView 物件來渲染 view,並在 url mapping 中描述 Owner 的範例。

@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;
}

像這樣的 controller RequestMapping 可以轉換為等效的 RESTful 服務,該服務根據 ownerId 回傳 owner。然後,你可以將你的 template 移到 AngularJS 中,該 AngularJS 會將 owner 物件綁定到 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 Context xml 檔案中新增了 xml 片段,以描述 JSON ObjectMapper Factory 的日期格式,以便它知道 Jackson2 ObjectMapper 需要這種格式的日期。你可以在下面看到執行此 Spring context 配置的程式碼片段。如果沒有對日期格式(或任何其他序列化需求)進行自定義,你可以使用預設的,這意味著你甚至不需要包含這個部分,因為 Spring MVC 預設會 component scan ObjectMapper 並透過 autowiring 將其注入到你的 controller 類別中。

<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>

一旦你將 controllers 轉換為 RESTful 服務,你就可以從你的 AngularJS 應用程式存取這些資源。

在 AngularJS 中存取 RESTful 服務的一個好方法是使用內建的 ngResource 指令,它允許你以優雅且簡潔的方式存取 RESTful 服務。以下程式碼片段說明了如何使用此指令來存取 RESTful 服務。

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 服務來建立「資源」。然後,controller 可以使用此服務從 RESTful endpoint 查詢 Owners。透過這種方式,你可以輕鬆建立你的應用程式需要的資源,並將其輕鬆對應到你的 business domain model。此宣告只需在 app.js 檔案中完成一次。你實際上可以查看此檔案的實際運作情況 這裡

在遷移到 RestAPI 時,重要的是要記住,RestAPI 是公共介面,而不是網站內容。JSON 模型對使用者是完全可見的。例如,如果我們需要顯示使用者個人資料,則應該在 JSON 物件上完成密碼遮罩,而不是在樣板中。為了做到這一點,有時我們需要為我們的 RestAPI 建立 DTO 物件。

在後端和你的 AngularJS 應用程式之間同步狀態

當你開發客戶端-伺服器架構時,同步狀態是需要管理的事情。你需要考慮你的應用程式如何從後端更新其狀態,或在某些狀態變更時刷新其 view。

身份驗證

將你的客戶端程式碼暴露給公眾,使得思考你將如何驗證你的使用者身份以及維護與你的應用程式的 session 變得更加重要。在決定你的身份驗證方法時,一個重要的考慮因素是根據你的應用程式架構,選擇有狀態 session 或無狀態 session。

你可以查看 Dave Syer 的一系列部落格文章,了解如何將 AngularJS 與 Spring Security 整合 這裡

測試

AngularJS 提供了必要的工具來幫助你在 Javascript 開發的所有層級執行測試,從單元測試到功能測試。規劃你如何測試以及執行包含這些測試的建置,將決定你的前端客戶端的品質。我們使用一個名為 frontend-maven-plugin 的 maven 外掛程式來協助我們進行建置測試。

結論

從 JSP 遷移到 AngularJS 可能看起來令人望而卻步,但從長遠來看,它可以非常有益,因為它可以使使用者介面更易於維護和測試。客戶端渲染 view 的趨勢也鼓勵構建更具回應性的 Web 應用程式,而這些應用程式以前受到伺服器端渲染設計的阻礙。HTML 5 和 CSS3 的出現引領我們進入了 View 渲染技術的新時代,其中有不同的競爭框架,如 EmberJs、ReactJs、BackboneJs 等。然而,就勢頭而言,AngularJS 一直受到很多關注,並且在使用了一段時間後,我們就可以明白為什麼。我們希望本文包含對打算採取行動的人們有用的提示。你可以檢查 Spring Petclinic 的 fork,其中包含一些程式碼範例,以了解我們是如何做到的。

附錄

AngularJS 簡介

AngularJS 是 Google 創建的一個 Javascript 框架,它將自己標榜為「Superheroic Web MVW Framework」(其中「MVW」中的「W」是對所有各種 MVx 架構的一種玩笑說法)。由於它基於 MVx 架構,AngularJS 為 Javascript 開發提供了一個結構,因此與傳統的 Spring + JSP 應用程式相比,它賦予了 Javascript 更高的地位,後者僅使用 Javascript 在使用者介面上提供一些互動性。

透過 AngularJS,你的基於 Javascript 的 view 層也繼承了諸如 Dependency-Injection、HTML-vocabulary 擴展(透過使用自定義指令)、單元測試和功能測試整合以及 DOM-selectors ala JQuery(使用 jqlite,因為它僅提供 JQuery 的一個子集,但如果你願意,你也可以輕鬆使用 JQuery)等功能。AngularJS 還為你的 Javascript 程式碼引入了 scopes,以便你在程式碼中宣告的變數僅綁定到所需的 scope。這可以防止當你的 Javascript 大小增長時無意中產生的變數污染。

當你使用 JSP 開發 Spring Web MVC 應用程式時,你可能會使用 Spring 提供的 form 標籤將你的 form 輸入綁定到伺服器端模型。同樣,AngularJS 提供了一種將 form 輸入綁定到客戶端模型的方式。事實上,它提供了從 form 輸入到 Javascript 應用程式上的模型即時的雙向資料綁定。這意味著你不僅可以享受 view 透過 Javascript 模型中的變更進行更新的好處,而且你對 UI 進行的任何變更也會更新 Javascript 模型(以及因此綁定到該模型的任何其他 view)。看到綁定到應用程式上相同 JS 模型的所有 view 自動更新模型幾乎是神奇的。

此外,由於您的模型可以設定為特定的作用範圍,因此只會影響屬於相同作用範圍的檢視,讓您可以對只應限於檢視特定部分的程式碼進行沙箱化。(這是透過名為 ng-controller 的 AngularJS 屬性完成的,該屬性是在您的 HTML 範本中設定)。您可以在後面的章節中看到 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 的模型的資訊的標記替換標記元素 <wds-custom company="wds">。該模型元素是在包裝指令的控制器作用範圍中宣告的。您可以查看檔案 app.jsindex.html 和指令範本 wds-custom-directive.html,以查看此程式碼如何在 這裡 提供的 plunkr 片段中工作。

由於本文不試圖教您如何編寫指令,您可以參考官方文件 這裡

取得 Spring 電子報

與 Spring 電子報保持聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

獲得支援

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部