Это обещанное продолжение Java EE 7 с Angular JS — часть 1 . Это заняло больше времени, чем я ожидал (чтобы найти время для подготовки кода и публикации в блоге), но это, наконец, здесь!
Приложение
Исходное приложение в части 1 — это всего лишь простой список с нумерацией страниц и службой REST, которая подает данные списка.
В этом посте мы собираемся добавить возможности CRUD (создание, чтение, обновление, удаление), привязать сервисы REST для выполнения этих операций на стороне сервера и проверить данные.
Настройка
Настройка аналогична части 1 , но вот список для справки:
Код
Бэкэнд — Java EE 7
Серверная часть не требует много изменений. Поскольку нам нужна возможность создавать, читать, обновлять и удалять, нам необходимо добавить соответствующие методы в службу REST для выполнения этих операций:
PersonResource
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
package com.cortez.samples.javaee7angular.rest;import com.cortez.samples.javaee7angular.data.Person;import com.cortez.samples.javaee7angular.pagination.PaginatedListWrapper;import javax.ejb.Stateless;import javax.persistence.EntityManager;import javax.persistence.PersistenceContext;import javax.persistence.Query;import javax.ws.rs.*;import javax.ws.rs.core.Application;import javax.ws.rs.core.MediaType;import java.util.List;@Stateless@ApplicationPath("/resources")@Path("persons")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)public class PersonResource extends Application { @PersistenceContext private EntityManager entityManager; private Integer countPersons() { Query query = entityManager.createQuery("SELECT COUNT(p.id) FROM Person p"); return ((Long) query.getSingleResult()).intValue(); } @SuppressWarnings("unchecked") private List<Person> findPersons(int startPosition, int maxResults, String sortFields, String sortDirections) { Query query = entityManager.createQuery("SELECT p FROM Person p ORDER BY " + sortFields + " " + sortDirections); query.setFirstResult(startPosition); query.setMaxResults(maxResults); return query.getResultList(); } private PaginatedListWrapper<Person> findPersons(PaginatedListWrapper<Person> wrapper) { wrapper.setTotalResults(countPersons()); int start = (wrapper.getCurrentPage() - 1) * wrapper.getPageSize(); wrapper.setList(findPersons(start, wrapper.getPageSize(), wrapper.getSortFields(), wrapper.getSortDirections())); return wrapper; } @GET public PaginatedListWrapper<Person> listPersons(@DefaultValue("1") @QueryParam("page") Integer page, @DefaultValue("id") @QueryParam("sortFields") String sortFields, @DefaultValue("asc") @QueryParam("sortDirections") String sortDirections) { PaginatedListWrapper<Person> paginatedListWrapper = new PaginatedListWrapper<>(); paginatedListWrapper.setCurrentPage(page); paginatedListWrapper.setSortFields(sortFields); paginatedListWrapper.setSortDirections(sortDirections); paginatedListWrapper.setPageSize(10); return findPersons(paginatedListWrapper); } @GET @Path("{id}") public Person getPerson( @PathParam("id") Long id) { return entityManager.find(Person.class, id); } @POST public Person savePerson(Person person) { if (person.getId() == null) { Person personToSave = new Person(); personToSave.setName(person.getName()); personToSave.setDescription(person.getDescription()); personToSave.setImageUrl(person.getImageUrl()); entityManager.persist(person); } else { Person personToUpdate = getPerson(person.getId()); personToUpdate.setName(person.getName()); personToUpdate.setDescription(person.getDescription()); personToUpdate.setImageUrl(person.getImageUrl()); person = entityManager.merge(personToUpdate); } return person; } @DELETE @Path("{id}") public void deletePerson(@PathParam("id") Long id) { entityManager.remove(getPerson(id)); }} |
Код точно такой же, как обычный Java POJO, но использует аннотации Java EE для улучшения поведения. @ApplicationPath("/resources") и @Path("persons") предоставят службу REST по URL-адресу yourdomain/resources/persons @Path("persons") будет хостом, на котором выполняется приложение). @Consumes(MediaType.APPLICATION_JSON) и @Produces(MediaType.APPLICATION_JSON) принимают и форматируют запрос и ответ REST как JSON.
Для операций REST:
| Аннотация / HTTP метод | Java-метод | URL | Поведение |
|---|---|---|---|
@GET / GET |
listPersons | Http: // Имя_домена / ресурсы / чел | Возвращает нумерованный список из 10 человек. |
@GET / GET |
getPerson | Http: // имя_домена / ресурсы / чел / {ID} | Возвращает сущность Person по ее идентификатору. |
@POST / POST |
savePerson | Http: // Имя_домена / ресурсы / чел | Создает или обновляет человека. |
@DELETE / DELETE |
deletePerson | Http: // имя_домена / ресурсы / чел / {ID} | Удаляет сущность Person по ее идентификатору. |
URL, вызываемый для каждой операции, очень похож. Магия, позволяющая определить, какая операция должна быть вызвана, определяется в самом методе HTTP при отправке запроса. Проверьте определения метода HTTP .
Для getPerson и deletePerson обратите внимание, что мы добавили аннотацию @Path("{id}") которая определяет необязательный путь для вызова службы. Поскольку нам нужно знать, какой объект мы хотим получить или удалить, нам нужно как-то указать id . Это делается в URL-адресе службы, который необходимо вызвать, поэтому, если мы хотим удалить Person с идентификатором 1, мы будем вызывать http://yourdomain/resources/persons/1 с HTTP-методом DELETE.
Вот и все, что касается бэкэнда. Только 30 строк кода добавлено к старому сервису REST. Я также добавил новое свойство в объект Person для хранения ссылки на изображение с целью отображения аватара человека.
UI — угловой JS
Что касается пользовательского интерфейса, я решил разделить его на 3 секции: сетка, форма и секции сообщений обратной связи, каждая со своим собственным угловым контроллером. Сетка в основном такая же, как и в первой части , но она требует некоторых настроек для нового материала:
Сетка HTML
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
<!-- Specify a Angular controller script that binds Javascript variables to the grid.--><div class="grid" ng-controller="personsListController"> <div> <h3>List Persons</h3> </div> <!-- Binds the grid component to be displayed. --> <div class="gridStyle" ng-grid="gridOptions"></div> <!-- Bind the pagination component to be displayed. --> <pagination direction-links="true" boundary-links="true" total-items="persons.totalResults" items-per-page="persons.pageSize" ng-model="persons.currentPage" ng-change="refreshGrid()"> </pagination></div> |
Здесь нет ничего особенного. Практически так же, как часть 1 .
Сетчатый угловой контроллер
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
app.controller('personsListController', function ($scope, $rootScope, personService) { // Initialize required information: sorting, the first page to show and the grid options. $scope.sortInfo = {fields: ['id'], directions: ['asc']}; $scope.persons = {currentPage: 1}; $scope.gridOptions = { data: 'persons.list', useExternalSorting: true, sortInfo: $scope.sortInfo, columnDefs: [ { field: 'id', displayName: 'Id' }, { field: 'name', displayName: 'Name' }, { field: 'description', displayName: 'Description' }, { field: '', width: 30, cellTemplate: '<span class="glyphicon glyphicon-remove remove" ng-click="deleteRow(row)"></span>' } ], multiSelect: false, selectedItems: [], // Broadcasts an event when a row is selected, to signal the form that it needs to load the row data. afterSelectionChange: function (rowItem) { if (rowItem.selected) { $rootScope.$broadcast('personSelected', $scope.gridOptions.selectedItems[0].id); } } }; // Refresh the grid, calling the appropriate rest method. $scope.refreshGrid = function () { var listPersonsArgs = { page: $scope.persons.currentPage, sortFields: $scope.sortInfo.fields[0], sortDirections: $scope.sortInfo.directions[0] }; personService.get(listPersonsArgs, function (data) { $scope.persons = data; }) }; // Broadcast an event when an element in the grid is deleted. No real deletion is perfomed at this point. $scope.deleteRow = function (row) { $rootScope.$broadcast('deletePerson', row.entity.id); }; // Watch the sortInfo variable. If changes are detected than we need to refresh the grid. // This also works for the first page access, since we assign the initial sorting in the initialize section. $scope.$watch('sortInfo.fields[0]', function () { $scope.refreshGrid(); }, true); // Do something when the grid is sorted. // The grid throws the ngGridEventSorted that gets picked up here and assigns the sortInfo to the scope. // This will allow to watch the sortInfo in the scope for changed and refresh the grid. $scope.$on('ngGridEventSorted', function (event, sortInfo) { $scope.sortInfo = sortInfo; }); // Picks the event broadcasted when a person is saved or deleted to refresh the grid elements with the most // updated information. $scope.$on('refreshGrid', function () { $scope.refreshGrid(); }); // Picks the event broadcasted when the form is cleared to also clear the grid selection. $scope.$on('clear', function () { $scope.gridOptions.selectAll(false); });}); |
Для настройки поведения сетки требуется еще несколько атрибутов. Важными битами являются data: 'persons.list' который привязывает данные сетки к значению угловой модели $scope.persons , columnDefs которое позволяет нам моделировать сетку по своему усмотрению. Поскольку я хотел добавить опцию для удаления каждой строки, мне нужно было добавить новую ячейку, которая вызывает функцию deleteRow когда вы щелкаете по крестовому значку. Функция afterSelectionChanges требуется для обновления данных формы с человеком, выбранным в сетке. Вы можете проверить другие параметры сетки здесь.
Остальная часть кода не требует пояснений, и там также есть несколько комментариев. Специальное примечание о $rootScope.$broadcast : используется для отправки события всем остальным контроллерам. Это способ связи между контроллерами, поскольку сетка, форма и сообщения обратной связи имеют отдельные контроллеры. Если бы все было только в одном контроллере, это не требовалось, и простого вызова функции было бы достаточно. Другим возможным решением, если мы хотим сохранить несколько контроллеров, было бы использование сервисов Angular. Используемый подход кажется намного чище, поскольку он разделяет проблемы приложения и не требует от вас реализации дополнительных сервисов Angular, но в случае необходимости его отладка может оказаться немного сложнее.
Форма HTML
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
<div class="form" ng-controller="personsFormController"> <!-- Verify person, if there is no id present, that we are Adding a Person --> <div ng-if="person.id == null"> <h3>Add Person</h3> </div> <!-- Otherwise it's an Edit --> <div ng-if="person.id != null"> <h3>Edit Person</h3> </div> <div> <!-- Specify the function to be called on submit and disable HTML5 validation, since we're using Angular validation--> <form name="personForm" ng-submit="updatePerson()" novalidate> <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) --> <div class="form-group" ng-class="{'has-error' : personForm.name.$invalid && personForm.name.$dirty}"> <label for="name">Name:</label> <!-- Display a check when the field is valid and was modified --> <span ng-class="{'glyphicon glyphicon-ok' : personForm.name.$valid && personForm.name.$dirty}"></span> <input id="name" name="name" type="text" class="form-control" maxlength="50" ng-model="person.name" required ng-minlength="2" ng-maxlength="50"/> <!-- Validation messages to be displayed on required, minlength and maxlength --> <p class="help-block" ng-show="personForm.name.$error.required">Add Name.</p> <p class="help-block" ng-show="personForm.name.$error.minlength">Name must be at least 2 characters long.</p> <p class="help-block" ng-show="personForm.name.$error.maxlength">Name cannot be longer than 50 characters.</p> </div> <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) --> <div class="form-group" ng-class="{'has-error' : personForm.description.$invalid && personForm.description.$dirty}"> <label for="description">Description:</label> <!-- Display a check when the field is valid and was modified --> <span ng-class="{'glyphicon glyphicon-ok' : personForm.description.$valid && personForm.description.$dirty}"></span> <input id="description" name="description" type="text" class="form-control" maxlength="100" ng-model="person.description" required ng-minlength="5" ng-maxlength="100"/> <!-- Validation messages to be displayed on required, minlength and maxlength --> <p class="help-block" ng-show="personForm.description.$error.required">Add Description.</p> <p class="help-block" ng-show="personForm.description.$error.minlength">Description must be at least 5 characters long.</p> <p class="help-block" ng-show="personForm.description.$error.maxlength">Description cannot be longer than 100 characters.</p> </div> <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) --> <div class="form-group" ng-class="{'has-error' : personForm.imageUrl.$invalid && personForm.imageUrl.$dirty}"> <label for="imageUrl">Image URL:</label> <!-- Display a check when the field is valid and was modified --> <span ng-class="{'glyphicon glyphicon-ok' : personForm.imageUrl.$valid && personForm.imageUrl.$dirty}"></span> <input id="imageUrl" name="imageUrl" type="url" class="form-control" maxlength="500" ng-model="person.imageUrl" required/> <!-- Validation messages to be displayed on required and invalid. Type 'url' makes checks to a proper url format. --> <p class="help-block" ng-show="personForm.imageUrl.$error.required">Add Image URL.</p> <p class="help-block" ng-show="personForm.imageUrl.$invalid && personForm.imageUrl.$dirty">Invalid Image URL.</p> </div> <div class="avatar" ng-if="person.imageUrl"> <img ng-src="{{person.imageUrl}}" width="400" height="250"/> </div> <!-- Form buttons. The 'Save' button is only enabled when the form is valid. --> <div class="buttons"> <button type="button" class="btn btn-primary" ng-click="clearForm()">Clear</button> <button type="submit" class="btn btn-primary" ng-disabled="personForm.$invalid">Save</button> </div> </form> </div></div> |
Вот выглядит:
Много кода для целей проверки, но давайте рассмотрим это немного подробнее: каждый элемент input связывает свое значение с person.something . Это позволяет моделировать данные между HTML и контроллером Javascript, поэтому мы можем написать $scope.person.name в нашем контроллере, чтобы получить значение, заполненное в форме ввода с именем name . Для доступа к данным внутри HTML-формы мы используем имя формы personForm плюс имя поля ввода.
HTML5 имеет собственный набор проверок в полях ввода, но мы хотим использовать угловые. В этом случае нам нужно отключить проверку формы, используя novalidate в элементе form . Теперь, чтобы использовать угловые проверки, мы можем использовать несколько угловых директив во input элементах. Для этой очень простой формы мы используем только required ng-minlength и ng-maxlength , но вы можете использовать и другие. Просто загляните в документацию .
Angular назначает CSS-классы на основе состояния проверки ввода. Чтобы иметь представление, это возможные значения:
| государство | CSS | На |
|---|---|---|
valid |
нг-Валид | Когда поле действительно. |
invalid |
нг-инвалид | Когда поле недействительно. |
pristine |
нг-нетронутые | Когда поле никогда не трогали раньше. |
dirty |
нг-грязная | Когда поле изменилось. |
Эти CSS-классы пусты. Вам необходимо создать их и назначить их стили на прилагаемом листе CSS для приложения. Вместо этого мы собираемся использовать стили из Bootstrap, которые очень хороши. Чтобы они работали, к элементам необходимо применить несколько дополнительных классов. div заключающему в себя вход, требуется группа form-group класса CSS, а элементу input нужен элемент управления form-control CSS.
Чтобы отобразить недопустимое поле ввода, мы добавляем ng-class="{'has-error' : personForm.name.$invalid && personForm.name.$dirty}" в содержащий входной div. Этот код оценивает, является ли имя в personForm недействительным и является ли оно грязным. Если условие проверяется, то ввод отображается как недействительный.
Наконец, для сообщений проверки формы нам нужно проверить директиву $error для каждого из входных данных и типов выполняемых проверок. Просто добавьте ng-show="personForm.name.$error.minlength" в элемент отображения HTML с сообщением, чтобы предупредить пользователя о том, что поле ввода имени слишком короткое.
Форма угловой контроллер
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
// Create a controller with name personsFormController to bind to the form section.app.controller('personsFormController', function ($scope, $rootScope, personService) { // Clears the form. Either by clicking the 'Clear' button in the form, or when a successfull save is performed. $scope.clearForm = function () { $scope.person = null; // For some reason, I was unable to clear field values with type 'url' if the value is invalid. // This is a workaroud. Needs proper investigation. document.getElementById('imageUrl').value = null; // Resets the form validation state. $scope.personForm.$setPristine(); // Broadcast the event to also clear the grid selection. $rootScope.$broadcast('clear'); }; // Calls the rest method to save a person. $scope.updatePerson = function () { personService.save($scope.person).$promise.then( function () { // Broadcast the event to refresh the grid. $rootScope.$broadcast('refreshGrid'); // Broadcast the event to display a save message. $rootScope.$broadcast('personSaved'); $scope.clearForm(); }, function () { // Broadcast the event for a server error. $rootScope.$broadcast('error'); }); }; // Picks up the event broadcasted when the person is selected from the grid and perform the person load by calling // the appropiate rest service. $scope.$on('personSelected', function (event, id) { $scope.person = personService.get({id: id}); }); // Picks us the event broadcasted when the person is deleted from the grid and perform the actual person delete by // calling the appropiate rest service. $scope.$on('deletePerson', function (event, id) { personService.delete({id: id}).$promise.then( function () { // Broadcast the event to refresh the grid. $rootScope.$broadcast('refreshGrid'); // Broadcast the event to display a delete message. $rootScope.$broadcast('personDeleted'); $scope.clearForm(); }, function () { // Broadcast the event for a server error. $rootScope.$broadcast('error'); }); });}); |
Для контроллера формы нам нужны две функции, которые выполняют операции, связанные с кнопкой «Очистить» и кнопкой «Сохранить», которые не требуют пояснений. Небольшое примечание: по какой-то причине Angular не очищает поля ввода, которые находятся в недопустимом состоянии. Я нашел несколько человек, жалующихся на ту же проблему, но мне нужно исследовать это дальше. Может быть, я что-то не так делаю.
Службы REST вызываются с помощью save и delete из объекта $resource который уже реализует соответствующие методы HTTP. Проверьте документацию . Вы можете получить $resource со следующей фабрикой:
ОТДЫХ Сервис
|
1
2
3
4
|
// Service that provides persons operationsapp.factory('personService', function ($resource) { return $resource('resources/persons/:id');}); |
Остальная часть кода контроллера — это функции для сбора событий, созданных сеткой, для загрузки данных о человеке в форму и удаления человека. Этот контроллер также создает несколько событий. Если мы добавляем или удаляем лиц, необходимо обновить сетку, чтобы сгенерировалось событие, запрашивающее обновление сетки.
Обратная связь Сообщения HTML
|
1
2
3
4
|
<!-- Specify a Angular controller script that binds Javascript variables to the feedback messages.--><div class="message" ng-controller="alertMessagesController"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</alert></div> |
Это только верхняя часть приложения, отображающая сообщения об успехах или ошибках, основанные на сохранении, удалении или ошибке сервера.
Сообщения обратной связи Угловой контроллер
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// Create a controller with name alertMessagesController to bind to the feedback messages section.app.controller('alertMessagesController', function ($scope) { // Picks up the event to display a saved message. $scope.$on('personSaved', function () { $scope.alerts = [ { type: 'success', msg: 'Record saved successfully!' } ]; }); // Picks up the event to display a deleted message. $scope.$on('personDeleted', function () { $scope.alerts = [ { type: 'success', msg: 'Record deleted successfully!' } ]; }); // Picks up the event to display a server error message. $scope.$on('error', function () { $scope.alerts = [ { type: 'danger', msg: 'There was a problem in the server!' } ]; }); $scope.closeAlert = function (index) { $scope.alerts.splice(index, 1); };}); |
Это контроллер, который выдвигает сообщения к представлению. Прослушивает события, созданные сеткой и контроллерами форм.
Конечный результат
Уф … это было много кода и новой информации. Давайте посмотрим на конечный результат:
Существует также живая версия, запущенная на http://javaee7-angular.radcortez.cloudbees.net , благодаря Cloudbees . Открытие облачного хранилища может занять некоторое время (из-за отсутствия использования).
Ресурсы
Вы можете клонировать полную рабочую копию из моего репозитория github и развернуть ее в Wildfly. Вы можете найти там инструкции по его развертыванию. Также должен работать на Glassfish.
Поскольку я могу изменить код в будущем, вы можете скачать исходный код этого поста с версии 3.0 . В качестве альтернативы, клонируйте репозиторий и извлеките тег из версии 3.0 с помощью следующей команды: git checkout 3.0 .
Проверьте также:
Последние мысли
- Проверка формы запускается сразу после того, как вы начнете печатать. У Angular 1.3 будет свойство on blur, которое можно проверить только после потери фокуса, но я все еще использую Angular 1.2.x.
- Я должен признаться, что я нашел код проверки слишком многословным. Я не знаю, есть ли способ упростить его, но вам не нужно добавлять проверку каждого сообщения к каждому входу.
- Здесь все еще не хватает некоторых вещей, таких как очистка параметров или проверка на стороне сервера. Я расскажу об этом в следующем посте.
Это был очень длинный пост, самый длинный, который я написал в своем блоге. Если вы дошли до этого места, большое спасибо за ваше время, прочитав этот пост . Надеюсь, вам понравилось! Дайте мне знать, если у вас есть какие-либо комментарии.
| Ссылка: | Java EE 7 с Angular JS — CRUD, REST, валидации — часть 2 от нашего партнера по JCG Роберто Кортеса в блоге |


