[Перевод] Архитектура веб-приложений. Стек Spring MVC + AngularJs
Здравствуйте, Хабр.
Ниже вашему вниманию предлагается перевод, посвященный разработке веб-приложений. Описываемый автором стек демонстрирует интересную возможность комбинации 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 с этим приложением.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.