Статьи

Миграция Spring Web MVC-приложения из JSP в AngularJS

Этот пост является гостевым постом Хан Лим и Тони Нгуен . Хан и Тони провели отличную презентацию в нашей группе пользователей Spring Spring на Spring + Angular JS. Этот блог основан на их презентации.

Аннотация

В этой статье мы попытаемся описать наш опыт перехода от технологий отображения на стороне сервера, таких как JSP, Struts и Velocity, к технологиям отображения на стороне клиента с использованием AngularJS, популярной платформы JavaScript для современных браузеров. Мы поговорим о некоторых вещах, на которые следует обратить внимание при внесении этих изменений, и о возможных подводных камнях, с которыми вы можете столкнуться. Если вы имеете опыт разработки Spring Web MVC и JSP и хотите узнать, как Spring MVC может работать вместе с клиентским JavaScript, таким как AngularJS, эта статья может быть именно для вас.

Существует также приложение, которое дает некоторые дополнительные сведения об AngularJS, которые могут показаться странными или незнакомыми людям из мира JSP.

Образец Petclinic для справки

Мы создали форк приложения Spring Petclinic и экспериментировали с его преобразованием в AngularJS (с новым дизайном, предоставленным Эндрю Абогадо ). Нашу вилку можно найти здесь .

подготовка

Когда вы начнете переходить с серверного шаблонизатора на стороне клиента, такого как JSP или Thymeleaf, на шаблонизатор на основе JavaScript на стороне клиента, вам нужно будет перейти от парадигмы к архитектуре клиент-сервер. Вы должны перестать думать о представлении как о части веб-приложения и вместо этого воспринимать веб-приложение как 2 отдельных приложения на стороне клиента и на стороне сервера. Таким образом, приложение AngularJS становится самостоятельным приложением, которое запускается в вашем веб-браузере и взаимодействует с внутренними службами, предоставляемыми Spring MVC. Единственное сходство между приложением Spring MVC и AngularJS, вероятно, заключается в том, что они развернуты в одном и том же файле WAR WAR Java и что индексный файл обслуживается из JSP.

Иллюстрацией этого является приведенная ниже диаграмма, на которой показано, как приложение Spring становится поставщиком веб-сервисов RESTful, обслуживая различные интерфейсные приложения, включая приложение на основе браузера AngularJS, а также предоставляет возможность предоставлять услуги для мобильных клиентов, таких как планшеты или смартфоны. , Эти сервисы могут включать OAuth, Authentication и другие сервисы бизнес-логики, которые следует скрыть от общественности. Следует иметь в виду, что любые данные или бизнес-логика, которые публикуются в форме файлов JSON или JavaScript, открыты для просмотра на стороне клиента. Таким образом, если существует какая-либо бизнес-чувствительная логика или рабочий процесс, который не должен быть представлен, он должен выполняться только на бэкэнде.

Еще одно отличие, которое следует отметить при использовании AngularJS вместо JSP, заключается в том, что мы предпочли бы не использовать формы HTML и традиционные представления форм для передачи данных на серверную часть. Вместо этого мы предпочитаем инкапсулировать отправленные формы в объект JSON, который отправляется в бэкэнд-сервис RESTful через вызов метода AngularJS HTTP Post. На самом деле, мы предпочитаем использовать полный спектр HTTP-глаголов, которые поощряются при разработке сервисов RESTful.

Если вам нужно выполнить проверку пользовательских входов, это можно сделать на внешнем интерфейсе, используя встроенную проверку AngularJS или собственную пользовательскую проверку ввода. Вы всегда должны проверять свои данные, прежде чем публиковать их на сервере. Целесообразно также проверять те же данные на стороне сервера, чтобы гарантировать, что клиенты, которые не проверяют свои данные, не нарушат целостность данных на стороне сервера.

Архитектура

Структура приложения

Давайте теперь обсудим, как вы можете организовать свое приложение Spring + AngularJS. В WDS (наша компания) мы используем Maven в качестве инструмента управления зависимостями и пакетами для Java / Spring, и это повлияло на то, как мы решили разместить наше JavaScript-приложение AngularJS. Приложение 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 ниже.

папки

Ресурсы здесь организованы по feature-groupingметоду. Есть также способы сгруппировать ваши ресурсы по типам, например, сгруппировать все ваши контроллеры, сервисы и представления в его одноименную папку. Есть плюсы и минусы для каждого из этих вариантов.

Есть также некоторые менеджеры пакетов на основе JavaScript, такие как npm или bower, которые вы можете использовать для упрощения управления внешними зависимостями. Если вы используете bower , у вас будет создана папка bower_components, в которую будут установлены все ресурсы зависимости. Затем вам нужно будет включить их в ваши шаблоны, как если бы вы делали это для любой библиотеки JavaScript. Что касается npm , вы можете использовать его для управления всеми вашими системными инструментами на стороне сервера JavaScript, такими как Grunt (своего рода Ant- like runner)

Использование AngularJS директив против пользовательских тегов JSP

Если вы использовали собственные теги форм Spring в своих JSP для разработки форм, вам может быть интересно, предоставляет ли 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. Ниже приведен пример того, как стандартный контроллер Spring MVC RequestMappingиспользует ModelAndViewобъект для визуализации представления с владельцем, как описано в отображении URL.

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

@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, поэтому мы добавили фрагмент XML в наш XML-файл Spring Context, чтобы описать формат даты для нашей фабрики 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.

Хороший способ получить доступ к сервисам RESTful в AngularJS — это использовать встроенную ngResourceдирективу, которая позволяет вам получить доступ к вашим сервисам RESTful элегантно и лаконично. Пример кода JavaScript для доступа к службам 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();
}];

Приведенный выше фрагмент кода показывает, как можно создать «ресурс», объявив ресурс «Владелец», а затем инициализировав его как службу «Владелец». Затем контроллер может использовать эту службу для запроса владельцев с конечной точки RESTful. Таким образом, вы можете легко создавать ресурсы, которые требуются вашему приложению, и легко сопоставлять их с моделью вашей бизнес-области. Это объявление выполняется только один раз в файле app.js. Вы можете посмотреть на этот файл в действии здесь .

При переходе на RestAPI важно помнить, что RestAPI является общедоступным интерфейсом, а не содержимым веб-сайта. Модель JSON полностью видна пользователям.

Например, если нам нужно отобразить профили пользователей, маскирование паролей должно выполняться для объекта JSON, а не в шаблоне. Чтобы сделать это, иногда нам нужно создавать объекты DTO для нашего RestAPI.

Синхронизация состояний между бэкендом и вашим приложением AngularJS

Синхронизация состояний — это то, чем нужно управлять при разработке клиент-серверной архитектуры. Вам нужно будет подумать о том, как ваше приложение обновляет свое состояние из серверной части или обновлять свое представление при изменении какого-либо состояния.

Аутентификация

Благодаря общедоступному клиентскому коду становится еще важнее продумать, как вы хотите аутентифицировать своих пользователей и поддерживать сеанс с вашим приложением. Одним из важных соображений при выборе метода аутентификации является выбор между сеансом с сохранением состояния или сеансом без сохранения состояния в зависимости от архитектуры вашего приложения.

Вы можете проверить серию блогов Дэйва Сайера о том, как интегрировать AngularJS с Spring Security здесь .

тестирование

AngularJS поставляется с необходимыми инструментами, которые помогут вам выполнить тестирование на всех уровнях разработки JavaScript от модульного до функционального тестирования. Планирование тестирования и выполнения сборок, включающих эти тесты, определит качество вашего клиентского интерфейса. Мы используем плагин maven, призванный frontend-maven-pluginпомочь нам в наших тестах сборки.

Вывод

Миграция на AngularJS из JSP может показаться сложной, но в долгосрочной перспективе она может быть очень полезной, поскольку она обеспечивает более удобный и проверяемый пользовательский интерфейс. Тенденция к отображаемым представлениям на стороне клиента также стимулирует создание более отзывчивых веб-приложений, которым ранее мешал дизайн в рендеринге на стороне сервера. Появление HTML 5 и CSS3 открыло нам новую эру в технологиях рендеринга View с различными конкурирующими средами, такими как EmberJs, ReactJs, BackboneJs и т. Д. Однако, с точки зрения импульса, AngularJS привлекает много внимания и использует это на некоторое время, мы можем понять, почему. Мы надеемся, что эта статья содержит полезные советы для людей, которые намерены сделать решающий шаг. Вы можете изучить форк Spring Petclinic, в котором есть несколько примеров кода, чтобы увидеть, как мы это сделали.

аппендикс

Краткое введение в AngularJS

AngularJS — это JavaScript-фреймворк, созданный в Google, который позиционируется как «Superheroic Web MVW Framework» (где «W» в «MVW» — это насмешливая ссылка на «что угодно» для всех различных архитектур MVx) . Основанный на архитектуре MVx, AngularJS обеспечивает структуру для разработки JavaScript и, таким образом, дает JavaScript повышенный статус по сравнению с традиционными приложениями Spring + JSP, которые используют JavaScript только для обеспечения этой интерактивности в пользовательском интерфейсе.

С AngularJS ваш слой представлений на основе JavaScript также наследует такие функции, как Dependency-Injection, расширение HTML-словаря (посредством использования пользовательских директив), интеграция модульного тестирования и функционального тестирования, а также селекторы DOM ala JQuery (используя в качестве jqlite его предоставляет только часть JQuery, но вы также можете легко использовать JQuery, если хотите). AngularJS также вводит области действия в ваш код JavaScript, так что переменные, объявленные в вашем коде, привязываются только к необходимой области. Это предотвращает загрязнение переменных, которое непреднамеренно возникает при увеличении размера вашего JavaScript.

При разработке приложения Spring Web MVC с использованием JSP вы, скорее всего, будете использовать предоставленные Spring теги формы, чтобы связать входные данные формы с моделью на стороне сервера. Аналогично, AngularJS предоставляет способ привязки входных данных формы к моделям на стороне клиента. Фактически, он обеспечивает мгновенную двустороннюю привязку данных от ввода формы к вашей модели в приложении JavaScript. Это означает, что вы не только получаете преимущества от обновления вашего представления с изменениями в вашей модели JavaScript, любые изменения, которые вы вносите в свой пользовательский интерфейс, также будут обновлять модель JavaScript (и, следовательно, любые другие представления, связанные с этой моделью). Почти волшебно видеть, что все представления, привязанные к одной и той же модели JS в приложении, автоматически обновляют модель.

Более того, поскольку ваша модель может быть настроена на определенную область, будут затронуты только представления, принадлежащие к одной и той же области, что позволит вам помещать в песочницу код, который должен быть локальным только для определенной части вашего представления. (Это делается с помощью атрибута AngularJS, ng-controllerкоторый задается в ваших шаблонах HTML). Вы можете увидеть разницу в следующем разделе, сравнивающем теги JSP и директивы AngularJS.

Двухстороннее связывание данных

В веб-приложении Spring-JSP существует один способ привязки данных из модели Spring в представление jsp. Любое изменение модели будет отражено в представлении Jsp, но не наоборот. Это природа веб-приложений. Если мы создаем настольное приложение, можно сделать обратную привязку данных с помощью Swing UI.

Однако для веб-приложения, предоставляющего ресурсы REST, прямой привязки данных может не быть. Данные отправляются с сервера в браузер в виде объектов JSON. Без AngularJS и тому подобного разработчики должны написать код JavaScript, чтобы связать объект JavaScript с элементами управления html.

Поскольку ручное связывание данных является утомительной задачей, некоторые разработчики пытаются автоматизировать задачу, создавая среду JavaScript для связывания данных. Стоит помнить, что эта привязка данных происходит на стороне клиента, и модель привязки данных является объектом JavaScript, а не моделью на стороне сервера.

Angular продвигает эту идею дальше, создавая двустороннюю привязку. Изменение значений в элементе управления HTML будет отражено в объекте в режиме реального времени.

Сфера

Связывание является полезной концепцией, если вам нужно иметь дело со сложными компонентами пользовательского интерфейса, такими как таблицы AJAX.

For example: we need to render a list of users and roles in an AngularJs application, with the following html template:

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

The code to add a user can be this simple:

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

If the array users has one more element, the table will automatically have one more row.

AngularJS Templates

Using AngularJS, it is possible to write relatively complex User Interfaces in an organized and elegant manner, always encapsulating the required logic within your components and never running the risk of errant global JavaScript variables polluting your scope. It is also very testable, and there are built-in mechanisms to perform tests at the unit and functional level, ensuring that your User Interface codebase goes through the same rigorous testing that your Java/Spring code undergoes, ensuring quality even at the user interface level.

Another advantage of using AngularJS to write your html templates is that the templates are essentially similar to html even with the various front end logic baked into your view. It is possible to incorporate AngularJS logic into your template and still do a client-side validation control. In the JSP world, you can try viewing a JSP file from a browser with all the template logic in place and most likely your browser will give up rendering the page.

You can see how a typical AngularJS template looks like :

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

You can probably spot some non-HTML additions to the template. It includes attributes like data-ng-click which maps a click on a button to a method name call. There’s also data-ng-repeat which loops through a JSON array and generates the necessary html code to render the same view for each item in the array. Yet with all the logic in place, we are still able to validate and view the html template from the browser.
AngularJS calls all the non-html tags and attributes “directives” and the purpose of these directives is to enhance the capabilities of HTML. AngularJS also supports both HTML 4 and 5 so if you have templates that are still relying on HTML 4 DOCTYPEs, it should still work fine (although the validators for HTML 4 will not recognize data-ng-x attributes).

One big difference between using AngularJS and JSP is the rendering time. If you use JSPs, the server renders html content. In contrast, if you use AngularJS, the rendering is happening in browser. Therefore, both the templates and JSON objects are to be sent to client side. It is worth to notice that AngularJS may briefly display the template before running DOM manipulation to generate content. For example, if AngularJS has not completed loaded, the date of birth in the page will be shown with an empty value before showing the real value.

Scopes in AngularJS

One important concept to grasp in AngularJS is that of scopes. In the past, whenever I had to write JavaScript for my web application, I had to manage the variable names and construct special name-spaced objects in order to store my scoped properties. However, AngularJS does it for you automatically based on its MVx concept. Every directive will inherit a scope from its controller (or if you would like, an isolated scope that does not inherit other scope properties). The properties and variables created in this scope do not pollute the rest of the scopes or global context.

Scopes are used as the “glue” of an AngularJS application. Controllers in AngularJS use scopes to interact with the views. Scopes are also used to pass models and properties between directives and controllers. The advantage of this is that we are now forced to design our application in a way that components are self-contained and relationships between components have to be considered carefully through a use of a model that can be prototypically inherited from a parent scope.

A scope can be nested in another scope prototypically in the same way JavaScript implements its inheritance model via prototyping. However, any property name that is declared in the child scope that is similar to the parent will hide the parent property from the child scope thereafter. An example of this can be described in the code below:

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

At the very top in the hierarchy of scopes is the $rootScope, a scope that is accessible globally and can be used as the last resort to share properties and models across the whole application. The use of this should be minimized as it introduces a sort of “global” variable that can pose the same problems when it is overused.

More information about scopes can be gleaned from the AngularJS documentation found here.

Directives in AngularJS

Directives are one of the most important concepts in AngularJS. They bring all the additional customized markup in the HTML elements, attributes, classes or comments. They are the ones giving the markup new functionalities.

The following code snippet demonstrates a customized directive called wdsCustom that will replace the markup element <wds-custom company="wds"> with markup that contains information about a model called wds. That model element is declared in the controller scope that wraps the directive. You can have a look at the files app.js, index.html and directive template wds-custom-directive.html to see how this works in the plunkr snippet available here.

As this article does not attempt to teach you how to write a directive, you can refer to the official documentation here.