Статьи

Студент проекта: поддержка Webapp (только для чтения)

Это часть проекта Студент . Другие публикации — это клиент 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=Courses
delete-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
        final String resource = "http://localhost:8080/student-ws-webapp/rest/course/";
        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"
   },
   ....
 ]
}

Исходный код