Это часть проекта Студент . Другие публикации — это клиент Webservice с Джерси , сервер Webservice с Джерси , бизнес-уровень , постоянство с данными Spring , тестовые данные интеграции Sharding, интеграция с Webservice и запросы критериев JPA .
Когда я начал этот проект, у меня было четыре цели. Ни в каком конкретном порядке они не должны были:
- узнать о jQuery и других технологиях AJAX. Для этого мне нужен REST-сервер, который я понял,
- захватить недавно полученные знания о трикотаже и гобелене,
- создать структуру, которую я мог бы использовать, чтобы узнать о других технологиях (например, Spring MVC, Restlet, Netty), и
- есть что обсудить на собеседовании
Если это было полезно для других — отлично! Вот почему он доступен под лицензией Apache.
(Само собой разумеется, что приемлемое использование не включает превращение «Студента проекта» в проект студента без надлежащей атрибуции!)
Проблема с изучением AJAX заключается в том, что я изначально не уверен, в чем проблема. Это плохо, JQuery? Плохой REST сервис? Что-то другое? Обширные модульные и интеграционные тесты — хорошее начало, но всегда будет некоторая неопределенность.
Другое соображение заключается в том, что в реальном мире нам часто требуется базовое представление о базе данных. Это не будет для общественного потребления — это для внутреннего использования, когда мы дойдем до момента WTF. Его также можно использовать для хранения информации, которой мы не хотим управлять через общедоступный интерфейс, например, значения в выпадающих меню.
Небольшой поворот в этом может быть использован для обеспечения скромного уровня масштабируемости. Используйте здоровенные серверы для своей базы данных и службы REST, затем используйте N интерфейсных серверов, на которых работают обычные веб-приложения, выступающие в качестве посредника между пользователем и службой REST. Внешние серверы могут быть достаточно легкими и запускаться по мере необходимости. Бонусные баллы за размещение кэширующего сервера между интерфейсами и сервером REST, поскольку будет прочитано подавляющее большинство обращений.
Этот подход не будет масштабироваться до масштабов Amazon или Facebook, но он будет достаточно хорош для многих сайтов.
Веб-приложение обслуживания
Это подводит нас к дополнительным слоям веб-приложения — обычного веб-приложения, которое выступает в качестве внешнего интерфейса для службы REST. По разным причинам я использую Tapestry 5 для приложения, но это произвольное решение, и я не буду тратить много времени на изучение кода, специфичного для Tapestry.
Вы можете создать новый проект гобелена с
|
1
|
$ mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org |
Я нашел примеры на http://jumpstart.doublenegative.com.au/jumpstart/examples/ бесценными. Я держал в атрибуции, где это необходимо
Позже я также создам второй дополнительный слой веб-приложения — функциональные и регрессионные тесты с Selenium и WebDriver (то есть Selenium 2.0).
Ограничения
Только для чтения — раскрутка веб-приложения требует большой работы, поэтому в начальной версии доступ к простым таблицам будет только для чтения. Нет обновлений, нет сопоставлений один ко многим.
Аутентификация пользователя — не предпринималось никаких усилий для аутентификации пользователей.
Шифрование — не было предпринято никаких усилий для шифрования сообщений.
Блокировки базы данных — мы используем оппортунистическую блокировку в спящих версиях вместо явных блокировок базы данных. Примечание по безопасности: по принципу наименьшего раскрытия мы не хотим делать версию видимой, если она не требуется. Хорошим правилом является то, что вы увидите, запрашиваете ли вы конкретный объект, но не видите его в списках.
REST Client — обработчик GET по умолчанию для каждого типа очень грубый — он просто возвращает список объектов. Нам нужен более сложный ответ (например, количество записей, начальный и конечный индексы, код состояния и т. Д.), И мы позволим интерфейсу управлять им. На данный момент единственное, что нам действительно нужно, это счетчик, и мы можем просто запросить список и подсчитать количество элементов.
Цель
Мы хотим страницу, которая перечисляет все курсы в базе данных. Не нужно беспокоиться о разбиении на страницы, сортировке и т. Д. В нем должны быть ссылки (возможно, неактивные) для редактирования и удаления записи. Для добавления нового курса не нужно иметь ссылку.
Страница должна выглядеть примерно так:
Шаблон курса
Страница со списком курсов на Гобелене проста — в основном это просто декорированная сетка ценностей.
(См. Архетип гобелена для Layout.tml и т. Д.)
|
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
|
<html t:type="layout" title="Course List" t:sidebarTitle="Framework Version" xmlns:p="tapestry:parameter"> <!-- Most of the page content, including <head>, <body>, etc. tags, comes from Layout.tml --> <t:zone t:id="zone"> <p> "Course" page </p> <t:grid source="courses" row="course" include="uuid,name,creationdate" add="edit,delete"> <p:name> <t:pagelink page="CourseEditor" context="course.uuid">${course.name}</t:pagelink> </p:name> <p:editcell> <t:actionlink t:id="edit" context="course.uuid">Edit</t:actionlink> </p:editcell> <p:deletecell> <t:actionlink t:id="delete" context="course.uuid">Delete</t:actionlink> </p:deletecell> <p:empty> <p>There are no courses to display; you can <t:pagelink page="Course/Editor" parameters="{ 'mode':'create', 'courseUuid':null }">add some</t:pagelink>.</p> </p:empty> </t:grid> </t:zone> <p:sidebar> <p> [ <t:pagelink page="Index">Index</t:pagelink> ]<br/> [ <t:pagelink page="Course/List">Courses</t:pagelink> ] </p> </p:sidebar></html> |
с файлом свойств
|
1
2
|
title=Coursesdelete-course=Delete course? |
Я включил ссылки для редактирования и удаления, но они не работают.
GridDataSources
Нашей странице нужен источник значений для отображения. Это требует двух классов. Первый определяет свойство курсов, использованное выше.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package com.invariantproperties.sandbox.student.maintenance.web.tables;import com.invariantproperties.sandbox.student.business.CourseFinderService;public class GridDataSources { // Screen fields @Property private GridDataSource courses; // Generally useful bits and pieces @Inject private CourseFinderService courseFinderService; @InjectComponent private Grid grid; // The code void setupRender() { courses = new CoursePagedDataSource(courseFinderService); }} |
и фактическая реализация
|
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
|
package com.invariantproperties.sandbox.student.maintenance.web.tables;import com.invariantproperties.sandbox.student.business.CourseFinderService;import com.invariantproperties.sandbox.student.domain.Course;import com.invariantproperties.sandbox.student.maintenance.query.SortCriterion;import com.invariantproperties.sandbox.student.maintenance.query.SortDirection;public class CoursePagedDataSource implements GridDataSource { private int startIndex; private List<Course> preparedResults; private final CourseFinderService courseFinderService; public CoursePagedDataSource(CourseFinderService courseFinderService) { this.courseFinderService = courseFinderService; } @Override public int getAvailableRows() { long count = courseFinderService.count(); return (int) count; } @Override public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) { // Get a page of courses - ask business service to find them (from the // database) // List<SortCriterion> sortCriteria = toSortCriteria(sortConstraints); // preparedResults = courseFinderService.findCourses(startIndex, // endIndex - startIndex + 1, sortCriteria); preparedResults = courseFinderService.findAllCourses(); this.startIndex = startIndex; } @Override public Object getRowValue(final int index) { return preparedResults.get(index - startIndex); } @Override public Class<Course> getRowType() { return Course.class; } /** * Converts a list of Tapestry's SortConstraint to a list of our business * tier's SortCriterion. The business tier does not use SortConstraint * because that would create a dependency on Tapestry. */ private List<SortCriterion> toSortCriteria(List<SortConstraint> sortConstraints) { List<SortCriterion> sortCriteria = new ArrayList<>(); for (SortConstraint sortConstraint : sortConstraints) { String propertyName = sortConstraint.getPropertyModel().getPropertyName(); SortDirection sortDirection = SortDirection.UNSORTED; switch (sortConstraint.getColumnSort()) { case ASCENDING: sortDirection = SortDirection.ASCENDING; break; case DESCENDING: sortDirection = SortDirection.DESCENDING; break; default: } SortCriterion sortCriterion = new SortCriterion(propertyName, sortDirection); sortCriteria.add(sortCriterion); } return sortCriteria; }} |
AppModule
Теперь, когда у нас есть GridDataSource, мы можем видеть, что ему нужно — CourseFinderService. Несмотря на то, что существует интеграция Tapestry-Spring, мы хотим, чтобы веб-приложение для технического обслуживания было как можно более тонким, поэтому сейчас мы используем стандартную инъекцию Tapestry.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
package com.invariantproperties.sandbox.student.maintenance.web.services;import com.invariantproperties.sandbox.student.business.CourseFinderService;import com.invariantproperties.sandbox.student.business.CourseManagerService;import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseFinderServiceTapestryImpl;import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseManagerServiceTapestryImpl;/** * This module is automatically included as part of the Tapestry IoC Registry, * it's a good place to configure and extend Tapestry, or to place your own * service definitions. */public class AppModule { public static void bind(ServiceBinder binder) { binder.bind(CourseFinderService.class, CourseFinderServiceTapestryImpl.class); binder.bind(CourseManagerService.class, CourseManagerServiceTapestryImpl.class); } ....} |
Обратите внимание, что мы используем стандартный интерфейс CourseFinderService со специфичной для гобелена реализацией. Это означает, что мы можем использовать стандартную реализацию напрямую, не внося никаких изменений в файлы конфигурации!
CourseFinderServiceTapestryImpl
Локальная реализация интерфейса CourseFinderService должна использовать клиент REST вместо реализации Spring Data. При использовании внешнего подхода, который использовался ранее, потребности шаблона Tapestry должны определять потребности реализации Сервиса, а это, в свою очередь, определяет потребности клиента и сервера REST.
|
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
|
package com.invariantproperties.sandbox.student.maintenance.service.impl;public class CourseFinderServiceTapestryImpl implements CourseFinderService { private final CourseFinderRestClient finder; public CourseFinderServiceTapestryImpl() { // resource should be loaded as tapestry resource finder = new CourseFinderRestClientImpl(resource); // load some initial data initCache(new CourseManagerRestClientImpl(resource)); } @Override public long count() { // FIXME: grossly inefficient but good enough for now. return finder.getAllCourses().length; } @Override public long countByTestRun(TestRun testRun) { // FIXME: grossly inefficient but good enough for now. return finder.getAllCourses().length; } @Override public Course findCourseById(Integer id) { // unsupported operation! throw new ObjectNotFoundException(id); } @Override public Course findCourseByUuid(String uuid) { return finder.getCourse(uuid); } @Override public List<Course> findAllCourses() { return Arrays.asList(finder.getAllCourses()); } @Override public List<Course> findCoursesByTestRun(TestRun testRun) { return Collections.emptyList(); } // method to load some test data into the database. private void initCache(CourseManagerRestClient manager) { manager.createCourse("physics 101"); manager.createCourse("physics 201"); manager.createCourse("physics 202"); }} |
Наш запрос JPA Criteria может быстро подсчитать, но наш клиент REST пока не поддерживает это.
Завершение
После того, как мы закончим грубую работу, у нас будет файл .war обслуживания. Мы можем развернуть его с помощью веб-сервиса .war на нашем сервере приложений — или нет. Нет причин, по которым два файла .war должны находиться в одной системе, кроме временно закодированного URL-адреса для веб-службы.
Сначала нужно перейти по адресу http: // localhost: 8080 / student-maintenance-webapp / course / list. Мы должны увидеть короткий список курсов, как показано выше. (В этом случае я перезапустил веб-приложение три раза, поэтому каждая запись дублируется в три раза.)
Теперь нам нужно зайти в наш веб-сервис веб-сервиса по адресу http: // localhost: 8080 / student-ws-webapp / rest / course и убедиться, что мы можем получать данные и через браузер. После небольшой очистки мы должны увидеть:
|
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
|
{"course": [ { "creationDate":"2013-12-28T14:40:21.369-07:00", "uuid":"500069e4-444d-49bc-80f0-4894c2d13f6a", "version":"0", "name":"physics 101" }, { "creationDate":"2013-12-28T14:40:21.777-07:00", "uuid":"54001b2a-abbb-4a75-a289-e1f09173fa04", "version":"0", "name":"physics 201" }, { "creationDate":"2013-12-28T14:40:21.938-07:00", "uuid":"cfaf892b-7ead-4d64-8659-8f87756bed62", "version":"0", "name":"physics 202" }, { "creationDate":"2013-12-28T16:17:54.608-07:00", "uuid":"d29735ff-f614-4979-a0de-e1d134e859f4", "version":"0", "name":"physics 101" }, .... ]} |
Исходный код
- Исходный код находится по адресу https://github.com/beargiles/project-student [github] и http://beargiles.github.io/project-student/ [github pages].
