Здравствуйте, Хабр.

Ниже вашему вниманию предлагается перевод, посвященный разработке веб-приложений. Описываемый автором стек демонстрирует интересную возможность комбинации Java и JavaScript, а также позволяет по-новому взглянуть на создание одностраничных веб-приложений.

При этом поинтересуемся, хотите ли вы увидеть на полке перевод следующих книг по Spring и AngularJS




Технологии Spring MVC и AngularJs вместе образуют по-настоящему продуктивный и привлекательный стек для разработки веб-приложений, в особенности таких, где требуется интенсивно работать с формами.

В этой статье будет рассмотрено, как построить именно такое приложение. Этот подход мы сравним с другими имеющимися вариантами. Полнофункциональное защищенное веб-приложение, написанное с применением Spring MVC/AngularJs находится в этом репозитории на GitHub.

Мы рассмотрим следующие вопросы:

  • Архитектура одностраничного веб-приложения Spring MVC + Angular
  • Как структурировать пользовательский веб-интерфейс при помощи Angular
  • Какие библиотеки JavaScript/CSS хорошо сочетаются с Angular
  • Как построить машинный интерфейс REST API с применением Spring MVC
  • Защита REST API при помощи Spring Security
  • Сравнение этого подхода с другими, где весь проект реализуется на Java?


Архитектура одностраничного веб-приложения Spring MVC + Angular

Приложения для корпоративной среды, предполагающие интенсивную работу с формами, лучше всего делать в виде одностраничных веб-приложений. Основная идея, выделяющая их на фоне более традиционных серверных архитектур — создание сервера в виде набора переиспользуемых REST-сервисов, не сохраняющих состояния. В контексте MVC важно изъять контроллер из машинного интерфейса и перенести его в браузер:



Клиент поддерживает шаблон MVC и содержит всю логику представления, разделяемую на уровень представления, уровень контроллера и уровень клиентских сервисов. Когда приложение будет запущено, клиент и сервер будут обмениваться только данными JSON.

Как реализуется машинный интерфейс?

Машинный интерфейс корпоративного приложения, обладающего клиентской частью, логично и удобно писать как REST API. Та же технология может использоваться для предоставления веб-сервисов сторонним приложениям. Зачастую в таких случаях исчезает необходимость в отдельном стеке веб-сервисов SOAP.

C точки зрения DDD модель предметной области остается на машинном интерфейсе, там же, где находятся уровень сервисов и уровень сохраняемости. По сети передаются лишь DTO, но не модель предметной области.

Как структурировать клиентскую часть веб-приложения при помощи Angular

Клиентская часть должна выстраиваться вокруг модели, относящейся к представлению (а не к предметной области). Здесь должна обрабатываться только логика представления, но не бизнес-логика. В клиентской части выделяется три уровня:

Уровень представления

Уровень представления состоит из HTML-шаблонов, таблиц CSS и директив Angular, соответствующих различным компонентам пользовательского интерфейса. Вот пример простого представления для формы входа:

<form ng-submit="onLogin()" name="form" novalidate="" ng-controller="LoginCtrl">  
    <fieldset>
    <legend>Log In</legend>
    <div class="form-field">
         <input type="text" ng-model="vm.username" name="username" required="" ng-minlength="6">
    <div class="form-field">
         <input type="password" ng-model="vm.password" name="password" required="" ng-minlength="6" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}">
    </div></div></fieldset>
    <button type="submit">Log In</button>
    <a href="/resources/public/new-user.html">New user?</a>
 </form>


Уровень управления (контроллера)

Уровень управления состоит из контроллеров Angular, склеивающий вместе данные, извлекаемые, соответственно, из машинного интерфейса и из представления. Контроллер инициализирует модель представления и определяет, как представление должно реагировать на изменения модели и наоборот:

angular.module('loginApp', ['common',  'editableTableWidgets'])  
    .controller('LoginCtrl', function ($scope, LoginService) {
 
        $scope.onLogin = function () {
            console.log('Attempting login with username ' + $scope.vm.username + ' and password ' + $scope.vm.password);
 
            if ($scope.form.$invalid) {
                return;
            }
 
            LoginService.login($scope.vm.userName, $scope.vm.password);
 
        };
 
    });


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

Уровень клиентских сервисов

В контроллеры Angular можно внедрить набор сервисов Angular, которые могут взаимодействовать с машинным интерфейсом:

angular.module('frontendServices', [])  
    .service('UserService', ['$http','$q', function($http, $q) {
        return {
            getUserInfo: function() {
                var deferred = $q.defer();
 
                $http.get('/user')
                    .then(function (response) {
                        if (response.status == 200) {
                            deferred.resolve(response.data);
                        }
                        else {
                            deferred.reject('Error retrieving user info');
                        }
                });
 
                return deferred.promise;
            }


Давайте рассмотрим, какие еще библиотеки нам понадобятся, чтобы запустить работу клиентской части.

Какие библиотеки JavaScript/CSS должны дополнять Angular?

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

  • Библиотека PureCSS от Yahoo. Она написана на чистом CSS, обеспечивает удобное оформление при помощи имеющихся в ней тем и весит всего 4k. Ее компонент Skin Builder позволяет с легкостью сгенерировать тему, исходя из основного цвета. Это решение из разряда BYOJ (Bring Your Own Javascript), помогающее писать код «в духе Angular».
  • Библиотека для операций с данными в стиле функционального программирования. Кажется, эта библиотека может похвастаться превосходной поддержкой и документацией, непревзойденной со времен lodash.


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

  • Удобно иметь систему модулей наподобие requirejs, но поскольку система модулей Angular не обрабатывает извлечения файлов, возникает определенное дублирование при объявлении зависимостей requirejs и модулей angular.
  • Angular-модуль CSRF, предотвращающий атаки, связанные с подделкой межсайтовых запросов.
  • Модуль интернационализации


Как построить машинный интерфейс REST API при помощи Spring MVC

Этот машинный интерфейс содержит обычные уровни:

  • Уровень маршрутизации: определяет, какие точки входа сервисов соответствуют конкретным HTTP URL, и как будут считываться параметры из HTTP-запроса
  • Уровень сервисов: содержит лишь бизнес-логику (например, обеспечивает валидацию), определяет область применения бизнес-транзакций
  • Уровень сохраняемости: Отображает базу данных на объекты предметной области, хранящиеся в памяти и наоборот


В настоящее время наилучшая конфигурация Spring MVC подразумевает только использование Java-конфигурации. Даже web.xml уже, в общем, не требуется. См. здесь пример полностью сконфигурированного приложения, где задействуется только Java.

Уровни сервисов и сохраняемости создаются по обычной модели DDD, поэтому давайте обратим внимание на уровень маршрутизации.

Уровень маршрутизации

Те же аннотации Spring MVC, что применяются для создания приложения JSP/Thymeleaf, также могут использоваться и при разработке REST API.

Большая разница заключается в том, что методы контроллера не возвращают объект String, который бы определял, какой шаблон представления следует отобразить. Вместо этого применяется аннотация@ResponseBody, указывающая, что возвращаемое значение метода контроллера должно непосредственно отображаться и стать телом отклика:

@ResponseBody
@ResponseStatus(HttpStatus.OK)
@RequestMapping(method = RequestMethod.GET)
public UserInfoDTO getUserInfo(Principal principal) {
    User user = userService.findUserByUsername(principal.getName());
    Long todaysCalories = userService.findTodaysCaloriesForUser(principal.getName());
 
    return user != null ? new UserInfoDTO(user.getUsername(), user.getMaxCaloriesPerDay(), todaysCalories) : null;
}


Если все методы класса должны аннотироваться @ResponseBody , то лучше снабдить весь класс аннотацией @RestController .

Если добавить библиотеку Jackson JSON, то возвращаемое значение метода будет преобразовываться непосредственно в JSON без какой-либо дальнейшей конфигурации. Кроме того, это значение можно преобразовать в XML или другие форматы, в зависимости от значения HTTP-заголовка Accept , указанного клиентом.

Здесь показана пара контроллеров, у которых сконфигурирована обработка ошибок.

Как защитить REST API при помощи Spring Security

Интерфейс REST API можно защитить при помощи конфигурации Spring Security Java. В данном случае целесообразно использовать форму входа с аутентификацией HTTP Basic в качестве резервного варианта, а также подключать защиту от CSRF и возможность жестко задавать, что все методы машинного интерфейса могут быть доступны только через HTTPS.

Таким образом, машина предложит пользователю форму для входа, а после успешного входа присвоит сеансовый cookie браузерным клиентам, но при этом будет работать и с другими клиентами, поддерживая откат к обычному HTTP в случаях, когда учетные данные будут передаваться при помощи HTTP-заголовка Authorization.

В соответствии с рекомендациями OWASP REST-сервисы можно программировать с минимальным сохранением состояния (вся информация о состоянии сервера ограничивается тем сеансовым cookie, который использовался для аутентификации). Это делается, чтобы не пересылать учетные данные по сети при каждом запросе.

Вот пример конфигурирования безопасности REST API:

http
      .authorizeRequests()
      .antMatchers("/resources/public/**").permitAll()
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .defaultSuccessUrl("/resources/calories-tracker.html")
      .loginProcessingUrl("/authenticate")
      .loginPage("/resources/public/login.html")
      .and()
      .httpBasic()
      .and()
      .logout()
      .logoutUrl("/logout");
 
  if ("true".equals(System.getProperty("httpsOnly"))) {
      LOGGER.info("launching the application in HTTPS-only mode");
      http.requiresChannel().anyRequest().requiresSecure();
  }  


Такая конфигурация учитывает аутентификацию лишь в контексте безопасности, при этом стратегия авторизации выбирается в зависимости от требований безопасности, предъявляемых API. Если вам нужен тонкий контроль над авторизацией, познакомьтесь со списками контроля доступа Spring Security ACLs и проверьте, подходят ли они для решения стоящей перед вами задачи.

Теперь сравним такой способ создания веб-приложений с другими распространенными подходами.

Сравнение стека Spring MVC/Angular с другими распространенными вариантами

Такой способ использования JavaScript в клиентской части и Java — в работе с базой данных упрощает рабочий процесс и повышает его продуктивность.

Когда машинный интерфейс уже работает, не требуется никаких специальных инструментов или плагинов, чтобы разогнать горячее развертывание в клиентской части на полную мощность: просто опубликуйте ресурсы на сервере при помощи IDE (например, нажмите Ctrl+F10 в IntelliJ) и обновите страницу в браузере.

Классы машинного интерфейса по-прежнему можно перезагрузить при помощи JRebel, но в клиентской части ничего отдельно делать не требуется. В принципе, можно выстроить всю клиентскую часть, сымитировав машинный интерфейс при помощи, скажем, json-server. В таком случае различные специалисты смогут параллельно разрабатывать клиентскую часть и машинный интерфейс, если это потребуется.

Повышение продуктивности или полностековая разработка?

По моему опыту, возможность непосредственно редактировать HTML/CSS без всяких уровней косвенности (см. например общее сравнение Angular с GWT и JSF) помогает снизить умственную нагрузку и не усложнять работу. Цикл разработки «отредактировать-сохранить-обновить» очень быстр и надежен, позволяет работать значительно продуктивнее.

Наибольший выигрыш в продуктивности достигается в случаях, когда одни и те же разработчики пишут как клиентскую часть на JavaScript, так и машинный интерфейс на Java, поскольку для реализации большинства возможностей обычно требуются одновременные изменения и там, и там.

Потенциальный недостаток данного подхода таков: эти разработчики должны знать и HTML, и CSS, и JavaScript, но в последние годы такая компетенция встречается все чаще.

Мой опыт подсказывает, что полностековая разработка позволяет реализовывать в клиентской части самые непростые кейсы за толику того времени, которое требуется на создание полномасштабного решения на Java (дни, а не недели), и такой рост продуктивности определенно оправдывает дополнительное обучение.

Выводы

Комбинация Spring MVC и Angular позволяет действительно по-новому взглянуть на разработку веб-приложений, связанных с интенсивным заполнением форм. Данный подход настолько повышает продуктивность, что к нему определенно следует присмотреться.

Отсутствие привязки к состоянию сервера между запросами (если не считать аутентификации с cookie) по определению избавляет нас от целого класса багов.

Дополнительно предлагаю ознакомиться на github с этим приложением.
AngularJS + Spring MVC

Проголосовало 239 человек. Воздержалось 47 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (9)


  1. Alexufo
    21.10.2015 10:27
    +5

    Ну как бе кто знает ангуляр, тот знает. Но в ожидании второй версии первый уж изучать совсем нет смысла. А второй изучать еще невозможно за отсутствием рабочих примеров :-)


    1. Tonkonozhenko
      22.10.2015 01:41

      Вполне логично, но возникает вопрос на чем писать новые приложений сейчас.


      1. Alexufo
        22.10.2015 01:44

        Есть предположение, что скорость тыбриния фишек среди фреймворков высока и возможно стоит посмотреть на ember.js


        1. Tonkonozhenko
          22.10.2015 01:46

          Около полугода назад игрался с ним, очень нравилось. Но работать в данный момент приходится с Angular 1.2, а переписать сильно затратно.


    1. FaNtAsY
      23.10.2015 14:54

      Но в ожидании второй версии первый уж изучать совсем нет смысла.

      Не могу согласиться. Это хоть и две версии одного продукта, но совершенно не совместимые. Следовательно можно рассматривать Angular 1.x и Angular 2.x как два разных фреймворка.


      1. Alexufo
        23.10.2015 15:00

        Так вопрос не в совместимости, а в том что ветке 1.4 разработчики сказали гудбай — переосмыслили всю архитектуру. А с веткой 2.0 история как с изучением китайского без китайского словаря. Поэтому и существует проблема, накой ломать мозг с заведомо отпущенном в свободное плавание кораблем, если есть 2.0, только 2.0 осваивать на данный момент невозможно из за отсутствия каких то примеров.
        И обратно не особо хочется и нового нет. И туда и сюда незя — вот и ангуляр :-)


        1. FaNtAsY
          23.10.2015 15:06

          ветке 1.4 разработчики сказали гудбай

          Если я правильно понимаю, ветка 1.4 будет поддерживаться в текущем стабильном состоянии. Т. е. писать на ней в production в принципе, можно — большинство подводных камней уже известно.

          накой ломать мозг с заведомо отпущенном в свободное плавание кораблем

          Для того, чтобы использовать Angular 1.x в настоящее время, пока нет 2.x

          А версию 2.0 имеет смысл осваивать как отдельный фреймворк, когда разработчики выпустят словарь :)


  1. ua9msn
    21.10.2015 14:47

    React, Redux, OpenData protocol, one endpoint API… Так много нового в этом мире WEB-а. Сегодня прилетел Марти МакФлай, вы с ним?


    1. AlexGousev
      21.10.2015 19:46

      Еще не прилетел. Он должен прилететь в 4 вечера по тихоокеанскому времени (Калифорния), что примерно будет завтра в 3 утра по МСК.